diff --git a/fedora/.config/Thunar/uca.xml b/fedora/.config/Thunar/uca.xml new file mode 100644 index 0000000..73b5163 --- /dev/null +++ b/fedora/.config/Thunar/uca.xml @@ -0,0 +1,175 @@ + + + + utilities-terminal + Open Terminal Here + + open-terminal + alacritty + Example for a custom action + + * + + + + + meld + Compare + + 1492232362549799-1 + meld %F + + + * + + + + + + system-file-manager-root + Open folder as root + + 1493475601060449-3 + pkexec thunar %f + + + * + + + + checkbox + Check sha1sum + + 1526633271260079-26 + zenity --info --title="Check sha1 for %n" --text="$(sha1sum %f)" + Check sha1sum + + *.iso;*.ISO + + + + checkbox + Check sha256sum + + 1577688162350307-1 + zenity --info --title="Check sha256 for %n" --text="$(sha256sum %f)" + Check sha256sum + + *.iso;*.ISO + + + + checkbox + Check md5sum + + 1526736788575383-2 + zenity --info --title="Check md5 for %n" --text="$(md5sum %f)" + Check md5sum + + *.iso;*.ISO;*.tar.gz;*.TAR.GZ;*.zip;*.ZIP + + + + catfish + Search with catfish + + 1489089852658523-2 + catfish --path=$f$d + + + * + + + + archive-extract + Extract here + + 1489091300385082-4 + tar xjf %n + + + *.tar.bz2;*.tbz2 + + + + cs-startup-programs + Run + + 1526630606202525-4 + alacritty -e %f + + + * + + + + + system-reboot + Convert png to jpg + + 1510662686010963-1 + convert %f -quality 90 `basename %f .png`.jpg + Convert png to jpg + + *.png + + + + system-reboot + Convert webp to jpg + + 1510662686010963-2 + convert %f -quality 90 `basename %f .webp`.jpg + Convert webp to jpg + + *.webp + + + + application-x-executable + Make executable + + 1526670239648084-2 + chmod +x %f + Make a file execuble + + * + + + + + com.deepin.ScreenShot + Set as wallpaper DS + + 1637670737487118-1 + feh --bg-fill --no-xinerama %f + Set as wallpaper using feh dual screen + + * + + + + kscreenshot + Set as wallpaper SS + + 1637670897220972-2 + feh --bg-fill %f + Set as wallpaper using feh single screen + + * + + + + insert-link-symbolic + Create symlink + + 1526631663081446-14 + ln -s %f "%n" + Softlink between two files or directories + + * + + + + + diff --git a/fedora/.config/btop/btop.conf b/fedora/.config/btop/btop.conf new file mode 100644 index 0000000..ce54a93 --- /dev/null +++ b/fedora/.config/btop/btop.conf @@ -0,0 +1,254 @@ +#? Config file for btop v. 1.4.3 + +#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes. +#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes" +color_theme = "Default" + +#* If the theme set background should be shown, set to False if you want terminal background transparency. +theme_background = True + +#* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false. +truecolor = True + +#* Set to true to force tty mode regardless if a real tty has been detected or not. +#* Will force 16-color mode and TTY theme, set all graph symbols to "tty" and swap out other non tty friendly symbols. +force_tty = False + +#* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets. +#* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box. +#* Use whitespace " " as separator between different presets. +#* Example: "cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty" +presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty" + +#* Set to True to enable "h,j,k,l,g,G" keys for directional control in lists. +#* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift. +vim_keys = False + +#* Rounded corners on boxes, is ignored if TTY mode is ON. +rounded_corners = True + +#* Default symbols to use for graph creation, "braille", "block" or "tty". +#* "braille" offers the highest resolution but might not be included in all fonts. +#* "block" has half the resolution of braille but uses more common characters. +#* "tty" uses only 3 different symbols but will work with most fonts and should work in a real TTY. +#* Note that "tty" only has half the horizontal resolution of the other two, so will show a shorter historical view. +graph_symbol = "braille" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_cpu = "default" + +# Graph symbol to use for graphs in gpu box, "default", "braille", "block" or "tty". +graph_symbol_gpu = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_mem = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_net = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_proc = "default" + +#* Manually set which boxes to show. Available values are "cpu mem net proc" and "gpu0" through "gpu5", separate values with whitespace. +shown_boxes = "cpu mem net proc" + +#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs. +update_ms = 2000 + +#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct", +#* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly. +proc_sorting = "cpu lazy" + +#* Reverse sorting order, True or False. +proc_reversed = False + +#* Show processes as a tree. +proc_tree = False + +#* Use the cpu graph colors in the process list. +proc_colors = True + +#* Use a darkening gradient in the process list. +proc_gradient = True + +#* If process cpu usage should be of the core it's running on or usage of the total available cpu power. +proc_per_core = False + +#* Show process memory as bytes instead of percent. +proc_mem_bytes = True + +#* Show cpu graph for each process. +proc_cpu_graphs = True + +#* Use /proc/[pid]/smaps for memory information in the process info box (very slow but more accurate) +proc_info_smaps = False + +#* Show proc box on left side of screen instead of right. +proc_left = False + +#* (Linux) Filter processes tied to the Linux kernel(similar behavior to htop). +proc_filter_kernel = False + +#* In tree-view, always accumulate child process resources in the parent process. +proc_aggregate = False + +#* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available. +#* Select from a list of detected attributes from the options menu. +cpu_graph_upper = "Auto" + +#* Sets the CPU stat shown in lower half of the CPU graph, "total" is always available. +#* Select from a list of detected attributes from the options menu. +cpu_graph_lower = "Auto" + +#* If gpu info should be shown in the cpu box. Available values = "Auto", "On" and "Off". +show_gpu_info = "Auto" + +#* Toggles if the lower CPU graph should be inverted. +cpu_invert_lower = True + +#* Set to True to completely disable the lower CPU graph. +cpu_single_graph = False + +#* Show cpu box at bottom of screen instead of top. +cpu_bottom = False + +#* Shows the system uptime in the CPU box. +show_uptime = True + +#* Show cpu temperature. +check_temp = True + +#* Which sensor to use for cpu temperature, use options menu to select from list of available sensors. +cpu_sensor = "Auto" + +#* Show temperatures for cpu cores also if check_temp is True and sensors has been found. +show_coretemp = True + +#* Set a custom mapping between core and coretemp, can be needed on certain cpus to get correct temperature for correct core. +#* Use lm-sensors or similar to see which cores are reporting temperatures on your machine. +#* Format "x:y" x=core with wrong temp, y=core with correct temp, use space as separator between multiple entries. +#* Example: "4:0 5:1 6:3" +cpu_core_map = "" + +#* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine". +temp_scale = "celsius" + +#* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024. +base_10_sizes = False + +#* Show CPU frequency. +show_cpu_freq = True + +#* Draw a clock at top of screen, formatting according to strftime, empty string to disable. +#* Special formatting: /host = hostname | /user = username | /uptime = system uptime +clock_format = "%X" + +#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort. +background_update = True + +#* Custom cpu model name, empty string to disable. +custom_cpu_name = "" + +#* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with whitespace " ". +#* Begin line with "exclude=" to change to exclude filter, otherwise defaults to "most include" filter. Example: disks_filter="exclude=/boot /home/user". +disks_filter = "" + +#* Show graphs instead of meters for memory values. +mem_graphs = True + +#* Show mem box below net box instead of above. +mem_below_net = False + +#* Count ZFS ARC in cached and available memory. +zfs_arc_cached = True + +#* If swap memory should be shown in memory box. +show_swap = True + +#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk. +swap_disk = True + +#* If mem box should be split to also show disks info. +show_disks = True + +#* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar. +only_physical = True + +#* Read disks list from /etc/fstab. This also disables only_physical. +use_fstab = True + +#* Setting this to True will hide all datasets, and only show ZFS pools. (IO stats will be calculated per-pool) +zfs_hide_datasets = False + +#* Set to true to show available disk space for privileged users. +disk_free_priv = False + +#* Toggles if io activity % (disk busy time) should be shown in regular disk usage view. +show_io_stat = True + +#* Toggles io mode for disks, showing big graphs for disk read/write speeds. +io_mode = False + +#* Set to True to show combined read/write io graphs in io mode. +io_graph_combined = False + +#* Set the top speed for the io graphs in MiB/s (100 by default), use format "mountpoint:speed" separate disks with whitespace " ". +#* Example: "/mnt/media:100 /:20 /boot:1". +io_graph_speeds = "" + +#* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False. +net_download = 100 + +net_upload = 100 + +#* Use network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest. +net_auto = True + +#* Sync the auto scaling for download and upload to whichever currently has the highest scale. +net_sync = True + +#* Starts with the Network Interface specified here. +net_iface = "" + +#* "True" shows bitrates in base 10 (Kbps, Mbps). "False" shows bitrates in binary sizes (Kibps, Mibps, etc.). "Auto" uses base_10_sizes. +base_10_bitrate = "Auto" + +#* Show battery stats in top right if battery is present. +show_battery = True + +#* Which battery to use if multiple are present. "Auto" for auto detection. +selected_battery = "Auto" + +#* Show power stats of battery next to charge indicator. +show_battery_watts = True + +#* Set loglevel for "~/.config/btop/btop.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG". +#* The level set includes all lower levels, i.e. "DEBUG" will show all logging info. +log_level = "WARNING" + +#* Measure PCIe throughput on NVIDIA cards, may impact performance on certain cards. +nvml_measure_pcie_speeds = True + +#* Measure PCIe throughput on AMD cards, may impact performance on certain cards. +rsmi_measure_pcie_speeds = True + +#* Horizontally mirror the GPU graph. +gpu_mirror_graph = True + +#* Custom gpu0 model name, empty string to disable. +custom_gpu_name0 = "" + +#* Custom gpu1 model name, empty string to disable. +custom_gpu_name1 = "" + +#* Custom gpu2 model name, empty string to disable. +custom_gpu_name2 = "" + +#* Custom gpu3 model name, empty string to disable. +custom_gpu_name3 = "" + +#* Custom gpu4 model name, empty string to disable. +custom_gpu_name4 = "" + +#* Custom gpu5 model name, empty string to disable. +custom_gpu_name5 = "" diff --git a/fedora/.config/btop/themes/catppuccin_frappe.theme b/fedora/.config/btop/themes/catppuccin_frappe.theme new file mode 100644 index 0000000..000a9b0 --- /dev/null +++ b/fedora/.config/btop/themes/catppuccin_frappe.theme @@ -0,0 +1,42 @@ +theme[main_bg]="#303446" +theme[main_fg]="#C6D0F5" +theme[title]="#C6D0F5" +theme[hi_fg]="#8CAAEE" +theme[selected_bg]="#51576D" +theme[selected_fg]="#8CAAEE" +theme[inactive_fg]="#838BA7" +theme[graph_text]="#F2D5CF" +theme[meter_bg]="#51576D" +theme[proc_misc]="#F2D5CF" +theme[cpu_box]="#85C1DC" +theme[mem_box]="#A6D189" +theme[net_box]="#CA9EE6" +theme[proc_box]="#EEBEBE" +theme[div_line]="#737994" +theme[temp_start]="#E5C890" +theme[temp_mid]="#EF9F76" +theme[temp_end]="#E78284" +theme[cpu_start]="#85C1DC" +theme[cpu_mid]="#99D1DB" +theme[cpu_end]="#81C8BE" +theme[free_start]="#81C8BE" +theme[free_mid]="#81C8BE" +theme[free_end]="#A6D189" +theme[cached_start]="#F4B8E4" +theme[cached_mid]="#F4B8E4" +theme[cached_end]="#CA9EE6" +theme[available_start]="#F2D5CF" +theme[available_mid]="#EEBEBE" +theme[available_end]="#EEBEBE" +theme[used_start]="#EF9F76" +theme[used_mid]="#EF9F76" +theme[used_end]="#E78284" +theme[download_start]="#BABBF1" +theme[download_mid]="#BABBF1" +theme[download_end]="#CA9EE6" +theme[upload_start]="#BABBF1" +theme[upload_mid]="#BABBF1" +theme[upload_end]="#CA9EE6" +theme[process_start]="#85C1DC" +theme[process_mid]="#99D1DB" +theme[process_end]="#81C8BE" diff --git a/fedora/.config/btop/themes/catppuccin_latte.theme b/fedora/.config/btop/themes/catppuccin_latte.theme new file mode 100644 index 0000000..2975ba7 --- /dev/null +++ b/fedora/.config/btop/themes/catppuccin_latte.theme @@ -0,0 +1,42 @@ +theme[main_bg]="#EFF1F5" +theme[main_fg]="#4C4F69" +theme[title]="#4C4F69" +theme[hi_fg]="#1E66F5" +theme[selected_bg]="#BCC0CC" +theme[selected_fg]="#1E66F5" +theme[inactive_fg]="#8C8FA1" +theme[graph_text]="#DC8A78" +theme[meter_bg]="#BCC0CC" +theme[proc_misc]="#DC8A78" +theme[cpu_box]="#209FB5" +theme[mem_box]="#40A02B" +theme[net_box]="#8839EF" +theme[proc_box]="#DD7878" +theme[div_line]="#9CA0B0" +theme[temp_start]="#DF8E1D" +theme[temp_mid]="#FE640B" +theme[temp_end]="#D20F39" +theme[cpu_start]="#209FB5" +theme[cpu_mid]="#04A5E5" +theme[cpu_end]="#179299" +theme[free_start]="#179299" +theme[free_mid]="#179299" +theme[free_end]="#40A02B" +theme[cached_start]="#EA76CB" +theme[cached_mid]="#EA76CB" +theme[cached_end]="#8839EF" +theme[available_start]="#DC8A78" +theme[available_mid]="#DD7878" +theme[available_end]="#DD7878" +theme[used_start]="#FE640B" +theme[used_mid]="#FE640B" +theme[used_end]="#D20F39" +theme[download_start]="#7287FD" +theme[download_mid]="#7287FD" +theme[download_end]="#8839EF" +theme[upload_start]="#7287FD" +theme[upload_mid]="#7287FD" +theme[upload_end]="#8839EF" +theme[process_start]="#209FB5" +theme[process_mid]="#04A5E5" +theme[process_end]="#179299" diff --git a/fedora/.config/btop/themes/catppuccin_macchiato.theme b/fedora/.config/btop/themes/catppuccin_macchiato.theme new file mode 100644 index 0000000..7abd0bf --- /dev/null +++ b/fedora/.config/btop/themes/catppuccin_macchiato.theme @@ -0,0 +1,42 @@ +theme[main_bg]="#24273A" +theme[main_fg]="#CAD3F5" +theme[title]="#CAD3F5" +theme[hi_fg]="#8AADF4" +theme[selected_bg]="#494D64" +theme[selected_fg]="#8AADF4" +theme[inactive_fg]="#8087A2" +theme[graph_text]="#F4DBD6" +theme[meter_bg]="#494D64" +theme[proc_misc]="#F4DBD6" +theme[cpu_box]="#7DC4E4" +theme[mem_box]="#A6DA95" +theme[net_box]="#C6A0F6" +theme[proc_box]="#F0C6C6" +theme[div_line]="#6E738D" +theme[temp_start]="#EED49F" +theme[temp_mid]="#F5A97F" +theme[temp_end]="#ED8796" +theme[cpu_start]="#7DC4E4" +theme[cpu_mid]="#91D7E3" +theme[cpu_end]="#8BD5CA" +theme[free_start]="#8BD5CA" +theme[free_mid]="#8BD5CA" +theme[free_end]="#A6DA95" +theme[cached_start]="#F5BDE6" +theme[cached_mid]="#F5BDE6" +theme[cached_end]="#C6A0F6" +theme[available_start]="#F4DBD6" +theme[available_mid]="#F0C6C6" +theme[available_end]="#F0C6C6" +theme[used_start]="#F5A97F" +theme[used_mid]="#F5A97F" +theme[used_end]="#ED8796" +theme[download_start]="#B7BDF8" +theme[download_mid]="#B7BDF8" +theme[download_end]="#C6A0F6" +theme[upload_start]="#B7BDF8" +theme[upload_mid]="#B7BDF8" +theme[upload_end]="#C6A0F6" +theme[process_start]="#7DC4E4" +theme[process_mid]="#91D7E3" +theme[process_end]="#8BD5CA" diff --git a/fedora/.config/btop/themes/catppuccin_mocha.theme b/fedora/.config/btop/themes/catppuccin_mocha.theme new file mode 100644 index 0000000..13cec40 --- /dev/null +++ b/fedora/.config/btop/themes/catppuccin_mocha.theme @@ -0,0 +1,42 @@ +theme[main_bg]="#1E1E2E" +theme[main_fg]="#CDD6F4" +theme[title]="#CDD6F4" +theme[hi_fg]="#89B4FA" +theme[selected_bg]="#45475A" +theme[selected_fg]="#89B4FA" +theme[inactive_fg]="#7F849C" +theme[graph_text]="#F5E0DC" +theme[meter_bg]="#45475A" +theme[proc_misc]="#F5E0DC" +theme[cpu_box]="#74C7EC" +theme[mem_box]="#A6E3A1" +theme[net_box]="#CBA6F7" +theme[proc_box]="#F2CDCD" +theme[div_line]="#6C7086" +theme[temp_start]="#F9E2AF" +theme[temp_mid]="#FAB387" +theme[temp_end]="#F38BA8" +theme[cpu_start]="#74C7EC" +theme[cpu_mid]="#89DCEB" +theme[cpu_end]="#94E2D5" +theme[free_start]="#94E2D5" +theme[free_mid]="#94E2D5" +theme[free_end]="#A6E3A1" +theme[cached_start]="#F5C2E7" +theme[cached_mid]="#F5C2E7" +theme[cached_end]="#CBA6F7" +theme[available_start]="#F5E0DC" +theme[available_mid]="#F2CDCD" +theme[available_end]="#F2CDCD" +theme[used_start]="#FAB387" +theme[used_mid]="#FAB387" +theme[used_end]="#F38BA8" +theme[download_start]="#B4BEFE" +theme[download_mid]="#B4BEFE" +theme[download_end]="#CBA6F7" +theme[upload_start]="#B4BEFE" +theme[upload_mid]="#B4BEFE" +theme[upload_end]="#CBA6F7" +theme[process_start]="#74C7EC" +theme[process_mid]="#89DCEB" +theme[process_end]="#94E2D5" diff --git a/fedora/.config/fastfetch/config.jsonc b/fedora/.config/fastfetch/config.jsonc new file mode 100644 index 0000000..aa31abf --- /dev/null +++ b/fedora/.config/fastfetch/config.jsonc @@ -0,0 +1,143 @@ +{ + "$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json", + "logo": { + "type": "builtin", + "height": 15, + "width": 30, + "padding": { + "top": 5, + "left": 3 + } + }, + "modules": [ + "break", + { + "type": "custom", + "format": "┌──────────────────────Hardware──────────────────────┐" + }, + { + "type": "host", + "key": " PC", + "keyColor": "green" + }, + { + "type": "cpu", + "key": "│ ├", + "keyColor": "green" + }, + { + "type": "gpu", + "key": "│ ├󰍛", + "keyColor": "green" + }, + { + "type": "memory", + "key": "│ ├󰍛", + "keyColor": "green" + }, + { + "type": "disk", + "key": "└ └", + "keyColor": "green" + }, + { + "type": "custom", + "format": "└────────────────────────────────────────────────────┘" + }, + + { + "type": "custom", + "format": "┌──────────────────────Software──────────────────────┐" + }, + { + "type": "os", + "key": " OS", + "keyColor": "yellow" + }, + { + "type": "kernel", + "key": "│ ├", + "keyColor": "yellow" + }, + { + "type": "packages", + "key": "│ ├󰏖", + "keyColor": "yellow" + }, + { + "type": "shell", + "key": "└ └", + "keyColor": "yellow" + }, + "break", + { + "type": "de", + "key": " DE", + "keyColor": "blue" + }, + { + "type": "lm", + "key": "│ ├", + "keyColor": "blue" + }, + { + "type": "wm", + "key": "│ ├", + "keyColor": "blue" + }, + { + "type": "theme", + "key": "│ ├󰉼", + "keyColor": "blue" + }, + { + "type": "icons", + "key": "│ ├󰉼", + "keyColor": "blue" + }, { + "type": "terminal", + "key": "└ └", + "keyColor": "blue" + }, + { + "type": "custom", + "format": "└────────────────────────────────────────────────────┘" + }, + + { + "type": "custom", + "format": "┌────────────────────Uptime/Age/DT───────────────────┐" + }, + { + "type": "command", + "key": " OS Age ", + "keyColor": "magenta", + "text": "birth_install=$(stat -c %W /); current=$(date +%s); time_progression=$((current - birth_install)); days_difference=$((time_progression / 86400)); echo $days_difference days" + }, + { + "type": "uptime", + "key": " Uptime ", + "keyColor": "magenta" + }, + { + "type": "datetime", + "key": " DateTime ", + "keyColor": "magenta" + }, + { + "type": "custom", + "format": "└────────────────────────────────────────────────────┘" + }, + +// { +// "type": "colors" +// }, + + { + "type": "colors", + "paddingLeft": 2, + "symbol": "circle" + } + + ] +} \ No newline at end of file diff --git a/fedora/.config/fuzzel/fuzzel.ini b/fedora/.config/fuzzel/fuzzel.ini new file mode 100644 index 0000000..dfc1505 --- /dev/null +++ b/fedora/.config/fuzzel/fuzzel.ini @@ -0,0 +1 @@ +include=~/.config/fuzzel/themes/noctalia diff --git a/fedora/.config/fuzzel/themes/noctalia b/fedora/.config/fuzzel/themes/noctalia new file mode 100644 index 0000000..2cfde55 --- /dev/null +++ b/fedora/.config/fuzzel/themes/noctalia @@ -0,0 +1,15 @@ +# Fuzzel Colors +# Generated by Noctalia's Template Processor + +[colors] +background=111416CC +text=e2e2e5ff +prompt=b7c9d9ff +placeholder=cec0e8ff +input=8aceffff +match=cec0e8ff +selection=8aceff80 +selection-text=e2e2e5ff +selection-match=00344dff +counter=b7c9d9ff +border=8aceffff diff --git a/fedora/.config/gtk-3.0/bookmarks b/fedora/.config/gtk-3.0/bookmarks new file mode 100644 index 0000000..c9019cb --- /dev/null +++ b/fedora/.config/gtk-3.0/bookmarks @@ -0,0 +1,11 @@ +file:///home/yuri/Documents Documents +file:///home/yuri/Downloads Downloads +file:///home/yuri/Pictures Pictures +file:///home/yuri/.config .config +file:///home/yuri/DATA DATA +file:///home/yuri/Insync Insync +file:///home/yuri/DATA/Blender3D Blender +smb://hades.home.ku-it.org/documents/ Docs-share +smb://hades.home.ku-it.org/downloads/ Download-share +smb://hades.home.ku-it.org/media/ Media-share +smb://hades.home.ku-it.org/software/ Software-share diff --git a/fedora/.config/gtk-3.0/settings.ini b/fedora/.config/gtk-3.0/settings.ini new file mode 100644 index 0000000..b6e2e52 --- /dev/null +++ b/fedora/.config/gtk-3.0/settings.ini @@ -0,0 +1,16 @@ +[Settings] +gtk-theme-name=Arc-Dark +gtk-icon-theme-name=Surfn-Papirus-Grey +gtk-font-name=Noto Sans 11 +gtk-cursor-theme-name=Bibata-Modern-Ice +gtk-cursor-theme-size=0 +gtk-toolbar-style=GTK_TOOLBAR_ICONS +gtk-toolbar-icon-size=GTK_ICON_SIZE_LARGE_TOOLBAR +gtk-button-images=1 +gtk-menu-images=1 +gtk-enable-event-sounds=0 +gtk-enable-input-feedback-sounds=0 +gtk-xft-antialias=1 +gtk-xft-hinting=1 +gtk-xft-hintstyle=hintslight +gtk-xft-rgba=rgb diff --git a/fedora/.config/niri/config.kdl b/fedora/.config/niri/config.kdl new file mode 100644 index 0000000..30ffac4 --- /dev/null +++ b/fedora/.config/niri/config.kdl @@ -0,0 +1,559 @@ +// This config is in the KDL format: https://kdl.dev +// "/-" comments out the following node. +// Check the wiki for a full description of the configuration: +// https://yalter.github.io/niri/Configuration:-Introduction + +// Input device configuration. +// Find the full list of options on the wiki: +// https://yalter.github.io/niri/Configuration:-Input + +prefer-no-csd +input { + keyboard { + xkb { + // repeat-rate 35 + // repeat-delay 200 + } + numlock + } + + touchpad { + // off + tap + // dwt + // dwtp + // drag false + // drag-lock + natural-scroll + // accel-speed 0.2 + // accel-profile "flat" + // scroll-method "two-finger" + // disabled-on-external-mouse + } + + mouse { + } + + trackpoint { + } + + // Uncomment this to make the mouse warp to the center of newly focused windows. + // warp-mouse-to-focus + focus-follows-mouse max-scroll-amount="0%" +} + +// You can configure outputs by their name, which you can find +// by running `niri msg outputs` while inside a niri instance. +// The built-in laptop monitor is usually called "eDP-1". +// Find more information on the wiki: +// https://yalter.github.io/niri/Configuration:-Outputs +// Remember to uncomment the node by removing "/-"! +output "eDP-1" { + mode "1920x1080@60" + transform "normal" + position x=1280 y=0 +} + +// Settings that influence how windows are positioned and sized. +// Find more information on the wiki: +// https://yalter.github.io/niri/Configuration:-Layout +layout { + // Set gaps around windows in logical pixels. + gaps 10 + + // When to center a column when changing focus, options are: + // - "never", default behavior, focusing an off-screen column will keep at the left + // or right edge of the screen. + // - "always", the focused column will always be centered. + // - "on-overflow", focusing a column will center it if it doesn't fit + // together with the previously focused column. + center-focused-column "never" + + // You can customize the widths that "switch-preset-column-width" (Mod+R) toggles between. + preset-column-widths { + // Proportion sets the width as a fraction of the output width, taking gaps into account. + // For example, you can perfectly fit four windows sized "proportion 0.25" on an output. + // The default preset widths are 1/3, 1/2 and 2/3 of the output. + proportion 0.33333 + proportion 0.5 + proportion 0.66667 + + // Fixed sets the width in logical pixels exactly. + // fixed 1920 + } + + // You can also customize the heights that "switch-preset-window-height" (Mod+Shift+R) toggles between. + // preset-window-heights { } + + // You can change the default width of the new windows. + default-column-width { proportion 0.5; } + // If you leave the brackets empty, the windows themselves will decide their initial width. + // default-column-width {} + + // By default focus ring and border are rendered as a solid background rectangle + // behind windows. That is, they will show up through semitransparent windows. + // This is because windows using client-side decorations can have an arbitrary shape. + // + // If you don't like that, you should uncomment `prefer-no-csd` below. + // Niri will draw focus ring and border *around* windows that agree to omit their + // client-side decorations. + // + // Alternatively, you can override it with a window rule called + // `draw-border-with-background`. + + // You can change how the focus ring looks. + focus-ring { + // Uncomment this line to disable the focus ring. + // off + + // How many logical pixels the ring extends out from the windows. + width 2 + + // Colors can be set in a variety of ways: + // - CSS named colors: "red" + // - RGB hex: "#rgb", "#rgba", "#rrggbb", "#rrggbbaa" + // - CSS-like notation: "rgb(255, 127, 0)", rgba(), hsl() and a few others. + + // Color of the ring on the active monitor. + active-color "#7fc8ff" + + // Color of the ring on inactive monitors. + // + // The focus ring only draws around the active window, so the only place + // where you can see its inactive-color is on other monitors. + inactive-color "#505050" + + // You can also use gradients. They take precedence over solid colors. + // Gradients are rendered the same as CSS linear-gradient(angle, from, to). + // The angle is the same as in linear-gradient, and is optional, + // defaulting to 180 (top-to-bottom gradient). + // You can use any CSS linear-gradient tool on the web to set these up. + // Changing the color space is also supported, check the wiki for more info. + // + // active-gradient from="#80c8ff" to="#c7ff7f" angle=45 + + // You can also color the gradient relative to the entire view + // of the workspace, rather than relative to just the window itself. + // To do that, set relative-to="workspace-view". + // + // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" + } + + // You can also add a border. It's similar to the focus ring, but always visible. + border { + // The settings are the same as for the focus ring. + // If you enable the border, you probably want to disable the focus ring. + off + + width 2 + active-color "#ffc87f" + inactive-color "#505050" + + // Color of the border around windows that request your attention. + urgent-color "#9b0000" + + // Gradients can use a few different interpolation color spaces. + // For example, this is a pastel rainbow gradient via in="oklch longer hue". + // + // active-gradient from="#e5989b" to="#ffb4a2" angle=45 relative-to="workspace-view" in="oklch longer hue" + + // inactive-gradient from="#505050" to="#808080" angle=45 relative-to="workspace-view" + } + + // You can enable drop shadows for windows. + shadow { + // Uncomment the next line to enable shadows. + // on + + // By default, the shadow draws only around its window, and not behind it. + // Uncomment this setting to make the shadow draw behind its window. + // + // Note that niri has no way of knowing about the CSD window corner + // radius. It has to assume that windows have square corners, leading to + // shadow artifacts inside the CSD rounded corners. This setting fixes + // those artifacts. + // + // However, instead you may want to set prefer-no-csd and/or + // geometry-corner-radius. Then, niri will know the corner radius and + // draw the shadow correctly, without having to draw it behind the + // window. These will also remove client-side shadows if the window + // draws any. + // + // draw-behind-window true + + // You can change how shadows look. The values below are in logical + // pixels and match the CSS box-shadow properties. + + // Softness controls the shadow blur radius. + softness 30 + + // Spread expands the shadow. + spread 5 + + // Offset moves the shadow relative to the window. + offset x=0 y=5 + + // You can also change the shadow color and opacity. + color "#0007" + } + + // Struts shrink the area occupied by windows, similarly to layer-shell panels. + // You can think of them as a kind of outer gaps. They are set in logical pixels. + // Left and right struts will cause the next window to the side to always be visible. + // Top and bottom struts will simply add outer gaps in addition to the area occupied by + // layer-shell panels and regular gaps. + struts { + // left 64 + // right 64 + // top 64 + // bottom 64 + } +} + +// Add lines like this to spawn processes at startup. +// Note that running niri as a session supports xdg-desktop-autostart, +// which may be more convenient to use. +// See the binds section below for more spawn examples. + +spawn-sh-at-startup "swaybg -i /mnt/DISK2/_ISO/wallpapers/2.jpg" +spawn-sh-at-startup "qs -c ~/.config/quickshell/noctalia-shell/" + +hotkey-overlay { + // Uncomment this line to disable the "Important Hotkeys" pop-up at startup. + skip-at-startup +} + +// Uncomment this line to ask the clients to omit their client-side decorations if possible. +// If the client will specifically ask for CSD, the request will be honored. +// Additionally, clients will be informed that they are tiled, removing some client-side rounded corners. +// This option will also fix border/focus ring drawing behind some semitransparent windows. +// After enabling or disabling this, you need to restart the apps for this to take effect. +// prefer-no-csd + +// You can change the path where screenshots are saved. +// A ~ at the front will be expanded to the home directory. +// The path is formatted with strftime(3) to give you the screenshot date and time. +screenshot-path "~/Pictures/Screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png" + +// You can also set this to null to disable saving screenshots to disk. +// screenshot-path null + +// Animation settings. +// The wiki explains how to configure individual animations: +// https://yalter.github.io/niri/Configuration:-Animations +animations { + // Uncomment to turn off all animations. + // off + + // Slow down all animations by this factor. Values below 1 speed them up instead. + // slowdown 3.0 +} + +// Window rules let you adjust behavior for individual windows. +// Find more information on the wiki: +// https://yalter.github.io/niri/Configuration:-Window-Rules + +// Work around WezTerm's initial configure bug +// by setting an empty default-column-width. +window-rule { + // This regular expression is intentionally made as specific as possible, + // since this is the default config, and we want no false positives. + // You can get away with just app-id="wezterm" if you want. + match app-id=r#"^org\.wezfurlong\.wezterm$"# + default-column-width {} +} + +// Open the Firefox picture-in-picture player as floating by default. +window-rule { + // This app-id regular expression will work for both: + // - host Firefox (app-id is "firefox") + // - Flatpak Firefox (app-id is "org.mozilla.firefox") + match app-id=r#"firefox$"# title="^Picture-in-Picture$" + open-floating true +} + +// Example: block out two password managers from screen capture. +// (This example rule is commented out with a "/-" in front.) +/-window-rule { + match app-id=r#"^org\.keepassxc\.KeePassXC$"# + match app-id=r#"^org\.gnome\.World\.Secrets$"# + + block-out-from "screen-capture" + + // Use this instead if you want them visible on third-party screenshot tools. + // block-out-from "screencast" +} + +// Example: enable rounded corners for all windows. +// (This example rule is commented out with a "/-" in front.) +/-window-rule { + geometry-corner-radius 12 + clip-to-geometry true +} + +binds { + Mod+Shift+Slash { show-hotkey-overlay; } + + // Suggested binds for running programs: terminal, app launcher, screen locker. + Mod+T hotkey-overlay-title="Open a Terminal: alacritty" { spawn "alacritty"; } + Mod+D hotkey-overlay-title="Run an Application: fuzzel" { spawn "fuzzel"; } + Super+Alt+L hotkey-overlay-title="Lock the Screen: swaylock" { spawn "swaylock"; } + + // Use spawn-sh to run a shell command. Do this if you need pipes, multiple commands, etc. + // Note: the entire command goes as a single argument. It's passed verbatim to `sh -c`. + Super+Alt+S allow-when-locked=true hotkey-overlay-title=null { spawn-sh "pkill orca || exec orca"; } + + // Example volume keys mappings for PipeWire & WirePlumber. + // The allow-when-locked=true property makes them work even when the session is locked. + // Using spawn-sh allows to pass multiple arguments together with the command. + // "-l 1.0" limits the volume to 100%. + XF86AudioRaiseVolume allow-when-locked=true { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.05+ -l 1.0"; } + XF86AudioLowerVolume allow-when-locked=true { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.05-"; } + XF86AudioMute allow-when-locked=true { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"; } + XF86AudioMicMute allow-when-locked=true { spawn-sh "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"; } + + // Example media keys mapping using playerctl. + // This will work with any MPRIS-enabled media player. + XF86AudioPlay allow-when-locked=true { spawn-sh "playerctl play-pause"; } + XF86AudioStop allow-when-locked=true { spawn-sh "playerctl stop"; } + XF86AudioPrev allow-when-locked=true { spawn-sh "playerctl previous"; } + XF86AudioNext allow-when-locked=true { spawn-sh "playerctl next"; } + + // Example brightness key mappings for brightnessctl. + // You can use regular spawn with multiple arguments too (to avoid going through "sh"), + // but you need to manually put each argument in separate "" quotes. + XF86MonBrightnessUp allow-when-locked=true { spawn "brightnessctl" "--class=backlight" "set" "+10%"; } + XF86MonBrightnessDown allow-when-locked=true { spawn "brightnessctl" "--class=backlight" "set" "10%-"; } + + // Open/close the Overview: a zoomed-out view of workspaces and windows. + // You can also move the mouse into the top-left hot corner, + // or do a four-finger swipe up on a touchpad. + Mod+O repeat=false { toggle-overview; } + + Mod+Q repeat=false { close-window; } + + Mod+Left { focus-column-left; } + Mod+Down { focus-window-down; } + Mod+Up { focus-window-up; } + Mod+Right { focus-column-right; } + Mod+H { focus-column-left; } + Mod+J { focus-window-down; } + Mod+K { focus-window-up; } + Mod+L { focus-column-right; } + + Mod+Ctrl+Left { move-column-left; } + Mod+Ctrl+Down { move-window-down; } + Mod+Ctrl+Up { move-window-up; } + Mod+Ctrl+Right { move-column-right; } + Mod+Ctrl+H { move-column-left; } + Mod+Ctrl+J { move-window-down; } + Mod+Ctrl+K { move-window-up; } + Mod+Ctrl+L { move-column-right; } + + // Alternative commands that move across workspaces when reaching + // the first or last window in a column. + // Mod+J { focus-window-or-workspace-down; } + // Mod+K { focus-window-or-workspace-up; } + // Mod+Ctrl+J { move-window-down-or-to-workspace-down; } + // Mod+Ctrl+K { move-window-up-or-to-workspace-up; } + + Mod+Home { focus-column-first; } + Mod+End { focus-column-last; } + Mod+Ctrl+Home { move-column-to-first; } + Mod+Ctrl+End { move-column-to-last; } + + Mod+Shift+Left { focus-monitor-left; } + Mod+Shift+Down { focus-monitor-down; } + Mod+Shift+Up { focus-monitor-up; } + Mod+Shift+Right { focus-monitor-right; } + Mod+Shift+H { focus-monitor-left; } + Mod+Shift+J { focus-monitor-down; } + Mod+Shift+K { focus-monitor-up; } + Mod+Shift+L { focus-monitor-right; } + + Mod+Shift+Ctrl+Left { move-column-to-monitor-left; } + Mod+Shift+Ctrl+Down { move-column-to-monitor-down; } + Mod+Shift+Ctrl+Up { move-column-to-monitor-up; } + Mod+Shift+Ctrl+Right { move-column-to-monitor-right; } + Mod+Shift+Ctrl+H { move-column-to-monitor-left; } + Mod+Shift+Ctrl+J { move-column-to-monitor-down; } + Mod+Shift+Ctrl+K { move-column-to-monitor-up; } + Mod+Shift+Ctrl+L { move-column-to-monitor-right; } + + // Alternatively, there are commands to move just a single window: + // Mod+Shift+Ctrl+Left { move-window-to-monitor-left; } + // ... + + // And you can also move a whole workspace to another monitor: + // Mod+Shift+Ctrl+Left { move-workspace-to-monitor-left; } + // ... + + Mod+Page_Down { focus-workspace-down; } + Mod+Page_Up { focus-workspace-up; } + Mod+U { focus-workspace-down; } + Mod+I { focus-workspace-up; } + Mod+Ctrl+Page_Down { move-column-to-workspace-down; } + Mod+Ctrl+Page_Up { move-column-to-workspace-up; } + Mod+Ctrl+U { move-column-to-workspace-down; } + Mod+Ctrl+I { move-column-to-workspace-up; } + + // Alternatively, there are commands to move just a single window: + // Mod+Ctrl+Page_Down { move-window-to-workspace-down; } + // ... + + Mod+Shift+Page_Down { move-workspace-down; } + Mod+Shift+Page_Up { move-workspace-up; } + Mod+Shift+U { move-workspace-down; } + Mod+Shift+I { move-workspace-up; } + + // You can bind mouse wheel scroll ticks using the following syntax. + // These binds will change direction based on the natural-scroll setting. + // + // To avoid scrolling through workspaces really fast, you can use + // the cooldown-ms property. The bind will be rate-limited to this value. + // You can set a cooldown on any bind, but it's most useful for the wheel. + Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; } + Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; } + Mod+Ctrl+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; } + Mod+Ctrl+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; } + + Mod+WheelScrollRight { focus-column-right; } + Mod+WheelScrollLeft { focus-column-left; } + Mod+Ctrl+WheelScrollRight { move-column-right; } + Mod+Ctrl+WheelScrollLeft { move-column-left; } + + // Usually scrolling up and down with Shift in applications results in + // horizontal scrolling; these binds replicate that. + Mod+Shift+WheelScrollDown { focus-column-right; } + Mod+Shift+WheelScrollUp { focus-column-left; } + Mod+Ctrl+Shift+WheelScrollDown { move-column-right; } + Mod+Ctrl+Shift+WheelScrollUp { move-column-left; } + + // Similarly, you can bind touchpad scroll "ticks". + // Touchpad scrolling is continuous, so for these binds it is split into + // discrete intervals. + // These binds are also affected by touchpad's natural-scroll, so these + // example binds are "inverted", since we have natural-scroll enabled for + // touchpads by default. + // Mod+TouchpadScrollDown { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.02+"; } + // Mod+TouchpadScrollUp { spawn-sh "wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.02-"; } + + // You can refer to workspaces by index. However, keep in mind that + // niri is a dynamic workspace system, so these commands are kind of + // "best effort". Trying to refer to a workspace index bigger than + // the current workspace count will instead refer to the bottommost + // (empty) workspace. + // + // For example, with 2 workspaces + 1 empty, indices 3, 4, 5 and so on + // will all refer to the 3rd workspace. + Mod+1 { focus-workspace 1; } + Mod+2 { focus-workspace 2; } + Mod+3 { focus-workspace 3; } + Mod+4 { focus-workspace 4; } + Mod+5 { focus-workspace 5; } + Mod+6 { focus-workspace 6; } + Mod+7 { focus-workspace 7; } + Mod+8 { focus-workspace 8; } + Mod+9 { focus-workspace 9; } + Mod+Shift+1 { move-column-to-workspace 1; } + Mod+Shift+2 { move-column-to-workspace 2; } + Mod+Shift+3 { move-column-to-workspace 3; } + Mod+Shift+4 { move-column-to-workspace 4; } + Mod+Shift+5 { move-column-to-workspace 5; } + Mod+Shift+6 { move-column-to-workspace 6; } + Mod+Shift+7 { move-column-to-workspace 7; } + Mod+Shift+8 { move-column-to-workspace 8; } + Mod+Shift+9 { move-column-to-workspace 9; } + + // Alternatively, there are commands to move just a single window: + // Mod+Ctrl+1 { move-window-to-workspace 1; } + + // Switches focus between the current and the previous workspace. + // Mod+Tab { focus-workspace-previous; } + + // The following binds move the focused window in and out of a column. + // If the window is alone, they will consume it into the nearby column to the side. + // If the window is already in a column, they will expel it out. + Mod+BracketLeft { consume-or-expel-window-left; } + Mod+BracketRight { consume-or-expel-window-right; } + + // Consume one window from the right to the bottom of the focused column. + Mod+Comma { consume-window-into-column; } + // Expel the bottom window from the focused column to the right. + Mod+Period { expel-window-from-column; } + + Mod+R { switch-preset-column-width; } + // Cycling through the presets in reverse order is also possible. + // Mod+R { switch-preset-column-width-back; } + Mod+Shift+R { switch-preset-window-height; } + Mod+Ctrl+R { reset-window-height; } + Mod+F { maximize-column; } + Mod+Shift+F { fullscreen-window; } + + // Expand the focused column to space not taken up by other fully visible columns. + // Makes the column "fill the rest of the space". + Mod+Ctrl+F { expand-column-to-available-width; } + + Mod+C { center-column; } + + // Center all fully visible columns on screen. + Mod+Ctrl+C { center-visible-columns; } + + // Finer width adjustments. + // This command can also: + // * set width in pixels: "1000" + // * adjust width in pixels: "-5" or "+5" + // * set width as a percentage of screen width: "25%" + // * adjust width as a percentage of screen width: "-10%" or "+10%" + // Pixel sizes use logical, or scaled, pixels. I.e. on an output with scale 2.0, + // set-column-width "100" will make the column occupy 200 physical screen pixels. + Mod+Minus { set-column-width "-10%"; } + Mod+Equal { set-column-width "+10%"; } + + // Finer height adjustments when in column with other windows. + Mod+Shift+Minus { set-window-height "-10%"; } + Mod+Shift+Equal { set-window-height "+10%"; } + + // Move the focused window between the floating and the tiling layout. + Mod+V { toggle-window-floating; } + Mod+Shift+V { switch-focus-between-floating-and-tiling; } + + // Toggle tabbed column display mode. + // Windows in this column will appear as vertical tabs, + // rather than stacked on top of each other. + Mod+W { toggle-column-tabbed-display; } + + // Actions to switch layouts. + // Note: if you uncomment these, make sure you do NOT have + // a matching layout switch hotkey configured in xkb options above. + // Having both at once on the same hotkey will break the switching, + // since it will switch twice upon pressing the hotkey (once by xkb, once by niri). + // Mod+Space { switch-layout "next"; } + // Mod+Shift+Space { switch-layout "prev"; } + + Print { screenshot; } + Ctrl+Print { screenshot-screen; } + Alt+Print { screenshot-window; } + + // Applications such as remote-desktop clients and software KVM switches may + // request that niri stops processing the keyboard shortcuts defined here + // so they may, for example, forward the key presses as-is to a remote machine. + // It's a good idea to bind an escape hatch to toggle the inhibitor, + // so a buggy application can't hold your session hostage. + // + // The allow-inhibiting=false property can be applied to other binds as well, + // which ensures niri always processes them, even when an inhibitor is active. + Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; } + + // The quit action will show a confirmation dialog to avoid accidental exits. + Mod+Shift+X { spawn "qs" "-c" "noctalia-shell" "ipc" "call" "sessionMenu" "toggle"; } + // Mod+Shift+E { quit; } + Ctrl+Alt+Delete { quit; } + + // Powers off the monitors. To turn them back on, do any input like + // moving the mouse or pressing any other key. + Mod+Shift+P { power-off-monitors; } +} + +include "./noctalia.kdl" + diff --git a/fedora/.config/niri/noctalia.kdl b/fedora/.config/niri/noctalia.kdl new file mode 100644 index 0000000..bb31684 --- /dev/null +++ b/fedora/.config/niri/noctalia.kdl @@ -0,0 +1,35 @@ +layout { + + focus-ring { + active-color "#8aceff" + inactive-color "#111416" + urgent-color "#ffb4ab" + } + + border { + active-color "#8aceff" + inactive-color "#111416" + urgent-color "#ffb4ab" + } + + shadow { + color "#00000070" + } + + tab-indicator { + active-color "#8aceff" + inactive-color "#004c6e" + urgent-color "#ffb4ab" + } + + insert-hint { + color "#8aceff80" + } +} + +recent-windows { + highlight { + active-color "#8aceff" + urgent-color "#ffb4ab" + } +} diff --git a/fedora/.config/noctalia/colors.json b/fedora/.config/noctalia/colors.json new file mode 100644 index 0000000..08a4913 --- /dev/null +++ b/fedora/.config/noctalia/colors.json @@ -0,0 +1,25 @@ +{ + "mPrimary": "#8aceff", + "mOnPrimary": "#00344d", + + "mSecondary": "#b7c9d9", + "mOnSecondary": "#22323f", + + "mTertiary": "#cec0e8", + "mOnTertiary": "#352b4b", + + "mError": "#ffb4ab", + "mOnError": "#690005", + + "mSurface": "#111416", + "mOnSurface": "#e2e2e5", + + "mSurfaceVariant": "#1e2022", + "mOnSurfaceVariant": "#c1c7ce", + + "mOutline": "#41474d", + "mShadow": "#000000", + + "mHover": "#cec0e8", + "mOnHover": "#352b4b" +} \ No newline at end of file diff --git a/fedora/.config/noctalia/plugins.json b/fedora/.config/noctalia/plugins.json new file mode 100644 index 0000000..04b0a22 --- /dev/null +++ b/fedora/.config/noctalia/plugins.json @@ -0,0 +1,12 @@ +{ + "sources": [ + { + "enabled": true, + "name": "Noctalia Plugins", + "url": "https://github.com/noctalia-dev/noctalia-plugins" + } + ], + "states": { + }, + "version": 2 +} diff --git a/fedora/.config/noctalia/settings.json b/fedora/.config/noctalia/settings.json new file mode 100644 index 0000000..603cb38 --- /dev/null +++ b/fedora/.config/noctalia/settings.json @@ -0,0 +1,710 @@ +{ + "appLauncher": { + "autoPasteClipboard": false, + "clipboardWatchImageCommand": "wl-paste --type image --watch cliphist store", + "clipboardWatchTextCommand": "wl-paste --type text --watch cliphist store", + "clipboardWrapText": true, + "customLaunchPrefix": "", + "customLaunchPrefixEnabled": false, + "density": "default", + "enableClipPreview": true, + "enableClipboardChips": true, + "enableClipboardHistory": false, + "enableClipboardSmartIcons": true, + "enableSessionSearch": true, + "enableSettingsSearch": true, + "enableWindowsSearch": true, + "iconMode": "tabler", + "ignoreMouseInput": false, + "overviewLayer": false, + "pinnedApps": [ + ], + "position": "center", + "screenshotAnnotationTool": "", + "showCategories": true, + "showIconBackground": false, + "sortByMostUsed": true, + "terminalCommand": "alacritty -e", + "viewMode": "list" + }, + "audio": { + "mprisBlacklist": [ + ], + "preferredPlayer": "", + "spectrumFrameRate": 30, + "spectrumMirrored": true, + "visualizerType": "linear", + "volumeFeedback": false, + "volumeFeedbackSoundFile": "", + "volumeOverdrive": false, + "volumeStep": 5 + }, + "bar": { + "autoHideDelay": 500, + "autoShowDelay": 150, + "backgroundOpacity": 0.93, + "barType": "simple", + "capsuleColorKey": "none", + "capsuleOpacity": 1, + "contentPadding": 2, + "density": "compact", + "displayMode": "always_visible", + "enableExclusionZoneInset": true, + "fontScale": 1, + "frameRadius": 12, + "frameThickness": 8, + "hideOnOverview": false, + "marginHorizontal": 4, + "marginVertical": 4, + "middleClickAction": "none", + "middleClickCommand": "", + "middleClickFollowMouse": false, + "monitors": [ + ], + "mouseWheelAction": "none", + "mouseWheelWrap": true, + "outerCorners": true, + "position": "top", + "reverseScroll": false, + "rightClickAction": "controlCenter", + "rightClickCommand": "", + "rightClickFollowMouse": true, + "screenOverrides": [ + ], + "showCapsule": true, + "showOnWorkspaceSwitch": true, + "showOutline": false, + "useSeparateOpacity": false, + "widgetSpacing": 6, + "widgets": { + "center": [ + { + "characterCount": 2, + "colorizeIcons": false, + "emptyColor": "secondary", + "enableScrollWheel": true, + "focusedColor": "primary", + "followFocusedScreen": false, + "fontWeight": "bold", + "groupedBorderOpacity": 1, + "hideUnoccupied": false, + "iconScale": 0.8, + "id": "Workspace", + "labelMode": "index", + "occupiedColor": "secondary", + "pillSize": 0.6, + "showApplications": false, + "showApplicationsHover": false, + "showBadge": true, + "showLabelsOnlyWhenOccupied": true, + "unfocusedIconsOpacity": 1 + } + ], + "left": [ + { + "colorizeSystemIcon": "none", + "customIconPath": "", + "enableColorization": false, + "icon": "rocket", + "iconColor": "none", + "id": "Launcher", + "useDistroLogo": false + }, + { + "clockColor": "none", + "customFont": "", + "formatHorizontal": "HH:mm ddd, MMM dd", + "formatVertical": "HH mm - dd MM", + "id": "Clock", + "tooltipFormat": "HH:mm ddd, MMM dd", + "useCustomFont": false + }, + { + "compactMode": true, + "diskPath": "/", + "iconColor": "none", + "id": "SystemMonitor", + "showCpuCores": false, + "showCpuFreq": false, + "showCpuTemp": true, + "showCpuUsage": true, + "showDiskAvailable": false, + "showDiskUsage": false, + "showDiskUsageAsPercent": false, + "showGpuTemp": false, + "showLoadAverage": false, + "showMemoryAsPercent": false, + "showMemoryUsage": true, + "showNetworkStats": false, + "showSwapUsage": false, + "textColor": "none", + "useMonospaceFont": true, + "usePadding": false + }, + { + "colorizeIcons": false, + "hideMode": "hidden", + "id": "ActiveWindow", + "maxWidth": 145, + "scrollingMode": "hover", + "showIcon": true, + "showText": true, + "textColor": "none", + "useFixedWidth": false + }, + { + "compactMode": false, + "hideMode": "hidden", + "hideWhenIdle": false, + "id": "MediaMini", + "maxWidth": 145, + "panelShowAlbumArt": true, + "scrollingMode": "hover", + "showAlbumArt": true, + "showArtistFirst": true, + "showProgressRing": true, + "showVisualizer": false, + "textColor": "none", + "useFixedWidth": false, + "visualizerType": "linear" + } + ], + "right": [ + { + "blacklist": [ + ], + "chevronColor": "none", + "colorizeIcons": false, + "drawerEnabled": true, + "hidePassive": false, + "id": "Tray", + "pinned": [ + ] + }, + { + "hideWhenZero": false, + "hideWhenZeroUnread": false, + "iconColor": "none", + "id": "NotificationHistory", + "showUnreadBadge": true, + "unreadBadgeColor": "primary" + }, + { + "displayMode": "alwaysShow", + "iconColor": "none", + "id": "Network", + "textColor": "none" + }, + { + "deviceNativePath": "__default__", + "displayMode": "icon-always", + "hideIfIdle": false, + "hideIfNotDetected": true, + "id": "Battery", + "showNoctaliaPerformance": false, + "showPowerProfiles": false + }, + { + "displayMode": "onhover", + "iconColor": "none", + "id": "Volume", + "middleClickCommand": "pwvucontrol || pavucontrol", + "textColor": "none" + }, + { + "applyToAllMonitors": false, + "displayMode": "onhover", + "iconColor": "none", + "id": "Brightness", + "textColor": "none" + }, + { + "colorizeDistroLogo": false, + "colorizeSystemIcon": "none", + "customIconPath": "", + "enableColorization": false, + "icon": "noctalia", + "id": "ControlCenter", + "useDistroLogo": false + } + ] + } + }, + "brightness": { + "backlightDeviceMappings": [ + ], + "brightnessStep": 5, + "enableDdcSupport": false, + "enforceMinimum": true + }, + "calendar": { + "cards": [ + { + "enabled": true, + "id": "calendar-header-card" + }, + { + "enabled": true, + "id": "calendar-month-card" + }, + { + "enabled": true, + "id": "weather-card" + } + ] + }, + "colorSchemes": { + "darkMode": true, + "generationMethod": "tonal-spot", + "manualSunrise": "06:30", + "manualSunset": "18:30", + "monitorForColors": "", + "predefinedScheme": "Noctalia (default)", + "schedulingMode": "off", + "syncGsettings": true, + "useWallpaperColors": true + }, + "controlCenter": { + "cards": [ + { + "enabled": true, + "id": "profile-card" + }, + { + "enabled": true, + "id": "shortcuts-card" + }, + { + "enabled": true, + "id": "audio-card" + }, + { + "enabled": false, + "id": "brightness-card" + }, + { + "enabled": true, + "id": "weather-card" + }, + { + "enabled": true, + "id": "media-sysmon-card" + } + ], + "diskPath": "/", + "position": "close_to_bar_button", + "shortcuts": { + "left": [ + { + "id": "Network" + }, + { + "id": "Bluetooth" + }, + { + "id": "WallpaperSelector" + }, + { + "id": "NoctaliaPerformance" + } + ], + "right": [ + { + "id": "Notifications" + }, + { + "id": "PowerProfile" + }, + { + "id": "KeepAwake" + }, + { + "id": "NightLight" + } + ] + } + }, + "desktopWidgets": { + "enabled": false, + "gridSnap": false, + "gridSnapScale": false, + "monitorWidgets": [ + ], + "overviewEnabled": true + }, + "dock": { + "animationSpeed": 1, + "backgroundOpacity": 1, + "colorizeIcons": false, + "deadOpacity": 0.6, + "displayMode": "auto_hide", + "dockType": "floating", + "enabled": false, + "floatingRatio": 1, + "groupApps": false, + "groupClickAction": "cycle", + "groupContextMenuMode": "extended", + "groupIndicatorStyle": "dots", + "inactiveIndicators": false, + "indicatorColor": "primary", + "indicatorOpacity": 0.6, + "indicatorThickness": 3, + "launcherIcon": "", + "launcherIconColor": "none", + "launcherPosition": "end", + "launcherUseDistroLogo": false, + "monitors": [ + ], + "onlySameOutput": true, + "pinnedApps": [ + ], + "pinnedStatic": false, + "position": "bottom", + "showDockIndicator": false, + "showLauncherIcon": false, + "sitOnFrame": false, + "size": 1 + }, + "general": { + "allowPanelsOnScreenWithoutBar": true, + "allowPasswordWithFprintd": false, + "animationDisabled": false, + "animationSpeed": 1, + "autoStartAuth": false, + "avatarImage": "/home/yuri/.face", + "boxRadiusRatio": 1, + "clockFormat": "hh\\nmm", + "clockStyle": "custom", + "compactLockScreen": false, + "dimmerOpacity": 0.2, + "enableBlurBehind": true, + "enableLockScreenCountdown": true, + "enableLockScreenMediaControls": false, + "enableShadows": false, + "forceBlackScreenCorners": false, + "iRadiusRatio": 1, + "keybinds": { + "keyDown": [ + "Down" + ], + "keyEnter": [ + "Return", + "Enter" + ], + "keyEscape": [ + "Esc" + ], + "keyLeft": [ + "Left" + ], + "keyRemove": [ + "Del" + ], + "keyRight": [ + "Right" + ], + "keyUp": [ + "Up" + ] + }, + "language": "", + "lockOnSuspend": true, + "lockScreenAnimations": false, + "lockScreenBlur": 0, + "lockScreenCountdownDuration": 10000, + "lockScreenMonitors": [ + ], + "lockScreenTint": 0, + "passwordChars": false, + "radiusRatio": 1, + "reverseScroll": false, + "scaleRatio": 1, + "screenRadiusRatio": 1, + "shadowDirection": "bottom_right", + "shadowOffsetX": 2, + "shadowOffsetY": 3, + "showChangelogOnStartup": true, + "showHibernateOnLockScreen": false, + "showScreenCorners": false, + "showSessionButtonsOnLockScreen": true, + "smoothScrollEnabled": true, + "telemetryEnabled": false + }, + "hooks": { + "colorGeneration": "", + "darkModeChange": "", + "enabled": false, + "performanceModeDisabled": "", + "performanceModeEnabled": "", + "screenLock": "", + "screenUnlock": "", + "session": "", + "startup": "", + "wallpaperChange": "" + }, + "idle": { + "customCommands": "[]", + "enabled": false, + "fadeDuration": 5, + "lockCommand": "", + "lockTimeout": 660, + "resumeLockCommand": "", + "resumeScreenOffCommand": "", + "resumeSuspendCommand": "", + "screenOffCommand": "", + "screenOffTimeout": 600, + "suspendCommand": "", + "suspendTimeout": 1800 + }, + "location": { + "analogClockInCalendar": false, + "autoLocate": true, + "firstDayOfWeek": -1, + "hideWeatherCityName": false, + "hideWeatherTimezone": false, + "name": "Nieuw-Vennep", + "showCalendarEvents": true, + "showCalendarWeather": true, + "showWeekNumberInCalendar": false, + "use12hourFormat": false, + "useFahrenheit": false, + "weatherEnabled": true, + "weatherShowEffects": true, + "weatherTaliaMascotAlways": false + }, + "network": { + "bluetoothAutoConnect": true, + "bluetoothDetailsViewMode": "grid", + "bluetoothHideUnnamedDevices": false, + "bluetoothRssiPollIntervalMs": 60000, + "bluetoothRssiPollingEnabled": false, + "disableDiscoverability": false, + "networkPanelView": "wifi", + "wifiDetailsViewMode": "grid" + }, + "nightLight": { + "autoSchedule": true, + "dayTemp": "6500", + "enabled": false, + "forced": false, + "manualSunrise": "06:30", + "manualSunset": "18:30", + "nightTemp": "4000" + }, + "noctaliaPerformance": { + "disableDesktopWidgets": true, + "disableWallpaper": true + }, + "notifications": { + "backgroundOpacity": 1, + "clearDismissed": true, + "criticalUrgencyDuration": 15, + "density": "default", + "enableBatteryToast": true, + "enableKeyboardLayoutToast": true, + "enableMarkdown": false, + "enableMediaToast": false, + "enabled": true, + "location": "top_right", + "lowUrgencyDuration": 3, + "monitors": [ + ], + "normalUrgencyDuration": 8, + "overlayLayer": true, + "respectExpireTimeout": false, + "saveToHistory": { + "critical": true, + "low": true, + "normal": true + }, + "sounds": { + "criticalSoundFile": "", + "enabled": false, + "excludedApps": "discord,firefox,chrome,chromium,edge", + "lowSoundFile": "", + "normalSoundFile": "", + "separateSounds": false, + "volume": 0.5 + } + }, + "osd": { + "autoHideMs": 2000, + "backgroundOpacity": 1, + "enabled": true, + "enabledTypes": [ + 0, + 1, + 2 + ], + "location": "top_right", + "monitors": [ + ], + "overlayLayer": true + }, + "plugins": { + "autoUpdate": false, + "notifyUpdates": true + }, + "sessionMenu": { + "countdownDuration": 10000, + "enableCountdown": true, + "largeButtonsLayout": "single-row", + "largeButtonsStyle": true, + "position": "center", + "powerOptions": [ + { + "action": "lock", + "enabled": true, + "keybind": "1" + }, + { + "action": "suspend", + "enabled": true, + "keybind": "2" + }, + { + "action": "hibernate", + "enabled": true, + "keybind": "3" + }, + { + "action": "reboot", + "enabled": true, + "keybind": "4" + }, + { + "action": "logout", + "enabled": true, + "keybind": "5" + }, + { + "action": "shutdown", + "enabled": true, + "keybind": "6" + }, + { + "action": "rebootToUefi", + "enabled": true, + "keybind": "7" + } + ], + "showHeader": true, + "showKeybinds": true + }, + "settingsVersion": 59, + "systemMonitor": { + "batteryCriticalThreshold": 5, + "batteryWarningThreshold": 20, + "cpuCriticalThreshold": 90, + "cpuWarningThreshold": 80, + "criticalColor": "", + "diskAvailCriticalThreshold": 10, + "diskAvailWarningThreshold": 20, + "diskCriticalThreshold": 90, + "diskWarningThreshold": 80, + "enableDgpuMonitoring": false, + "externalMonitor": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "gpuCriticalThreshold": 90, + "gpuWarningThreshold": 80, + "memCriticalThreshold": 90, + "memWarningThreshold": 80, + "swapCriticalThreshold": 90, + "swapWarningThreshold": 80, + "tempCriticalThreshold": 90, + "tempWarningThreshold": 80, + "useCustomColors": false, + "warningColor": "" + }, + "templates": { + "activeTemplates": [ + { + "enabled": true, + "id": "btop" + }, + { + "enabled": true, + "id": "fuzzel" + }, + { + "enabled": true, + "id": "niri" + }, + { + "enabled": true, + "id": "gtk" + }, + { + "enabled": true, + "id": "code" + }, + { + "enabled": true, + "id": "qt" + } + ], + "enableUserTheming": false + }, + "ui": { + "boxBorderEnabled": false, + "fontDefault": "Sans Serif", + "fontDefaultScale": 1, + "fontFixed": "monospace", + "fontFixedScale": 1, + "panelBackgroundOpacity": 0.9, + "panelsAttachedToBar": true, + "scrollbarAlwaysVisible": true, + "settingsPanelMode": "attached", + "settingsPanelSideBarCardStyle": false, + "tooltipsEnabled": true, + "translucentWidgets": false + }, + "wallpaper": { + "automationEnabled": false, + "directory": "/mnt/DISK2/_ISO/wallpapers", + "enableMultiMonitorDirectories": false, + "enabled": true, + "favorites": [ + ], + "fillColor": "#000000", + "fillMode": "crop", + "hideWallpaperFilenames": false, + "linkLightAndDarkWallpapers": true, + "monitorDirectories": [ + ], + "overviewBlur": 0.4, + "overviewEnabled": true, + "overviewTint": 0.6, + "panelPosition": "follow_bar", + "randomIntervalSec": 300, + "setWallpaperOnAllMonitors": true, + "showHiddenFiles": false, + "skipStartupTransition": false, + "solidColor": "#1a1a2e", + "sortOrder": "name", + "transitionDuration": 1500, + "transitionEdgeSmoothness": 0.05, + "transitionType": [ + "fade", + "disc", + "stripes", + "wipe", + "pixelate", + "honeycomb" + ], + "useOriginalImages": false, + "useSolidColor": false, + "useWallhaven": false, + "viewMode": "single", + "wallhavenApiKey": "", + "wallhavenCategories": "111", + "wallhavenOrder": "desc", + "wallhavenPurity": "100", + "wallhavenQuery": "", + "wallhavenRatios": "", + "wallhavenResolutionHeight": "", + "wallhavenResolutionMode": "atleast", + "wallhavenResolutionWidth": "", + "wallhavenSorting": "relevance", + "wallpaperChangeMode": "random" + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/.gitignore b/fedora/.config/quickshell/noctalia-shell/.gitignore new file mode 100644 index 0000000..d500121 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/.gitignore @@ -0,0 +1,12 @@ +.qmlls.ini +.zed +.idea +.vscode +*.code-workspace +RELEASE.md +__pycache__ +result +build +/debian/ +.direnv/ +.envrc diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Ayu/Ayu.json b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Ayu/Ayu.json new file mode 100644 index 0000000..bf08032 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Ayu/Ayu.json @@ -0,0 +1,94 @@ +{ + "dark": { + "mPrimary": "#E6B450", + "mOnPrimary": "#0B0E14", + "mSecondary": "#AAD94C", + "mOnSecondary": "#0B0E14", + "mTertiary": "#39BAE6", + "mOnTertiary": "#0B0E14", + "mError": "#D95757", + "mOnError": "#0B0E14", + "mSurface": "#0B0E14", + "mOnSurface": "#D1D1C7", + "mSurfaceVariant": "#1e222a", + "mOnSurfaceVariant": "#8E959E", + "mOutline": "#565B66", + "mShadow": "#000000", + "mHover": "#39BAE6", + "mOnHover": "#0B0E14", + "terminal": { + "normal": { + "black": "#171b24", + "red": "#ed8274", + "green": "#87d96c", + "yellow": "#facc6e", + "blue": "#6dcbfa", + "magenta": "#dabafa", + "cyan": "#90e1c6", + "white": "#c7c7c7" + }, + "bright": { + "black": "#686868", + "red": "#f28779", + "green": "#d5ff80", + "yellow": "#ffd173", + "blue": "#73d0ff", + "magenta": "#dfbfff", + "cyan": "#95e6cb", + "white": "#ffffff" + }, + "foreground": "#D1D1C7", + "background": "#1f2430", + "selectionFg": "#1f2430", + "selectionBg": "#409fff", + "cursorText": "#1f2430", + "cursor": "#ffcc66" + } + }, + "light": { + "mPrimary": "#FF8F40", + "mOnPrimary": "#F8F9FA", + "mSecondary": "#86B300", + "mOnSecondary": "#F8F9FA", + "mTertiary": "#55B4D4", + "mOnTertiary": "#F8F9FA", + "mError": "#E65050", + "mOnError": "#F8F9FA", + "mSurface": "#F8F9FA", + "mOnSurface": "#42474C", + "mSurfaceVariant": "#E4E6E9", + "mOnSurfaceVariant": "#6E757C", + "mOutline": "#8A9199", + "mShadow": "#F8F9FA", + "mHover": "#55B4D4", + "mOnHover": "#F8F9FA", + "terminal": { + "normal": { + "black": "#000000", + "red": "#ea6c6d", + "green": "#6cbf43", + "yellow": "#eca944", + "blue": "#3199e1", + "magenta": "#9e75c7", + "cyan": "#46ba94", + "white": "#bababa" + }, + "bright": { + "black": "#686868", + "red": "#f07171", + "green": "#86b300", + "yellow": "#f2ae49", + "blue": "#399ee6", + "magenta": "#a37acc", + "cyan": "#4cbf99", + "white": "#d1d1d1" + }, + "foreground": "#42474C", + "background": "#f8f9fa", + "selectionFg": "#f8f9fa", + "selectionBg": "#035bd6", + "cursorText": "#f8f9fa", + "cursor": "#ffaa33" + } + } +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Catppuccin/Catppuccin.json b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Catppuccin/Catppuccin.json new file mode 100644 index 0000000..1d1f51f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Catppuccin/Catppuccin.json @@ -0,0 +1,94 @@ +{ + "dark": { + "mPrimary": "#cba6f7", + "mOnPrimary": "#11111b", + "mSecondary": "#fab387", + "mOnSecondary": "#11111b", + "mTertiary": "#94e2d5", + "mOnTertiary": "#11111b", + "mError": "#f38ba8", + "mOnError": "#11111b", + "mSurface": "#1e1e2e", + "mOnSurface": "#cdd6f4", + "mSurfaceVariant": "#313244", + "mOnSurfaceVariant": "#a3b4eb", + "mOutline": "#4c4f69", + "mShadow": "#11111b", + "mHover": "#94e2d5", + "mOnHover": "#11111b", + "terminal": { + "normal": { + "black": "#45475a", + "red": "#f38ba8", + "green": "#a6e3a1", + "yellow": "#f9e2af", + "blue": "#89b4fa", + "magenta": "#f5c2e7", + "cyan": "#94e2d5", + "white": "#a6adc8" + }, + "bright": { + "black": "#585b70", + "red": "#f37799", + "green": "#89d88b", + "yellow": "#ebd391", + "blue": "#74a8fc", + "magenta": "#f2aede", + "cyan": "#6bd7ca", + "white": "#bac2de" + }, + "foreground": "#cdd6f4", + "background": "#1e1e2e", + "selectionFg": "#cdd6f4", + "selectionBg": "#585b70", + "cursorText": "#1e1e2e", + "cursor": "#f5e0dc" + } + }, + "light": { + "mPrimary": "#8839ef", + "mOnPrimary": "#eff1f5", + "mSecondary": "#fe640b", + "mOnSecondary": "#eff1f5", + "mTertiary": "#40a02b", + "mOnTertiary": "#eff1f5", + "mError": "#d20f39", + "mOnError": "#dce0e8", + "mSurface": "#eff1f5", + "mOnSurface": "#4c4f69", + "mSurfaceVariant": "#ccd0da", + "mOnSurfaceVariant": "#6c6f85", + "mOutline": "#a5adcb", + "mShadow": "#dce0e8", + "mHover": "#40a02b", + "mOnHover": "#eff1f5", + "terminal": { + "normal": { + "black": "#51576d", + "red": "#e78284", + "green": "#a6d189", + "yellow": "#e5c890", + "blue": "#8caaee", + "magenta": "#f4b8e4", + "cyan": "#81c8be", + "white": "#a5adce" + }, + "bright": { + "black": "#626880", + "red": "#e67172", + "green": "#8ec772", + "yellow": "#d9ba73", + "blue": "#7b9ef0", + "magenta": "#f2a4db", + "cyan": "#5abfb5", + "white": "#b5bfe2" + }, + "foreground": "#c6d0f5", + "background": "#303446", + "selectionFg": "#c6d0f5", + "selectionBg": "#626880", + "cursorText": "#303446", + "cursor": "#f2d5cf" + } + } +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Dracula/Dracula.json b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Dracula/Dracula.json new file mode 100644 index 0000000..b4e5990 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Dracula/Dracula.json @@ -0,0 +1,94 @@ +{ + "dark": { + "mPrimary": "#bd93f9", + "mOnPrimary": "#282A36", + "mSecondary": "#ff79c6", + "mOnSecondary": "#4e1d32", + "mTertiary": "#8be9fd", + "mOnTertiary": "#003543", + "mError": "#FF5555", + "mOnError": "#282A36", + "mSurface": "#282A36", + "mOnSurface": "#F8F8F2", + "mSurfaceVariant": "#44475A", + "mOnSurfaceVariant": "#d6d8e0", + "mOutline": "#5a5e77", + "mShadow": "#282A36", + "mHover": "#8be9fd", + "mOnHover": "#003543", + "terminal": { + "normal": { + "black": "#21222c", + "red": "#ff5555", + "green": "#50fa7b", + "yellow": "#f1fa8c", + "blue": "#bd93f9", + "magenta": "#ff79c6", + "cyan": "#8be9fd", + "white": "#f8f8f2" + }, + "bright": { + "black": "#6272a4", + "red": "#ff6e6e", + "green": "#69ff94", + "yellow": "#ffffa5", + "blue": "#d6acff", + "magenta": "#ff92df", + "cyan": "#a4ffff", + "white": "#ffffff" + }, + "foreground": "#f8f8f2", + "background": "#282a36", + "selectionFg": "#ffffff", + "selectionBg": "#44475a", + "cursorText": "#282a36", + "cursor": "#f8f8f2" + } + }, + "light": { + "mPrimary": "#8332f4", + "mOnPrimary": "#ffffff", + "mSecondary": "#ff1399", + "mOnSecondary": "#ffffff", + "mTertiary": "#0398b9", + "mOnTertiary": "#ffffff", + "mError": "#FF5555", + "mOnError": "#282A36", + "mSurface": "#f8f8f2", + "mOnSurface": "#282a36", + "mSurfaceVariant": "#e6e6ea", + "mOnSurfaceVariant": "#44475a", + "mOutline": "#cacad3", + "mShadow": "#d6d8e0", + "mHover": "#0398b9", + "mOnHover": "#ffffff", + "terminal": { + "normal": { + "black": "#f8f8f2", + "red": "#ff5555", + "green": "#50fa7b", + "yellow": "#f1fa8c", + "blue": "#bd93f9", + "magenta": "#ff79c6", + "cyan": "#8be9fd", + "white": "#282a36" + }, + "bright": { + "black": "#6272a4", + "red": "#ff6e6e", + "green": "#69ff94", + "yellow": "#ffffa5", + "blue": "#d6acff", + "magenta": "#ff92df", + "cyan": "#a4ffff", + "white": "#000000" + }, + "foreground": "#282a36", + "background": "#ffffff", + "selectionFg": "#ffffff", + "selectionBg": "#6272a4", + "cursorText": "#ffffff", + "cursor": "#282a36" + } + } +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Eldritch/Eldritch.json b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Eldritch/Eldritch.json new file mode 100644 index 0000000..6074bf2 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Eldritch/Eldritch.json @@ -0,0 +1,94 @@ +{ + "dark": { + "mPrimary": "#37f499", + "mOnPrimary": "#171928", + "mSecondary": "#04d1f9", + "mOnSecondary": "#171928", + "mTertiary": "#a48cf2", + "mOnTertiary": "#171928", + "mError": "#f16c75", + "mOnError": "#171928", + "mSurface": "#212337", + "mOnSurface": "#ebfafa", + "mSurfaceVariant": "#292e42", + "mOnSurfaceVariant": "#ABB4DA", + "mOutline": "#3b4261", + "mShadow": "#414868", + "mHover": "#a48cf2", + "mOnHover": "#171928", + "terminal": { + "normal": { + "black": "#21222c", + "red": "#f9515d", + "green": "#37f499", + "yellow": "#e9f941", + "blue": "#9071f4", + "magenta": "#f265b5", + "cyan": "#04d1f9", + "white": "#ebfafa" + }, + "bright": { + "black": "#7081d0", + "red": "#f16c75", + "green": "#69F8B3", + "yellow": "#f1fc79", + "blue": "#a48cf2", + "magenta": "#FD92CE", + "cyan": "#66e4fd", + "white": "#ffffff" + }, + "foreground": "#ebfafa", + "background": "#212337", + "selectionFg": "#ebfafa", + "selectionBg": "#bf4f8e", + "cursorText": "#37f499", + "cursor": "#f8f8f2" + } + }, + "light": { + "mPrimary": "#37f499", + "mOnPrimary": "#171928", + "mSecondary": "#04d1f9", + "mOnSecondary": "#171928", + "mTertiary": "#a48cf2", + "mOnTertiary": "#171928", + "mError": "#f16c75", + "mOnError": "#171928", + "mSurface": "#ffffff", + "mOnSurface": "#171928", + "mSurfaceVariant": "#f2f4f8", + "mOnSurfaceVariant": "#3b4261", + "mOutline": "#b0b6c3", + "mShadow": "#e0e3e8", + "mHover": "#a48cf2", + "mOnHover": "#171928", + "terminal": { + "normal": { + "black": "#ebfafa", + "red": "#ba1a1a", + "green": "#1a7f4c", + "yellow": "#9e8c13", + "blue": "#3a3e8c", + "magenta": "#8c2a6c", + "cyan": "#1a6c8c", + "white": "#212337" + }, + "bright": { + "black": "#7081d0", + "red": "#d23b3b", + "green": "#37f499", + "yellow": "#e9f941", + "blue": "#9071f4", + "magenta": "#f265b5", + "cyan": "#04d1f9", + "white": "#212337" + }, + "foreground": "#212337", + "background": "#ebfafa", + "selectionFg": "#212337", + "selectionBg": "#bf4f8e", + "cursorText": "#1a7f4c", + "cursor": "#212337" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Gruvbox/Gruvbox.json b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Gruvbox/Gruvbox.json new file mode 100644 index 0000000..ff6f074 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Gruvbox/Gruvbox.json @@ -0,0 +1,94 @@ +{ + "dark": { + "mPrimary": "#b8bb26", + "mOnPrimary": "#282828", + "mSecondary": "#fabd2f", + "mOnSecondary": "#282828", + "mTertiary": "#83a598", + "mOnTertiary": "#282828", + "mError": "#fb4934", + "mOnError": "#282828", + "mSurface": "#282828", + "mOnSurface": "#fbf1c7", + "mSurfaceVariant": "#3c3836", + "mOnSurfaceVariant": "#ebdbb2", + "mOutline": "#57514e", + "mShadow": "#282828", + "mHover": "#83a598", + "mOnHover": "#282828", + "terminal": { + "normal": { + "black": "#282828", + "red": "#cc241d", + "green": "#98971a", + "yellow": "#d79921", + "blue": "#458588", + "magenta": "#b16286", + "cyan": "#689d6a", + "white": "#a89984" + }, + "bright": { + "black": "#928374", + "red": "#fb4934", + "green": "#b8bb26", + "yellow": "#fabd2f", + "blue": "#83a598", + "magenta": "#d3869b", + "cyan": "#8ec07c", + "white": "#ebdbb2" + }, + "foreground": "#ebdbb2", + "background": "#282828", + "selectionFg": "#ebdbb2", + "selectionBg": "#665c54", + "cursorText": "#282828", + "cursor": "#ebdbb2" + } + }, + "light": { + "mPrimary": "#98971a", + "mOnPrimary": "#fbf1c7", + "mSecondary": "#d79921", + "mOnSecondary": "#fbf1c7", + "mTertiary": "#458588", + "mOnTertiary": "#fbf1c7", + "mError": "#cc241d", + "mOnError": "#fbf1c7", + "mSurface": "#fbf1c7", + "mOnSurface": "#3c3836", + "mSurfaceVariant": "#ebdbb2", + "mOnSurfaceVariant": "#7c6f64", + "mOutline": "#bdae93", + "mShadow": "#d5c4a1", + "mHover": "#458588", + "mOnHover": "#fbf1c7", + "terminal": { + "normal": { + "black": "#fbf1c7", + "red": "#cc241d", + "green": "#98971a", + "yellow": "#d79921", + "blue": "#458588", + "magenta": "#b16286", + "cyan": "#689d6a", + "white": "#7c6f64" + }, + "bright": { + "black": "#928374", + "red": "#9d0006", + "green": "#79740e", + "yellow": "#b57614", + "blue": "#076678", + "magenta": "#8f3f71", + "cyan": "#427b58", + "white": "#3c3836" + }, + "foreground": "#3c3836", + "background": "#fbf1c7", + "selectionFg": "#fbf1c7", + "selectionBg": "#3c3836", + "cursorText": "#625e5c", + "cursor": "#3c3836" + } + } +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Kanagawa/Kanagawa.json b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Kanagawa/Kanagawa.json new file mode 100644 index 0000000..0acc151 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Kanagawa/Kanagawa.json @@ -0,0 +1,94 @@ +{ + "dark": { + "mPrimary": "#76946a", + "mOnPrimary": "#1f1f28", + "mSecondary": "#c0a36e", + "mOnSecondary": "#1f1f28", + "mTertiary": "#7e9cd8", + "mOnTertiary": "#1f1f28", + "mError": "#c34043", + "mOnError": "#1f1f28", + "mSurface": "#1f1f28", + "mOnSurface": "#c8c093", + "mSurfaceVariant": "#2a2a37", + "mOnSurfaceVariant": "#717c7c", + "mOutline": "#363646", + "mShadow": "#1f1f28", + "mHover": "#7e9cd8", + "mOnHover": "#1f1f28", + "terminal": { + "normal": { + "black": "#090618", + "red": "#c34043", + "green": "#76946a", + "yellow": "#c0a36e", + "blue": "#7e9cd8", + "magenta": "#957fb8", + "cyan": "#6a9589", + "white": "#c8c093" + }, + "bright": { + "black": "#727169", + "red": "#e82424", + "green": "#98bb6c", + "yellow": "#e6c384", + "blue": "#7fb4ca", + "magenta": "#938aa9", + "cyan": "#7aa89f", + "white": "#dcd7ba" + }, + "foreground": "#dcd7ba", + "background": "#1f1f28", + "selectionFg": "#c8c093", + "selectionBg": "#2d4f67", + "cursorText": "#1f1f28", + "cursor": "#c8c093" + } + }, + "light": { + "mPrimary": "#6f894e", + "mOnPrimary": "#f2ecbc", + "mSecondary": "#77713f", + "mOnSecondary": "#f2ecbc", + "mTertiary": "#4d699b", + "mOnTertiary": "#f2ecbc", + "mError": "#c84053", + "mOnError": "#f2ecbc", + "mSurface": "#f2ecbc", + "mOnSurface": "#545464", + "mSurfaceVariant": "#e5ddb0", + "mOnSurfaceVariant": "#8a8980", + "mOutline": "#cfc49c", + "mShadow": "#f2ecbc", + "mHover": "#4d699b", + "mOnHover": "#f2ecbc", + "terminal": { + "normal": { + "black": "#1F1F28", + "red": "#c84053", + "green": "#6f894e", + "yellow": "#77713f", + "blue": "#4d699b", + "magenta": "#b35b79", + "cyan": "#597b75", + "white": "#545464" + }, + "bright": { + "black": "#8a8980", + "red": "#d7474b", + "green": "#6e915f", + "yellow": "#836f4a", + "blue": "#6693bf", + "magenta": "#624c83", + "cyan": "#5e857a", + "white": "#43436c" + }, + "foreground": "#545464", + "background": "#f2ecbc", + "selectionFg": "#f2ecbc", + "selectionBg": "#c9cbd1", + "cursorText": "#f2ecbc", + "cursor": "#43436c" + } + } +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Noctalia-default/Noctalia-default.json b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Noctalia-default/Noctalia-default.json new file mode 100644 index 0000000..16ffab4 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Noctalia-default/Noctalia-default.json @@ -0,0 +1,94 @@ +{ + "dark": { + "mPrimary": "#fff59b", + "mOnPrimary": "#0e0e43", + "mSecondary": "#a9aefe", + "mOnSecondary": "#0e0e43", + "mTertiary": "#9BFECE", + "mOnTertiary": "#0e0e43", + "mError": "#FD4663", + "mOnError": "#0e0e43", + "mSurface": "#070722", + "mOnSurface": "#f3edf7", + "mSurfaceVariant": "#11112d", + "mOnSurfaceVariant": "#7c80b4", + "mOutline": "#21215F", + "mShadow": "#070722", + "mHover": "#9BFECE", + "mOnHover": "#0e0e43", + "terminal": { + "normal": { + "black": "#11112d", + "red": "#FD4663", + "green": "#9BFECE", + "yellow": "#fff59b", + "blue": "#a9aefe", + "magenta": "#FD4663", + "cyan": "#9BFECE", + "white": "#f3edf7" + }, + "bright": { + "black": "#21215F", + "red": "#FD4663", + "green": "#9BFECE", + "yellow": "#fff59b", + "blue": "#a9aefe", + "magenta": "#FD4663", + "cyan": "#9BFECE", + "white": "#ffffff" + }, + "foreground": "#f3edf7", + "background": "#070722", + "selectionFg": "#070722", + "selectionBg": "#f3edf7", + "cursorText": "#070722", + "cursor": "#fff59b" + } + }, + "light": { + "mPrimary": "#5d65f5", + "mOnPrimary": "#dadcff", + "mSecondary": "#8E93D8", + "mOnSecondary": "#dadcff", + "mTertiary": "#0e0e43", + "mOnTertiary": "#fef29a", + "mError": "#FD4663", + "mOnError": "#0e0e43", + "mSurface": "#e6e8fa", + "mOnSurface": "#0e0e43", + "mSurfaceVariant": "#eff0ff", + "mOnSurfaceVariant": "#4b55c8", + "mOutline": "#8288fc", + "mShadow": "#f3edf7", + "mHover": "#0e0e43", + "mOnHover": "#fef29a", + "terminal": { + "normal": { + "black": "#eff0ff", + "red": "#FD4663", + "green": "#0e0e43", + "yellow": "#5d65f5", + "blue": "#8E93D8", + "magenta": "#FD4663", + "cyan": "#0e0e43", + "white": "#4b55c8" + }, + "bright": { + "black": "#8288fc", + "red": "#FD4663", + "green": "#0e0e43", + "yellow": "#5d65f5", + "blue": "#8E93D8", + "magenta": "#FD4663", + "cyan": "#0e0e43", + "white": "#0e0e43" + }, + "foreground": "#4b55c8", + "background": "#e6e8fa", + "selectionFg": "#e6e8fa", + "selectionBg": "#4b55c8", + "cursorText": "#e6e8fa", + "cursor": "#5d65f5" + } + } +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Nord/Nord.json b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Nord/Nord.json new file mode 100644 index 0000000..9c3dbda --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Nord/Nord.json @@ -0,0 +1,94 @@ +{ + "dark": { + "mPrimary": "#8fbcbb", + "mOnPrimary": "#2e3440", + "mSecondary": "#88c0d0", + "mOnSecondary": "#2e3440", + "mTertiary": "#5e81ac", + "mOnTertiary": "#2e3440", + "mError": "#bf616a", + "mOnError": "#2e3440", + "mSurface": "#2e3440", + "mOnSurface": "#eceff4", + "mSurfaceVariant": "#3b4252", + "mOnSurfaceVariant": "#d8dee9", + "mOutline": "#505a70", + "mShadow": "#2e3440", + "mHover": "#5e81ac", + "mOnHover": "#2e3440", + "terminal": { + "normal": { + "black": "#3b4252", + "red": "#bf616a", + "green": "#a3be8c", + "yellow": "#ebcb8b", + "blue": "#81a1c1", + "magenta": "#b48ead", + "cyan": "#88c0d0", + "white": "#e5e9f0" + }, + "bright": { + "black": "#596377", + "red": "#bf616a", + "green": "#a3be8c", + "yellow": "#ebcb8b", + "blue": "#81a1c1", + "magenta": "#b48ead", + "cyan": "#8fbcbb", + "white": "#eceff4" + }, + "foreground": "#d8dee9", + "background": "#2e3440", + "selectionFg": "#4c566a", + "selectionBg": "#eceff4", + "cursorText": "#282828", + "cursor": "#eceff4" + } + }, + "light": { + "mPrimary": "#5e81ac", + "mOnPrimary": "#eceff4", + "mSecondary": "#64adc2", + "mOnSecondary": "#eceff4", + "mTertiary": "#6fa9a8", + "mOnTertiary": "#eceff4", + "mError": "#bf616a", + "mOnError": "#eceff4", + "mSurface": "#eceff4", + "mOnSurface": "#2e3440", + "mSurfaceVariant": "#e5e9f0", + "mOnSurfaceVariant": "#4c566a", + "mOutline": "#c5cedd", + "mShadow": "#d8dee9", + "mHover": "#6fa9a8", + "mOnHover": "#eceff4", + "terminal": { + "normal": { + "black": "#3b4252", + "red": "#bf616a", + "green": "#96b17f", + "yellow": "#c5a565", + "blue": "#81a1c1", + "magenta": "#b48ead", + "cyan": "#7bb3c3", + "white": "#a5abb6" + }, + "bright": { + "black": "#4c566a", + "red": "#bf616a", + "green": "#96b17f", + "yellow": "#c5a565", + "blue": "#81a1c1", + "magenta": "#b48ead", + "cyan": "#82afae", + "white": "#eceff4" + }, + "foreground": "#414858", + "background": "#eceff4", + "selectionFg": "#4c556a", + "selectionBg": "#d8dee9", + "cursorText": "#3b4252", + "cursor": "#7bb3c3" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Rosepine/Rosepine.json b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Rosepine/Rosepine.json new file mode 100644 index 0000000..ffb4fc2 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Rosepine/Rosepine.json @@ -0,0 +1,94 @@ +{ + "dark": { + "mPrimary": "#ebbcba", + "mOnPrimary": "#191724", + "mSecondary": "#9ccfd8", + "mOnSecondary": "#191724", + "mTertiary": "#31748f", + "mOnTertiary": "#e0def4", + "mError": "#eb6f92", + "mOnError": "#191724", + "mSurface": "#191724", + "mOnSurface": "#e0def4", + "mSurfaceVariant": "#26233a", + "mOnSurfaceVariant": "#908caa", + "mOutline": "#403d52", + "mShadow": "#191724", + "mHover": "#524f67", + "mOnHover": "#e0def4", + "terminal": { + "normal": { + "black": "#26233a", + "red": "#eb6f92", + "green": "#31748f", + "yellow": "#f6c177", + "blue": "#9ccfd8", + "magenta": "#c4a7e7", + "cyan": "#ebbcba", + "white": "#e0def4" + }, + "bright": { + "black": "#6e6a86", + "red": "#eb6f92", + "green": "#31748f", + "yellow": "#f6c177", + "blue": "#9ccfd8", + "magenta": "#c4a7e7", + "cyan": "#ebbcba", + "white": "#e0def4" + }, + "foreground": "#e0def4", + "background": "#191724", + "selectionFg": "#e0def4", + "selectionBg": "#403d52", + "cursorText": "#191724", + "cursor": "#e0def4" + } + }, + "light": { + "mPrimary": "#d7827e", + "mOnPrimary": "#faf4ed", + "mSecondary": "#56949f", + "mOnSecondary": "#faf4ed", + "mTertiary": "#286983", + "mOnTertiary": "#faf4ed", + "mError": "#b4637a", + "mOnError": "#faf4ed", + "mSurface": "#fffaf3", + "mOnSurface": "#575279", + "mSurfaceVariant": "#f2e9e1", + "mOnSurfaceVariant": "#797593", + "mOutline": "#dfdad9", + "mShadow": "#faf4ed", + "mHover": "#cecacd", + "mOnHover": "#575279", + "terminal": { + "normal": { + "black": "#f2e9e1", + "red": "#b4637a", + "green": "#286983", + "yellow": "#ea9d34", + "blue": "#56949f", + "magenta": "#907aa9", + "cyan": "#d7827e", + "white": "#575279" + }, + "bright": { + "black": "#9893a5", + "red": "#b4637a", + "green": "#286983", + "yellow": "#ea9d34", + "blue": "#56949f", + "magenta": "#907aa9", + "cyan": "#d7827e", + "white": "#575279" + }, + "foreground": "#575279", + "background": "#faf4ed", + "selectionFg": "#575279", + "selectionBg": "#dfdad9", + "cursorText": "#faf4ed", + "cursor": "#575279" + } + } +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Tokyo-Night/Tokyo-Night.json b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Tokyo-Night/Tokyo-Night.json new file mode 100644 index 0000000..ae96d38 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/ColorScheme/Tokyo-Night/Tokyo-Night.json @@ -0,0 +1,94 @@ +{ + "dark": { + "mPrimary": "#7aa2f7", + "mOnPrimary": "#16161e", + "mSecondary": "#bb9af7", + "mOnSecondary": "#16161e", + "mTertiary": "#9ece6a", + "mOnTertiary": "#16161e", + "mError": "#f7768e", + "mOnError": "#16161e", + "mSurface": "#1a1b26", + "mOnSurface": "#c0caf5", + "mSurfaceVariant": "#24283b", + "mOnSurfaceVariant": "#9aa5ce", + "mOutline": "#353D57", + "mShadow": "#15161e", + "mHover": "#9ece6a", + "mOnHover": "#16161e", + "terminal": { + "normal": { + "black": "#15161e", + "red": "#f7768e", + "green": "#9ece6a", + "yellow": "#e0af68", + "blue": "#7aa2f7", + "magenta": "#bb9af7", + "cyan": "#7dcfff", + "white": "#a9b1d6" + }, + "bright": { + "black": "#414868", + "red": "#f7768e", + "green": "#9ece6a", + "yellow": "#e0af68", + "blue": "#7aa2f7", + "magenta": "#bb9af7", + "cyan": "#7dcfff", + "white": "#c0caf5" + }, + "foreground": "#c0caf5", + "background": "#1a1b26", + "selectionFg": "#c0caf5", + "selectionBg": "#283457", + "cursorText": "#1a1b26", + "cursor": "#c0caf5" + } + }, + "light": { + "mPrimary": "#2e7de9", + "mOnPrimary": "#e1e2e7", + "mSecondary": "#9854f1", + "mOnSecondary": "#e1e2e7", + "mTertiary": "#587539", + "mOnTertiary": "#e1e2e7", + "mError": "#f52a65", + "mOnError": "#e1e2e7", + "mSurface": "#e1e2e7", + "mOnSurface": "#3760bf", + "mSurfaceVariant": "#d0d5e3", + "mOnSurfaceVariant": "#6172b0", + "mOutline": "#b4b5b9", + "mShadow": "#a8aecb", + "mHover": "#587539", + "mOnHover": "#e1e2e7", + "terminal": { + "normal": { + "black": "#e9e9ed", + "red": "#f52a65", + "green": "#587539", + "yellow": "#8c6c3e", + "blue": "#2e7de9", + "magenta": "#9854f1", + "cyan": "#007197", + "white": "#6172b0" + }, + "bright": { + "black": "#a1a6c5", + "red": "#f52a65", + "green": "#587539", + "yellow": "#8c6c3e", + "blue": "#2e7de9", + "magenta": "#9854f1", + "cyan": "#007197", + "white": "#3760bf" + }, + "foreground": "#3760bf", + "background": "#e1e2e7", + "selectionFg": "#3760bf", + "selectionBg": "#99a7df", + "cursorText": "#e1e2e7", + "cursor": "#3760bf" + } + } +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Fonts/tabler/noctalia-tabler-icons.ttf b/fedora/.config/quickshell/noctalia-shell/Assets/Fonts/tabler/noctalia-tabler-icons.ttf new file mode 100644 index 0000000..1327521 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Fonts/tabler/noctalia-tabler-icons.ttf differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Fonts/tabler/tabler-icons-license.txt b/fedora/.config/quickshell/noctalia-shell/Assets/Fonts/tabler/tabler-icons-license.txt new file mode 100644 index 0000000..f08aa1c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Fonts/tabler/tabler-icons-license.txt @@ -0,0 +1,16 @@ +Tabler Licenses - Detailed Usage Rights and Guidelines + +This is a legal agreement between you, the Purchaser, and Tabler. Purchasing or downloading of any Tabler product (Tabler Admin Template, Tabler Icons, Tabler Emails, Tabler Illustrations), constitutes your acceptance of the terms of this license, Tabler terms of service and Tabler private policy. + +Tabler Admin Template and Tabler Icons License* +Tabler Admin Template and Tabler Icons are available under MIT License. + +Copyright (c) 2018-2025 Tabler + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +See more at Tabler Admin Template MIT License See more at Tabler Icons MIT License \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Launcher/emoji.json b/fedora/.config/quickshell/noctalia-shell/Assets/Launcher/emoji.json new file mode 100644 index 0000000..84b176f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Launcher/emoji.json @@ -0,0 +1,11222 @@ +[ + { + "emoji": "😀", + "name": "grinning face", + "keywords": ["smile", "happy"], + "category": "people" + }, + { + "emoji": "😃", + "name": "grinning face with big eyes", + "keywords": ["happy", "joy", "haha"], + "category": "people" + }, + { + "emoji": "😄", + "name": "grinning face with smiling eyes", + "keywords": ["happy", "joy", "laugh", "pleased"], + "category": "people" + }, + { + "emoji": "😁", + "name": "beaming face with smiling eyes", + "keywords": [], + "category": "people" + }, + { + "emoji": "😆", + "name": "grinning squinting face", + "keywords": ["happy", "haha"], + "category": "people" + }, + { + "emoji": "😅", + "name": "grinning face with sweat", + "keywords": ["hot"], + "category": "people" + }, + { + "emoji": "🤣", + "name": "rolling on the floor laughing", + "keywords": ["lol", "laughing"], + "category": "people" + }, + { + "emoji": "😂", + "name": "face with tears of joy", + "keywords": ["tears"], + "category": "people" + }, + { + "emoji": "🙂", + "name": "slightly smiling face", + "keywords": [], + "category": "people" + }, + { + "emoji": "🙃", + "name": "upside-down face", + "keywords": [], + "category": "people" + }, + { + "emoji": "🫠", + "name": "melting face", + "keywords": ["sarcasm", "dread"], + "category": "people" + }, + { + "emoji": "😉", + "name": "winking face", + "keywords": ["flirt"], + "category": "people" + }, + { + "emoji": "😊", + "name": "smiling face with smiling eyes", + "keywords": ["proud"], + "category": "people" + }, + { + "emoji": "😇", + "name": "smiling face with halo", + "keywords": ["angel"], + "category": "people" + }, + { + "emoji": "🥰", + "name": "smiling face with hearts", + "keywords": ["love"], + "category": "people" + }, + { + "emoji": "😍", + "name": "smiling face with heart-eyes", + "keywords": ["love", "crush"], + "category": "people" + }, + { + "emoji": "🤩", + "name": "star-struck", + "keywords": ["eyes"], + "category": "people" + }, + { + "emoji": "😘", + "name": "face blowing a kiss", + "keywords": ["flirt"], + "category": "people" + }, + { + "emoji": "😗", + "name": "kissing face", + "keywords": [], + "category": "people" + }, + { + "emoji": "☺️", + "name": "smiling face", + "keywords": ["blush", "pleased"], + "category": "people" + }, + { + "emoji": "😚", + "name": "kissing face with closed eyes", + "keywords": [], + "category": "people" + }, + { + "emoji": "😙", + "name": "kissing face with smiling eyes", + "keywords": [], + "category": "people" + }, + { + "emoji": "🥲", + "name": "smiling face with tear", + "keywords": [], + "category": "people" + }, + { + "emoji": "😋", + "name": "face savoring food", + "keywords": ["tongue", "lick"], + "category": "people" + }, + { + "emoji": "😛", + "name": "face with tongue", + "keywords": [], + "category": "people" + }, + { + "emoji": "😜", + "name": "winking face with tongue", + "keywords": ["prank", "silly"], + "category": "people" + }, + { + "emoji": "🤪", + "name": "zany face", + "keywords": ["goofy", "wacky"], + "category": "people" + }, + { + "emoji": "😝", + "name": "squinting face with tongue", + "keywords": ["prank"], + "category": "people" + }, + { + "emoji": "🤑", + "name": "money-mouth face", + "keywords": ["rich"], + "category": "people" + }, + { + "emoji": "🤗", + "name": "smiling face with open hands", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤭", + "name": "face with hand over mouth", + "keywords": ["quiet", "whoops"], + "category": "people" + }, + { + "emoji": "🫢", + "name": "face with open eyes and hand over mouth", + "keywords": ["gasp", "shock"], + "category": "people" + }, + { + "emoji": "🫣", + "name": "face with peeking eye", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤫", + "name": "shushing face", + "keywords": ["silence", "quiet"], + "category": "people" + }, + { + "emoji": "🤔", + "name": "thinking face", + "keywords": [], + "category": "people" + }, + { + "emoji": "🫡", + "name": "saluting face", + "keywords": ["respect"], + "category": "people" + }, + { + "emoji": "🤐", + "name": "zipper-mouth face", + "keywords": ["silence", "hush"], + "category": "people" + }, + { + "emoji": "🤨", + "name": "face with raised eyebrow", + "keywords": ["suspicious"], + "category": "people" + }, + { + "emoji": "😐", + "name": "neutral face", + "keywords": ["meh"], + "category": "people" + }, + { + "emoji": "😑", + "name": "expressionless face", + "keywords": [], + "category": "people" + }, + { + "emoji": "😶", + "name": "face without mouth", + "keywords": ["mute", "silence"], + "category": "people" + }, + { + "emoji": "🫥", + "name": "dotted line face", + "keywords": ["invisible"], + "category": "people" + }, + { + "emoji": "😶‍🌫️", + "name": "face in clouds", + "keywords": [], + "category": "people" + }, + { + "emoji": "😏", + "name": "smirking face", + "keywords": ["smug"], + "category": "people" + }, + { + "emoji": "😒", + "name": "unamused face", + "keywords": ["meh"], + "category": "people" + }, + { + "emoji": "🙄", + "name": "face with rolling eyes", + "keywords": [], + "category": "people" + }, + { + "emoji": "😬", + "name": "grimacing face", + "keywords": [], + "category": "people" + }, + { + "emoji": "😮‍💨", + "name": "face exhaling", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤥", + "name": "lying face", + "keywords": ["liar"], + "category": "people" + }, + { + "emoji": "🫨", + "name": "shaking face", + "keywords": ["shock"], + "category": "people" + }, + { + "emoji": "😌", + "name": "relieved face", + "keywords": ["whew"], + "category": "people" + }, + { + "emoji": "😔", + "name": "pensive face", + "keywords": [], + "category": "people" + }, + { + "emoji": "😪", + "name": "sleepy face", + "keywords": ["tired"], + "category": "people" + }, + { + "emoji": "🤤", + "name": "drooling face", + "keywords": [], + "category": "people" + }, + { + "emoji": "😴", + "name": "sleeping face", + "keywords": ["zzz"], + "category": "people" + }, + { + "emoji": "😷", + "name": "face with medical mask", + "keywords": ["sick", "ill"], + "category": "people" + }, + { + "emoji": "🤒", + "name": "face with thermometer", + "keywords": ["sick"], + "category": "people" + }, + { + "emoji": "🤕", + "name": "face with head-bandage", + "keywords": ["hurt"], + "category": "people" + }, + { + "emoji": "🤢", + "name": "nauseated face", + "keywords": ["sick", "barf", "disgusted"], + "category": "people" + }, + { + "emoji": "🤮", + "name": "face vomiting", + "keywords": ["barf", "sick"], + "category": "people" + }, + { + "emoji": "🤧", + "name": "sneezing face", + "keywords": ["achoo", "sick"], + "category": "people" + }, + { + "emoji": "🥵", + "name": "hot face", + "keywords": ["heat", "sweating"], + "category": "people" + }, + { + "emoji": "🥶", + "name": "cold face", + "keywords": ["freezing", "ice"], + "category": "people" + }, + { + "emoji": "🥴", + "name": "woozy face", + "keywords": ["groggy"], + "category": "people" + }, + { + "emoji": "😵", + "name": "face with crossed-out eyes", + "keywords": [], + "category": "people" + }, + { + "emoji": "😵‍💫", + "name": "face with spiral eyes", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤯", + "name": "exploding head", + "keywords": ["mind", "blown"], + "category": "people" + }, + { + "emoji": "🤠", + "name": "cowboy hat face", + "keywords": [], + "category": "people" + }, + { + "emoji": "🥳", + "name": "partying face", + "keywords": ["celebration", "birthday"], + "category": "people" + }, + { + "emoji": "🥸", + "name": "disguised face", + "keywords": [], + "category": "people" + }, + { + "emoji": "😎", + "name": "smiling face with sunglasses", + "keywords": ["cool"], + "category": "people" + }, + { + "emoji": "🤓", + "name": "nerd face", + "keywords": ["geek", "glasses"], + "category": "people" + }, + { + "emoji": "🧐", + "name": "face with monocle", + "keywords": [], + "category": "people" + }, + { + "emoji": "😕", + "name": "confused face", + "keywords": [], + "category": "people" + }, + { + "emoji": "🫤", + "name": "face with diagonal mouth", + "keywords": ["confused"], + "category": "people" + }, + { + "emoji": "😟", + "name": "worried face", + "keywords": ["nervous"], + "category": "people" + }, + { + "emoji": "🙁", + "name": "slightly frowning face", + "keywords": [], + "category": "people" + }, + { + "emoji": "☹️", + "name": "frowning face", + "keywords": [], + "category": "people" + }, + { + "emoji": "😮", + "name": "face with open mouth", + "keywords": ["surprise", "impressed", "wow"], + "category": "people" + }, + { + "emoji": "😯", + "name": "hushed face", + "keywords": ["silence", "speechless"], + "category": "people" + }, + { + "emoji": "😲", + "name": "astonished face", + "keywords": ["amazed", "gasp"], + "category": "people" + }, + { + "emoji": "😳", + "name": "flushed face", + "keywords": [], + "category": "people" + }, + { + "emoji": "🥺", + "name": "pleading face", + "keywords": ["puppy", "eyes"], + "category": "people" + }, + { + "emoji": "🥹", + "name": "face holding back tears", + "keywords": ["tears", "gratitude"], + "category": "people" + }, + { + "emoji": "😦", + "name": "frowning face with open mouth", + "keywords": [], + "category": "people" + }, + { + "emoji": "😧", + "name": "anguished face", + "keywords": ["stunned"], + "category": "people" + }, + { + "emoji": "😨", + "name": "fearful face", + "keywords": ["scared", "shocked", "oops"], + "category": "people" + }, + { + "emoji": "😰", + "name": "anxious face with sweat", + "keywords": ["nervous"], + "category": "people" + }, + { + "emoji": "😥", + "name": "sad but relieved face", + "keywords": ["phew", "sweat", "nervous"], + "category": "people" + }, + { + "emoji": "😢", + "name": "crying face", + "keywords": ["sad", "tear"], + "category": "people" + }, + { + "emoji": "😭", + "name": "loudly crying face", + "keywords": ["sad", "cry", "bawling", "sob"], + "category": "people" + }, + { + "emoji": "😱", + "name": "face screaming in fear", + "keywords": ["horror", "shocked"], + "category": "people" + }, + { + "emoji": "😖", + "name": "confounded face", + "keywords": [], + "category": "people" + }, + { + "emoji": "😣", + "name": "persevering face", + "keywords": ["struggling"], + "category": "people" + }, + { + "emoji": "😞", + "name": "disappointed face", + "keywords": ["sad"], + "category": "people" + }, + { + "emoji": "😓", + "name": "downcast face with sweat", + "keywords": [], + "category": "people" + }, + { + "emoji": "😩", + "name": "weary face", + "keywords": ["tired"], + "category": "people" + }, + { + "emoji": "😫", + "name": "tired face", + "keywords": ["upset", "whine"], + "category": "people" + }, + { + "emoji": "🥱", + "name": "yawning face", + "keywords": [], + "category": "people" + }, + { + "emoji": "😤", + "name": "face with steam from nose", + "keywords": ["smug"], + "category": "people" + }, + { + "emoji": "😡", + "name": "enraged face", + "keywords": ["angry"], + "category": "people" + }, + { + "emoji": "😠", + "name": "angry face", + "keywords": ["mad", "annoyed"], + "category": "people" + }, + { + "emoji": "🤬", + "name": "face with symbols on mouth", + "keywords": ["foul"], + "category": "people" + }, + { + "emoji": "😈", + "name": "smiling face with horns", + "keywords": ["devil", "evil", "horns"], + "category": "people" + }, + { + "emoji": "👿", + "name": "angry face with horns", + "keywords": ["angry", "devil", "evil", "horns"], + "category": "people" + }, + { + "emoji": "💀", + "name": "skull", + "keywords": ["dead", "danger", "poison"], + "category": "people" + }, + { + "emoji": "☠️", + "name": "skull and crossbones", + "keywords": ["danger", "pirate"], + "category": "people" + }, + { + "emoji": "💩", + "name": "pile of poo", + "keywords": ["crap"], + "category": "people" + }, + { + "emoji": "🤡", + "name": "clown face", + "keywords": [], + "category": "people" + }, + { + "emoji": "👹", + "name": "ogre", + "keywords": ["monster"], + "category": "people" + }, + { + "emoji": "👺", + "name": "goblin", + "keywords": [], + "category": "people" + }, + { + "emoji": "👻", + "name": "ghost", + "keywords": ["halloween"], + "category": "people" + }, + { + "emoji": "👽", + "name": "alien", + "keywords": ["ufo"], + "category": "people" + }, + { + "emoji": "👾", + "name": "alien monster", + "keywords": ["game", "retro"], + "category": "people" + }, + { + "emoji": "🤖", + "name": "robot", + "keywords": [], + "category": "people" + }, + { + "emoji": "😺", + "name": "grinning cat", + "keywords": [], + "category": "people" + }, + { + "emoji": "😸", + "name": "grinning cat with smiling eyes", + "keywords": [], + "category": "people" + }, + { + "emoji": "😹", + "name": "cat with tears of joy", + "keywords": [], + "category": "people" + }, + { + "emoji": "😻", + "name": "smiling cat with heart-eyes", + "keywords": [], + "category": "people" + }, + { + "emoji": "😼", + "name": "cat with wry smile", + "keywords": [], + "category": "people" + }, + { + "emoji": "😽", + "name": "kissing cat", + "keywords": [], + "category": "people" + }, + { + "emoji": "🙀", + "name": "weary cat", + "keywords": ["horror"], + "category": "people" + }, + { + "emoji": "😿", + "name": "crying cat", + "keywords": ["sad", "tear"], + "category": "people" + }, + { + "emoji": "😾", + "name": "pouting cat", + "keywords": [], + "category": "people" + }, + { + "emoji": "🙈", + "name": "see-no-evil monkey", + "keywords": ["monkey", "blind", "ignore"], + "category": "people" + }, + { + "emoji": "🙉", + "name": "hear-no-evil monkey", + "keywords": ["monkey", "deaf"], + "category": "people" + }, + { + "emoji": "🙊", + "name": "speak-no-evil monkey", + "keywords": ["monkey", "mute", "hush"], + "category": "people" + }, + { + "emoji": "💌", + "name": "love letter", + "keywords": ["email", "envelope"], + "category": "people" + }, + { + "emoji": "💘", + "name": "heart with arrow", + "keywords": ["love", "heart"], + "category": "people" + }, + { + "emoji": "💝", + "name": "heart with ribbon", + "keywords": ["chocolates"], + "category": "people" + }, + { + "emoji": "💖", + "name": "sparkling heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "💗", + "name": "growing heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "💓", + "name": "beating heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "💞", + "name": "revolving hearts", + "keywords": [], + "category": "people" + }, + { + "emoji": "💕", + "name": "two hearts", + "keywords": [], + "category": "people" + }, + { + "emoji": "💟", + "name": "heart decoration", + "keywords": [], + "category": "people" + }, + { + "emoji": "❣️", + "name": "heart exclamation", + "keywords": [], + "category": "people" + }, + { + "emoji": "💔", + "name": "broken heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "❤️‍🔥", + "name": "heart on fire", + "keywords": [], + "category": "people" + }, + { + "emoji": "❤️‍🩹", + "name": "mending heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "❤️", + "name": "red heart", + "keywords": ["love"], + "category": "people" + }, + { + "emoji": "🩷", + "name": "pink heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧡", + "name": "orange heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "💛", + "name": "yellow heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "💚", + "name": "green heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "💙", + "name": "blue heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "🩵", + "name": "light blue heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "💜", + "name": "purple heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤎", + "name": "brown heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "🖤", + "name": "black heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "🩶", + "name": "grey heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤍", + "name": "white heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "💋", + "name": "kiss mark", + "keywords": ["lipstick"], + "category": "people" + }, + { + "emoji": "💯", + "name": "hundred points", + "keywords": ["score", "perfect"], + "category": "people" + }, + { + "emoji": "💢", + "name": "anger symbol", + "keywords": ["angry"], + "category": "people" + }, + { + "emoji": "💥", + "name": "collision", + "keywords": ["explode"], + "category": "people" + }, + { + "emoji": "💫", + "name": "dizzy", + "keywords": ["star"], + "category": "people" + }, + { + "emoji": "💦", + "name": "sweat droplets", + "keywords": ["water", "workout"], + "category": "people" + }, + { + "emoji": "💨", + "name": "dashing away", + "keywords": ["wind", "blow", "fast"], + "category": "people" + }, + { + "emoji": "🕳️", + "name": "hole", + "keywords": [], + "category": "people" + }, + { + "emoji": "💬", + "name": "speech balloon", + "keywords": ["comment"], + "category": "people" + }, + { + "emoji": "👁️‍🗨️", + "name": "eye in speech bubble", + "keywords": [], + "category": "people" + }, + { + "emoji": "🗨️", + "name": "left speech bubble", + "keywords": [], + "category": "people" + }, + { + "emoji": "🗯️", + "name": "right anger bubble", + "keywords": [], + "category": "people" + }, + { + "emoji": "💭", + "name": "thought balloon", + "keywords": ["thinking"], + "category": "people" + }, + { + "emoji": "💤", + "name": "ZZZ", + "keywords": ["sleeping"], + "category": "people" + }, + { + "emoji": "👋", + "name": "waving hand", + "keywords": ["goodbye"], + "category": "people" + }, + { + "emoji": "🤚", + "name": "raised back of hand", + "keywords": [], + "category": "people" + }, + { + "emoji": "🖐️", + "name": "hand with fingers splayed", + "keywords": [], + "category": "people" + }, + { + "emoji": "✋", + "name": "raised hand", + "keywords": ["highfive", "stop"], + "category": "people" + }, + { + "emoji": "🖖", + "name": "vulcan salute", + "keywords": ["prosper", "spock"], + "category": "people" + }, + { + "emoji": "🫱", + "name": "rightwards hand", + "keywords": [], + "category": "people" + }, + { + "emoji": "🫲", + "name": "leftwards hand", + "keywords": [], + "category": "people" + }, + { + "emoji": "🫳", + "name": "palm down hand", + "keywords": [], + "category": "people" + }, + { + "emoji": "🫴", + "name": "palm up hand", + "keywords": [], + "category": "people" + }, + { + "emoji": "🫷", + "name": "leftwards pushing hand", + "keywords": [], + "category": "people" + }, + { + "emoji": "🫸", + "name": "rightwards pushing hand", + "keywords": [], + "category": "people" + }, + { + "emoji": "👌", + "name": "OK hand", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤌", + "name": "pinched fingers", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤏", + "name": "pinching hand", + "keywords": [], + "category": "people" + }, + { + "emoji": "✌️", + "name": "victory hand", + "keywords": ["victory", "peace"], + "category": "people" + }, + { + "emoji": "🤞", + "name": "crossed fingers", + "keywords": ["luck", "hopeful"], + "category": "people" + }, + { + "emoji": "🫰", + "name": "hand with index finger and thumb crossed", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤟", + "name": "love-you gesture", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤘", + "name": "sign of the horns", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤙", + "name": "call me hand", + "keywords": [], + "category": "people" + }, + { + "emoji": "👈", + "name": "backhand index pointing left", + "keywords": [], + "category": "people" + }, + { + "emoji": "👉", + "name": "backhand index pointing right", + "keywords": [], + "category": "people" + }, + { + "emoji": "👆", + "name": "backhand index pointing up", + "keywords": [], + "category": "people" + }, + { + "emoji": "🖕", + "name": "middle finger", + "keywords": [], + "category": "people" + }, + { + "emoji": "👇", + "name": "backhand index pointing down", + "keywords": [], + "category": "people" + }, + { + "emoji": "☝️", + "name": "index pointing up", + "keywords": [], + "category": "people" + }, + { + "emoji": "🫵", + "name": "index pointing at the viewer", + "keywords": [], + "category": "people" + }, + { + "emoji": "👍", + "name": "thumbs up", + "keywords": ["approve", "ok"], + "category": "people" + }, + { + "emoji": "👎", + "name": "thumbs down", + "keywords": ["disapprove", "bury"], + "category": "people" + }, + { + "emoji": "✊", + "name": "raised fist", + "keywords": ["power"], + "category": "people" + }, + { + "emoji": "👊", + "name": "oncoming fist", + "keywords": ["attack"], + "category": "people" + }, + { + "emoji": "🤛", + "name": "left-facing fist", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤜", + "name": "right-facing fist", + "keywords": [], + "category": "people" + }, + { + "emoji": "👏", + "name": "clapping hands", + "keywords": ["praise", "applause"], + "category": "people" + }, + { + "emoji": "🙌", + "name": "raising hands", + "keywords": ["hooray"], + "category": "people" + }, + { + "emoji": "🫶", + "name": "heart hands", + "keywords": ["love"], + "category": "people" + }, + { + "emoji": "👐", + "name": "open hands", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤲", + "name": "palms up together", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤝", + "name": "handshake", + "keywords": ["deal"], + "category": "people" + }, + { + "emoji": "🙏", + "name": "folded hands", + "keywords": ["please", "hope", "wish"], + "category": "people" + }, + { + "emoji": "✍️", + "name": "writing hand", + "keywords": [], + "category": "people" + }, + { + "emoji": "💅", + "name": "nail polish", + "keywords": ["beauty", "manicure"], + "category": "people" + }, + { + "emoji": "🤳", + "name": "selfie", + "keywords": [], + "category": "people" + }, + { + "emoji": "💪", + "name": "flexed biceps", + "keywords": ["flex", "bicep", "strong", "workout"], + "category": "people" + }, + { + "emoji": "🦾", + "name": "mechanical arm", + "keywords": [], + "category": "people" + }, + { + "emoji": "🦿", + "name": "mechanical leg", + "keywords": [], + "category": "people" + }, + { + "emoji": "🦵", + "name": "leg", + "keywords": [], + "category": "people" + }, + { + "emoji": "🦶", + "name": "foot", + "keywords": [], + "category": "people" + }, + { + "emoji": "👂", + "name": "ear", + "keywords": ["hear", "sound", "listen"], + "category": "people" + }, + { + "emoji": "🦻", + "name": "ear with hearing aid", + "keywords": [], + "category": "people" + }, + { + "emoji": "👃", + "name": "nose", + "keywords": ["smell"], + "category": "people" + }, + { + "emoji": "🧠", + "name": "brain", + "keywords": [], + "category": "people" + }, + { + "emoji": "🫀", + "name": "anatomical heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "🫁", + "name": "lungs", + "keywords": [], + "category": "people" + }, + { + "emoji": "🦷", + "name": "tooth", + "keywords": [], + "category": "people" + }, + { + "emoji": "🦴", + "name": "bone", + "keywords": [], + "category": "people" + }, + { + "emoji": "👀", + "name": "eyes", + "keywords": ["look", "see", "watch"], + "category": "people" + }, + { + "emoji": "👁️", + "name": "eye", + "keywords": [], + "category": "people" + }, + { + "emoji": "👅", + "name": "tongue", + "keywords": ["taste"], + "category": "people" + }, + { + "emoji": "👄", + "name": "mouth", + "keywords": ["kiss"], + "category": "people" + }, + { + "emoji": "🫦", + "name": "biting lip", + "keywords": [], + "category": "people" + }, + { + "emoji": "👶", + "name": "baby", + "keywords": ["child", "newborn"], + "category": "people" + }, + { + "emoji": "🧒", + "name": "child", + "keywords": [], + "category": "people" + }, + { + "emoji": "👦", + "name": "boy", + "keywords": ["child"], + "category": "people" + }, + { + "emoji": "👧", + "name": "girl", + "keywords": ["child"], + "category": "people" + }, + { + "emoji": "🧑", + "name": "person", + "keywords": [], + "category": "people" + }, + { + "emoji": "👱", + "name": "person: blond hair", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨", + "name": "man", + "keywords": ["mustache", "father", "dad"], + "category": "people" + }, + { + "emoji": "🧔", + "name": "person: beard", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧔‍♂️", + "name": "man: beard", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧔‍♀️", + "name": "woman: beard", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🦰", + "name": "man: red hair", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🦱", + "name": "man: curly hair", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🦳", + "name": "man: white hair", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🦲", + "name": "man: bald", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩", + "name": "woman", + "keywords": ["girls"], + "category": "people" + }, + { + "emoji": "👩‍🦰", + "name": "woman: red hair", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧑‍🦰", + "name": "person: red hair", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍🦱", + "name": "woman: curly hair", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧑‍🦱", + "name": "person: curly hair", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍🦳", + "name": "woman: white hair", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧑‍🦳", + "name": "person: white hair", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍🦲", + "name": "woman: bald", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧑‍🦲", + "name": "person: bald", + "keywords": [], + "category": "people" + }, + { + "emoji": "👱‍♀️", + "name": "woman: blond hair", + "keywords": [], + "category": "people" + }, + { + "emoji": "👱‍♂️", + "name": "man: blond hair", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧓", + "name": "older person", + "keywords": [], + "category": "people" + }, + { + "emoji": "👴", + "name": "old man", + "keywords": [], + "category": "people" + }, + { + "emoji": "👵", + "name": "old woman", + "keywords": [], + "category": "people" + }, + { + "emoji": "🙍", + "name": "person frowning", + "keywords": [], + "category": "people" + }, + { + "emoji": "🙍‍♂️", + "name": "man frowning", + "keywords": [], + "category": "people" + }, + { + "emoji": "🙍‍♀️", + "name": "woman frowning", + "keywords": [], + "category": "people" + }, + { + "emoji": "🙎", + "name": "person pouting", + "keywords": [], + "category": "people" + }, + { + "emoji": "🙎‍♂️", + "name": "man pouting", + "keywords": [], + "category": "people" + }, + { + "emoji": "🙎‍♀️", + "name": "woman pouting", + "keywords": [], + "category": "people" + }, + { + "emoji": "🙅", + "name": "person gesturing NO", + "keywords": ["stop", "halt", "denied"], + "category": "people" + }, + { + "emoji": "🙅‍♂️", + "name": "man gesturing NO", + "keywords": ["stop", "halt", "denied"], + "category": "people" + }, + { + "emoji": "🙅‍♀️", + "name": "woman gesturing NO", + "keywords": ["stop", "halt", "denied"], + "category": "people" + }, + { + "emoji": "🙆", + "name": "person gesturing OK", + "keywords": [], + "category": "people" + }, + { + "emoji": "🙆‍♂️", + "name": "man gesturing OK", + "keywords": [], + "category": "people" + }, + { + "emoji": "🙆‍♀️", + "name": "woman gesturing OK", + "keywords": [], + "category": "people" + }, + { + "emoji": "💁", + "name": "person tipping hand", + "keywords": [], + "category": "people" + }, + { + "emoji": "💁‍♂️", + "name": "man tipping hand", + "keywords": ["information"], + "category": "people" + }, + { + "emoji": "💁‍♀️", + "name": "woman tipping hand", + "keywords": ["information"], + "category": "people" + }, + { + "emoji": "🙋", + "name": "person raising hand", + "keywords": [], + "category": "people" + }, + { + "emoji": "🙋‍♂️", + "name": "man raising hand", + "keywords": [], + "category": "people" + }, + { + "emoji": "🙋‍♀️", + "name": "woman raising hand", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧏", + "name": "deaf person", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧏‍♂️", + "name": "deaf man", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧏‍♀️", + "name": "deaf woman", + "keywords": [], + "category": "people" + }, + { + "emoji": "🙇", + "name": "person bowing", + "keywords": ["respect", "thanks"], + "category": "people" + }, + { + "emoji": "🙇‍♂️", + "name": "man bowing", + "keywords": ["respect", "thanks"], + "category": "people" + }, + { + "emoji": "🙇‍♀️", + "name": "woman bowing", + "keywords": ["respect", "thanks"], + "category": "people" + }, + { + "emoji": "🤦", + "name": "person facepalming", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤦‍♂️", + "name": "man facepalming", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤦‍♀️", + "name": "woman facepalming", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤷", + "name": "person shrugging", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤷‍♂️", + "name": "man shrugging", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤷‍♀️", + "name": "woman shrugging", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧑‍⚕️", + "name": "health worker", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍⚕️", + "name": "man health worker", + "keywords": ["doctor", "nurse"], + "category": "people" + }, + { + "emoji": "👩‍⚕️", + "name": "woman health worker", + "keywords": ["doctor", "nurse"], + "category": "people" + }, + { + "emoji": "🧑‍🎓", + "name": "student", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🎓", + "name": "man student", + "keywords": ["graduation"], + "category": "people" + }, + { + "emoji": "👩‍🎓", + "name": "woman student", + "keywords": ["graduation"], + "category": "people" + }, + { + "emoji": "🧑‍🏫", + "name": "teacher", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🏫", + "name": "man teacher", + "keywords": ["school", "professor"], + "category": "people" + }, + { + "emoji": "👩‍🏫", + "name": "woman teacher", + "keywords": ["school", "professor"], + "category": "people" + }, + { + "emoji": "🧑‍⚖️", + "name": "judge", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍⚖️", + "name": "man judge", + "keywords": ["justice"], + "category": "people" + }, + { + "emoji": "👩‍⚖️", + "name": "woman judge", + "keywords": ["justice"], + "category": "people" + }, + { + "emoji": "🧑‍🌾", + "name": "farmer", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🌾", + "name": "man farmer", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍🌾", + "name": "woman farmer", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧑‍🍳", + "name": "cook", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🍳", + "name": "man cook", + "keywords": ["chef"], + "category": "people" + }, + { + "emoji": "👩‍🍳", + "name": "woman cook", + "keywords": ["chef"], + "category": "people" + }, + { + "emoji": "🧑‍🔧", + "name": "mechanic", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🔧", + "name": "man mechanic", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍🔧", + "name": "woman mechanic", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧑‍🏭", + "name": "factory worker", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🏭", + "name": "man factory worker", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍🏭", + "name": "woman factory worker", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧑‍💼", + "name": "office worker", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍💼", + "name": "man office worker", + "keywords": ["business"], + "category": "people" + }, + { + "emoji": "👩‍💼", + "name": "woman office worker", + "keywords": ["business"], + "category": "people" + }, + { + "emoji": "🧑‍🔬", + "name": "scientist", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🔬", + "name": "man scientist", + "keywords": ["research"], + "category": "people" + }, + { + "emoji": "👩‍🔬", + "name": "woman scientist", + "keywords": ["research"], + "category": "people" + }, + { + "emoji": "🧑‍💻", + "name": "technologist", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍💻", + "name": "man technologist", + "keywords": ["coder"], + "category": "people" + }, + { + "emoji": "👩‍💻", + "name": "woman technologist", + "keywords": ["coder"], + "category": "people" + }, + { + "emoji": "🧑‍🎤", + "name": "singer", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🎤", + "name": "man singer", + "keywords": ["rockstar"], + "category": "people" + }, + { + "emoji": "👩‍🎤", + "name": "woman singer", + "keywords": ["rockstar"], + "category": "people" + }, + { + "emoji": "🧑‍🎨", + "name": "artist", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🎨", + "name": "man artist", + "keywords": ["painter"], + "category": "people" + }, + { + "emoji": "👩‍🎨", + "name": "woman artist", + "keywords": ["painter"], + "category": "people" + }, + { + "emoji": "🧑‍✈️", + "name": "pilot", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍✈️", + "name": "man pilot", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍✈️", + "name": "woman pilot", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧑‍🚀", + "name": "astronaut", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🚀", + "name": "man astronaut", + "keywords": ["space"], + "category": "people" + }, + { + "emoji": "👩‍🚀", + "name": "woman astronaut", + "keywords": ["space"], + "category": "people" + }, + { + "emoji": "🧑‍🚒", + "name": "firefighter", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🚒", + "name": "man firefighter", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍🚒", + "name": "woman firefighter", + "keywords": [], + "category": "people" + }, + { + "emoji": "👮", + "name": "police officer", + "keywords": ["law"], + "category": "people" + }, + { + "emoji": "👮‍♂️", + "name": "man police officer", + "keywords": ["law", "cop"], + "category": "people" + }, + { + "emoji": "👮‍♀️", + "name": "woman police officer", + "keywords": ["law", "cop"], + "category": "people" + }, + { + "emoji": "🕵️", + "name": "detective", + "keywords": ["sleuth"], + "category": "people" + }, + { + "emoji": "🕵️‍♂️", + "name": "man detective", + "keywords": ["sleuth"], + "category": "people" + }, + { + "emoji": "🕵️‍♀️", + "name": "woman detective", + "keywords": ["sleuth"], + "category": "people" + }, + { + "emoji": "💂", + "name": "guard", + "keywords": [], + "category": "people" + }, + { + "emoji": "💂‍♂️", + "name": "man guard", + "keywords": [], + "category": "people" + }, + { + "emoji": "💂‍♀️", + "name": "woman guard", + "keywords": [], + "category": "people" + }, + { + "emoji": "🥷", + "name": "ninja", + "keywords": [], + "category": "people" + }, + { + "emoji": "👷", + "name": "construction worker", + "keywords": ["helmet"], + "category": "people" + }, + { + "emoji": "👷‍♂️", + "name": "man construction worker", + "keywords": ["helmet"], + "category": "people" + }, + { + "emoji": "👷‍♀️", + "name": "woman construction worker", + "keywords": ["helmet"], + "category": "people" + }, + { + "emoji": "🫅", + "name": "person with crown", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤴", + "name": "prince", + "keywords": ["crown", "royal"], + "category": "people" + }, + { + "emoji": "👸", + "name": "princess", + "keywords": ["crown", "royal"], + "category": "people" + }, + { + "emoji": "👳", + "name": "person wearing turban", + "keywords": [], + "category": "people" + }, + { + "emoji": "👳‍♂️", + "name": "man wearing turban", + "keywords": [], + "category": "people" + }, + { + "emoji": "👳‍♀️", + "name": "woman wearing turban", + "keywords": [], + "category": "people" + }, + { + "emoji": "👲", + "name": "person with skullcap", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧕", + "name": "woman with headscarf", + "keywords": ["hijab"], + "category": "people" + }, + { + "emoji": "🤵", + "name": "person in tuxedo", + "keywords": ["groom", "marriage", "wedding"], + "category": "people" + }, + { + "emoji": "🤵‍♂️", + "name": "man in tuxedo", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤵‍♀️", + "name": "woman in tuxedo", + "keywords": [], + "category": "people" + }, + { + "emoji": "👰", + "name": "person with veil", + "keywords": ["marriage", "wedding"], + "category": "people" + }, + { + "emoji": "👰‍♂️", + "name": "man with veil", + "keywords": [], + "category": "people" + }, + { + "emoji": "👰‍♀️", + "name": "woman with veil", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤰", + "name": "pregnant woman", + "keywords": [], + "category": "people" + }, + { + "emoji": "🫃", + "name": "pregnant man", + "keywords": [], + "category": "people" + }, + { + "emoji": "🫄", + "name": "pregnant person", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤱", + "name": "breast-feeding", + "keywords": ["nursing"], + "category": "people" + }, + { + "emoji": "👩‍🍼", + "name": "woman feeding baby", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🍼", + "name": "man feeding baby", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧑‍🍼", + "name": "person feeding baby", + "keywords": [], + "category": "people" + }, + { + "emoji": "👼", + "name": "baby angel", + "keywords": [], + "category": "people" + }, + { + "emoji": "🎅", + "name": "Santa Claus", + "keywords": ["christmas"], + "category": "people" + }, + { + "emoji": "🤶", + "name": "Mrs. Claus", + "keywords": ["santa"], + "category": "people" + }, + { + "emoji": "🧑‍🎄", + "name": "mx claus", + "keywords": [], + "category": "people" + }, + { + "emoji": "🦸", + "name": "superhero", + "keywords": [], + "category": "people" + }, + { + "emoji": "🦸‍♂️", + "name": "man superhero", + "keywords": [], + "category": "people" + }, + { + "emoji": "🦸‍♀️", + "name": "woman superhero", + "keywords": [], + "category": "people" + }, + { + "emoji": "🦹", + "name": "supervillain", + "keywords": [], + "category": "people" + }, + { + "emoji": "🦹‍♂️", + "name": "man supervillain", + "keywords": [], + "category": "people" + }, + { + "emoji": "🦹‍♀️", + "name": "woman supervillain", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧙", + "name": "mage", + "keywords": ["wizard"], + "category": "people" + }, + { + "emoji": "🧙‍♂️", + "name": "man mage", + "keywords": ["wizard"], + "category": "people" + }, + { + "emoji": "🧙‍♀️", + "name": "woman mage", + "keywords": ["wizard"], + "category": "people" + }, + { + "emoji": "🧚", + "name": "fairy", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧚‍♂️", + "name": "man fairy", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧚‍♀️", + "name": "woman fairy", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧛", + "name": "vampire", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧛‍♂️", + "name": "man vampire", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧛‍♀️", + "name": "woman vampire", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧜", + "name": "merperson", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧜‍♂️", + "name": "merman", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧜‍♀️", + "name": "mermaid", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧝", + "name": "elf", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧝‍♂️", + "name": "man elf", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧝‍♀️", + "name": "woman elf", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧞", + "name": "genie", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧞‍♂️", + "name": "man genie", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧞‍♀️", + "name": "woman genie", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧟", + "name": "zombie", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧟‍♂️", + "name": "man zombie", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧟‍♀️", + "name": "woman zombie", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧌", + "name": "troll", + "keywords": [], + "category": "people" + }, + { + "emoji": "💆", + "name": "person getting massage", + "keywords": ["spa"], + "category": "people" + }, + { + "emoji": "💆‍♂️", + "name": "man getting massage", + "keywords": ["spa"], + "category": "people" + }, + { + "emoji": "💆‍♀️", + "name": "woman getting massage", + "keywords": ["spa"], + "category": "people" + }, + { + "emoji": "💇", + "name": "person getting haircut", + "keywords": ["beauty"], + "category": "people" + }, + { + "emoji": "💇‍♂️", + "name": "man getting haircut", + "keywords": [], + "category": "people" + }, + { + "emoji": "💇‍♀️", + "name": "woman getting haircut", + "keywords": [], + "category": "people" + }, + { + "emoji": "🚶", + "name": "person walking", + "keywords": [], + "category": "people" + }, + { + "emoji": "🚶‍♂️", + "name": "man walking", + "keywords": [], + "category": "people" + }, + { + "emoji": "🚶‍♀️", + "name": "woman walking", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧍", + "name": "person standing", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧍‍♂️", + "name": "man standing", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧍‍♀️", + "name": "woman standing", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧎", + "name": "person kneeling", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧎‍♂️", + "name": "man kneeling", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧎‍♀️", + "name": "woman kneeling", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧑‍🦯", + "name": "person with white cane", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🦯", + "name": "man with white cane", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍🦯", + "name": "woman with white cane", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧑‍🦼", + "name": "person in motorized wheelchair", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🦼", + "name": "man in motorized wheelchair", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍🦼", + "name": "woman in motorized wheelchair", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧑‍🦽", + "name": "person in manual wheelchair", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍🦽", + "name": "man in manual wheelchair", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍🦽", + "name": "woman in manual wheelchair", + "keywords": [], + "category": "people" + }, + { + "emoji": "🏃", + "name": "person running", + "keywords": ["exercise", "workout", "marathon"], + "category": "people" + }, + { + "emoji": "🏃‍♂️", + "name": "man running", + "keywords": ["exercise", "workout", "marathon"], + "category": "people" + }, + { + "emoji": "🏃‍♀️", + "name": "woman running", + "keywords": ["exercise", "workout", "marathon"], + "category": "people" + }, + { + "emoji": "💃", + "name": "woman dancing", + "keywords": ["dress"], + "category": "people" + }, + { + "emoji": "🕺", + "name": "man dancing", + "keywords": ["dancer"], + "category": "people" + }, + { + "emoji": "🕴️", + "name": "person in suit levitating", + "keywords": [], + "category": "people" + }, + { + "emoji": "👯", + "name": "people with bunny ears", + "keywords": ["bunny"], + "category": "people" + }, + { + "emoji": "👯‍♂️", + "name": "men with bunny ears", + "keywords": ["bunny"], + "category": "people" + }, + { + "emoji": "👯‍♀️", + "name": "women with bunny ears", + "keywords": ["bunny"], + "category": "people" + }, + { + "emoji": "🧖", + "name": "person in steamy room", + "keywords": ["steamy"], + "category": "people" + }, + { + "emoji": "🧖‍♂️", + "name": "man in steamy room", + "keywords": ["steamy"], + "category": "people" + }, + { + "emoji": "🧖‍♀️", + "name": "woman in steamy room", + "keywords": ["steamy"], + "category": "people" + }, + { + "emoji": "🧗", + "name": "person climbing", + "keywords": ["bouldering"], + "category": "people" + }, + { + "emoji": "🧗‍♂️", + "name": "man climbing", + "keywords": ["bouldering"], + "category": "people" + }, + { + "emoji": "🧗‍♀️", + "name": "woman climbing", + "keywords": ["bouldering"], + "category": "people" + }, + { + "emoji": "🤺", + "name": "person fencing", + "keywords": [], + "category": "people" + }, + { + "emoji": "🏇", + "name": "horse racing", + "keywords": [], + "category": "people" + }, + { + "emoji": "⛷️", + "name": "skier", + "keywords": [], + "category": "people" + }, + { + "emoji": "🏂", + "name": "snowboarder", + "keywords": [], + "category": "people" + }, + { + "emoji": "🏌️", + "name": "person golfing", + "keywords": [], + "category": "people" + }, + { + "emoji": "🏌️‍♂️", + "name": "man golfing", + "keywords": [], + "category": "people" + }, + { + "emoji": "🏌️‍♀️", + "name": "woman golfing", + "keywords": [], + "category": "people" + }, + { + "emoji": "🏄", + "name": "person surfing", + "keywords": [], + "category": "people" + }, + { + "emoji": "🏄‍♂️", + "name": "man surfing", + "keywords": [], + "category": "people" + }, + { + "emoji": "🏄‍♀️", + "name": "woman surfing", + "keywords": [], + "category": "people" + }, + { + "emoji": "🚣", + "name": "person rowing boat", + "keywords": [], + "category": "people" + }, + { + "emoji": "🚣‍♂️", + "name": "man rowing boat", + "keywords": [], + "category": "people" + }, + { + "emoji": "🚣‍♀️", + "name": "woman rowing boat", + "keywords": [], + "category": "people" + }, + { + "emoji": "🏊", + "name": "person swimming", + "keywords": [], + "category": "people" + }, + { + "emoji": "🏊‍♂️", + "name": "man swimming", + "keywords": [], + "category": "people" + }, + { + "emoji": "🏊‍♀️", + "name": "woman swimming", + "keywords": [], + "category": "people" + }, + { + "emoji": "⛹️", + "name": "person bouncing ball", + "keywords": ["basketball"], + "category": "people" + }, + { + "emoji": "⛹️‍♂️", + "name": "man bouncing ball", + "keywords": [], + "category": "people" + }, + { + "emoji": "⛹️‍♀️", + "name": "woman bouncing ball", + "keywords": [], + "category": "people" + }, + { + "emoji": "🏋️", + "name": "person lifting weights", + "keywords": ["gym", "workout"], + "category": "people" + }, + { + "emoji": "🏋️‍♂️", + "name": "man lifting weights", + "keywords": ["gym", "workout"], + "category": "people" + }, + { + "emoji": "🏋️‍♀️", + "name": "woman lifting weights", + "keywords": ["gym", "workout"], + "category": "people" + }, + { + "emoji": "🚴", + "name": "person biking", + "keywords": [], + "category": "people" + }, + { + "emoji": "🚴‍♂️", + "name": "man biking", + "keywords": [], + "category": "people" + }, + { + "emoji": "🚴‍♀️", + "name": "woman biking", + "keywords": [], + "category": "people" + }, + { + "emoji": "🚵", + "name": "person mountain biking", + "keywords": [], + "category": "people" + }, + { + "emoji": "🚵‍♂️", + "name": "man mountain biking", + "keywords": [], + "category": "people" + }, + { + "emoji": "🚵‍♀️", + "name": "woman mountain biking", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤸", + "name": "person cartwheeling", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤸‍♂️", + "name": "man cartwheeling", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤸‍♀️", + "name": "woman cartwheeling", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤼", + "name": "people wrestling", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤼‍♂️", + "name": "men wrestling", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤼‍♀️", + "name": "women wrestling", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤽", + "name": "person playing water polo", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤽‍♂️", + "name": "man playing water polo", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤽‍♀️", + "name": "woman playing water polo", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤾", + "name": "person playing handball", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤾‍♂️", + "name": "man playing handball", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤾‍♀️", + "name": "woman playing handball", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤹", + "name": "person juggling", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤹‍♂️", + "name": "man juggling", + "keywords": [], + "category": "people" + }, + { + "emoji": "🤹‍♀️", + "name": "woman juggling", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧘", + "name": "person in lotus position", + "keywords": ["meditation"], + "category": "people" + }, + { + "emoji": "🧘‍♂️", + "name": "man in lotus position", + "keywords": ["meditation"], + "category": "people" + }, + { + "emoji": "🧘‍♀️", + "name": "woman in lotus position", + "keywords": ["meditation"], + "category": "people" + }, + { + "emoji": "🛀", + "name": "person taking bath", + "keywords": ["shower"], + "category": "people" + }, + { + "emoji": "🛌", + "name": "person in bed", + "keywords": [], + "category": "people" + }, + { + "emoji": "🧑‍🤝‍🧑", + "name": "people holding hands", + "keywords": ["couple", "date"], + "category": "people" + }, + { + "emoji": "👭", + "name": "women holding hands", + "keywords": ["couple", "date"], + "category": "people" + }, + { + "emoji": "👫", + "name": "woman and man holding hands", + "keywords": ["date"], + "category": "people" + }, + { + "emoji": "👬", + "name": "men holding hands", + "keywords": ["couple", "date"], + "category": "people" + }, + { + "emoji": "💏", + "name": "kiss", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍❤️‍💋‍👨", + "name": "kiss: woman, man", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍❤️‍💋‍👨", + "name": "kiss: man, man", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍❤️‍💋‍👩", + "name": "kiss: woman, woman", + "keywords": [], + "category": "people" + }, + { + "emoji": "💑", + "name": "couple with heart", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍❤️‍👨", + "name": "couple with heart: woman, man", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍❤️‍👨", + "name": "couple with heart: man, man", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍❤️‍👩", + "name": "couple with heart: woman, woman", + "keywords": [], + "category": "people" + }, + { + "emoji": "👪", + "name": "family", + "keywords": ["home", "parents", "child"], + "category": "people" + }, + { + "emoji": "👨‍👩‍👦", + "name": "family: man, woman, boy", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍👩‍👧", + "name": "family: man, woman, girl", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍👩‍👧‍👦", + "name": "family: man, woman, girl, boy", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍👩‍👦‍👦", + "name": "family: man, woman, boy, boy", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍👩‍👧‍👧", + "name": "family: man, woman, girl, girl", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍👨‍👦", + "name": "family: man, man, boy", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍👨‍👧", + "name": "family: man, man, girl", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍👨‍👧‍👦", + "name": "family: man, man, girl, boy", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍👨‍👦‍👦", + "name": "family: man, man, boy, boy", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍👨‍👧‍👧", + "name": "family: man, man, girl, girl", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍👩‍👦", + "name": "family: woman, woman, boy", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍👩‍👧", + "name": "family: woman, woman, girl", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍👩‍👧‍👦", + "name": "family: woman, woman, girl, boy", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍👩‍👦‍👦", + "name": "family: woman, woman, boy, boy", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍👩‍👧‍👧", + "name": "family: woman, woman, girl, girl", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍👦", + "name": "family: man, boy", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍👦‍👦", + "name": "family: man, boy, boy", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍👧", + "name": "family: man, girl", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍👧‍👦", + "name": "family: man, girl, boy", + "keywords": [], + "category": "people" + }, + { + "emoji": "👨‍👧‍👧", + "name": "family: man, girl, girl", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍👦", + "name": "family: woman, boy", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍👦‍👦", + "name": "family: woman, boy, boy", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍👧", + "name": "family: woman, girl", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍👧‍👦", + "name": "family: woman, girl, boy", + "keywords": [], + "category": "people" + }, + { + "emoji": "👩‍👧‍👧", + "name": "family: woman, girl, girl", + "keywords": [], + "category": "people" + }, + { + "emoji": "🗣️", + "name": "speaking head", + "keywords": [], + "category": "people" + }, + { + "emoji": "👤", + "name": "bust in silhouette", + "keywords": ["user"], + "category": "people" + }, + { + "emoji": "👥", + "name": "busts in silhouette", + "keywords": ["users", "group", "team"], + "category": "people" + }, + { + "emoji": "🫂", + "name": "people hugging", + "keywords": [], + "category": "people" + }, + { + "emoji": "👣", + "name": "footprints", + "keywords": ["feet", "tracks"], + "category": "people" + }, + { + "emoji": "🐵", + "name": "monkey face", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐒", + "name": "monkey", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦍", + "name": "gorilla", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦧", + "name": "orangutan", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐶", + "name": "dog face", + "keywords": ["pet"], + "category": "animals" + }, + { + "emoji": "🐕", + "name": "dog", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦮", + "name": "guide dog", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐕‍🦺", + "name": "service dog", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐩", + "name": "poodle", + "keywords": ["dog"], + "category": "animals" + }, + { + "emoji": "🐺", + "name": "wolf", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦊", + "name": "fox", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦝", + "name": "raccoon", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐱", + "name": "cat face", + "keywords": ["pet"], + "category": "animals" + }, + { + "emoji": "🐈", + "name": "cat", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐈‍⬛", + "name": "black cat", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦁", + "name": "lion", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐯", + "name": "tiger face", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐅", + "name": "tiger", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐆", + "name": "leopard", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐴", + "name": "horse face", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🫎", + "name": "moose", + "keywords": ["canada"], + "category": "animals" + }, + { + "emoji": "🫏", + "name": "donkey", + "keywords": ["mule"], + "category": "animals" + }, + { + "emoji": "🐎", + "name": "horse", + "keywords": ["speed"], + "category": "animals" + }, + { + "emoji": "🦄", + "name": "unicorn", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦓", + "name": "zebra", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦌", + "name": "deer", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦬", + "name": "bison", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐮", + "name": "cow face", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐂", + "name": "ox", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐃", + "name": "water buffalo", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐄", + "name": "cow", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐷", + "name": "pig face", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐖", + "name": "pig", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐗", + "name": "boar", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐽", + "name": "pig nose", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐏", + "name": "ram", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐑", + "name": "ewe", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐐", + "name": "goat", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐪", + "name": "camel", + "keywords": ["desert"], + "category": "animals" + }, + { + "emoji": "🐫", + "name": "two-hump camel", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦙", + "name": "llama", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦒", + "name": "giraffe", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐘", + "name": "elephant", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦣", + "name": "mammoth", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦏", + "name": "rhinoceros", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦛", + "name": "hippopotamus", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐭", + "name": "mouse face", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐁", + "name": "mouse", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐀", + "name": "rat", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐹", + "name": "hamster", + "keywords": ["pet"], + "category": "animals" + }, + { + "emoji": "🐰", + "name": "rabbit face", + "keywords": ["bunny"], + "category": "animals" + }, + { + "emoji": "🐇", + "name": "rabbit", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐿️", + "name": "chipmunk", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦫", + "name": "beaver", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦔", + "name": "hedgehog", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦇", + "name": "bat", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐻", + "name": "bear", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐻‍❄️", + "name": "polar bear", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐨", + "name": "koala", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐼", + "name": "panda", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦥", + "name": "sloth", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦦", + "name": "otter", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦨", + "name": "skunk", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦘", + "name": "kangaroo", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦡", + "name": "badger", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐾", + "name": "paw prints", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦃", + "name": "turkey", + "keywords": ["thanksgiving"], + "category": "animals" + }, + { + "emoji": "🐔", + "name": "chicken", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐓", + "name": "rooster", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐣", + "name": "hatching chick", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐤", + "name": "baby chick", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐥", + "name": "front-facing baby chick", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐦", + "name": "bird", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐧", + "name": "penguin", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🕊️", + "name": "dove", + "keywords": ["peace"], + "category": "animals" + }, + { + "emoji": "🦅", + "name": "eagle", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦆", + "name": "duck", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦢", + "name": "swan", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦉", + "name": "owl", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦤", + "name": "dodo", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🪶", + "name": "feather", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦩", + "name": "flamingo", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦚", + "name": "peacock", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦜", + "name": "parrot", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🪽", + "name": "wing", + "keywords": ["fly"], + "category": "animals" + }, + { + "emoji": "🐦‍⬛", + "name": "black bird", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🪿", + "name": "goose", + "keywords": ["honk"], + "category": "animals" + }, + { + "emoji": "🐸", + "name": "frog", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐊", + "name": "crocodile", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐢", + "name": "turtle", + "keywords": ["slow"], + "category": "animals" + }, + { + "emoji": "🦎", + "name": "lizard", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐍", + "name": "snake", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐲", + "name": "dragon face", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐉", + "name": "dragon", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦕", + "name": "sauropod", + "keywords": ["dinosaur"], + "category": "animals" + }, + { + "emoji": "🦖", + "name": "T-Rex", + "keywords": ["dinosaur"], + "category": "animals" + }, + { + "emoji": "🐳", + "name": "spouting whale", + "keywords": ["sea"], + "category": "animals" + }, + { + "emoji": "🐋", + "name": "whale", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐬", + "name": "dolphin", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦭", + "name": "seal", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐟", + "name": "fish", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐠", + "name": "tropical fish", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐡", + "name": "blowfish", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦈", + "name": "shark", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐙", + "name": "octopus", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🪼", + "name": "jellyfish", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐌", + "name": "snail", + "keywords": ["slow"], + "category": "animals" + }, + { + "emoji": "🦋", + "name": "butterfly", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐛", + "name": "bug", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐜", + "name": "ant", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐝", + "name": "honeybee", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🪲", + "name": "beetle", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐞", + "name": "lady beetle", + "keywords": ["bug"], + "category": "animals" + }, + { + "emoji": "🦗", + "name": "cricket", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🪳", + "name": "cockroach", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🕷️", + "name": "spider", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦂", + "name": "scorpion", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🦟", + "name": "mosquito", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🪰", + "name": "fly", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🪱", + "name": "worm", + "keywords": [], + "category": "animals" + }, + { + "emoji": "🐚", + "name": "spiral shell", + "keywords": ["sea", "beach"], + "category": "nature" + }, + { + "emoji": "🪸", + "name": "coral", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🕸️", + "name": "spider web", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🦠", + "name": "microbe", + "keywords": ["germ"], + "category": "nature" + }, + { + "emoji": "💐", + "name": "bouquet", + "keywords": ["flowers"], + "category": "nature" + }, + { + "emoji": "🌸", + "name": "cherry blossom", + "keywords": ["flower", "spring"], + "category": "nature" + }, + { + "emoji": "💮", + "name": "white flower", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🪷", + "name": "lotus", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🏵️", + "name": "rosette", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🌹", + "name": "rose", + "keywords": ["flower"], + "category": "nature" + }, + { + "emoji": "🥀", + "name": "wilted flower", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🌺", + "name": "hibiscus", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🌻", + "name": "sunflower", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🌼", + "name": "blossom", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🌷", + "name": "tulip", + "keywords": ["flower"], + "category": "nature" + }, + { + "emoji": "🪻", + "name": "hyacinth", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🌱", + "name": "seedling", + "keywords": ["plant"], + "category": "nature" + }, + { + "emoji": "🪴", + "name": "potted plant", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🌲", + "name": "evergreen tree", + "keywords": ["wood"], + "category": "nature" + }, + { + "emoji": "🌳", + "name": "deciduous tree", + "keywords": ["wood"], + "category": "nature" + }, + { + "emoji": "🌴", + "name": "palm tree", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🌵", + "name": "cactus", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🌾", + "name": "sheaf of rice", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🌿", + "name": "herb", + "keywords": [], + "category": "nature" + }, + { + "emoji": "☘️", + "name": "shamrock", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🍀", + "name": "four leaf clover", + "keywords": ["luck"], + "category": "nature" + }, + { + "emoji": "🍁", + "name": "maple leaf", + "keywords": ["canada"], + "category": "nature" + }, + { + "emoji": "🍂", + "name": "fallen leaf", + "keywords": ["autumn"], + "category": "nature" + }, + { + "emoji": "🍃", + "name": "leaf fluttering in wind", + "keywords": ["leaf"], + "category": "nature" + }, + { + "emoji": "🪹", + "name": "empty nest", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🪺", + "name": "nest with eggs", + "keywords": [], + "category": "nature" + }, + { + "emoji": "🍄", + "name": "mushroom", + "keywords": ["fungus"], + "category": "nature" + }, + { + "emoji": "🍇", + "name": "grapes", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍈", + "name": "melon", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍉", + "name": "watermelon", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍊", + "name": "tangerine", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍋", + "name": "lemon", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍌", + "name": "banana", + "keywords": ["fruit"], + "category": "food" + }, + { + "emoji": "🍍", + "name": "pineapple", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥭", + "name": "mango", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍎", + "name": "red apple", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍏", + "name": "green apple", + "keywords": ["fruit"], + "category": "food" + }, + { + "emoji": "🍐", + "name": "pear", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍑", + "name": "peach", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍒", + "name": "cherries", + "keywords": ["fruit"], + "category": "food" + }, + { + "emoji": "🍓", + "name": "strawberry", + "keywords": ["fruit"], + "category": "food" + }, + { + "emoji": "🫐", + "name": "blueberries", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥝", + "name": "kiwi fruit", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍅", + "name": "tomato", + "keywords": [], + "category": "food" + }, + { + "emoji": "🫒", + "name": "olive", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥥", + "name": "coconut", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥑", + "name": "avocado", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍆", + "name": "eggplant", + "keywords": ["aubergine"], + "category": "food" + }, + { + "emoji": "🥔", + "name": "potato", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥕", + "name": "carrot", + "keywords": [], + "category": "food" + }, + { + "emoji": "🌽", + "name": "ear of corn", + "keywords": [], + "category": "food" + }, + { + "emoji": "🌶️", + "name": "hot pepper", + "keywords": ["spicy"], + "category": "food" + }, + { + "emoji": "🫑", + "name": "bell pepper", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥒", + "name": "cucumber", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥬", + "name": "leafy green", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥦", + "name": "broccoli", + "keywords": [], + "category": "food" + }, + { + "emoji": "🧄", + "name": "garlic", + "keywords": [], + "category": "food" + }, + { + "emoji": "🧅", + "name": "onion", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥜", + "name": "peanuts", + "keywords": [], + "category": "food" + }, + { + "emoji": "🫘", + "name": "beans", + "keywords": [], + "category": "food" + }, + { + "emoji": "🌰", + "name": "chestnut", + "keywords": [], + "category": "food" + }, + { + "emoji": "🫚", + "name": "ginger root", + "keywords": [], + "category": "food" + }, + { + "emoji": "🫛", + "name": "pea pod", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍞", + "name": "bread", + "keywords": ["toast"], + "category": "food" + }, + { + "emoji": "🥐", + "name": "croissant", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥖", + "name": "baguette bread", + "keywords": [], + "category": "food" + }, + { + "emoji": "🫓", + "name": "flatbread", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥨", + "name": "pretzel", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥯", + "name": "bagel", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥞", + "name": "pancakes", + "keywords": [], + "category": "food" + }, + { + "emoji": "🧇", + "name": "waffle", + "keywords": [], + "category": "food" + }, + { + "emoji": "🧀", + "name": "cheese wedge", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍖", + "name": "meat on bone", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍗", + "name": "poultry leg", + "keywords": ["meat", "chicken"], + "category": "food" + }, + { + "emoji": "🥩", + "name": "cut of meat", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥓", + "name": "bacon", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍔", + "name": "hamburger", + "keywords": ["burger"], + "category": "food" + }, + { + "emoji": "🍟", + "name": "french fries", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍕", + "name": "pizza", + "keywords": [], + "category": "food" + }, + { + "emoji": "🌭", + "name": "hot dog", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥪", + "name": "sandwich", + "keywords": [], + "category": "food" + }, + { + "emoji": "🌮", + "name": "taco", + "keywords": [], + "category": "food" + }, + { + "emoji": "🌯", + "name": "burrito", + "keywords": [], + "category": "food" + }, + { + "emoji": "🫔", + "name": "tamale", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥙", + "name": "stuffed flatbread", + "keywords": [], + "category": "food" + }, + { + "emoji": "🧆", + "name": "falafel", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥚", + "name": "egg", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍳", + "name": "cooking", + "keywords": ["breakfast"], + "category": "food" + }, + { + "emoji": "🥘", + "name": "shallow pan of food", + "keywords": ["paella", "curry"], + "category": "food" + }, + { + "emoji": "🍲", + "name": "pot of food", + "keywords": [], + "category": "food" + }, + { + "emoji": "🫕", + "name": "fondue", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥣", + "name": "bowl with spoon", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥗", + "name": "green salad", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍿", + "name": "popcorn", + "keywords": [], + "category": "food" + }, + { + "emoji": "🧈", + "name": "butter", + "keywords": [], + "category": "food" + }, + { + "emoji": "🧂", + "name": "salt", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥫", + "name": "canned food", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍱", + "name": "bento box", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍘", + "name": "rice cracker", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍙", + "name": "rice ball", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍚", + "name": "cooked rice", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍛", + "name": "curry rice", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍜", + "name": "steaming bowl", + "keywords": ["noodle"], + "category": "food" + }, + { + "emoji": "🍝", + "name": "spaghetti", + "keywords": ["pasta"], + "category": "food" + }, + { + "emoji": "🍠", + "name": "roasted sweet potato", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍢", + "name": "oden", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍣", + "name": "sushi", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍤", + "name": "fried shrimp", + "keywords": ["tempura"], + "category": "food" + }, + { + "emoji": "🍥", + "name": "fish cake with swirl", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥮", + "name": "moon cake", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍡", + "name": "dango", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥟", + "name": "dumpling", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥠", + "name": "fortune cookie", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥡", + "name": "takeout box", + "keywords": [], + "category": "food" + }, + { + "emoji": "🦀", + "name": "crab", + "keywords": [], + "category": "food" + }, + { + "emoji": "🦞", + "name": "lobster", + "keywords": [], + "category": "food" + }, + { + "emoji": "🦐", + "name": "shrimp", + "keywords": [], + "category": "food" + }, + { + "emoji": "🦑", + "name": "squid", + "keywords": [], + "category": "food" + }, + { + "emoji": "🦪", + "name": "oyster", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍦", + "name": "soft ice cream", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍧", + "name": "shaved ice", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍨", + "name": "ice cream", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍩", + "name": "doughnut", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍪", + "name": "cookie", + "keywords": [], + "category": "food" + }, + { + "emoji": "🎂", + "name": "birthday cake", + "keywords": ["party"], + "category": "food" + }, + { + "emoji": "🍰", + "name": "shortcake", + "keywords": ["dessert"], + "category": "food" + }, + { + "emoji": "🧁", + "name": "cupcake", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥧", + "name": "pie", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍫", + "name": "chocolate bar", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍬", + "name": "candy", + "keywords": ["sweet"], + "category": "food" + }, + { + "emoji": "🍭", + "name": "lollipop", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍮", + "name": "custard", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍯", + "name": "honey pot", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍼", + "name": "baby bottle", + "keywords": ["milk"], + "category": "food" + }, + { + "emoji": "🥛", + "name": "glass of milk", + "keywords": [], + "category": "food" + }, + { + "emoji": "☕", + "name": "hot beverage", + "keywords": ["cafe", "espresso"], + "category": "food" + }, + { + "emoji": "🫖", + "name": "teapot", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍵", + "name": "teacup without handle", + "keywords": ["green", "breakfast"], + "category": "food" + }, + { + "emoji": "🍶", + "name": "sake", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍾", + "name": "bottle with popping cork", + "keywords": ["bottle", "bubbly", "celebration"], + "category": "food" + }, + { + "emoji": "🍷", + "name": "wine glass", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍸", + "name": "cocktail glass", + "keywords": ["drink"], + "category": "food" + }, + { + "emoji": "🍹", + "name": "tropical drink", + "keywords": ["summer", "vacation"], + "category": "food" + }, + { + "emoji": "🍺", + "name": "beer mug", + "keywords": ["drink"], + "category": "food" + }, + { + "emoji": "🍻", + "name": "clinking beer mugs", + "keywords": ["drinks"], + "category": "food" + }, + { + "emoji": "🥂", + "name": "clinking glasses", + "keywords": ["cheers", "toast"], + "category": "food" + }, + { + "emoji": "🥃", + "name": "tumbler glass", + "keywords": ["whisky"], + "category": "food" + }, + { + "emoji": "🫗", + "name": "pouring liquid", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥤", + "name": "cup with straw", + "keywords": [], + "category": "food" + }, + { + "emoji": "🧋", + "name": "bubble tea", + "keywords": [], + "category": "food" + }, + { + "emoji": "🧃", + "name": "beverage box", + "keywords": [], + "category": "food" + }, + { + "emoji": "🧉", + "name": "mate", + "keywords": [], + "category": "food" + }, + { + "emoji": "🧊", + "name": "ice", + "keywords": [], + "category": "food" + }, + { + "emoji": "🥢", + "name": "chopsticks", + "keywords": [], + "category": "food" + }, + { + "emoji": "🍽️", + "name": "fork and knife with plate", + "keywords": ["dining", "dinner"], + "category": "food" + }, + { + "emoji": "🍴", + "name": "fork and knife", + "keywords": ["cutlery"], + "category": "food" + }, + { + "emoji": "🥄", + "name": "spoon", + "keywords": [], + "category": "food" + }, + { + "emoji": "🔪", + "name": "kitchen knife", + "keywords": ["cut", "chop"], + "category": "food" + }, + { + "emoji": "🫙", + "name": "jar", + "keywords": [], + "category": "food" + }, + { + "emoji": "🏺", + "name": "amphora", + "keywords": [], + "category": "food" + }, + { + "emoji": "🌍", + "name": "globe showing Europe-Africa", + "keywords": ["globe", "world", "international"], + "category": "travel" + }, + { + "emoji": "🌎", + "name": "globe showing Americas", + "keywords": ["globe", "world", "international"], + "category": "travel" + }, + { + "emoji": "🌏", + "name": "globe showing Asia-Australia", + "keywords": ["globe", "world", "international"], + "category": "travel" + }, + { + "emoji": "🌐", + "name": "globe with meridians", + "keywords": ["world", "global", "international"], + "category": "travel" + }, + { + "emoji": "🗺️", + "name": "world map", + "keywords": ["travel"], + "category": "travel" + }, + { + "emoji": "🗾", + "name": "map of Japan", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🧭", + "name": "compass", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏔️", + "name": "snow-capped mountain", + "keywords": [], + "category": "travel" + }, + { + "emoji": "⛰️", + "name": "mountain", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌋", + "name": "volcano", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🗻", + "name": "mount fuji", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏕️", + "name": "camping", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏖️", + "name": "beach with umbrella", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏜️", + "name": "desert", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏝️", + "name": "desert island", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏞️", + "name": "national park", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏟️", + "name": "stadium", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏛️", + "name": "classical building", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏗️", + "name": "building construction", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🧱", + "name": "brick", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🪨", + "name": "rock", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🪵", + "name": "wood", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🛖", + "name": "hut", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏘️", + "name": "houses", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏚️", + "name": "derelict house", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏠", + "name": "house", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏡", + "name": "house with garden", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏢", + "name": "office building", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏣", + "name": "Japanese post office", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏤", + "name": "post office", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏥", + "name": "hospital", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏦", + "name": "bank", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏨", + "name": "hotel", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏩", + "name": "love hotel", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏪", + "name": "convenience store", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏫", + "name": "school", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏬", + "name": "department store", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏭", + "name": "factory", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏯", + "name": "Japanese castle", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏰", + "name": "castle", + "keywords": [], + "category": "travel" + }, + { + "emoji": "💒", + "name": "wedding", + "keywords": ["marriage"], + "category": "travel" + }, + { + "emoji": "🗼", + "name": "Tokyo tower", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🗽", + "name": "Statue of Liberty", + "keywords": [], + "category": "travel" + }, + { + "emoji": "⛪", + "name": "church", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕌", + "name": "mosque", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🛕", + "name": "hindu temple", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕍", + "name": "synagogue", + "keywords": [], + "category": "travel" + }, + { + "emoji": "⛩️", + "name": "shinto shrine", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕋", + "name": "kaaba", + "keywords": [], + "category": "travel" + }, + { + "emoji": "⛲", + "name": "fountain", + "keywords": [], + "category": "travel" + }, + { + "emoji": "⛺", + "name": "tent", + "keywords": ["camping"], + "category": "travel" + }, + { + "emoji": "🌁", + "name": "foggy", + "keywords": ["karl"], + "category": "travel" + }, + { + "emoji": "🌃", + "name": "night with stars", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏙️", + "name": "cityscape", + "keywords": ["skyline"], + "category": "travel" + }, + { + "emoji": "🌄", + "name": "sunrise over mountains", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌅", + "name": "sunrise", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌆", + "name": "cityscape at dusk", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌇", + "name": "sunset", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌉", + "name": "bridge at night", + "keywords": [], + "category": "travel" + }, + { + "emoji": "♨️", + "name": "hot springs", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🎠", + "name": "carousel horse", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🛝", + "name": "playground slide", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🎡", + "name": "ferris wheel", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🎢", + "name": "roller coaster", + "keywords": [], + "category": "travel" + }, + { + "emoji": "💈", + "name": "barber pole", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🎪", + "name": "circus tent", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚂", + "name": "locomotive", + "keywords": ["train"], + "category": "travel" + }, + { + "emoji": "🚃", + "name": "railway car", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚄", + "name": "high-speed train", + "keywords": ["train"], + "category": "travel" + }, + { + "emoji": "🚅", + "name": "bullet train", + "keywords": ["train"], + "category": "travel" + }, + { + "emoji": "🚆", + "name": "train", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚇", + "name": "metro", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚈", + "name": "light rail", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚉", + "name": "station", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚊", + "name": "tram", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚝", + "name": "monorail", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚞", + "name": "mountain railway", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚋", + "name": "tram car", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚌", + "name": "bus", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚍", + "name": "oncoming bus", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚎", + "name": "trolleybus", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚐", + "name": "minibus", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚑", + "name": "ambulance", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚒", + "name": "fire engine", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚓", + "name": "police car", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚔", + "name": "oncoming police car", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚕", + "name": "taxi", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚖", + "name": "oncoming taxi", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚗", + "name": "automobile", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚘", + "name": "oncoming automobile", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚙", + "name": "sport utility vehicle", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🛻", + "name": "pickup truck", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚚", + "name": "delivery truck", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚛", + "name": "articulated lorry", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚜", + "name": "tractor", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏎️", + "name": "racing car", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🏍️", + "name": "motorcycle", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🛵", + "name": "motor scooter", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🦽", + "name": "manual wheelchair", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🦼", + "name": "motorized wheelchair", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🛺", + "name": "auto rickshaw", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚲", + "name": "bicycle", + "keywords": ["bicycle"], + "category": "travel" + }, + { + "emoji": "🛴", + "name": "kick scooter", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🛹", + "name": "skateboard", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🛼", + "name": "roller skate", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚏", + "name": "bus stop", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🛣️", + "name": "motorway", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🛤️", + "name": "railway track", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🛢️", + "name": "oil drum", + "keywords": [], + "category": "travel" + }, + { + "emoji": "⛽", + "name": "fuel pump", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🛞", + "name": "wheel", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚨", + "name": "police car light", + "keywords": ["911", "emergency"], + "category": "travel" + }, + { + "emoji": "🚥", + "name": "horizontal traffic light", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚦", + "name": "vertical traffic light", + "keywords": ["semaphore"], + "category": "travel" + }, + { + "emoji": "🛑", + "name": "stop sign", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚧", + "name": "construction", + "keywords": ["wip"], + "category": "travel" + }, + { + "emoji": "⚓", + "name": "anchor", + "keywords": ["ship"], + "category": "travel" + }, + { + "emoji": "🛟", + "name": "ring buoy", + "keywords": ["life preserver"], + "category": "travel" + }, + { + "emoji": "⛵", + "name": "sailboat", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🛶", + "name": "canoe", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚤", + "name": "speedboat", + "keywords": ["ship"], + "category": "travel" + }, + { + "emoji": "🛳️", + "name": "passenger ship", + "keywords": ["cruise"], + "category": "travel" + }, + { + "emoji": "⛴️", + "name": "ferry", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🛥️", + "name": "motor boat", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚢", + "name": "ship", + "keywords": [], + "category": "travel" + }, + { + "emoji": "✈️", + "name": "airplane", + "keywords": ["flight"], + "category": "travel" + }, + { + "emoji": "🛩️", + "name": "small airplane", + "keywords": ["flight"], + "category": "travel" + }, + { + "emoji": "🛫", + "name": "airplane departure", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🛬", + "name": "airplane arrival", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🪂", + "name": "parachute", + "keywords": [], + "category": "travel" + }, + { + "emoji": "💺", + "name": "seat", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚁", + "name": "helicopter", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚟", + "name": "suspension railway", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚠", + "name": "mountain cableway", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🚡", + "name": "aerial tramway", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🛰️", + "name": "satellite", + "keywords": ["orbit", "space"], + "category": "travel" + }, + { + "emoji": "🚀", + "name": "rocket", + "keywords": ["ship", "launch"], + "category": "travel" + }, + { + "emoji": "🛸", + "name": "flying saucer", + "keywords": ["ufo"], + "category": "travel" + }, + { + "emoji": "🛎️", + "name": "bellhop bell", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🧳", + "name": "luggage", + "keywords": [], + "category": "travel" + }, + { + "emoji": "⌛", + "name": "hourglass done", + "keywords": ["time"], + "category": "travel" + }, + { + "emoji": "⏳", + "name": "hourglass not done", + "keywords": ["time"], + "category": "travel" + }, + { + "emoji": "⌚", + "name": "watch", + "keywords": ["time"], + "category": "travel" + }, + { + "emoji": "⏰", + "name": "alarm clock", + "keywords": ["morning"], + "category": "travel" + }, + { + "emoji": "⏱️", + "name": "stopwatch", + "keywords": [], + "category": "travel" + }, + { + "emoji": "⏲️", + "name": "timer clock", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕰️", + "name": "mantelpiece clock", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕛", + "name": "twelve o’clock", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕧", + "name": "twelve-thirty", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕐", + "name": "one o’clock", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕜", + "name": "one-thirty", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕑", + "name": "two o’clock", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕝", + "name": "two-thirty", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕒", + "name": "three o’clock", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕞", + "name": "three-thirty", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕓", + "name": "four o’clock", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕟", + "name": "four-thirty", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕔", + "name": "five o’clock", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕠", + "name": "five-thirty", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕕", + "name": "six o’clock", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕡", + "name": "six-thirty", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕖", + "name": "seven o’clock", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕢", + "name": "seven-thirty", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕗", + "name": "eight o’clock", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕣", + "name": "eight-thirty", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕘", + "name": "nine o’clock", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕤", + "name": "nine-thirty", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕙", + "name": "ten o’clock", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕥", + "name": "ten-thirty", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕚", + "name": "eleven o’clock", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🕦", + "name": "eleven-thirty", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌑", + "name": "new moon", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌒", + "name": "waxing crescent moon", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌓", + "name": "first quarter moon", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌔", + "name": "waxing gibbous moon", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌕", + "name": "full moon", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌖", + "name": "waning gibbous moon", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌗", + "name": "last quarter moon", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌘", + "name": "waning crescent moon", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌙", + "name": "crescent moon", + "keywords": ["night"], + "category": "travel" + }, + { + "emoji": "🌚", + "name": "new moon face", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌛", + "name": "first quarter moon face", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌜", + "name": "last quarter moon face", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌡️", + "name": "thermometer", + "keywords": [], + "category": "travel" + }, + { + "emoji": "☀️", + "name": "sun", + "keywords": ["weather"], + "category": "travel" + }, + { + "emoji": "🌝", + "name": "full moon face", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌞", + "name": "sun with face", + "keywords": ["summer"], + "category": "travel" + }, + { + "emoji": "🪐", + "name": "ringed planet", + "keywords": [], + "category": "travel" + }, + { + "emoji": "⭐", + "name": "star", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌟", + "name": "glowing star", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌠", + "name": "shooting star", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌌", + "name": "milky way", + "keywords": [], + "category": "travel" + }, + { + "emoji": "☁️", + "name": "cloud", + "keywords": [], + "category": "travel" + }, + { + "emoji": "⛅", + "name": "sun behind cloud", + "keywords": ["weather", "cloud"], + "category": "travel" + }, + { + "emoji": "⛈️", + "name": "cloud with lightning and rain", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌤️", + "name": "sun behind small cloud", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌥️", + "name": "sun behind large cloud", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌦️", + "name": "sun behind rain cloud", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌧️", + "name": "cloud with rain", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌨️", + "name": "cloud with snow", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌩️", + "name": "cloud with lightning", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌪️", + "name": "tornado", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌫️", + "name": "fog", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌬️", + "name": "wind face", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌀", + "name": "cyclone", + "keywords": ["swirl"], + "category": "travel" + }, + { + "emoji": "🌈", + "name": "rainbow", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🌂", + "name": "closed umbrella", + "keywords": ["weather", "rain"], + "category": "travel" + }, + { + "emoji": "☂️", + "name": "umbrella", + "keywords": [], + "category": "travel" + }, + { + "emoji": "☔", + "name": "umbrella with rain drops", + "keywords": ["rain", "weather"], + "category": "travel" + }, + { + "emoji": "⛱️", + "name": "umbrella on ground", + "keywords": ["beach_umbrella"], + "category": "travel" + }, + { + "emoji": "⚡", + "name": "high voltage", + "keywords": ["lightning", "thunder"], + "category": "travel" + }, + { + "emoji": "❄️", + "name": "snowflake", + "keywords": ["winter", "cold", "weather"], + "category": "travel" + }, + { + "emoji": "☃️", + "name": "snowman", + "keywords": ["winter", "christmas"], + "category": "travel" + }, + { + "emoji": "⛄", + "name": "snowman without snow", + "keywords": ["winter"], + "category": "travel" + }, + { + "emoji": "☄️", + "name": "comet", + "keywords": [], + "category": "travel" + }, + { + "emoji": "🔥", + "name": "fire", + "keywords": ["burn"], + "category": "travel" + }, + { + "emoji": "💧", + "name": "droplet", + "keywords": ["water"], + "category": "travel" + }, + { + "emoji": "🌊", + "name": "water wave", + "keywords": ["sea"], + "category": "travel" + }, + { + "emoji": "🎃", + "name": "jack-o-lantern", + "keywords": ["halloween"], + "category": "activity" + }, + { + "emoji": "🎄", + "name": "Christmas tree", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎆", + "name": "fireworks", + "keywords": ["festival", "celebration"], + "category": "activity" + }, + { + "emoji": "🎇", + "name": "sparkler", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🧨", + "name": "firecracker", + "keywords": [], + "category": "activity" + }, + { + "emoji": "✨", + "name": "sparkles", + "keywords": ["shiny"], + "category": "activity" + }, + { + "emoji": "🎈", + "name": "balloon", + "keywords": ["party", "birthday"], + "category": "activity" + }, + { + "emoji": "🎉", + "name": "party popper", + "keywords": ["hooray", "party"], + "category": "activity" + }, + { + "emoji": "🎊", + "name": "confetti ball", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎋", + "name": "tanabata tree", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎍", + "name": "pine decoration", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎎", + "name": "Japanese dolls", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎏", + "name": "carp streamer", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎐", + "name": "wind chime", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎑", + "name": "moon viewing ceremony", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🧧", + "name": "red envelope", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎀", + "name": "ribbon", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎁", + "name": "wrapped gift", + "keywords": ["present", "birthday", "christmas"], + "category": "activity" + }, + { + "emoji": "🎗️", + "name": "reminder ribbon", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎟️", + "name": "admission tickets", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎫", + "name": "ticket", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎖️", + "name": "military medal", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🏆", + "name": "trophy", + "keywords": ["award", "contest", "winner"], + "category": "activity" + }, + { + "emoji": "🏅", + "name": "sports medal", + "keywords": ["gold", "winner"], + "category": "activity" + }, + { + "emoji": "🥇", + "name": "1st place medal", + "keywords": ["gold"], + "category": "activity" + }, + { + "emoji": "🥈", + "name": "2nd place medal", + "keywords": ["silver"], + "category": "activity" + }, + { + "emoji": "🥉", + "name": "3rd place medal", + "keywords": ["bronze"], + "category": "activity" + }, + { + "emoji": "⚽", + "name": "soccer ball", + "keywords": ["sports"], + "category": "activity" + }, + { + "emoji": "⚾", + "name": "baseball", + "keywords": ["sports"], + "category": "activity" + }, + { + "emoji": "🥎", + "name": "softball", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🏀", + "name": "basketball", + "keywords": ["sports"], + "category": "activity" + }, + { + "emoji": "🏐", + "name": "volleyball", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🏈", + "name": "american football", + "keywords": ["sports"], + "category": "activity" + }, + { + "emoji": "🏉", + "name": "rugby football", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎾", + "name": "tennis", + "keywords": ["sports"], + "category": "activity" + }, + { + "emoji": "🥏", + "name": "flying disc", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎳", + "name": "bowling", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🏏", + "name": "cricket game", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🏑", + "name": "field hockey", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🏒", + "name": "ice hockey", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🥍", + "name": "lacrosse", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🏓", + "name": "ping pong", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🏸", + "name": "badminton", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🥊", + "name": "boxing glove", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🥋", + "name": "martial arts uniform", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🥅", + "name": "goal net", + "keywords": [], + "category": "activity" + }, + { + "emoji": "⛳", + "name": "flag in hole", + "keywords": [], + "category": "activity" + }, + { + "emoji": "⛸️", + "name": "ice skate", + "keywords": ["skating"], + "category": "activity" + }, + { + "emoji": "🎣", + "name": "fishing pole", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🤿", + "name": "diving mask", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎽", + "name": "running shirt", + "keywords": ["marathon"], + "category": "activity" + }, + { + "emoji": "🎿", + "name": "skis", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🛷", + "name": "sled", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🥌", + "name": "curling stone", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎯", + "name": "bullseye", + "keywords": ["target"], + "category": "activity" + }, + { + "emoji": "🪀", + "name": "yo-yo", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🪁", + "name": "kite", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🔫", + "name": "water pistol", + "keywords": ["shoot", "weapon"], + "category": "activity" + }, + { + "emoji": "🎱", + "name": "pool 8 ball", + "keywords": ["pool", "billiards"], + "category": "activity" + }, + { + "emoji": "🔮", + "name": "crystal ball", + "keywords": ["fortune"], + "category": "activity" + }, + { + "emoji": "🪄", + "name": "magic wand", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎮", + "name": "video game", + "keywords": ["play", "controller", "console"], + "category": "activity" + }, + { + "emoji": "🕹️", + "name": "joystick", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎰", + "name": "slot machine", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎲", + "name": "game die", + "keywords": ["dice", "gambling"], + "category": "activity" + }, + { + "emoji": "🧩", + "name": "puzzle piece", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🧸", + "name": "teddy bear", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🪅", + "name": "piñata", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🪩", + "name": "mirror ball", + "keywords": ["disco", "party"], + "category": "activity" + }, + { + "emoji": "🪆", + "name": "nesting dolls", + "keywords": [], + "category": "activity" + }, + { + "emoji": "♠️", + "name": "spade suit", + "keywords": [], + "category": "activity" + }, + { + "emoji": "♥️", + "name": "heart suit", + "keywords": [], + "category": "activity" + }, + { + "emoji": "♦️", + "name": "diamond suit", + "keywords": [], + "category": "activity" + }, + { + "emoji": "♣️", + "name": "club suit", + "keywords": [], + "category": "activity" + }, + { + "emoji": "♟️", + "name": "chess pawn", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🃏", + "name": "joker", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🀄", + "name": "mahjong red dragon", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎴", + "name": "flower playing cards", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎭", + "name": "performing arts", + "keywords": ["theater", "drama"], + "category": "activity" + }, + { + "emoji": "🖼️", + "name": "framed picture", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🎨", + "name": "artist palette", + "keywords": ["design", "paint"], + "category": "activity" + }, + { + "emoji": "🧵", + "name": "thread", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🪡", + "name": "sewing needle", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🧶", + "name": "yarn", + "keywords": [], + "category": "activity" + }, + { + "emoji": "🪢", + "name": "knot", + "keywords": [], + "category": "activity" + }, + { + "emoji": "👓", + "name": "glasses", + "keywords": ["glasses"], + "category": "objects" + }, + { + "emoji": "🕶️", + "name": "sunglasses", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🥽", + "name": "goggles", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🥼", + "name": "lab coat", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🦺", + "name": "safety vest", + "keywords": [], + "category": "objects" + }, + { + "emoji": "👔", + "name": "necktie", + "keywords": ["shirt", "formal"], + "category": "objects" + }, + { + "emoji": "👕", + "name": "t-shirt", + "keywords": [], + "category": "objects" + }, + { + "emoji": "👖", + "name": "jeans", + "keywords": ["pants"], + "category": "objects" + }, + { + "emoji": "🧣", + "name": "scarf", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧤", + "name": "gloves", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧥", + "name": "coat", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧦", + "name": "socks", + "keywords": [], + "category": "objects" + }, + { + "emoji": "👗", + "name": "dress", + "keywords": [], + "category": "objects" + }, + { + "emoji": "👘", + "name": "kimono", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🥻", + "name": "sari", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🩱", + "name": "one-piece swimsuit", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🩲", + "name": "briefs", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🩳", + "name": "shorts", + "keywords": [], + "category": "objects" + }, + { + "emoji": "👙", + "name": "bikini", + "keywords": ["beach"], + "category": "objects" + }, + { + "emoji": "👚", + "name": "woman’s clothes", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪭", + "name": "folding hand fan", + "keywords": ["sensu"], + "category": "objects" + }, + { + "emoji": "👛", + "name": "purse", + "keywords": [], + "category": "objects" + }, + { + "emoji": "👜", + "name": "handbag", + "keywords": ["bag"], + "category": "objects" + }, + { + "emoji": "👝", + "name": "clutch bag", + "keywords": ["bag"], + "category": "objects" + }, + { + "emoji": "🛍️", + "name": "shopping bags", + "keywords": ["bags"], + "category": "objects" + }, + { + "emoji": "🎒", + "name": "backpack", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🩴", + "name": "thong sandal", + "keywords": [], + "category": "objects" + }, + { + "emoji": "👞", + "name": "man’s shoe", + "keywords": [], + "category": "objects" + }, + { + "emoji": "👟", + "name": "running shoe", + "keywords": ["sneaker", "sport", "running"], + "category": "objects" + }, + { + "emoji": "🥾", + "name": "hiking boot", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🥿", + "name": "flat shoe", + "keywords": [], + "category": "objects" + }, + { + "emoji": "👠", + "name": "high-heeled shoe", + "keywords": ["shoe"], + "category": "objects" + }, + { + "emoji": "👡", + "name": "woman’s sandal", + "keywords": ["shoe"], + "category": "objects" + }, + { + "emoji": "🩰", + "name": "ballet shoes", + "keywords": [], + "category": "objects" + }, + { + "emoji": "👢", + "name": "woman’s boot", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪮", + "name": "hair pick", + "keywords": [], + "category": "objects" + }, + { + "emoji": "👑", + "name": "crown", + "keywords": ["king", "queen", "royal"], + "category": "objects" + }, + { + "emoji": "👒", + "name": "woman’s hat", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🎩", + "name": "top hat", + "keywords": ["hat", "classy"], + "category": "objects" + }, + { + "emoji": "🎓", + "name": "graduation cap", + "keywords": ["education", "college", "university", "graduation"], + "category": "objects" + }, + { + "emoji": "🧢", + "name": "billed cap", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪖", + "name": "military helmet", + "keywords": [], + "category": "objects" + }, + { + "emoji": "⛑️", + "name": "rescue worker’s helmet", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📿", + "name": "prayer beads", + "keywords": [], + "category": "objects" + }, + { + "emoji": "💄", + "name": "lipstick", + "keywords": ["makeup"], + "category": "objects" + }, + { + "emoji": "💍", + "name": "ring", + "keywords": ["wedding", "marriage", "engaged"], + "category": "objects" + }, + { + "emoji": "💎", + "name": "gem stone", + "keywords": ["diamond"], + "category": "objects" + }, + { + "emoji": "🔇", + "name": "muted speaker", + "keywords": ["sound", "volume"], + "category": "objects" + }, + { + "emoji": "🔈", + "name": "speaker low volume", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🔉", + "name": "speaker medium volume", + "keywords": ["volume"], + "category": "objects" + }, + { + "emoji": "🔊", + "name": "speaker high volume", + "keywords": ["volume"], + "category": "objects" + }, + { + "emoji": "📢", + "name": "loudspeaker", + "keywords": ["announcement"], + "category": "objects" + }, + { + "emoji": "📣", + "name": "megaphone", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📯", + "name": "postal horn", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🔔", + "name": "bell", + "keywords": ["sound", "notification"], + "category": "objects" + }, + { + "emoji": "🔕", + "name": "bell with slash", + "keywords": ["volume", "off"], + "category": "objects" + }, + { + "emoji": "🎼", + "name": "musical score", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🎵", + "name": "musical note", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🎶", + "name": "musical notes", + "keywords": ["music"], + "category": "objects" + }, + { + "emoji": "🎙️", + "name": "studio microphone", + "keywords": ["podcast"], + "category": "objects" + }, + { + "emoji": "🎚️", + "name": "level slider", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🎛️", + "name": "control knobs", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🎤", + "name": "microphone", + "keywords": ["sing"], + "category": "objects" + }, + { + "emoji": "🎧", + "name": "headphone", + "keywords": ["music", "earphones"], + "category": "objects" + }, + { + "emoji": "📻", + "name": "radio", + "keywords": ["podcast"], + "category": "objects" + }, + { + "emoji": "🎷", + "name": "saxophone", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪗", + "name": "accordion", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🎸", + "name": "guitar", + "keywords": ["rock"], + "category": "objects" + }, + { + "emoji": "🎹", + "name": "musical keyboard", + "keywords": ["piano"], + "category": "objects" + }, + { + "emoji": "🎺", + "name": "trumpet", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🎻", + "name": "violin", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪕", + "name": "banjo", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🥁", + "name": "drum", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪘", + "name": "long drum", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪇", + "name": "maracas", + "keywords": ["shaker"], + "category": "objects" + }, + { + "emoji": "🪈", + "name": "flute", + "keywords": ["recorder"], + "category": "objects" + }, + { + "emoji": "📱", + "name": "mobile phone", + "keywords": ["smartphone", "mobile"], + "category": "objects" + }, + { + "emoji": "📲", + "name": "mobile phone with arrow", + "keywords": ["call", "incoming"], + "category": "objects" + }, + { + "emoji": "☎️", + "name": "telephone", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📞", + "name": "telephone receiver", + "keywords": ["phone", "call"], + "category": "objects" + }, + { + "emoji": "📟", + "name": "pager", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📠", + "name": "fax machine", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🔋", + "name": "battery", + "keywords": ["power"], + "category": "objects" + }, + { + "emoji": "🪫", + "name": "low battery", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🔌", + "name": "electric plug", + "keywords": [], + "category": "objects" + }, + { + "emoji": "💻", + "name": "laptop", + "keywords": ["desktop", "screen"], + "category": "objects" + }, + { + "emoji": "🖥️", + "name": "desktop computer", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🖨️", + "name": "printer", + "keywords": [], + "category": "objects" + }, + { + "emoji": "⌨️", + "name": "keyboard", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🖱️", + "name": "computer mouse", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🖲️", + "name": "trackball", + "keywords": [], + "category": "objects" + }, + { + "emoji": "💽", + "name": "computer disk", + "keywords": [], + "category": "objects" + }, + { + "emoji": "💾", + "name": "floppy disk", + "keywords": ["save"], + "category": "objects" + }, + { + "emoji": "💿", + "name": "optical disk", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📀", + "name": "dvd", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧮", + "name": "abacus", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🎥", + "name": "movie camera", + "keywords": ["film", "video"], + "category": "objects" + }, + { + "emoji": "🎞️", + "name": "film frames", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📽️", + "name": "film projector", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🎬", + "name": "clapper board", + "keywords": ["film"], + "category": "objects" + }, + { + "emoji": "📺", + "name": "television", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📷", + "name": "camera", + "keywords": ["photo"], + "category": "objects" + }, + { + "emoji": "📸", + "name": "camera with flash", + "keywords": ["photo"], + "category": "objects" + }, + { + "emoji": "📹", + "name": "video camera", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📼", + "name": "videocassette", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🔍", + "name": "magnifying glass tilted left", + "keywords": ["search", "zoom"], + "category": "objects" + }, + { + "emoji": "🔎", + "name": "magnifying glass tilted right", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🕯️", + "name": "candle", + "keywords": [], + "category": "objects" + }, + { + "emoji": "💡", + "name": "light bulb", + "keywords": ["idea", "light"], + "category": "objects" + }, + { + "emoji": "🔦", + "name": "flashlight", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🏮", + "name": "red paper lantern", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪔", + "name": "diya lamp", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📔", + "name": "notebook with decorative cover", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📕", + "name": "closed book", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📖", + "name": "open book", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📗", + "name": "green book", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📘", + "name": "blue book", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📙", + "name": "orange book", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📚", + "name": "books", + "keywords": ["library"], + "category": "objects" + }, + { + "emoji": "📓", + "name": "notebook", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📒", + "name": "ledger", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📃", + "name": "page with curl", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📜", + "name": "scroll", + "keywords": ["document"], + "category": "objects" + }, + { + "emoji": "📄", + "name": "page facing up", + "keywords": ["document"], + "category": "objects" + }, + { + "emoji": "📰", + "name": "newspaper", + "keywords": ["press"], + "category": "objects" + }, + { + "emoji": "🗞️", + "name": "rolled-up newspaper", + "keywords": ["press"], + "category": "objects" + }, + { + "emoji": "📑", + "name": "bookmark tabs", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🔖", + "name": "bookmark", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🏷️", + "name": "label", + "keywords": ["tag"], + "category": "objects" + }, + { + "emoji": "💰", + "name": "money bag", + "keywords": ["dollar", "cream"], + "category": "objects" + }, + { + "emoji": "🪙", + "name": "coin", + "keywords": [], + "category": "objects" + }, + { + "emoji": "💴", + "name": "yen banknote", + "keywords": [], + "category": "objects" + }, + { + "emoji": "💵", + "name": "dollar banknote", + "keywords": ["money"], + "category": "objects" + }, + { + "emoji": "💶", + "name": "euro banknote", + "keywords": [], + "category": "objects" + }, + { + "emoji": "💷", + "name": "pound banknote", + "keywords": [], + "category": "objects" + }, + { + "emoji": "💸", + "name": "money with wings", + "keywords": ["dollar"], + "category": "objects" + }, + { + "emoji": "💳", + "name": "credit card", + "keywords": ["subscription"], + "category": "objects" + }, + { + "emoji": "🧾", + "name": "receipt", + "keywords": [], + "category": "objects" + }, + { + "emoji": "💹", + "name": "chart increasing with yen", + "keywords": [], + "category": "objects" + }, + { + "emoji": "✉️", + "name": "envelope", + "keywords": ["letter", "email"], + "category": "objects" + }, + { + "emoji": "📧", + "name": "e-mail", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📨", + "name": "incoming envelope", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📩", + "name": "envelope with arrow", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📤", + "name": "outbox tray", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📥", + "name": "inbox tray", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📦", + "name": "package", + "keywords": ["shipping"], + "category": "objects" + }, + { + "emoji": "📫", + "name": "closed mailbox with raised flag", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📪", + "name": "closed mailbox with lowered flag", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📬", + "name": "open mailbox with raised flag", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📭", + "name": "open mailbox with lowered flag", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📮", + "name": "postbox", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🗳️", + "name": "ballot box with ballot", + "keywords": [], + "category": "objects" + }, + { + "emoji": "✏️", + "name": "pencil", + "keywords": [], + "category": "objects" + }, + { + "emoji": "✒️", + "name": "black nib", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🖋️", + "name": "fountain pen", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🖊️", + "name": "pen", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🖌️", + "name": "paintbrush", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🖍️", + "name": "crayon", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📝", + "name": "memo", + "keywords": ["document", "note"], + "category": "objects" + }, + { + "emoji": "💼", + "name": "briefcase", + "keywords": ["business"], + "category": "objects" + }, + { + "emoji": "📁", + "name": "file folder", + "keywords": ["directory"], + "category": "objects" + }, + { + "emoji": "📂", + "name": "open file folder", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🗂️", + "name": "card index dividers", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📅", + "name": "calendar", + "keywords": ["calendar", "schedule"], + "category": "objects" + }, + { + "emoji": "📆", + "name": "tear-off calendar", + "keywords": ["schedule"], + "category": "objects" + }, + { + "emoji": "🗒️", + "name": "spiral notepad", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🗓️", + "name": "spiral calendar", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📇", + "name": "card index", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📈", + "name": "chart increasing", + "keywords": ["graph", "metrics"], + "category": "objects" + }, + { + "emoji": "📉", + "name": "chart decreasing", + "keywords": ["graph", "metrics"], + "category": "objects" + }, + { + "emoji": "📊", + "name": "bar chart", + "keywords": ["stats", "metrics"], + "category": "objects" + }, + { + "emoji": "📋", + "name": "clipboard", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📌", + "name": "pushpin", + "keywords": ["location"], + "category": "objects" + }, + { + "emoji": "📍", + "name": "round pushpin", + "keywords": ["location"], + "category": "objects" + }, + { + "emoji": "📎", + "name": "paperclip", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🖇️", + "name": "linked paperclips", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📏", + "name": "straight ruler", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📐", + "name": "triangular ruler", + "keywords": [], + "category": "objects" + }, + { + "emoji": "✂️", + "name": "scissors", + "keywords": ["cut"], + "category": "objects" + }, + { + "emoji": "🗃️", + "name": "card file box", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🗄️", + "name": "file cabinet", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🗑️", + "name": "wastebasket", + "keywords": ["trash"], + "category": "objects" + }, + { + "emoji": "🔒", + "name": "locked", + "keywords": ["security", "private"], + "category": "objects" + }, + { + "emoji": "🔓", + "name": "unlocked", + "keywords": ["security"], + "category": "objects" + }, + { + "emoji": "🔏", + "name": "locked with pen", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🔐", + "name": "locked with key", + "keywords": ["security"], + "category": "objects" + }, + { + "emoji": "🔑", + "name": "key", + "keywords": ["lock", "password"], + "category": "objects" + }, + { + "emoji": "🗝️", + "name": "old key", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🔨", + "name": "hammer", + "keywords": ["tool"], + "category": "objects" + }, + { + "emoji": "🪓", + "name": "axe", + "keywords": [], + "category": "objects" + }, + { + "emoji": "⛏️", + "name": "pick", + "keywords": [], + "category": "objects" + }, + { + "emoji": "⚒️", + "name": "hammer and pick", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🛠️", + "name": "hammer and wrench", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🗡️", + "name": "dagger", + "keywords": [], + "category": "objects" + }, + { + "emoji": "⚔️", + "name": "crossed swords", + "keywords": [], + "category": "objects" + }, + { + "emoji": "💣", + "name": "bomb", + "keywords": ["boom"], + "category": "objects" + }, + { + "emoji": "🪃", + "name": "boomerang", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🏹", + "name": "bow and arrow", + "keywords": ["archery"], + "category": "objects" + }, + { + "emoji": "🛡️", + "name": "shield", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪚", + "name": "carpentry saw", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🔧", + "name": "wrench", + "keywords": ["tool"], + "category": "objects" + }, + { + "emoji": "🪛", + "name": "screwdriver", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🔩", + "name": "nut and bolt", + "keywords": [], + "category": "objects" + }, + { + "emoji": "⚙️", + "name": "gear", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🗜️", + "name": "clamp", + "keywords": [], + "category": "objects" + }, + { + "emoji": "⚖️", + "name": "balance scale", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🦯", + "name": "white cane", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🔗", + "name": "link", + "keywords": [], + "category": "objects" + }, + { + "emoji": "⛓️", + "name": "chains", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪝", + "name": "hook", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧰", + "name": "toolbox", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧲", + "name": "magnet", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪜", + "name": "ladder", + "keywords": [], + "category": "objects" + }, + { + "emoji": "⚗️", + "name": "alembic", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧪", + "name": "test tube", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧫", + "name": "petri dish", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧬", + "name": "dna", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🔬", + "name": "microscope", + "keywords": ["science", "laboratory", "investigate"], + "category": "objects" + }, + { + "emoji": "🔭", + "name": "telescope", + "keywords": [], + "category": "objects" + }, + { + "emoji": "📡", + "name": "satellite antenna", + "keywords": ["signal"], + "category": "objects" + }, + { + "emoji": "💉", + "name": "syringe", + "keywords": ["health", "hospital", "needle"], + "category": "objects" + }, + { + "emoji": "🩸", + "name": "drop of blood", + "keywords": [], + "category": "objects" + }, + { + "emoji": "💊", + "name": "pill", + "keywords": ["health", "medicine"], + "category": "objects" + }, + { + "emoji": "🩹", + "name": "adhesive bandage", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🩼", + "name": "crutch", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🩺", + "name": "stethoscope", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🩻", + "name": "x-ray", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🚪", + "name": "door", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🛗", + "name": "elevator", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪞", + "name": "mirror", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪟", + "name": "window", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🛏️", + "name": "bed", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🛋️", + "name": "couch and lamp", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪑", + "name": "chair", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🚽", + "name": "toilet", + "keywords": ["wc"], + "category": "objects" + }, + { + "emoji": "🪠", + "name": "plunger", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🚿", + "name": "shower", + "keywords": ["bath"], + "category": "objects" + }, + { + "emoji": "🛁", + "name": "bathtub", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪤", + "name": "mouse trap", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪒", + "name": "razor", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧴", + "name": "lotion bottle", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧷", + "name": "safety pin", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧹", + "name": "broom", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧺", + "name": "basket", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧻", + "name": "roll of paper", + "keywords": ["toilet"], + "category": "objects" + }, + { + "emoji": "🪣", + "name": "bucket", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧼", + "name": "soap", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🫧", + "name": "bubbles", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪥", + "name": "toothbrush", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧽", + "name": "sponge", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧯", + "name": "fire extinguisher", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🛒", + "name": "shopping cart", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🚬", + "name": "cigarette", + "keywords": ["cigarette"], + "category": "objects" + }, + { + "emoji": "⚰️", + "name": "coffin", + "keywords": ["funeral"], + "category": "objects" + }, + { + "emoji": "🪦", + "name": "headstone", + "keywords": [], + "category": "objects" + }, + { + "emoji": "⚱️", + "name": "funeral urn", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🧿", + "name": "nazar amulet", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪬", + "name": "hamsa", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🗿", + "name": "moai", + "keywords": ["stone"], + "category": "objects" + }, + { + "emoji": "🪧", + "name": "placard", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🪪", + "name": "identification card", + "keywords": [], + "category": "objects" + }, + { + "emoji": "🏧", + "name": "ATM sign", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🚮", + "name": "litter in bin sign", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🚰", + "name": "potable water", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "♿", + "name": "wheelchair symbol", + "keywords": ["accessibility"], + "category": "symbols" + }, + { + "emoji": "🚹", + "name": "men’s room", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🚺", + "name": "women’s room", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🚻", + "name": "restroom", + "keywords": ["toilet"], + "category": "symbols" + }, + { + "emoji": "🚼", + "name": "baby symbol", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🚾", + "name": "water closet", + "keywords": ["toilet", "restroom"], + "category": "symbols" + }, + { + "emoji": "🛂", + "name": "passport control", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🛃", + "name": "customs", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🛄", + "name": "baggage claim", + "keywords": ["airport"], + "category": "symbols" + }, + { + "emoji": "🛅", + "name": "left luggage", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⚠️", + "name": "warning", + "keywords": ["wip"], + "category": "symbols" + }, + { + "emoji": "🚸", + "name": "children crossing", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⛔", + "name": "no entry", + "keywords": ["limit"], + "category": "symbols" + }, + { + "emoji": "🚫", + "name": "prohibited", + "keywords": ["block", "forbidden"], + "category": "symbols" + }, + { + "emoji": "🚳", + "name": "no bicycles", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🚭", + "name": "no smoking", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🚯", + "name": "no littering", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🚱", + "name": "non-potable water", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🚷", + "name": "no pedestrians", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "📵", + "name": "no mobile phones", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔞", + "name": "no one under eighteen", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "☢️", + "name": "radioactive", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "☣️", + "name": "biohazard", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⬆️", + "name": "up arrow", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "↗️", + "name": "up-right arrow", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "➡️", + "name": "right arrow", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "↘️", + "name": "down-right arrow", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⬇️", + "name": "down arrow", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "↙️", + "name": "down-left arrow", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⬅️", + "name": "left arrow", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "↖️", + "name": "up-left arrow", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "↕️", + "name": "up-down arrow", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "↔️", + "name": "left-right arrow", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "↩️", + "name": "right arrow curving left", + "keywords": ["return"], + "category": "symbols" + }, + { + "emoji": "↪️", + "name": "left arrow curving right", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⤴️", + "name": "right arrow curving up", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⤵️", + "name": "right arrow curving down", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔃", + "name": "clockwise vertical arrows", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔄", + "name": "counterclockwise arrows button", + "keywords": ["sync"], + "category": "symbols" + }, + { + "emoji": "🔙", + "name": "BACK arrow", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔚", + "name": "END arrow", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔛", + "name": "ON! arrow", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔜", + "name": "SOON arrow", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔝", + "name": "TOP arrow", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🛐", + "name": "place of worship", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⚛️", + "name": "atom symbol", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🕉️", + "name": "om", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "✡️", + "name": "star of David", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "☸️", + "name": "wheel of dharma", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "☯️", + "name": "yin yang", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "✝️", + "name": "latin cross", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "☦️", + "name": "orthodox cross", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "☪️", + "name": "star and crescent", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "☮️", + "name": "peace symbol", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🕎", + "name": "menorah", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔯", + "name": "dotted six-pointed star", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🪯", + "name": "khanda", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "♈", + "name": "Aries", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "♉", + "name": "Taurus", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "♊", + "name": "Gemini", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "♋", + "name": "Cancer", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "♌", + "name": "Leo", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "♍", + "name": "Virgo", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "♎", + "name": "Libra", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "♏", + "name": "Scorpio", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "♐", + "name": "Sagittarius", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "♑", + "name": "Capricorn", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "♒", + "name": "Aquarius", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "♓", + "name": "Pisces", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⛎", + "name": "Ophiuchus", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔀", + "name": "shuffle tracks button", + "keywords": ["shuffle"], + "category": "symbols" + }, + { + "emoji": "🔁", + "name": "repeat button", + "keywords": ["loop"], + "category": "symbols" + }, + { + "emoji": "🔂", + "name": "repeat single button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "▶️", + "name": "play button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⏩", + "name": "fast-forward button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⏭️", + "name": "next track button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⏯️", + "name": "play or pause button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "◀️", + "name": "reverse button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⏪", + "name": "fast reverse button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⏮️", + "name": "last track button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔼", + "name": "upwards button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⏫", + "name": "fast up button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔽", + "name": "downwards button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⏬", + "name": "fast down button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⏸️", + "name": "pause button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⏹️", + "name": "stop button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⏺️", + "name": "record button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⏏️", + "name": "eject button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🎦", + "name": "cinema", + "keywords": ["film", "movie"], + "category": "symbols" + }, + { + "emoji": "🔅", + "name": "dim button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔆", + "name": "bright button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "📶", + "name": "antenna bars", + "keywords": ["wifi"], + "category": "symbols" + }, + { + "emoji": "🛜", + "name": "wireless", + "keywords": ["wifi"], + "category": "symbols" + }, + { + "emoji": "📳", + "name": "vibration mode", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "📴", + "name": "mobile phone off", + "keywords": ["mute", "off"], + "category": "symbols" + }, + { + "emoji": "♀️", + "name": "female sign", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "♂️", + "name": "male sign", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⚧️", + "name": "transgender symbol", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "✖️", + "name": "multiply", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "➕", + "name": "plus", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "➖", + "name": "minus", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "➗", + "name": "divide", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🟰", + "name": "heavy equals sign", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "♾️", + "name": "infinity", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "‼️", + "name": "double exclamation mark", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⁉️", + "name": "exclamation question mark", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "❓", + "name": "red question mark", + "keywords": ["confused"], + "category": "symbols" + }, + { + "emoji": "❔", + "name": "white question mark", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "❕", + "name": "white exclamation mark", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "❗", + "name": "red exclamation mark", + "keywords": ["bang"], + "category": "symbols" + }, + { + "emoji": "〰️", + "name": "wavy dash", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "💱", + "name": "currency exchange", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "💲", + "name": "heavy dollar sign", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⚕️", + "name": "medical symbol", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "♻️", + "name": "recycling symbol", + "keywords": ["environment", "green"], + "category": "symbols" + }, + { + "emoji": "⚜️", + "name": "fleur-de-lis", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔱", + "name": "trident emblem", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "📛", + "name": "name badge", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔰", + "name": "Japanese symbol for beginner", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⭕", + "name": "hollow red circle", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "✅", + "name": "check mark button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "☑️", + "name": "check box with check", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "✔️", + "name": "check mark", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "❌", + "name": "cross mark", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "❎", + "name": "cross mark button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "➰", + "name": "curly loop", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "➿", + "name": "double curly loop", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "〽️", + "name": "part alternation mark", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "✳️", + "name": "eight-spoked asterisk", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "✴️", + "name": "eight-pointed star", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "❇️", + "name": "sparkle", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "©️", + "name": "copyright", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "®️", + "name": "registered", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "™️", + "name": "trade mark", + "keywords": ["trademark"], + "category": "symbols" + }, + { + "emoji": "#️⃣", + "name": "keycap: #", + "keywords": ["number"], + "category": "symbols" + }, + { + "emoji": "*️⃣", + "name": "keycap: *", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "0️⃣", + "name": "keycap: 0", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "1️⃣", + "name": "keycap: 1", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "2️⃣", + "name": "keycap: 2", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "3️⃣", + "name": "keycap: 3", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "4️⃣", + "name": "keycap: 4", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "5️⃣", + "name": "keycap: 5", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "6️⃣", + "name": "keycap: 6", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "7️⃣", + "name": "keycap: 7", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "8️⃣", + "name": "keycap: 8", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "9️⃣", + "name": "keycap: 9", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔟", + "name": "keycap: 10", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔠", + "name": "input latin uppercase", + "keywords": ["letters"], + "category": "symbols" + }, + { + "emoji": "🔡", + "name": "input latin lowercase", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔢", + "name": "input numbers", + "keywords": ["numbers"], + "category": "symbols" + }, + { + "emoji": "🔣", + "name": "input symbols", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔤", + "name": "input latin letters", + "keywords": ["alphabet"], + "category": "symbols" + }, + { + "emoji": "🅰️", + "name": "A button (blood type)", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🆎", + "name": "AB button (blood type)", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🅱️", + "name": "B button (blood type)", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🆑", + "name": "CL button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🆒", + "name": "COOL button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🆓", + "name": "FREE button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "ℹ️", + "name": "information", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🆔", + "name": "ID button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "Ⓜ️", + "name": "circled M", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🆕", + "name": "NEW button", + "keywords": ["fresh"], + "category": "symbols" + }, + { + "emoji": "🆖", + "name": "NG button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🅾️", + "name": "O button (blood type)", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🆗", + "name": "OK button", + "keywords": ["yes"], + "category": "symbols" + }, + { + "emoji": "🅿️", + "name": "P button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🆘", + "name": "SOS button", + "keywords": ["help", "emergency"], + "category": "symbols" + }, + { + "emoji": "🆙", + "name": "UP! button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🆚", + "name": "VS button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🈁", + "name": "Japanese “here” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🈂️", + "name": "Japanese “service charge” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🈷️", + "name": "Japanese “monthly amount” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🈶", + "name": "Japanese “not free of charge” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🈯", + "name": "Japanese “reserved” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🉐", + "name": "Japanese “bargain” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🈹", + "name": "Japanese “discount” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🈚", + "name": "Japanese “free of charge” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🈲", + "name": "Japanese “prohibited” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🉑", + "name": "Japanese “acceptable” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🈸", + "name": "Japanese “application” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🈴", + "name": "Japanese “passing grade” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🈳", + "name": "Japanese “vacancy” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "㊗️", + "name": "Japanese “congratulations” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "㊙️", + "name": "Japanese “secret” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🈺", + "name": "Japanese “open for business” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🈵", + "name": "Japanese “no vacancy” button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔴", + "name": "red circle", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🟠", + "name": "orange circle", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🟡", + "name": "yellow circle", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🟢", + "name": "green circle", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔵", + "name": "blue circle", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🟣", + "name": "purple circle", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🟤", + "name": "brown circle", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⚫", + "name": "black circle", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⚪", + "name": "white circle", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🟥", + "name": "red square", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🟧", + "name": "orange square", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🟨", + "name": "yellow square", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🟩", + "name": "green square", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🟦", + "name": "blue square", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🟪", + "name": "purple square", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🟫", + "name": "brown square", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⬛", + "name": "black large square", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "⬜", + "name": "white large square", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "◼️", + "name": "black medium square", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "◻️", + "name": "white medium square", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "◾", + "name": "black medium-small square", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "◽", + "name": "white medium-small square", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "▪️", + "name": "black small square", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "▫️", + "name": "white small square", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔶", + "name": "large orange diamond", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔷", + "name": "large blue diamond", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔸", + "name": "small orange diamond", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔹", + "name": "small blue diamond", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔺", + "name": "red triangle pointed up", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔻", + "name": "red triangle pointed down", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "💠", + "name": "diamond with a dot", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔘", + "name": "radio button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔳", + "name": "white square button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🔲", + "name": "black square button", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🏁", + "name": "chequered flag", + "keywords": ["milestone", "finish"], + "category": "symbols" + }, + { + "emoji": "🚩", + "name": "triangular flag", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🎌", + "name": "crossed flags", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🏴", + "name": "black flag", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🏳️", + "name": "white flag", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🏳️‍🌈", + "name": "rainbow flag", + "keywords": ["pride"], + "category": "symbols" + }, + { + "emoji": "🏳️‍⚧️", + "name": "transgender flag", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🏴‍☠️", + "name": "pirate flag", + "keywords": [], + "category": "symbols" + }, + { + "emoji": "🇦🇨", + "name": "Ascension Island", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇦🇩", + "name": "Andorra", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇦🇪", + "name": "United Arab Emirates", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇦🇫", + "name": "Afghanistan", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇦🇬", + "name": "Antigua & Barbuda", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇦🇮", + "name": "Anguilla", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇦🇱", + "name": "Albania", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇦🇲", + "name": "Armenia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇦🇴", + "name": "Angola", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇦🇶", + "name": "Antarctica", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇦🇷", + "name": "Argentina", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇦🇸", + "name": "American Samoa", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇦🇹", + "name": "Austria", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇦🇺", + "name": "Australia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇦🇼", + "name": "Aruba", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇦🇽", + "name": "Åland Islands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇦🇿", + "name": "Azerbaijan", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇦", + "name": "Bosnia & Herzegovina", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇧", + "name": "Barbados", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇩", + "name": "Bangladesh", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇪", + "name": "Belgium", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇫", + "name": "Burkina Faso", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇬", + "name": "Bulgaria", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇭", + "name": "Bahrain", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇮", + "name": "Burundi", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇯", + "name": "Benin", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇱", + "name": "St. Barthélemy", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇲", + "name": "Bermuda", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇳", + "name": "Brunei", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇴", + "name": "Bolivia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇶", + "name": "Caribbean Netherlands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇷", + "name": "Brazil", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇸", + "name": "Bahamas", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇹", + "name": "Bhutan", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇻", + "name": "Bouvet Island", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇼", + "name": "Botswana", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇾", + "name": "Belarus", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇧🇿", + "name": "Belize", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇦", + "name": "Canada", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇨", + "name": "Cocos (Keeling) Islands", + "keywords": ["keeling"], + "category": "flags" + }, + { + "emoji": "🇨🇩", + "name": "Congo - Kinshasa", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇫", + "name": "Central African Republic", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇬", + "name": "Congo - Brazzaville", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇭", + "name": "Switzerland", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇮", + "name": "Côte d’Ivoire", + "keywords": ["ivory"], + "category": "flags" + }, + { + "emoji": "🇨🇰", + "name": "Cook Islands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇱", + "name": "Chile", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇲", + "name": "Cameroon", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇳", + "name": "China", + "keywords": ["china"], + "category": "flags" + }, + { + "emoji": "🇨🇴", + "name": "Colombia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇵", + "name": "Clipperton Island", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇷", + "name": "Costa Rica", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇺", + "name": "Cuba", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇻", + "name": "Cape Verde", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇼", + "name": "Curaçao", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇽", + "name": "Christmas Island", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇾", + "name": "Cyprus", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇨🇿", + "name": "Czechia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇩🇪", + "name": "Germany", + "keywords": ["flag", "germany"], + "category": "flags" + }, + { + "emoji": "🇩🇬", + "name": "Diego Garcia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇩🇯", + "name": "Djibouti", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇩🇰", + "name": "Denmark", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇩🇲", + "name": "Dominica", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇩🇴", + "name": "Dominican Republic", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇩🇿", + "name": "Algeria", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇪🇦", + "name": "Ceuta & Melilla", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇪🇨", + "name": "Ecuador", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇪🇪", + "name": "Estonia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇪🇬", + "name": "Egypt", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇪🇭", + "name": "Western Sahara", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇪🇷", + "name": "Eritrea", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇪🇸", + "name": "Spain", + "keywords": ["spain"], + "category": "flags" + }, + { + "emoji": "🇪🇹", + "name": "Ethiopia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇪🇺", + "name": "European Union", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇫🇮", + "name": "Finland", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇫🇯", + "name": "Fiji", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇫🇰", + "name": "Falkland Islands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇫🇲", + "name": "Micronesia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇫🇴", + "name": "Faroe Islands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇫🇷", + "name": "France", + "keywords": ["france", "french"], + "category": "flags" + }, + { + "emoji": "🇬🇦", + "name": "Gabon", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇧", + "name": "United Kingdom", + "keywords": ["flag", "british"], + "category": "flags" + }, + { + "emoji": "🇬🇩", + "name": "Grenada", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇪", + "name": "Georgia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇫", + "name": "French Guiana", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇬", + "name": "Guernsey", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇭", + "name": "Ghana", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇮", + "name": "Gibraltar", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇱", + "name": "Greenland", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇲", + "name": "Gambia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇳", + "name": "Guinea", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇵", + "name": "Guadeloupe", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇶", + "name": "Equatorial Guinea", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇷", + "name": "Greece", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇸", + "name": "South Georgia & South Sandwich Islands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇹", + "name": "Guatemala", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇺", + "name": "Guam", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇼", + "name": "Guinea-Bissau", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇬🇾", + "name": "Guyana", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇭🇰", + "name": "Hong Kong SAR China", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇭🇲", + "name": "Heard & McDonald Islands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇭🇳", + "name": "Honduras", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇭🇷", + "name": "Croatia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇭🇹", + "name": "Haiti", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇭🇺", + "name": "Hungary", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇮🇨", + "name": "Canary Islands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇮🇩", + "name": "Indonesia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇮🇪", + "name": "Ireland", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇮🇱", + "name": "Israel", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇮🇲", + "name": "Isle of Man", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇮🇳", + "name": "India", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇮🇴", + "name": "British Indian Ocean Territory", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇮🇶", + "name": "Iraq", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇮🇷", + "name": "Iran", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇮🇸", + "name": "Iceland", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇮🇹", + "name": "Italy", + "keywords": ["italy"], + "category": "flags" + }, + { + "emoji": "🇯🇪", + "name": "Jersey", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇯🇲", + "name": "Jamaica", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇯🇴", + "name": "Jordan", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇯🇵", + "name": "Japan", + "keywords": ["japan"], + "category": "flags" + }, + { + "emoji": "🇰🇪", + "name": "Kenya", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇰🇬", + "name": "Kyrgyzstan", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇰🇭", + "name": "Cambodia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇰🇮", + "name": "Kiribati", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇰🇲", + "name": "Comoros", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇰🇳", + "name": "St. Kitts & Nevis", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇰🇵", + "name": "North Korea", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇰🇷", + "name": "South Korea", + "keywords": ["korea"], + "category": "flags" + }, + { + "emoji": "🇰🇼", + "name": "Kuwait", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇰🇾", + "name": "Cayman Islands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇰🇿", + "name": "Kazakhstan", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇱🇦", + "name": "Laos", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇱🇧", + "name": "Lebanon", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇱🇨", + "name": "St. Lucia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇱🇮", + "name": "Liechtenstein", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇱🇰", + "name": "Sri Lanka", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇱🇷", + "name": "Liberia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇱🇸", + "name": "Lesotho", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇱🇹", + "name": "Lithuania", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇱🇺", + "name": "Luxembourg", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇱🇻", + "name": "Latvia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇱🇾", + "name": "Libya", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇦", + "name": "Morocco", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇨", + "name": "Monaco", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇩", + "name": "Moldova", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇪", + "name": "Montenegro", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇫", + "name": "St. Martin", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇬", + "name": "Madagascar", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇭", + "name": "Marshall Islands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇰", + "name": "North Macedonia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇱", + "name": "Mali", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇲", + "name": "Myanmar (Burma)", + "keywords": ["burma"], + "category": "flags" + }, + { + "emoji": "🇲🇳", + "name": "Mongolia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇴", + "name": "Macao SAR China", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇵", + "name": "Northern Mariana Islands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇶", + "name": "Martinique", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇷", + "name": "Mauritania", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇸", + "name": "Montserrat", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇹", + "name": "Malta", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇺", + "name": "Mauritius", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇻", + "name": "Maldives", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇼", + "name": "Malawi", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇽", + "name": "Mexico", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇾", + "name": "Malaysia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇲🇿", + "name": "Mozambique", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇳🇦", + "name": "Namibia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇳🇨", + "name": "New Caledonia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇳🇪", + "name": "Niger", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇳🇫", + "name": "Norfolk Island", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇳🇬", + "name": "Nigeria", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇳🇮", + "name": "Nicaragua", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇳🇱", + "name": "Netherlands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇳🇴", + "name": "Norway", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇳🇵", + "name": "Nepal", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇳🇷", + "name": "Nauru", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇳🇺", + "name": "Niue", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇳🇿", + "name": "New Zealand", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇴🇲", + "name": "Oman", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇵🇦", + "name": "Panama", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇵🇪", + "name": "Peru", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇵🇫", + "name": "French Polynesia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇵🇬", + "name": "Papua New Guinea", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇵🇭", + "name": "Philippines", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇵🇰", + "name": "Pakistan", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇵🇱", + "name": "Poland", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇵🇲", + "name": "St. Pierre & Miquelon", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇵🇳", + "name": "Pitcairn Islands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇵🇷", + "name": "Puerto Rico", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇵🇸", + "name": "Palestinian Territories", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇵🇹", + "name": "Portugal", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇵🇼", + "name": "Palau", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇵🇾", + "name": "Paraguay", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇶🇦", + "name": "Qatar", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇷🇪", + "name": "Réunion", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇷🇴", + "name": "Romania", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇷🇸", + "name": "Serbia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇷🇺", + "name": "Russia", + "keywords": ["russia"], + "category": "flags" + }, + { + "emoji": "🇷🇼", + "name": "Rwanda", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇦", + "name": "Saudi Arabia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇧", + "name": "Solomon Islands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇨", + "name": "Seychelles", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇩", + "name": "Sudan", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇪", + "name": "Sweden", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇬", + "name": "Singapore", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇭", + "name": "St. Helena", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇮", + "name": "Slovenia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇯", + "name": "Svalbard & Jan Mayen", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇰", + "name": "Slovakia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇱", + "name": "Sierra Leone", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇲", + "name": "San Marino", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇳", + "name": "Senegal", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇴", + "name": "Somalia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇷", + "name": "Suriname", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇸", + "name": "South Sudan", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇹", + "name": "São Tomé & Príncipe", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇻", + "name": "El Salvador", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇽", + "name": "Sint Maarten", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇾", + "name": "Syria", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇸🇿", + "name": "Eswatini", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇹🇦", + "name": "Tristan da Cunha", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇹🇨", + "name": "Turks & Caicos Islands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇹🇩", + "name": "Chad", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇹🇫", + "name": "French Southern Territories", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇹🇬", + "name": "Togo", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇹🇭", + "name": "Thailand", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇹🇯", + "name": "Tajikistan", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇹🇰", + "name": "Tokelau", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇹🇱", + "name": "Timor-Leste", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇹🇲", + "name": "Turkmenistan", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇹🇳", + "name": "Tunisia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇹🇴", + "name": "Tonga", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇹🇷", + "name": "Turkey", + "keywords": ["turkey"], + "category": "flags" + }, + { + "emoji": "🇹🇹", + "name": "Trinidad & Tobago", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇹🇻", + "name": "Tuvalu", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇹🇼", + "name": "Taiwan", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇹🇿", + "name": "Tanzania", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇺🇦", + "name": "Ukraine", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇺🇬", + "name": "Uganda", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇺🇲", + "name": "U.S. Outlying Islands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇺🇳", + "name": "United Nations", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇺🇸", + "name": "United States", + "keywords": ["flag", "united", "america"], + "category": "flags" + }, + { + "emoji": "🇺🇾", + "name": "Uruguay", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇺🇿", + "name": "Uzbekistan", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇻🇦", + "name": "Vatican City", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇻🇨", + "name": "St. Vincent & Grenadines", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇻🇪", + "name": "Venezuela", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇻🇬", + "name": "British Virgin Islands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇻🇮", + "name": "U.S. Virgin Islands", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇻🇳", + "name": "Vietnam", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇻🇺", + "name": "Vanuatu", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇼🇫", + "name": "Wallis & Futuna", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇼🇸", + "name": "Samoa", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇽🇰", + "name": "Kosovo", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇾🇪", + "name": "Yemen", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇾🇹", + "name": "Mayotte", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇿🇦", + "name": "South Africa", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇿🇲", + "name": "Zambia", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🇿🇼", + "name": "Zimbabwe", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🏴󠁧󠁢󠁥󠁮󠁧󠁿", + "name": "England", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🏴󠁧󠁢󠁳󠁣󠁴󠁿", + "name": "Scotland", + "keywords": [], + "category": "flags" + }, + { + "emoji": "🏴󠁧󠁢󠁷󠁬󠁳󠁿", + "name": "Wales", + "keywords": [], + "category": "flags" + } +] diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-dark-1.png b/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-dark-1.png new file mode 100644 index 0000000..bc40edc Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-dark-1.png differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-dark-2.png b/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-dark-2.png new file mode 100644 index 0000000..f6a3d79 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-dark-2.png differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-dark-3.png b/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-dark-3.png new file mode 100644 index 0000000..4660f7b Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-dark-3.png differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-light-1.png b/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-light-1.png new file mode 100644 index 0000000..5046ebf Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-light-1.png differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-light-2.png b/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-light-2.png new file mode 100644 index 0000000..ecfd234 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-light-2.png differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-light-3.png b/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-light-3.png new file mode 100644 index 0000000..cb7ad2b Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Screenshots/noctalia-light-3.png differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Services/fastfetch/system-info.jsonc b/fedora/.config/quickshell/noctalia-shell/Assets/Services/fastfetch/system-info.jsonc new file mode 100644 index 0000000..d9f5a2d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Services/fastfetch/system-info.jsonc @@ -0,0 +1,17 @@ +{ + "$schema": "https://github.com/fastfetch-cli/fastfetch/raw/dev/doc/json_schema.json", + "modules": [ + "os", + "kernel", + "title", + "board", + "host", + "uptime", + "cpu", + "gpu", + "memory", + "disk", + "wm", + "packages" + ] +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Sounds/alarm-beep.wav b/fedora/.config/quickshell/noctalia-shell/Assets/Sounds/alarm-beep.wav new file mode 100644 index 0000000..002c235 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Sounds/alarm-beep.wav differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Sounds/notification-generic.wav b/fedora/.config/quickshell/noctalia-shell/Assets/Sounds/notification-generic.wav new file mode 100644 index 0000000..aa04aa1 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Sounds/notification-generic.wav differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Sounds/volume-change.wav b/fedora/.config/quickshell/noctalia-shell/Assets/Sounds/volume-change.wav new file mode 100644 index 0000000..24bcc44 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Sounds/volume-change.wav differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaBlank.png b/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaBlank.png new file mode 100644 index 0000000..b79e53c Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaBlank.png differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaDazed.png b/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaDazed.png new file mode 100644 index 0000000..580c807 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaDazed.png differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaFear.png b/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaFear.png new file mode 100644 index 0000000..beb366c Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaFear.png differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaParty.png b/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaParty.png new file mode 100644 index 0000000..64c0a96 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaParty.png differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaSob.png b/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaSob.png new file mode 100644 index 0000000..7970024 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaSob.png differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaVampire.png b/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaVampire.png new file mode 100644 index 0000000..896109a Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Talia/TaliaVampire.png differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/btop.theme b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/btop.theme new file mode 100644 index 0000000..3139e29 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/btop.theme @@ -0,0 +1,39 @@ +# btop theme generated by noctalia + +theme[main_bg]="{{colors.surface.default.hex}}" +theme[main_fg]="{{colors.on_surface.default.hex}}" +theme[title]="{{colors.primary.default.hex}}" +theme[hi_fg]="{{colors.tertiary.default.hex}}" +theme[selected_bg]="{{colors.surface_container_highest.default.hex}}" +theme[selected_fg]="{{colors.on_surface.default.hex}}" +theme[inactive_fg]="{{colors.on_surface_variant.default.hex}}" +theme[proc_misc]="{{colors.secondary.default.hex}}" +theme[cpu_box]="{{colors.outline.default.hex}}" +theme[mem_box]="{{colors.outline.default.hex}}" +theme[net_box]="{{colors.outline.default.hex}}" +theme[proc_box]="{{colors.outline.default.hex}}" +theme[div_line]="{{colors.outline_variant.default.hex}}" +theme[temp_start]="{{colors.primary.default.hex}}" +theme[temp_mid]="{{colors.secondary.default.hex}}" +theme[temp_end]="{{colors.tertiary.default.hex}}" +theme[cpu_start]="{{colors.primary.default.hex}}" +theme[cpu_mid]="{{colors.secondary.default.hex}}" +theme[cpu_end]="{{colors.tertiary.default.hex}}" +theme[free_start]="{{colors.primary.default.hex}}" +theme[free_mid]="{{colors.secondary.default.hex}}" +theme[free_end]="{{colors.tertiary.default.hex}}" +theme[cached_start]="{{colors.primary.default.hex}}" +theme[cached_mid]="{{colors.secondary.default.hex}}" +theme[cached_end]="{{colors.tertiary.default.hex}}" +theme[available_start]="{{colors.primary.default.hex}}" +theme[available_mid]="{{colors.secondary.default.hex}}" +theme[available_end]="{{colors.tertiary.default.hex}}" +theme[used_start]="{{colors.primary.default.hex}}" +theme[used_mid]="{{colors.secondary.default.hex}}" +theme[used_end]="{{colors.tertiary.default.hex}}" +theme[download_start]="{{colors.primary.default.hex}}" +theme[download_mid]="{{colors.secondary.default.hex}}" +theme[download_end]="{{colors.tertiary.default.hex}}" +theme[upload_start]="{{colors.primary.default.hex}}" +theme[upload_mid]="{{colors.secondary.default.hex}}" +theme[upload_end]="{{colors.tertiary.default.hex}}" diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/cava.ini b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/cava.ini new file mode 100644 index 0000000..a036e22 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/cava.ini @@ -0,0 +1,15 @@ +[color] +foreground = '{{colors.primary.default.hex}}' + +gradient = 1 +gradient_color_1 = '{{colors.primary_container.default.hex}}' +gradient_color_2 = '{{colors.primary.default.hex}}' +gradient_color_3 = '{{colors.on_primary_container.default.hex}}' + +horizontal_gradient = 0 +horizontal_gradient_color_1 = '{{colors.primary_container.default.hex}}' +horizontal_gradient_color_2 = '{{colors.primary.default.hex}}' +horizontal_gradient_color_3 = '{{colors.on_primary_container.default.hex}}' +horizontal_gradient_color_4 = '{{colors.primary.default.hex}}' +horizontal_gradient_color_5 = '{{colors.primary_container.default.hex}}' + diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/code.json b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/code.json new file mode 100644 index 0000000..f78157a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/code.json @@ -0,0 +1,1987 @@ +{ + "name": "NoctaliaTheme", + "type": "dark", + "semanticHighlighting": true, + "semanticTokenColors": { + "enumMember": { + "foreground": "{{colors.primary.default.hex}}" + }, + "variable.constant": { + "foreground": "{{colors.tertiary.default.hex}}" + }, + "variable.defaultLibrary": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + "tokenColors": [ + { + "name": "unison punctuation", + "scope": "punctuation.definition.delayed.unison,punctuation.definition.list.begin.unison,punctuation.definition.list.end.unison,punctuation.definition.ability.begin.unison,punctuation.definition.ability.end.unison,punctuation.operator.assignment.as.unison,punctuation.separator.pipe.unison,punctuation.separator.delimiter.unison,punctuation.definition.hash.unison", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "haskell variable generic-type", + "scope": "variable.other.generic-type.haskell", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "haskell storage type", + "scope": "storage.type.haskell", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "support.variable.magic.python", + "scope": "support.variable.magic.python", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "punctuation.separator.parameters.python", + "scope": "punctuation.separator.period.python,punctuation.separator.element.python,punctuation.parenthesis.begin.python,punctuation.parenthesis.end.python", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "variable.parameter.function.language.special.self.python", + "scope": "variable.parameter.function.language.special.self.python", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "storage.modifier.lifetime.rust", + "scope": "storage.modifier.lifetime.rust", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "support.function.std.rust", + "scope": "support.function.std.rust", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "entity.name.lifetime.rust", + "scope": "entity.name.lifetime.rust", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "variable.language.rust", + "scope": "variable.language.rust", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "support.constant.edge", + "scope": "support.constant.edge", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "regexp constant character-class", + "scope": "constant.other.character-class.regexp", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "regexp operator.quantifier", + "scope": "keyword.operator.quantifier.regexp", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "punctuation.definition", + "scope": "punctuation.definition.string.begin,punctuation.definition.string.end", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Text", + "scope": "variable.parameter.function", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "Comment Markup Link", + "scope": "comment markup.link", + "settings": { + "foreground": "{{colors.outline.default.hex}}" + } + }, + { + "name": "markup diff", + "scope": "markup.changed.diff", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "diff", + "scope": "meta.diff.header.from-file,meta.diff.header.to-file,punctuation.definition.from-file.diff,punctuation.definition.to-file.diff", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "inserted.diff", + "scope": "markup.inserted.diff", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "deleted.diff", + "scope": "markup.deleted.diff", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "c++ function", + "scope": "meta.function.c,meta.function.cpp", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "c++ block", + "scope": "punctuation.section.block.begin.bracket.curly.cpp,punctuation.section.block.end.bracket.curly.cpp,punctuation.terminator.statement.c,punctuation.section.block.begin.bracket.curly.c,punctuation.section.block.end.bracket.curly.c,punctuation.section.parens.begin.bracket.round.c,punctuation.section.parens.end.bracket.round.c,punctuation.section.parameters.begin.bracket.round.c,punctuation.section.parameters.end.bracket.round.c", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "js/ts punctuation separator key-value", + "scope": "punctuation.separator.key-value", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "js/ts import keyword", + "scope": "keyword.operator.expression.import", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "math js/ts", + "scope": "support.constant.math", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "math property js/ts", + "scope": "support.constant.property.math", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "js/ts variable.other.constant", + "scope": "variable.other.constant", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "java type", + "scope": [ + "storage.type.annotation.java", + "storage.type.object.array.java" + ], + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "java source", + "scope": "source.java", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "java modifier.import", + "scope": "punctuation.section.block.begin.java,punctuation.section.block.end.java,punctuation.definition.method-parameters.begin.java,punctuation.definition.method-parameters.end.java,meta.method.identifier.java,punctuation.section.method.begin.java,punctuation.section.method.end.java,punctuation.terminator.java,punctuation.section.class.begin.java,punctuation.section.class.end.java,punctuation.section.inner-class.begin.java,punctuation.section.inner-class.end.java,meta.method-call.java,punctuation.section.class.begin.bracket.curly.java,punctuation.section.class.end.bracket.curly.java,punctuation.section.method.begin.bracket.curly.java,punctuation.section.method.end.bracket.curly.java,punctuation.separator.period.java,punctuation.bracket.angle.java,punctuation.definition.annotation.java,meta.method.body.java", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "java modifier.import", + "scope": "meta.method.java", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "java modifier.import", + "scope": "storage.modifier.import.java,storage.type.java,storage.type.generic.java", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "java instanceof", + "scope": "keyword.operator.instanceof.java", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "java variable.name", + "scope": "meta.definition.variable.name.java", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "operator logical", + "scope": "keyword.operator.logical", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "operator bitwise", + "scope": "keyword.operator.bitwise", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "operator channel", + "scope": "keyword.operator.channel", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "support.constant.property-value.scss", + "scope": "support.constant.property-value.scss,support.constant.property-value.css", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "CSS/SCSS/LESS Operators", + "scope": "keyword.operator.css,keyword.operator.scss,keyword.operator.less", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "keyword.operator", + "scope": "keyword.operator.arithmetic,keyword.operator.comparison,keyword.operator.decrement,keyword.operator.increment,keyword.operator.relational", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "C operator assignment", + "scope": "keyword.operator.assignment.c,keyword.operator.comparison.c,keyword.operator.c,keyword.operator.increment.c,keyword.operator.decrement.c,keyword.operator.bitwise.shift.c,keyword.operator.assignment.cpp,keyword.operator.comparison.cpp,keyword.operator.cpp,keyword.operator.increment.cpp,keyword.operator.decrement.cpp,keyword.operator.bitwise.shift.cpp", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Punctuation", + "scope": "punctuation.separator.delimiter", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "Other punctuation .c", + "scope": "punctuation.separator.c,punctuation.separator.cpp", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "C type posix-reserved", + "scope": "support.type.posix-reserved.c,support.type.posix-reserved.cpp", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "keyword.operator.sizeof.c", + "scope": "keyword.operator.sizeof.c,keyword.operator.sizeof.cpp", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "python parameter", + "scope": "variable.parameter.function.language.python", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "python type", + "scope": "support.type.python", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "python logical", + "scope": "keyword.operator.logical.python", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Meta tag", + "scope": "meta.tag", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "Strings", + "scope": "string", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Inherited Class", + "scope": "entity.other.inherited-class", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Constant other symbol", + "scope": "constant.other.symbol", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Integers", + "scope": "constant.numeric", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "Constants", + "scope": "constant", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "Constants", + "scope": "punctuation.definition.constant", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "Tags", + "scope": "entity.name.tag", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "Attributes", + "scope": "entity.other.attribute-name", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "Attribute IDs", + "scope": "entity.other.attribute-name.id", + "settings": { + "fontStyle": "normal", + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Attribute class", + "scope": "entity.other.attribute-name.class.css", + "settings": { + "fontStyle": "normal", + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "Selector", + "scope": "meta.selector", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Headings", + "scope": "markup.heading", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "Headings", + "scope": "markup.heading punctuation.definition.heading, entity.name.section", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Units", + "scope": "keyword.other.unit", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "Bold", + "scope": "markup.bold,todo.bold", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "Bold", + "scope": "punctuation.definition.bold", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "markup Italic", + "scope": "markup.italic, punctuation.definition.italic,todo.emphasis", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "emphasis md", + "scope": "emphasis md", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "[VSCODE-CUSTOM] Markdown headings", + "scope": "entity.name.section.markdown", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "[VSCODE-CUSTOM] Markdown heading Punctuation Definition", + "scope": "punctuation.definition.heading.markdown", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "punctuation.definition.list.begin.markdown", + "scope": "punctuation.definition.list.begin.markdown", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "[VSCODE-CUSTOM] Markdown heading setext", + "scope": "markup.heading.setext", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "[VSCODE-CUSTOM] Markdown Punctuation Definition Bold", + "scope": "punctuation.definition.bold.markdown", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "[VSCODE-CUSTOM] Markdown Inline Raw", + "scope": "markup.inline.raw.markdown", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "[VSCODE-CUSTOM] Markdown Inline Raw", + "scope": "markup.inline.raw.string.markdown", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "[VSCODE-CUSTOM] Markdown Punctuation Definition Italic", + "scope": "punctuation.definition.italic.markdown", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "[VSCODE-CUSTOM] Markdown List Punctuation Definition", + "scope": "beginning.punctuation.definition.list.markdown", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "[VSCODE-CUSTOM] Markdown Quote", + "scope": "markup.quote.markdown", + "settings": { + "foreground": "{{colors.outline.default.hex}}", + "fontStyle": "italic" + } + }, + { + "name": "[VSCODE-CUSTOM] Markdown Punctuation Definition String", + "scope": [ + "punctuation.definition.string.begin.markdown", + "punctuation.definition.string.end.markdown", + "punctuation.definition.metadata.markdown" + ], + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "[VSCODE-CUSTOM] Markdown Punctuation Definition Link", + "scope": "punctuation.definition.metadata.markdown", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "[VSCODE-CUSTOM] Markdown Underline Link/Image", + "scope": "markup.underline.link.markdown,markup.underline.link.image.markdown", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "[VSCODE-CUSTOM] Markdown Link Title/Description", + "scope": "string.other.link.title.markdown,string.other.link.description.markdown", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Escape Characters", + "scope": "constant.character.escape", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Embedded", + "scope": "punctuation.section.embedded, variable.interpolation", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "Embedded", + "scope": "punctuation.section.embedded.begin,punctuation.section.embedded.end", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "illegal", + "scope": "invalid.illegal", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "illegal", + "scope": "invalid.illegal.bad-ampersand.html", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "Broken", + "scope": "invalid.broken", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "Deprecated", + "scope": "invalid.deprecated", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "Unimplemented", + "scope": "invalid.unimplemented", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "Source Json Meta Structure Dictionary Json > String Quoted Json", + "scope": "source.json meta.structure.dictionary.json > string.quoted.json", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "Source Json Meta Structure Dictionary Json > String Quoted Json > Punctuation String", + "scope": "source.json meta.structure.dictionary.json > string.quoted.json > punctuation.string", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "Source Json Meta Structure Dictionary Json > Value Json > String Quoted Json,source Json Meta Structure Array Json > Value Json > String Quoted Json,source Json Meta Structure Dictionary Json > Value Json > String Quoted Json > Punctuation,source Json Meta Structure Array Json > Value Json > String Quoted Json > Punctuation", + "scope": "source.json meta.structure.dictionary.json > value.json > string.quoted.json,source.json meta.structure.array.json > value.json > string.quoted.json,source.json meta.structure.dictionary.json > value.json > string.quoted.json > punctuation,source.json meta.structure.array.json > value.json > string.quoted.json > punctuation", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Source Json Meta Structure Dictionary Json > Constant Language Json,source Json Meta Structure Array Json > Constant Language Json", + "scope": "source.json meta.structure.dictionary.json > constant.language.json,source.json meta.structure.array.json > constant.language.json", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "ts primitive/builtin types", + "scope": "support.type.primitive.ts,support.type.builtin.ts,support.type.primitive.tsx,support.type.builtin.tsx", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "block scope", + "scope": "block.scope.end,block.scope.begin", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "cs storage type", + "scope": "storage.type.cs", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "cs local variable", + "scope": "entity.name.variable.local.cs", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "scope": "token.info-token", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "scope": "token.warn-token", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "scope": "token.error-token", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "scope": "token.debug-token", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "String interpolation", + "scope": [ + "punctuation.definition.template-expression.begin", + "punctuation.definition.template-expression.end", + "punctuation.section.embedded" + ], + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Reset JavaScript string interpolation expression", + "scope": [ + "meta.template.expression" + ], + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "Import module JS", + "scope": [ + "keyword.operator.module" + ], + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "js Flowtype", + "scope": [ + "support.type.type.flowtype" + ], + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "js Flow", + "scope": [ + "support.type.primitive" + ], + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "js class prop", + "scope": [ + "meta.property.object" + ], + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "js func parameter", + "scope": [ + "variable.parameter.function.js" + ], + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "js template literals begin", + "scope": [ + "keyword.other.template.begin" + ], + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "js template literals end", + "scope": [ + "keyword.other.template.end" + ], + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "js template literals variable braces begin", + "scope": [ + "keyword.other.substitution.begin" + ], + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "js template literals variable braces end", + "scope": [ + "keyword.other.substitution.end" + ], + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "js operator.assignment", + "scope": [ + "keyword.operator.assignment" + ], + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "go operator", + "scope": [ + "keyword.operator.assignment.go" + ], + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "go operator", + "scope": [ + "keyword.operator.arithmetic.go", + "keyword.operator.address.go" + ], + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "js/ts italic", + "scope": "entity.other.attribute-name.js,entity.other.attribute-name.ts,entity.other.attribute-name.jsx,entity.other.attribute-name.tsx,variable.parameter,variable.language.super", + "settings": { + "fontStyle": "italic" + } + }, + { + "name": "comment", + "scope": "comment.line.double-slash,comment.block.documentation", + "settings": { + "fontStyle": "italic" + } + }, + { + "name": "Python Keyword Control", + "scope": "keyword.control.import.python,keyword.control.flow.python", + "settings": { + "fontStyle": "italic" + } + }, + { + "name": "markup.italic.markdown", + "scope": "markup.italic.markdown", + "settings": { + "fontStyle": "italic" + } + }, + { + "name": "css color standard name", + "scope": "support.constant.color.w3c-standard-color-name.css,support.constant.color.w3c-standard-color-name.scss", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "css comma", + "scope": "punctuation.separator.list.comma.css", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "css attribute-name.id", + "scope": "support.constant.color.w3c-standard-color-name.css", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "css property-name", + "scope": "support.type.vendored.property-name.css", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "js/ts module", + "scope": "support.module.node,support.type.object.module,support.module.node", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "entity.name.type.module", + "scope": "entity.name.type.module", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "js variable readwrite", + "scope": "variable.other.readwrite,meta.object-literal.key,support.variable.property,support.variable.object.process,support.variable.object.node", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "js/ts json", + "scope": "support.constant.json", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "js/ts Keyword", + "scope": [ + "keyword.operator.expression.instanceof", + "keyword.operator.new", + "keyword.operator.ternary", + "keyword.operator.optional", + "keyword.operator.expression.keyof" + ], + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "js/ts console", + "scope": "support.type.object.console", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "js/ts support.variable.property.process", + "scope": "support.variable.property.process", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "js console function", + "scope": "entity.name.function,support.function.console", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "keyword.operator.misc.rust", + "scope": "keyword.operator.misc.rust", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "keyword.operator.sigil.rust", + "scope": "keyword.operator.sigil.rust", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "operator", + "scope": "keyword.operator.delete", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "js dom", + "scope": "support.type.object.dom", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "js dom variable", + "scope": "support.variable.dom,support.variable.property.dom", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "python placeholder reset to normal string", + "scope": "constant.character.format.placeholder.other.python", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "Operators", + "scope": "keyword.operator", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "Compound Assignment Operators", + "scope": "keyword.operator.assignment.compound", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Compound Assignment Operators js/ts", + "scope": "keyword.operator.assignment.compound.js,keyword.operator.assignment.compound.ts", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Keywords", + "scope": "keyword", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Namespaces", + "scope": "entity.name.namespace", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Variables", + "scope": "variable", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "Variables", + "scope": "variable.c", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "Language variables", + "scope": "variable.language", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Functions", + "scope": [ + "entity.name.function", + "meta.require", + "support.function.any-method", + "variable.function" + ], + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Classes", + "scope": "entity.name.type.namespace", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Classes", + "scope": "support.class, entity.name.type.class", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Class name", + "scope": "entity.name.class.identifier.namespace.type", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Class name", + "scope": [ + "entity.name.class", + "variable.other.class.js", + "variable.other.class.ts" + ], + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Storage", + "scope": "storage", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Storage JS TS", + "scope": "token.storage", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Source Js Keyword Operator Delete,source Js Keyword Operator In,source Js Keyword Operator Of,source Js Keyword Operator Instanceof,source Js Keyword Operator New,source Js Keyword Operator Typeof,source Js Keyword Operator Void", + "scope": "keyword.operator.expression.delete,keyword.operator.expression.in,keyword.operator.expression.of,keyword.operator.expression.instanceof,keyword.operator.new,keyword.operator.expression.typeof,keyword.operator.expression.void", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Support type", + "scope": "support.type.property-name", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "Support type", + "scope": "support.constant.property-value", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "Support type", + "scope": "support.constant.font-name", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "Regular Expressions", + "scope": "string.regexp", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Go package name", + "scope": [ + "entity.name.package.go" + ], + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "elm prelude", + "scope": [ + "support.type.prelude.elm" + ], + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "elm constant", + "scope": [ + "support.constant.elm" + ], + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "template literal", + "scope": [ + "punctuation.quasi.element" + ], + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "html/pug (jade) escaped characters and entities", + "scope": [ + "constant.character.entity" + ], + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "styling css pseudo-elements/classes to be able to differentiate from classes which are the same colour", + "scope": [ + "entity.other.attribute-name.pseudo-element", + "entity.other.attribute-name.pseudo-class" + ], + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "[VSCODE-CUSTOM] JSON Property Name", + "scope": "support.type.property-name.json", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "[VSCODE-CUSTOM] JSON Punctuation for Property Name", + "scope": "support.type.property-name.json punctuation", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "Comments", + "scope": "comment, punctuation.definition.comment", + "settings": { + "fontStyle": "italic", + "foreground": "{{colors.outline.default.hex}}" + } + }, + { + "name": "punctuation.definition.block.sequence.item.yaml", + "scope": "punctuation.definition.block.sequence.item.yaml", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "scope": [ + "constant.language.symbol.elixir" + ], + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "python block", + "scope": "punctuation.definition.arguments.begin.python,punctuation.definition.arguments.end.python,punctuation.separator.arguments.python,punctuation.definition.list.begin.python,punctuation.definition.list.end.python", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "python function-call.generic", + "scope": "meta.function-call.generic.python", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "pyCs", + "scope": "variable.parameter.function.python", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "python function decorator @", + "scope": "meta.function.decorator.python", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "python function support", + "scope": "support.token.decorator.python,meta.function.decorator.identifier.python", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "parameter function js/ts", + "scope": "function.parameter", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "brace function", + "scope": "function.brace", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "parameter function ruby cs", + "scope": "function.parameter.ruby, function.parameter.cs", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "constant.language.symbol.ruby", + "scope": "constant.language.symbol.ruby", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "rgb-value", + "scope": "rgb-value", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "rgb value", + "scope": "inline-color-decoration rgb-value", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "rgb value less", + "scope": "less rgb-value", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "Clojure globals", + "scope": [ + "entity.global.clojure" + ], + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Clojure symbols", + "scope": [ + "meta.symbol.clojure" + ], + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "Clojure constants", + "scope": [ + "constant.keyword.clojure" + ], + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "CoffeeScript Function Argument", + "scope": [ + "meta.arguments.coffee", + "variable.parameter.function.coffee" + ], + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "Ini Default Text", + "scope": [ + "source.ini" + ], + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Makefile prerequisities", + "scope": [ + "meta.scope.prerequisites.makefile" + ], + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "Makefile text colour", + "scope": [ + "source.makefile" + ], + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Groovy import names", + "scope": [ + "storage.modifier.import.groovy" + ], + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Groovy Methods", + "scope": [ + "meta.method.groovy" + ], + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Groovy Variables", + "scope": [ + "meta.definition.variable.name.groovy" + ], + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "Groovy Inheritance", + "scope": [ + "meta.definition.class.inherited.classes.groovy" + ], + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "HLSL Semantic", + "scope": [ + "support.variable.semantic.hlsl" + ], + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "HLSL Types", + "scope": [ + "support.type.texture.hlsl", + "support.type.sampler.hlsl", + "support.type.object.hlsl", + "support.type.object.rw.hlsl", + "support.type.fx.hlsl", + "support.type.object.hlsl" + ], + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "SQL Variables", + "scope": [ + "text.variable", + "text.bracketed" + ], + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "types", + "scope": [ + "support.type.swift", + "support.type.vb.asp" + ], + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Java Variables", + "scope": "token.variable.parameter.java", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "Java Imports", + "scope": "import.storage.java", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Packages", + "scope": "token.package.keyword", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Packages", + "scope": "token.package", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "Type Name", + "scope": "entity.name.type", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Keyword Control", + "scope": "keyword.control", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Control Elements", + "scope": "control.elements, keyword.operator.less", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "Methods", + "scope": "keyword.other.special-method", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Java Storage", + "scope": "token.storage.type.java", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "Support", + "scope": "support.function", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "Class name php", + "scope": "variable.other.class.php", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "laravel blade tag", + "scope": "text.html.laravel-blade source.php.embedded.line.html entity.name.tag.laravel-blade", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "laravel blade @", + "scope": "text.html.laravel-blade source.php.embedded.line.html support.constant.laravel-blade", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "use statement for other classes", + "scope": "support.other.namespace.use.php,support.other.namespace.use-as.php,support.other.namespace.php,entity.other.alias.php,meta.interface.php", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "error suppression", + "scope": "keyword.operator.error-control.php", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "php instanceof", + "scope": "keyword.operator.type.php", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "style double quoted array index normal begin", + "scope": "punctuation.section.array.begin.php", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "style double quoted array index normal end", + "scope": "punctuation.section.array.end.php", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "php illegal.non-null-typehinted", + "scope": "invalid.illegal.non-null-typehinted.php", + "settings": { + "foreground": "{{colors.error.default.hex}}" + } + }, + { + "name": "php types", + "scope": "storage.type.php,meta.other.type.phpdoc.php,keyword.other.type.php,keyword.other.array.phpdoc.php", + "settings": { + "foreground": "{{colors.secondary.default.hex}}" + } + }, + { + "name": "php call-function", + "scope": "meta.function-call.php,meta.function-call.object.php,meta.function-call.static.php", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "php function-resets", + "scope": "punctuation.definition.parameters.begin.bracket.round.php,punctuation.definition.parameters.end.bracket.round.php,punctuation.separator.delimiter.php,punctuation.section.scope.begin.php,punctuation.section.scope.end.php,punctuation.terminator.expression.php,punctuation.definition.arguments.begin.bracket.round.php,punctuation.definition.arguments.end.bracket.round.php,punctuation.definition.storage-type.begin.bracket.round.php,punctuation.definition.storage-type.end.bracket.round.php,punctuation.definition.array.begin.bracket.round.php,punctuation.definition.array.end.bracket.round.php,punctuation.definition.begin.bracket.round.php,punctuation.definition.end.bracket.round.php,punctuation.definition.begin.bracket.curly.php,punctuation.definition.end.bracket.curly.php,punctuation.definition.section.switch-block.end.bracket.curly.php,punctuation.definition.section.switch-block.start.bracket.curly.php,punctuation.definition.section.switch-block.begin.bracket.curly.php,punctuation.definition.section.switch-block.end.bracket.curly.php", + "settings": { + "foreground": "{{colors.on_background.default.hex}}" + } + }, + { + "name": "support php constants", + "scope": "support.constant.core.rust", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "support php constants", + "scope": "support.constant.ext.php,support.constant.std.php,support.constant.core.php,support.constant.parser-token.php", + "settings": { + "foreground": "{{colors.tertiary.default.hex}}" + } + }, + { + "name": "php goto", + "scope": "entity.name.goto-label.php,support.other.php", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "php logical/bitwise operator", + "scope": "keyword.operator.logical.php,keyword.operator.bitwise.php,keyword.operator.arithmetic.php", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "php regexp operator", + "scope": "keyword.operator.regexp.php", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "php comparison", + "scope": "keyword.operator.comparison.php", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + }, + { + "name": "php heredoc/nowdoc", + "scope": "keyword.operator.heredoc.php,keyword.operator.nowdoc.php", + "settings": { + "foreground": "{{colors.primary.default.hex}}" + } + } + ], + "colors": { + "activityBar.background": "{{colors.surface.default.hex}}", + "activityBar.foreground": "{{colors.on_surface.default.hex}}", + "activityBar.inactiveForeground": "{{colors.on_surface_variant.default.hex}}", + "activityBarBadge.background": "{{colors.primary.default.hex}}", + "activityBarBadge.foreground": "{{colors.on_primary.default.hex}}", + "badge.background": "{{colors.primary.default.hex}}", + "badge.foreground": "{{colors.on_primary.default.hex}}", + "breadcrumb.activeSelectionForeground": "{{colors.primary.default.hex}}", + "breadcrumb.background": "{{colors.background.default.hex}}", + "breadcrumb.focusForeground": "{{colors.on_surface.default.hex}}", + "breadcrumb.foreground": "{{colors.on_surface_variant.default.hex}}", + "breadcrumbPicker.background": "{{colors.surface.default.hex}}", + "button.background": "{{colors.primary.default.hex}}", + "button.foreground": "{{colors.on_primary.default.hex}}", + "button.hoverBackground": "{{colors.primary_container.default.hex}}", + "button.secondaryBackground": "{{colors.secondary.default.hex}}", + "button.secondaryForeground": "{{colors.on_secondary.default.hex}}", + "button.secondaryHoverBackground": "{{colors.secondary_container.default.hex}}", + "checkbox.background": "{{colors.primary_container.default.hex}}", + "checkbox.foreground": "{{colors.on_primary_container.default.hex}}", + "debugExceptionWidget.background": "{{colors.surface.default.hex}}", + "debugExceptionWidget.border": "{{colors.outline.default.hex}}", + "debugToolBar.background": "{{colors.surface.default.hex}}", + "debugToolBar.border": "{{colors.outline.default.hex}}", + "diffEditor.diagonalFill": "{{colors.outline.default.hex}}40", + "diffEditor.insertedTextBackground": "{{colors.secondary.default.hex}}20", + "diffEditor.removedTextBackground": "{{colors.error.default.hex}}20", + "dropdown.background": "{{colors.surface.default.hex}}", + "dropdown.foreground": "{{colors.on_surface.default.hex}}", + "editor.background": "{{colors.background.default.hex}}", + "editor.findMatchBackground": "{{colors.tertiary.default.hex}}80", + "editor.findMatchHighlightBackground": "{{colors.tertiary.default.hex}}60", + "editor.findRangeHighlightBackground": "{{colors.surface_variant.default.hex}}", + "editor.foldBackground": "{{colors.primary.default.hex}}4d", + "editor.foreground": "{{colors.on_background.default.hex}}", + "editor.hoverHighlightBackground": "{{colors.surface_variant.default.hex}}50", + "editor.inactiveSelectionBackground": "{{colors.surface_variant.default.hex}}", + "editor.lineHighlightBackground": "{{colors.surface_variant.default.hex}}50", + "editor.lineHighlightBorder": "{{colors.outline.default.hex}}00", + "editor.selectionBackground": "{{colors.primary.default.hex}}40", + "editor.selectionHighlightBackground": "{{colors.surface_variant.default.hex}}80", + "editor.wordHighlightBackground": "{{colors.surface_variant.default.hex}}", + "editor.wordHighlightStrongBackground": "{{colors.primary_container.default.hex}}80", + "editorBracketHighlight.foreground1": "{{colors.primary.default.hex}}", + "editorBracketHighlight.foreground2": "{{colors.secondary.default.hex}}", + "editorBracketHighlight.foreground3": "{{colors.tertiary.default.hex}}", + "editorBracketHighlight.foreground4": "{{colors.primary_container.default.hex}}", + "editorBracketHighlight.foreground5": "{{colors.secondary_container.default.hex}}", + "editorBracketHighlight.foreground6": "{{colors.tertiary_container.default.hex}}", + "editorBracketHighlight.unexpectedBracket.foreground": "{{colors.error.default.hex}}", + "editorBracketMatch.background": "{{colors.primary.default.hex}}4d", + "editorBracketMatch.border": "{{colors.primary.default.hex}}", + "editorCodeLens.foreground": "{{colors.outline.default.hex}}", + "editorCursor.background": "{{colors.background.default.hex}}", + "editorCursor.foreground": "{{colors.primary.default.hex}}", + "editorError.background": "{{colors.error_container.default.hex}}00", + "editorError.border": "{{colors.outline.default.hex}}00", + "editorError.foreground": "{{colors.error.default.hex}}", + "editorGroup.border": "{{colors.outline.default.hex}}", + "editorGroup.dropBackground": "{{colors.primary_container.default.hex}}40", + "editorGroup.emptyBackground": "{{colors.background.default.hex}}", + "editorGroupHeader.noTabsBackground": "{{colors.background.default.hex}}", + "editorGroupHeader.tabsBackground": "{{colors.surface.default.hex}}", + "editorGroupHeader.tabsBorder": "{{colors.outline.default.hex}}00", + "editorGutter.addedBackground": "{{colors.secondary.default.hex}}80", + "editorGutter.background": "{{colors.background.default.hex}}", + "editorGutter.commentRangeForeground": "{{colors.on_surface_variant.default.hex}}", + "editorGutter.deletedBackground": "{{colors.error.default.hex}}80", + "editorGutter.foldingControlForeground": "{{colors.on_surface_variant.default.hex}}", + "editorGutter.modifiedBackground": "{{colors.primary.default.hex}}80", + "editorHoverWidget.background": "{{colors.surface.default.hex}}", + "editorHoverWidget.border": "{{colors.outline.default.hex}}", + "editorHoverWidget.foreground": "{{colors.on_surface.default.hex}}", + "editorIndentGuide.activeBackground": "{{colors.outline.default.hex}}", + "editorIndentGuide.background": "{{colors.outline.default.hex}}20", + "editorInfo.background": "{{colors.tertiary_container.default.hex}}00", + "editorInfo.border": "{{colors.outline.default.hex}}00", + "editorInfo.foreground": "{{colors.tertiary.default.hex}}", + "editorLineNumber.activeForeground": "{{colors.primary.default.hex}}", + "editorLineNumber.foreground": "{{colors.outline.default.hex}}", + "editorLink.activeForeground": "{{colors.primary.default.hex}}", + "editorMarkerNavigation.background": "{{colors.surface.default.hex}}", + "editorMarkerNavigationError.background": "{{colors.error.default.hex}}40", + "editorMarkerNavigationInfo.background": "{{colors.tertiary.default.hex}}40", + "editorMarkerNavigationWarning.background": "{{colors.secondary.default.hex}}40", + "editorOverviewRuler.addedForeground": "{{colors.secondary.default.hex}}80", + "editorOverviewRuler.border": "{{colors.outline.default.hex}}", + "editorOverviewRuler.deletedForeground": "{{colors.error.default.hex}}80", + "editorOverviewRuler.errorForeground": "{{colors.error.default.hex}}", + "editorOverviewRuler.findMatchForeground": "{{colors.primary.default.hex}}", + "editorOverviewRuler.infoForeground": "{{colors.tertiary.default.hex}}", + "editorOverviewRuler.modifiedForeground": "{{colors.primary.default.hex}}80", + "editorOverviewRuler.rangeHighlightForeground": "{{colors.primary.default.hex}}", + "editorOverviewRuler.selectionHighlightForeground": "{{colors.primary.default.hex}}", + "editorOverviewRuler.warningForeground": "{{colors.secondary.default.hex}}", + "editorOverviewRuler.wordHighlightForeground": "{{colors.secondary.default.hex}}", + "editorOverviewRuler.wordHighlightStrongForeground": "{{colors.secondary.default.hex}}", + "editorRuler.foreground": "{{colors.outline_variant.default.hex}}", + "editorStickyScrollHover.background": "{{colors.surface_variant.default.hex}}", + "editorSuggestWidget.background": "{{colors.surface.default.hex}}", + "editorSuggestWidget.border": "{{colors.outline.default.hex}}", + "editorSuggestWidget.foreground": "{{colors.on_surface.default.hex}}", + "editorSuggestWidget.highlightForeground": "{{colors.primary.default.hex}}", + "editorSuggestWidget.selectedBackground": "{{colors.surface_variant.default.hex}}", + "editorWarning.background": "{{colors.secondary_container.default.hex}}00", + "editorWarning.border": "{{colors.outline.default.hex}}00", + "editorWarning.foreground": "{{colors.secondary.default.hex}}", + "editorWhitespace.foreground": "{{colors.outline.default.hex}}29", + "editorWidget.background": "{{colors.surface.default.hex}}", + "editorWidget.border": "{{colors.outline.default.hex}}", + "editorWidget.foreground": "{{colors.on_surface.default.hex}}", + "editorWidget.resizeBorder": "{{colors.outline.default.hex}}", + "extensionButton.prominentBackground": "{{colors.primary.default.hex}}", + "extensionButton.prominentForeground": "{{colors.on_primary.default.hex}}", + "extensionButton.prominentHoverBackground": "{{colors.primary_container.default.hex}}", + "focusBorder": "{{colors.primary.default.hex}}", + "foreground": "{{colors.on_background.default.hex}}", + "gitDecoration.conflictingResourceForeground": "{{colors.tertiary.default.hex}}c0", + "gitDecoration.deletedResourceForeground": "{{colors.error.default.hex}}c0", + "gitDecoration.ignoredResourceForeground": "{{colors.outline.default.hex}}80", + "gitDecoration.modifiedResourceForeground": "{{colors.primary.default.hex}}c0", + "gitDecoration.submoduleResourceForeground": "{{colors.on_surface_variant.default.hex}}c0", + "gitDecoration.untrackedResourceForeground": "{{colors.secondary.default.hex}}c0", + "icon.foreground": "{{colors.on_background.default.hex}}", + "input.background": "{{colors.surface_variant.default.hex}}", + "input.border": "{{colors.outline.default.hex}}00", + "input.foreground": "{{colors.on_surface_variant.default.hex}}", + "input.placeholderForeground": "{{colors.on_surface_variant.default.hex}}9e", + "inputOption.activeBackground": "{{colors.primary.default.hex}}30", + "inputOption.activeBorder": "{{colors.primary.default.hex}}30", + "inputOption.activeForeground": "{{colors.primary.default.hex}}", + "inputValidation.errorBackground": "{{colors.error_container.default.hex}}", + "inputValidation.errorBorder": "{{colors.error.default.hex}}", + "inputValidation.errorForeground": "{{colors.on_error_container.default.hex}}", + "inputValidation.infoBackground": "{{colors.tertiary_container.default.hex}}", + "inputValidation.infoBorder": "{{colors.tertiary.default.hex}}", + "inputValidation.infoForeground": "{{colors.on_tertiary_container.default.hex}}", + "inputValidation.warningBackground": "{{colors.secondary_container.default.hex}}", + "inputValidation.warningBorder": "{{colors.secondary.default.hex}}", + "inputValidation.warningForeground": "{{colors.on_secondary_container.default.hex}}", + "list.activeSelectionBackground": "{{colors.primary.default.hex}}40", + "list.activeSelectionForeground": "{{colors.on_surface.default.hex}}", + "list.dropBackground": "{{colors.primary_container.default.hex}}40", + "list.focusBackground": "{{colors.primary.default.hex}}30", + "list.focusForeground": "{{colors.on_surface.default.hex}}", + "list.highlightForeground": "{{colors.primary.default.hex}}", + "list.hoverBackground": "{{colors.surface_variant.default.hex}}40", + "list.hoverForeground": "{{colors.on_surface_variant.default.hex}}", + "list.inactiveSelectionBackground": "{{colors.surface_variant.default.hex}}", + "list.inactiveSelectionForeground": "{{colors.on_surface_variant.default.hex}}", + "listFilterWidget.background": "{{colors.tertiary_container.default.hex}}", + "listFilterWidget.noMatchesOutline": "{{colors.error.default.hex}}", + "listFilterWidget.outline": "{{colors.outline.default.hex}}00", + "menu.background": "{{colors.surface.default.hex}}", + "menu.border": "{{colors.outline.default.hex}}85", + "menu.foreground": "{{colors.on_surface.default.hex}}", + "menu.selectionBackground": "{{colors.primary_container.default.hex}}", + "menu.selectionBorder": "{{colors.outline.default.hex}}00", + "menu.selectionForeground": "{{colors.on_primary_container.default.hex}}", + "menu.separatorBackground": "{{colors.outline.default.hex}}", + "menubar.selectionBackground": "{{colors.surface_variant.default.hex}}1a", + "menubar.selectionForeground": "{{colors.on_surface.default.hex}}", + "merge.border": "{{colors.outline.default.hex}}00", + "merge.commonContentBackground": "{{colors.outline.default.hex}}20", + "merge.commonHeaderBackground": "{{colors.outline.default.hex}}30", + "merge.currentContentBackground": "{{colors.primary.default.hex}}20", + "merge.currentHeaderBackground": "{{colors.primary.default.hex}}30", + "merge.incomingContentBackground": "{{colors.tertiary.default.hex}}20", + "merge.incomingHeaderBackground": "{{colors.tertiary.default.hex}}30", + "minimap.background": "{{colors.background.default.hex}}", + "minimap.errorHighlight": "{{colors.error.default.hex}}80", + "minimap.findMatchHighlight": "{{colors.primary.default.hex}}80", + "minimap.selectionHighlight": "{{colors.primary.default.hex}}40", + "minimap.warningHighlight": "{{colors.secondary.default.hex}}80", + "minimapGutter.addedBackground": "{{colors.secondary.default.hex}}80", + "minimapGutter.deletedBackground": "{{colors.error.default.hex}}80", + "minimapGutter.modifiedBackground": "{{colors.primary.default.hex}}80", + "notificationCenter.border": "{{colors.outline.default.hex}}", + "notificationCenterHeader.background": "{{colors.surface_variant.default.hex}}", + "notificationCenterHeader.foreground": "{{colors.on_surface_variant.default.hex}}", + "notificationToast.border": "{{colors.outline.default.hex}}", + "notifications.background": "{{colors.surface.default.hex}}", + "notifications.border": "{{colors.outline.default.hex}}", + "notifications.foreground": "{{colors.on_surface.default.hex}}", + "notificationsErrorIcon.foreground": "{{colors.error.default.hex}}", + "notificationsInfoIcon.foreground": "{{colors.tertiary.default.hex}}", + "notificationsWarningIcon.foreground": "{{colors.secondary.default.hex}}", + "panel.background": "{{colors.surface.default.hex}}", + "panel.border": "{{colors.outline.default.hex}}", + "panel.dropBorder": "{{colors.primary.default.hex}}", + "panelSection.border": "{{colors.outline.default.hex}}", + "panelTitle.activeBorder": "{{colors.primary.default.hex}}", + "panelTitle.activeForeground": "{{colors.primary.default.hex}}", + "panelTitle.inactiveForeground": "{{colors.on_surface_variant.default.hex}}", + "peekView.border": "{{colors.primary.default.hex}}", + "peekViewEditor.background": "{{colors.surface.default.hex}}", + "peekViewEditor.matchHighlightBackground": "{{colors.tertiary.default.hex}}99", + "peekViewEditor.matchHighlightBorder": "{{colors.tertiary.default.hex}}", + "peekViewEditorGutter.background": "{{colors.surface.default.hex}}", + "peekViewResult.background": "{{colors.surface_variant.default.hex}}", + "peekViewResult.fileForeground": "{{colors.on_surface_variant.default.hex}}", + "peekViewResult.lineForeground": "{{colors.on_surface_variant.default.hex}}", + "peekViewResult.matchHighlightBackground": "{{colors.tertiary.default.hex}}40", + "peekViewResult.selectionBackground": "{{colors.primary.default.hex}}40", + "peekViewResult.selectionForeground": "{{colors.on_primary_container.default.hex}}", + "peekViewTitle.background": "{{colors.surface_variant.default.hex}}", + "peekViewTitleDescription.foreground": "{{colors.on_surface_variant.default.hex}}", + "peekViewTitleLabel.foreground": "{{colors.primary.default.hex}}", + "pickerGroup.border": "{{colors.outline.default.hex}}", + "pickerGroup.foreground": "{{colors.primary.default.hex}}", + "progressBar.background": "{{colors.primary.default.hex}}", + "scrollbar.shadow": "{{colors.background.default.hex}}00", + "scrollbarSlider.activeBackground": "{{colors.on_surface.default.hex}}80", + "scrollbarSlider.background": "{{colors.on_surface.default.hex}}30", + "scrollbarSlider.hoverBackground": "{{colors.on_surface.default.hex}}50", + "selection.background": "{{colors.primary.default.hex}}66", + "settings.checkboxBackground": "{{colors.surface_variant.default.hex}}", + "settings.checkboxBorder": "{{colors.outline.default.hex}}", + "settings.checkboxForeground": "{{colors.on_surface_variant.default.hex}}", + "settings.dropdownBackground": "{{colors.surface_variant.default.hex}}", + "settings.dropdownBorder": "{{colors.outline.default.hex}}", + "settings.dropdownForeground": "{{colors.on_surface_variant.default.hex}}", + "settings.focusedRowBackground": "{{colors.surface_variant.default.hex}}50", + "settings.headerForeground": "{{colors.on_background.default.hex}}", + "settings.modifiedItemIndicator": "{{colors.primary.default.hex}}", + "settings.numberInputBackground": "{{colors.surface_variant.default.hex}}", + "settings.numberInputBorder": "{{colors.outline.default.hex}}", + "settings.numberInputForeground": "{{colors.on_surface_variant.default.hex}}", + "settings.textInputBackground": "{{colors.surface_variant.default.hex}}", + "settings.textInputBorder": "{{colors.outline.default.hex}}", + "settings.textInputForeground": "{{colors.on_surface_variant.default.hex}}", + "sideBar.background": "{{colors.surface_container.default.hex}}", + "sideBar.dropBackground": "{{colors.surface_variant.default.hex}}40", + "sideBar.foreground": "{{colors.on_surface_variant.default.hex}}", + "sideBarSectionHeader.background": "{{colors.surface.default.hex}}", + "sideBarSectionHeader.foreground": "{{colors.on_surface.default.hex}}", + "sideBarTitle.foreground": "{{colors.primary.default.hex}}", + "statusBar.background": "{{colors.surface_variant.default.hex}}", + "statusBar.debuggingBackground": "{{colors.tertiary.default.hex}}", + "statusBar.debuggingForeground": "{{colors.on_tertiary.default.hex}}", + "statusBar.foreground": "{{colors.on_surface_variant.default.hex}}", + "statusBar.noFolderBackground": "{{colors.surface.default.hex}}", + "statusBar.noFolderForeground": "{{colors.on_surface.default.hex}}", + "statusBarItem.activeBackground": "{{colors.primary.default.hex}}25", + "statusBarItem.hoverBackground": "{{colors.primary.default.hex}}1f", + "statusBarItem.remoteBackground": "{{colors.primary.default.hex}}", + "statusBarItem.remoteForeground": "{{colors.on_primary.default.hex}}", + "tab.activeBackground": "{{colors.surface_variant.default.hex}}", + "tab.activeBorder": "{{colors.primary.default.hex}}", + "tab.activeForeground": "{{colors.on_surface_variant.default.hex}}", + "tab.activeModifiedBorder": "{{colors.tertiary.default.hex}}", + "tab.border": "{{colors.outline.default.hex}}00", + "tab.inactiveBackground": "{{colors.surface.default.hex}}", + "tab.inactiveForeground": "{{colors.on_surface.default.hex}}", + "tab.inactiveModifiedBorder": "{{colors.tertiary.default.hex}}80", + "tab.unfocusedActiveBackground": "{{colors.surface.default.hex}}99", + "tab.unfocusedActiveBorder": "{{colors.primary.default.hex}}80", + "tab.unfocusedActiveForeground": "{{colors.primary.default.hex}}99", + "tab.unfocusedActiveModifiedBorder": "{{colors.tertiary.default.hex}}40", + "tab.unfocusedInactiveBackground": "{{colors.surface_variant.default.hex}}99", + "tab.unfocusedInactiveForeground": "{{colors.on_surface_variant.default.hex}}99", + "tab.unfocusedInactiveModifiedBorder": "{{colors.tertiary.default.hex}}20", + "terminal.ansiBlack": "{{colors.outline.default.hex}}", + "terminal.ansiBlue": "{{colors.primary.default.hex}}", + "terminal.ansiBrightBlack": "{{colors.on_surface_variant.default.hex}}", + "terminal.ansiBrightBlue": "{{colors.primary_container.default.hex}}", + "terminal.ansiBrightCyan": "{{colors.tertiary.default.hex}}", + "terminal.ansiBrightGreen": "{{colors.secondary_container.default.hex}}", + "terminal.ansiBrightMagenta": "{{colors.tertiary_container.default.hex}}", + "terminal.ansiBrightRed": "{{colors.error_container.default.hex}}", + "terminal.ansiBrightWhite": "{{colors.on_background.default.hex}}", + "terminal.ansiBrightYellow": "{{colors.secondary.default.hex}}", + "terminal.ansiCyan": "{{colors.tertiary_container.default.hex}}", + "terminal.ansiGreen": "{{colors.secondary.default.hex}}", + "terminal.ansiMagenta": "{{colors.tertiary.default.hex}}", + "terminal.ansiRed": "{{colors.error.default.hex}}", + "terminal.ansiWhite": "{{colors.on_surface.default.hex}}", + "terminal.ansiYellow": "{{colors.secondary_container.default.hex}}", + "terminal.background": "{{colors.background.default.hex}}", + "terminal.foreground": "{{colors.on_background.default.hex}}", + "terminalCursor.background": "{{colors.background.default.hex}}", + "terminalCursor.foreground": "{{colors.primary.default.hex}}", + "textLink.activeForeground": "{{colors.primary_container.default.hex}}", + "textLink.foreground": "{{colors.primary.default.hex}}", + "titleBar.activeBackground": "{{colors.surface.default.hex}}", + "titleBar.activeForeground": "{{colors.on_surface.default.hex}}", + "titleBar.border": "{{colors.outline.default.hex}}00", + "titleBar.inactiveBackground": "{{colors.surface_variant.default.hex}}", + "titleBar.inactiveForeground": "{{colors.on_surface_variant.default.hex}}", + "tree.indentGuidesStroke": "{{colors.outline.default.hex}}", + "walkThrough.embeddedEditorBackground": "{{colors.surface.default.hex}}50", + "welcomePage.buttonBackground": "{{colors.surface_variant.default.hex}}", + "welcomePage.buttonHoverBackground": "{{colors.primary.default.hex}}20", + "widget.shadow": "{{colors.background.default.hex}}5c" + } +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/discord-material.css b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/discord-material.css new file mode 100644 index 0000000..35927da --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/discord-material.css @@ -0,0 +1,94 @@ +/** + * @name Noctalia Material + * @version 3.0.6 + * @description A fork of 'Material Discord' for Noctalia Shell + * @author CapnKitten + * + * @website http://github.com/CapnKitten + * @source https://github.com/CapnKitten/BetterDiscord/blob/master/Themes/Material-Discord/css/source.css + * @donate https://paypal.me/capnkitten + * @invite jzJkA6Z + */ + +@import url("https://capnkitten.github.io/BetterDiscord/Themes/Material-Discord/css/source.css"); + +/* Material You addon */ +@import url("https://capnkitten.github.io/BetterDiscord/Themes/Material-Discord/css/addons/material-you/source.css"); + + + + +/* MATERIAL YOU DARK THEME SETTINGS */ +.theme-dark { + --saturation-modifier: 0.31; + --lightness-modifier: 0.225; + --text-lightness-modifier: 1.0; +} + +/* MATERIAL YOU LIGHT THEME SETTINGS */ +.theme-light { + --saturation-modifier: 0.74; + --lightness-modifier: 2.125; + --text-lightness-modifier: 1.0; +} + +:root, .theme-dark, .theme-light { + /* See options at https://github.com/CapnKitten/Material-Discord */ + /* Accent */ + --accent-hue: {{colors.primary.default.hue}}; + --accent-saturation: {{colors.primary.default.saturation}}%; + --accent-lightness: {{colors.primary.default.lightness}}%; + --accent-text-color: {{colors.on_primary.default.hex}}; + + /* App */ + --app-bg: {{colors.surface.default.hex}}; + --main-color: {{colors.surface.default.hex}}; + --main-alt: {{colors.surface.default.hex}}; + --server-color: {{colors.surface.default.hex}}; + --status-picker-color: {{colors.surface_container.default.hex}}; + --sidebar-panel-color: {{colors.surface_container.default.hex}}; + --chat-color: {{colors.surface.default.hex}}; + --tooltip-color: {{colors.surface.default.hex}}; + --tooltip-text-color: {{colors.on_surface.default.hex}}; + + /* Message */ + --message-color: {{colors.surface_container.default.hex}}; + --message-color-hover: hsl( + var(--accent-hue), + calc(var(--accent-saturation) * var(--saturation-modifier) * var(--saturation-factor)), + calc(var(--accent-lightness) * var(--lightness-modifier) * var(--ui-darkness-modifier) * 4) + ) !important; + --message-color-alt: {{colors.on_secondary.default.hex}}; + --attachment-color: {{colors.on_primary.default.hex}}; + --main-textarea-color: {{colors.primary.default.hex}}; + --main-textarea-button-color: {{colors.on_primary.default.hex}}; + --main-textarea-color-alt: {{colors.secondary.default.hex}}; + --main-textarea-border: {{colors.surface_container.default.hex}}; + + /* Card */ + --card-color-filled: {{colors.surface_container.default.hex}}; + --card-color-hover: {{colors.inverse_primary.default.hex}}; + --card-color-active: {{colors.inverse_primary.default.hex}}; + --card-color-alt: {{colors.secondary_container.default.hex}}; + --card-color-alt-hover: {{colors.secondary.default.hex}}; + --card-border-color: {{colors.surface_container.default.hex}}; + --card-header-text-color: {{colors.on_surface.default.hex}}; + + /* Slider & Switch */ + --switch-knob-color: {{colors.surface_container.default.hex}}; + --switch-slider-color: {{colors.surface.default.hex}}; + + /* Input */ + --input-text-color: {{colors.on_surface.default.hex}}; + + /* Misc */ + --app-watermark-one: "Noctalia"; + --app-watermark-two: "Discord"; + + /* Ensure channels-default is different from the active one - works in dark mode / light mode is another story... */ + --text-tertiary: hsl( + var(--accent-hue), + calc(var(--accent-saturation) * var(--saturation-modifier) * 0.78 * var(--saturation-factor)), + calc(var(--accent-lightness) * var(--lightness-modifier) * var(--ui-darkness-modifier) * var(--text-lightness-modifier) * 3.0) + ) !important; +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/discord-midnight.css b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/discord-midnight.css new file mode 100644 index 0000000..fc79d1a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/discord-midnight.css @@ -0,0 +1,370 @@ +/** +* @name Noctalia Midnight +* @description A fork of 'Midnight' for Noctalia Shell +* @author refact0r +* @version 2.1.1 +* @invite nz87hXyvcy +* @website https://github.com/refact0r/midnight-discord +* @source https://github.com/refact0r/midnight-discord/blob/master/themes/midnight.theme.css +* @authorId 508863359777505290 +* @authorLink https://www.refact0r.dev +*/ + +/* import theme modules */ +@import url('https://refact0r.github.io/midnight-discord/build/midnight.css'); + +body { + /* font options */ + --font: ''; /* change to '' for default discord font */ + --code-font: ''; /* change to '' for default discord font */ + font-weight: 400; /* normal text font weight. DOES NOT AFFECT BOLD TEXT */ + + /* sizes */ + --gap: 12px; /* spacing between panels */ + --divider-thickness: 4px; /* thickness of unread messages divider and highlighted message borders */ + --border-thickness: 1px; /* thickness of borders around main panels. DOES NOT AFFECT OTHER BORDERS */ + + /* animation/transition options */ + --animations: on; /* off: disable animations/transitions, on: enable animations/transitions */ + --list-item-transition: 0.2s ease; /* transition for list items */ + --dms-icon-svg-transition: 0.4s ease; /* transition for the dms icon */ + --border-hover-transition: 0.2s ease; /* transition for borders when hovered */ + + /* top bar options */ + --top-bar-height: var(--gap); /* height of the top bar (discord default is 36px, old discord style is 24px, var(--gap) recommended if button position is set to titlebar) */ + --top-bar-button-position: titlebar; /* off: default position, hide: hide buttons completely, serverlist: move inbox button to server list, titlebar: move inbox button to channel titlebar (will hide title) */ + --top-bar-title-position: off; /* off: default centered position, hide: hide title completely, left: left align title (like old discord) */ + --subtle-top-bar-title: off; /* off: default, on: hide the icon and use subtle text color (like old discord) */ + + /* window controls */ + --custom-window-controls: on; /* off: default window controls, on: custom window controls */ + --window-control-size: 14px; /* size of custom window controls */ + + /* dms button options */ + --custom-dms-icon: custom; /* off: use default discord icon, hide: remove icon entirely, custom: use custom icon */ + --dms-icon-svg-url: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB3aWR0aD0iMjU2IgogICBoZWlnaHQ9IjI1NiIKICAgdmlld0JveD0iMCAwIDY3LjczMzMzNCA2Ny43MzMzMzQiCiAgIHZlcnNpb249IjEuMSIKICAgaWQ9InN2ZzEiCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPGRlZnMKICAgICBpZD0iZGVmczEiIC8+CiAgPGcKICAgICBpZD0iZzEiCiAgICAgdHJhbnNmb3JtPSJtYXRyaXgoMS4xMDEwODY5LDAsMCwxLjEwMTA4NjksLTMuNDIzODM0NywtMy40MjM0ODkzKSI+CiAgICA8ZwogICAgICAgaWQ9Imc1NSIKICAgICAgIHN0eWxlPSJmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjEiCiAgICAgICB0cmFuc2Zvcm09Im1hdHJpeCg0LjIwNTU5MDUsMCwwLDQuMjA1NTkwNSwtMTQuNzg4ODk4LC0xODAuNDMzNjIpIj4KICAgICAgPHBhdGgKICAgICAgICAgaWQ9InBhdGg1NSIKICAgICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6OS4xO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtwYWludC1vcmRlcjpmaWxsIG1hcmtlcnMgc3Ryb2tlIgogICAgICAgICBkPSJtIDExLjY0NjQ4NCw0My42NDI1NzggYSA3LjMxNDEzNTYsNy4zMTQxMzU2IDAgMCAwIC03LjMxMjQ5OTYsNy4zMTI1IDcuMzE0MTM1Niw3LjMxNDEzNTYgMCAwIDAgNy4zMTI0OTk2LDcuMzE0NDUzIDcuMzE0MTM1Niw3LjMxNDEzNTYgMCAwIDAgNy4xNTgyMDQsLTUuODk4NDM3IDYuMjIwMzMwMiw2LjIyMDMzMDIgMCAwIDEgLTMuNDE2MDE2LDMuNDY0ODQ0IGMgMC4wMDE1LDAuMjc3NDA2IDAuMDAzOSwwLjUzMjU1IDAuMDAzOSwwLjcyNjU2MiAtMC43Mzk0NDUsMCAtMS4zNzU4MzUsLTAuMTAxODg2IC0xLjkyMTg3NSwtMC4yNzE0ODQgYSA2LjIyMDMzMDIsNi4yMjAzMzAyIDAgMCAxIC0wLjQ0MTQwNiwwLjAzNTE2IDYuMjIwMzMwMiw2LjIyMDMzMDIgMCAwIDEgLTYuMjIwNzAzMiwtNi4yMjA3MDMgNi4yMjAzMzAyLDYuMjIwMzMwMiAwIDAgMSA2LjIyMDcwMzIsLTYuMjIwNzAzIDYuMjIwMzMwMiw2LjIyMDMzMDIgMCAwIDEgMC41NzYxNzIsMC4wMjczNCA3LjMxNDEzNTYsNy4zMTQxMzU2IDAgMCAwIC0xLjk1ODk4NSwtMC4yNjk1MzEgeiIgLz4KICAgIDwvZz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDU2IgogICAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MS4zNDYwODtzdHJva2UtbWl0ZXJsaW1pdDo5LjE7cGFpbnQtb3JkZXI6ZmlsbCBtYXJrZXJzIHN0cm9rZSIKICAgICAgIGQ9Im0gNDIuNjY0NDQsMzEuMjkxNDc0IGMgLTEuODAyODgyLDIuMDIzMTA1IC00Ljk1MDEzOSwyLjQ5MjY2MSAtOC41MjU0LDIuNTAyOCAtMi43MTgxNiwtMC4wMDc3IC01LjQ1MDQ3NiwtMC4xMjgyMjcgLTcuMjY5NTYsLTEuMzg5NTUxIC0xLjgxNjU0MiwwLjMyNDE4MiAtMi41NzkxNTcsMi4zNDQwMzMgLTIuNDE1ODY2LDQuNzAyNzc3IDEuMTY0NjQ3LDE2LjgyMjk2NyAxMS44MTY3MiwxNS43MzQ2NDUgMTEuOTEyMTk4LDE1LjcwNzE0NSAtNy43OTMzNjYsLTMuODAyNDQ3IC02LjUxNjQ5OCwtMTAuMDAwNDggLTYuNzcyNjIyLC0xMS45NzUyOTMgMC4zMjA5MjgsMy4zNDIzMyAzLjg5NTkyNiwxMi44NTY3NjQgMTcuMjM1NDk0LDEyLjg1Njc2NCAwLC00LjAwOTY1NSAwLjAxOTkyLC0xNC4zMTEwMzUgLTEuMjc5MDgsLTE3LjQ4NzAyNCAtMC40MDkxMjQsLTEuMjM3MzEyIC0xLjUyNTI2LC00LjU0Njk2MyAtMi44ODUxNjQsLTQuOTE3NjE4IHoiIC8+CiAgICA8cGF0aAogICAgICAgaWQ9InBhdGg1NyIKICAgICAgIHN0eWxlPSJmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjMuNzY1NjU7c3Ryb2tlLW1pdGVybGltaXQ6OS4xO3BhaW50LW9yZGVyOmZpbGwgbWFya2VycyBzdHJva2UiCiAgICAgICBkPSJtIDIwLjk5MjAxMywxNi4wODcwMDQgYyAwLjYwODk0NCwxLjg2NTU0NSAxLjgzNTMzNCwzLjcwNDMxNCAzLjQ4MDA4Niw0LjQwMTc0NSAtMC40NTg3ODEsMS4zODI4MDQgLTAuNDYwODMxLDMuMDI0OTQ5IC0wLjQ2MDgzMSw0Ljc4MzEyNiAwLDQuOTI3ODk1IDQuNTMzNjk4LDguOTE5NDEzIDEwLjEzMDM1OSw4LjkzODU1OSA1LjU5NjY2NCwtMC4wMTkxMiAxMC4xMzAzNjcsLTQuMDEwNjY3IDEwLjEzMDM2NywtOC45Mzg1NTkgMCwtMS43NTgxNzcgLTAuMDAyMSwtMy40MDAzMjIgLTAuNDYwODMzLC00Ljc4MzEyNiAxLjY0NDc1MywtMC42OTc0MzEgMi44NzExNDIsLTIuNTM2MiAzLjQ4MDA4NCwtNC40MDE3NDUgLTEuOTg3Nzc2LC0wLjAzMDUgLTUuMTUwOTA4LDAuNzAwNzU3IC03Ljg1MDA0NywwLjc4NjU5NSAtMS4zNTE1MzksLTAuMzUxODQ1IC0zLjA4NjEzNywtMC41NDU4NjggLTUuMjk5NTcxLC0wLjU0ODIzMyAtMi4yMTM0MzEsMC4wMDI0IC0zLjk0ODAyNywwLjE5NjM2NyAtNS4yOTk1NjgsMC41NDgyMzMgLTIuNjk5MTM1LC0wLjA4NTg0IC01Ljg2MjI2NiwtMC44MTcxODcgLTcuODUwMDQ2LC0wLjc4NjU5NSB6IG0gNi43Nzc0MTksNC42NDgwNTMgYyAwLjU1NTQ3LC0wLjAxMzgzIDEuMjk5Nzc5LDAuMDYyMDQgMi4zMDQxNjEsMC4yNDYzMTkgMC42NzIxOTgsMC4xMjMzMDIgMS41MjM1MzYsMC40NTI0MDIgMi4wMzQwMTksMC43NTQ4MTMgMC45NTA4MDcsMC41NjMyNjQgMS4wNzM1OTUsMS4yMDc2OTcgMi4wMzQwMTUsMS4yMDc2OTcgMC45NjA0MiwwIDEuMDgzMjA3LC0wLjY0NDQzMyAyLjAzNDAxOCwtMS4yMDc2OTcgMC41MTA0ODMsLTAuMzAyNDE4IDEuMzYxODIyLC0wLjYzMTUxMSAyLjAzNDAyMSwtMC43NTQ4MTMgNC41OTE0NTksLTAuODQyMzI4IDMuODIxNzMyLDAuNTIzOTYyIDMuODIxNzMyLDIuODM2NTA1IDAsMi4zODcxMzIgLTEuOTM1MTU1LDQuMzIyMjkgLTQuMzIyMjkxLDQuMzIyMjkgLTAuNzYyOTUsLTAuMDAzNyAtMS41MTEzMzIsLTAuMjA5MzQgLTIuMTY5MDkxLC0wLjU5NTkwNSAtMC4wMTU0NiwwLjAzMzc1IC0wLjAyODg3LDAuMDY0ODQgLTAuMDQ3NjgsMC4wOTUzNSBsIC0wLjk2OTMzOCwxLjU3MzE4OSBjIC0wLjEwNTIzOSwwLjE3MTIyMiAtMC4yNDM3OTcsMC4yNTc0NjcgLTAuMzgxMzc4LDAuMjU0MjUxIC0wLjEzNzU4LDAuMDAzMSAtMC4yNzYxMzksLTAuMDgzMDMgLTAuMzgxMzc4LC0wLjI1NDI1MSBsIC0wLjk2OTMzOCwtMS41NzMxODkgYyAtMC4wMTg3MSwtMC4wMzA1IC0wLjAzMjEyLC0wLjA2MTU5IC0wLjA0NzY4LC0wLjA5NTM1IC0wLjY1Nzc2MywwLjM4NjU4NSAtMS40MDYxNDIsMC41OTIxODggLTIuMTY5MDkxLDAuNTk1OTA1IC0yLjM4NzEzNywwIC00LjMyMjI5LC0xLjkzNTE1OCAtNC4zMjIyOSwtNC4zMjIyOSAwLC0xLjgwNjY3MyAtMC40NjYyNzEsLTMuMDMzMTczIDEuNTE3NTY5LC0zLjA4MjgxNCB6IiAvPgogICAgPHBhdGgKICAgICAgIGlkPSJwYXRoNTgiCiAgICAgICBzdHlsZT0iZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxLjIyMDUyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjkuMTtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLW9wYWNpdHk6MTtwYWludC1vcmRlcjpmaWxsIG1hcmtlcnMgc3Ryb2tlIgogICAgICAgZD0iTSAzMi4zMDQyLDI1LjMwMDU5NiBBIDEuOTkxNDQzOCwxLjk5MTQ0MzggMCAwIDEgMzAuNTc5NTU5LDI2LjI5NjMyIDEuOTkxNDQzOCwxLjk5MTQ0MzggMCAwIDEgMjguODU0OTE4LDI1LjMwMDU5NiIgLz4KICAgIDxwYXRoCiAgICAgICBpZD0icGF0aDU5IgogICAgICAgc3R5bGU9ImZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MS4yMjA1MjtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDo5LjE7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1vcGFjaXR5OjE7cGFpbnQtb3JkZXI6ZmlsbCBtYXJrZXJzIHN0cm9rZSIKICAgICAgIGQ9Ik0gMzkuNDI4MzM0LDI1LjMwMDU5NiBBIDEuOTkxNDQzOCwxLjk5MTQ0MzggMCAwIDEgMzcuNzAzNjk2LDI2LjI5NjMyIDEuOTkxNDQzOCwxLjk5MTQ0MzggMCAwIDEgMzUuOTc5MDUzLDI1LjMwMDU5NiIgLz4KICA8L2c+Cjwvc3ZnPg=='); /* icon svg url. MUST BE A SVG. */ + --dms-icon-svg-size: 90%; /* size of the svg (css mask-size property) */ + --dms-icon-color-before: var(--icon-subtle); /* normal icon color */ + --dms-icon-color-after: var(--white); /* icon color when button is hovered/selected */ + --custom-dms-background: off; /* off to disable, image to use a background image (must set url variable below), color to use a custom color/gradient */ + --dms-background-image-url: url(''); /* url of the background image */ + --dms-background-image-size: cover; /* size of the background image (css background-size property) */ + --dms-background-color: linear-gradient(70deg, var(--blue-2), var(--purple-2), var(--red-2)); /* fixed color/gradient (css background property) */ + + /* background image options */ + --background-image: off; /* off: no background image, on: enable background image (must set url variable below) */ + --background-image-url: url(''); /* url of the background image */ + + /* transparency/blur options */ + /* NOTE: TO USE TRANSPARENCY/BLUR, YOU MUST HAVE TRANSPARENT BG COLORS. FOR EXAMPLE: --bg-4: hsla(220, 15%, 10%, 0.7); */ + --transparency-tweaks: off; /* off: no changes, on: remove some elements for better transparency */ + --remove-bg-layer: off; /* off: no changes, on: remove the base --bg-3 layer for use with window transparency (WILL OVERRIDE BACKGROUND IMAGE) */ + --panel-blur: off; /* off: no changes, on: blur the background of panels */ + --blur-amount: 12px; /* amount of blur */ + --bg-floating: var(--bg-3); /* set this to a more opaque color if floating panels look too transparent. only applies if panel blur is on */ + + /* chatbar options */ + --custom-chatbar: separated; /* off: default chatbar, separated: chatbar separated from chat */ + --chatbar-height: 56px; /* height of the chatbar (56px by default, 47px to align with user panel, 56px recommended for separated) */ + + /* other options */ + --small-user-panel: off; /* off: default user panel, on: smaller user panel like in old discord */ + +} + +/* color options */ +:root { + --colors: on; /* off: discord default colors, on: midnight custom colors */ + + /* text colors */ + --text-0: {{colors.background.default.hex}}; /* text on colored elements */ + --text-1: {{colors.on_background.default.hex}}; /* other normally white text */ + --text-2: {{colors.on_surface.default.hex}}; /* headings and important text */ + --text-3: {{colors.on_surface_variant.default.hex}}; /* normal text */ + --text-4: {{colors.outline.default.hex}}; /* icon buttons and channels */ + --text-5: {{colors.outline_variant.default.hex}}; /* muted channels/chats and timestamps */ + + /* background and dark colors */ + --bg-1: {{colors.surface_container_lowest.default.hex}}; /* dark buttons when clicked */ + --bg-2: {{colors.surface_container_low.default.hex}}; /* dark buttons */ + --bg-3: {{colors.surface_container.default.hex}}; /* spacing, secondary elements */ + --bg-4: {{colors.surface.default.hex}}; /* main background color */ + --hover: {{colors.on_surface.default.hex}}15; /* channels and buttons when hovered */ + --active: {{colors.surface_container_highest.default.hex}}; /* channels and buttons when clicked or selected */ + --active-2: {{colors.surface.default.hex}}; /* extra state for transparent buttons */ + --message-hover: {{colors.surface_variant.default.hex}}40; /* messages when hovered */ + + /* accent colors */ + --accent-1: {{colors.primary.default.hex}}; /* links and other accent text */ + --accent-2: {{colors.primary_container.default.hex}}; /* small accent elements */ + --accent-3: {{colors.secondary.default.hex}}; /* accent buttons */ + --accent-4: {{colors.on_secondary_container.default.hex}}; /* accent buttons when hovered */ + --accent-5: {{colors.secondary.default.hex}}; /* accent buttons when clicked */ + --accent-new: {{colors.error.default.hex}}; /* stuff that's normally red like mute/deafen buttons */ + + --mention: linear-gradient(to right, color-mix(in hsl, {{colors.primary_container.default.hex}}, transparent 90%) 40%, transparent); /* background of messages that mention you */ + --mention-hover: linear-gradient(to right, color-mix(in hsl, {{colors.primary_container.default.hex}}, transparent 95%) 40%, transparent); /* background of messages that mention you when hovered */ + + --reply: linear-gradient(to right, color-mix(in hsl, {{colors.tertiary_container.default.hex}}, transparent 90%) 40%, transparent); /* background of messages that reply to you */ + --reply-hover: linear-gradient(to right, color-mix(in hsl, {{colors.tertiary_container.default.hex}}, transparent 95%) 40%, transparent); /* background of messages that reply to you when hovered */ + + /* status indicator colors */ + --online: var(--green-2); /* change to #43a25a for default */ + --dnd: var(--red-2); /* change to #d83a42 for default */ + --idle: var(--yellow-2); /* change to #ca9654 for default */ + --streaming: var(--purple-2); /* change to #593695 for default */ + --offline: var(--text-4); /* change to #83838b for default offline color */ + + /* border colors */ + --border-light: var(--hover); /* general light border color */ + --border: var(--active); /* general normal border color */ + --border-hover: var(--active); /* border color of panels when hovered */ + --button-border: hsl(230, 0%, 100%, 0.1); /* neutral border color of buttons */ + + /* base colors */ + --red-1: hsl(351deg, 74%, 78%); + --red-2: hsl(351deg, 74%, 73%); + --red-3: hsl(351deg, 74%, 73%); + --red-4: hsl(351deg, 74%, 68%); + --red-5: hsl(351deg, 74%, 63%); + + --green-1: hsl(105deg, 48%, 77%); + --green-2: hsl(105deg, 48%, 72%); + --green-3: hsl(105deg, 48%, 72%); + --green-4: hsl(105deg, 48%, 67%); + --green-5: hsl(105deg, 48%, 62%); + + --blue-1: hsl(199deg, 66%, 74%); + --blue-2: hsl(199deg, 66%, 69%); + --blue-3: hsl(199deg, 66%, 69%); + --blue-4: hsl(199deg, 66%, 64%); + --blue-5: hsl(199deg, 66%, 59%); + + --yellow-1: hsl(40deg, 70%, 83%); + --yellow-2: hsl(40deg, 70%, 78%); + --yellow-3: hsl(40deg, 70%, 78%); + --yellow-4: hsl(40deg, 70%, 73%); + --yellow-5: hsl(40deg, 70%, 68%); + + --purple-1: hsl(267deg, 83%, 85%); + --purple-2: hsl(267deg, 83%, 80%); + --purple-3: hsl(267deg, 83%, 80%); + --purple-4: hsl(267deg, 83%, 75%); + --purple-5: hsl(267deg, 83%, 70%); +} + +/* Dividers (Channel list and messages) */ +[class*="divider"]:not([class*="isUnread"]) { + border-color: var(--bg-2) !important; +} + +/* Profile Banners */ +[class*="bannerButton"], +[class*="bannerColor"] { + color: var(--text-1) !important; +} + +/* Prevent scaling animation on Home/DMS icon */ +[class*="guildsnav"] [class*="wrapper"][data-list-item-id='guildsnav___home']:hover > [class*="childWrapper"]::before, +[class*="guildsnav"] [class*="wrapper"][data-list-item-id='guildsnav___home'][class*="selected"] > [class*="childWrapper"]::before { + transform: rotate(-360deg) scale(1) !important; +} + +/* Timestamps */ +[class*="timestamp"], +[class*="timestampInline"], +[class*="timestamp"] time { + color: var(--text-4) !important; +} + +/* Forum/Thread Titles */ +[class*="postTitleText"], +[class*="postTitleText"] [class*="highlight"] { + color: var(--text-2) !important; +} + +/* Message Content */ +[class*="messageContent"] { + color: var(--text-3) !important; +} + +[class*="messageContent"] [class*="highlight"] { + color: var(--text-2) !important; +} + +/* Slowmode/Cooldown Text */ +[class*="cooldownText"] { + color: var(--text-4) !important; +} + +/* Checkbox styling overrides */ +[class*="checkboxIndicator"] { + background-color: transparent !important; + border-color: var(--text-4) !important; +} + +[class*="checkboxOption"]:hover [class*="checkboxIndicator"] { + background-color: color-mix(in srgb, var(--accent-1) 20%, transparent) !important; + border-color: var(--accent-3) !important; +} + +[class*="checkboxOption"][data-selected] [class*="checkboxIndicator"] { + background-color: var(--accent-1) !important; + border-color: var(--accent-3) !important; +} + +[class*="checkboxOption"][data-selected]:hover [class*="checkboxIndicator"] { + background-color: var(--accent-1) !important; + border-color: var(--accent-3) !important; +} + +[data-slate-node="element"], +[data-slate-node="text"], +[data-slate-string="true"] { + color: var(--text-3) !important; +} + +[class*="eyebrow"][class*="badge"][class*="expressive"] { + color: var(--accent-1) !important; + background-color: color-mix(in srgb, var(--accent-1) 15%, transparent) !important; +} + +/* Outer circle (non-selected background) */ +[class*="outerRadioBase"] { + fill: var(--bg-1) !important; /* background for non-selected radios */ + stroke: var(--border) !important; /* ring border */ + stroke-width: 1px; + transition: stroke 0.2s ease; +} + +/* Outer circle fill (selected) */ +[class*="outerRadioFill"] { + fill: var(--bg-1) !important; /* accent color for selected */ + stroke: var(--border) !important; + opacity: 0; /* hidden by default */ + transition: opacity 0.2s ease; +} + +/* Inner dot (selected) */ +[class*="innerDotRadio"] { + fill: var(--accent-3) !important; /* contrast dot */ + opacity: 0; /* hidden by default */ + transition: opacity 0.2s ease; +} + +/* Show outer fill + inner dot when selected */ +[class*="radioSelection"][data-selected="true"] [class*="outerRadioFill"], +[class*="radioSelection"][data-selected="true"] [class*="innerDotRadio"], +[class*="radioSelection"][aria-checked="true"] [class*="outerRadioFill"], +[class*="radioSelection"][aria-checked="true"] [class*="innerDotRadio"], +[class*="parentSelected"] [class*="outerRadioFill"], +[class*="parentSelected"] [class*="innerDotRadio"] { + opacity: 1 !important; +} + +/* Hover effect for base ring */ +[class*="outerRadioBase"]:hover { + stroke: var(--accent-1) !important; +} + +/* Checkbox checkmark and background for selected */ +[class*="checkboxOption"][data-selected="true"] [class*="checkboxIndicator"] { + background-color: transparent !important; /* Removes the solid fill when selected */ + border-color: var(--accent-1) !important; /* Changes border to accent color when selected */ +} + +[class*="checkboxIndicator"] { + background-color: transparent !important; + border-color: var(--border) !important; + transition: border-color 0.2s ease; +} + +[class*="checkboxOption"][data-selected="true"] [class*="checkStroke"], +[class*="checkboxOption"][data-selected="true"] [class*="dot"] { + color: var(--accent-3) !important; /* checkmark color */ +} + +/* Hover effect for selected checkbox */ +[class*="checkboxOption"]:hover [class*="checkboxIndicator"] { + border-color: var(--accent-1) /* keep background consistent on hover */ +} + +/* Fix hover color for channel mentions */ +.channelMention:hover { + color: var(--text-1) !important; +} + +[class*="unreadTop"], +[class*="unreadBottom"], +[class*="unreadMentionsIndicator"] { + background-color: transparent !important; +} + +[class*="unreadBar"], +[class*="unreadMentionsBar"] { + background-color: var(--accent-1) !important; + border: 1px solid var(--border); +} + +[class*="unreadBar"][class*="mention"], +[class*="unreadMentionsBar"][class*="mention"] { + background-color: var(--accent-1) !important; +} + +[class*="unreadBar"][class*="unread"], +[class*="unreadMentionsBar"][class*="unread"] { + background-color: var(--bg-1) !important; + border-color: var(--accent-1) !important; +} + +[class*="unreadBar"] [class*="text"], +[class*="unreadMentionsBar"] [class*="text"] { + color: var(--text-1) !important; + font-weight: 700; +} + +/* Fix polls visibility */ +[class*="pollContainer_"] [class*="selected__"] [class*="radioForeground_"] { + fill: var(--accent-3) !important; +} + +[class*="pollContainer_"] [class*="selected__"] { + border: 2px solid var(--active) !important; +} + +[class*="votedStyles_"] [class*="votePercentageBar_"] { + background-color: var(--active-2) !important; + opacity: 0.5; +} + +[class*="pollContainer_"] [class*="answerSelectionIcon_"] circle { + fill: var(--accent-3) !important; +} + +[class*="pollContainer_"] [class*="votedStyles_"] [class*="answerSelectionIcon_"] path { + fill: var(--bg-1) !important; +} + +[class*="pollContainer_"] [class*="answer__"]:not([class*="votedStyles_"]) [class*="answerSelectionIcon_"] path { + fill: var(--accent-1) !important; +} + +[class*="pollContainer_"] [class*="votesData_"] { + color: var(--text-2) !important; +} + +[class*="pollContainer_"] [class*="votedStyles_"] [class*="answerInner_"] { + border: 2px solid var(--active) !important; +} + +/* Invite to server button override*/ +[class*="inviteButton_"] svg path, +[class*="actionIcon_"] path[d^="M14.5 8a3 3 0"] { + fill: var(--accent-1) !important; + color: var(--accent-1) !important; +} + +[class*="inviteButton_"]:hover svg path { + fill: var(--accent-3) !important; + color: var(--accent-3) !important; +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/emacs.el b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/emacs.el new file mode 100644 index 0000000..f4461fc --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/emacs.el @@ -0,0 +1,361 @@ +;;; noctalia-theme.el --- Theme using Template SCSS variables -*- lexical-binding: t -*- + +;; Copyright (C) 2025 + +;; Author: Generated (Improved) +;; Version: 1.2 +;; Package-Requires: ((emacs "24.1")) +;; Keywords: faces + +;;; Commentary: + +;; A theme using Template SCSS variables with quality of life improvements: +;; - Better source block distinction +;; - Improved text visibility when selected +;; - Refined org-mode styling with hidden asterisks +;; - Enhanced contrast and readability +;; - Seamless integration of source blocks with consistent styling + +;;; Code: + +(deftheme noctalia "Theme using Template variables with quality of life improvements.") + +;; Define all the color variables (replaced by template processor) +(let* ((bg "{{colors.background.default.hex}}") + (err "{{colors.error.default.hex}}") + (err-container "{{colors.error_container.default.hex}}") + (on-background "{{colors.on_background.default.hex}}") + (on-err "{{colors.on_error.default.hex}}") + (on-err-container "{{colors.on_error_container.default.hex}}") + (on-primary "{{colors.on_primary.default.hex}}") + (on-primary-container "{{colors.on_primary_container.default.hex}}") + (on-secondary "{{colors.on_secondary.default.hex}}") + (on-secondary-container "{{colors.on_secondary_container.default.hex}}") + (on-surface "{{colors.on_surface.default.hex}}") + (on-surface-variant "{{colors.on_surface_variant.default.hex}}") + (on-tertiary "{{colors.on_tertiary.default.hex}}") + (on-tertiary-container "{{colors.on_tertiary_container.default.hex}}") + (outline-color "{{colors.outline.default.hex}}") + (outline-variant "{{colors.outline_variant.default.hex}}") + (primary "{{colors.primary.default.hex}}") + (primary-container "{{colors.primary_container.default.hex}}") + (secondary "{{colors.secondary.default.hex}}") + (secondary-container "{{colors.secondary_container.default.hex}}") + (shadow "{{colors.shadow.default.hex}}") + (surface "{{colors.surface.default.hex}}") + (surface-container "{{colors.surface_container.default.hex}}") + (surface-container-high "{{colors.surface_container_high.default.hex}}") + (surface-container-highest "{{colors.surface_container_highest.default.hex}}") + (surface-container-low "{{colors.surface_container_low.default.hex}}") + (surface-container-lowest "{{colors.surface_container_lowest.default.hex}}") + (surface-variant "{{colors.surface_variant.default.hex}}") + (tertiary "{{colors.tertiary.default.hex}}") + (tertiary-container "{{colors.tertiary_container.default.hex}}") + ;; Map success colors to tertiary (as used in other templates) + (success "{{colors.tertiary.default.hex}}") + (on-success "{{colors.on_tertiary.default.hex}}") + (success-container "{{colors.tertiary_container.default.hex}}") + (on-success-container "{{colors.on_tertiary_container.default.hex}}") + ;; Map fixed colors to regular colors + (primary-fixed "{{colors.primary.default.hex}}") + (primary-fixed-dim "{{colors.primary_container.default.hex}}") + (secondary-fixed "{{colors.secondary.default.hex}}") + (secondary-fixed-dim "{{colors.secondary_container.default.hex}}") + (tertiary-fixed "{{colors.tertiary.default.hex}}") + (tertiary-fixed-dim "{{colors.tertiary_container.default.hex}}") + (on-primary-fixed "{{colors.on_primary.default.hex}}") + (on-primary-fixed-variant "{{colors.on_primary_container.default.hex}}") + (on-secondary-fixed "{{colors.on_secondary.default.hex}}") + (on-secondary-fixed-variant "{{colors.on_secondary_container.default.hex}}") + (on-tertiary-fixed "{{colors.on_tertiary.default.hex}}") + (on-tertiary-fixed-variant "{{colors.on_tertiary_container.default.hex}}") + ;; Map inverse colors to surface variants + (inverse-on-surface "{{colors.on_surface.default.hex}}") + (inverse-primary "{{colors.primary.default.hex}}") + (inverse-surface "{{colors.surface.default.hex}}") + ;; Map terminal colors (term0-term15) to available colors + (term0 "{{colors.surface.default.hex}}") + (term1 "{{colors.error.default.hex}}") + (term2 "{{colors.tertiary.default.hex}}") + (term3 "{{colors.secondary.default.hex}}") + (term4 "{{colors.primary.default.hex}}") + (term5 "{{colors.tertiary_container.default.hex}}") + (term6 "{{colors.secondary_container.default.hex}}") + (term7 "{{colors.on_surface.default.hex}}") + (term8 "{{colors.outline.default.hex}}") + (term9 "{{colors.error.default.hex}}") + (term10 "{{colors.tertiary.default.hex}}") + (term11 "{{colors.secondary.default.hex}}") + (term12 "{{colors.primary.default.hex}}") + (term13 "{{colors.tertiary_container.default.hex}}") + (term14 "{{colors.secondary_container.default.hex}}") + (term15 "{{colors.on_surface.default.hex}}")) + + (custom-theme-set-faces + 'noctalia + ;; Basic faces + `(default ((t (:background ,bg :foreground ,on-background)))) + `(cursor ((t (:background ,primary)))) + `(highlight ((t (:background ,surface-container-high)))) + `(region ((t (:background ,primary-container :foreground ,on-primary-container :extend t)))) + `(secondary-selection ((t (:background ,secondary-container :foreground ,on-secondary-container :extend t)))) + `(isearch ((t (:background ,tertiary-container :foreground ,on-tertiary-container :weight bold)))) + `(lazy-highlight ((t (:background ,secondary-container :foreground ,on-secondary-container)))) + `(vertical-border ((t (:foreground ,surface-variant)))) + `(border ((t (:background ,surface-variant :foreground ,surface-variant)))) + `(fringe ((t (:background ,surface :foreground ,outline-variant)))) + `(shadow ((t (:foreground ,outline-variant)))) + `(link ((t (:foreground ,primary :underline t)))) + `(link-visited ((t (:foreground ,tertiary :underline t)))) + `(success ((t (:foreground ,success)))) + `(warning ((t (:foreground ,secondary)))) + `(error ((t (:foreground ,err)))) + `(match ((t (:background ,secondary-container :foreground ,on-secondary-container)))) + + ;; Font-lock + `(font-lock-builtin-face ((t (:foreground ,primary)))) + `(font-lock-comment-face ((t (:foreground ,outline-color :slant italic)))) + `(font-lock-comment-delimiter-face ((t (:foreground ,outline-variant)))) + `(font-lock-constant-face ((t (:foreground ,tertiary :weight bold)))) + `(font-lock-doc-face ((t (:foreground ,on-surface-variant :slant italic)))) + `(font-lock-function-name-face ((t (:foreground ,primary :weight bold)))) + `(font-lock-keyword-face ((t (:foreground ,secondary :weight bold)))) + `(font-lock-string-face ((t (:foreground ,tertiary)))) + `(font-lock-type-face ((t (:foreground ,primary-fixed)))) + `(font-lock-variable-name-face ((t (:foreground ,on-surface)))) + `(font-lock-warning-face ((t (:foreground ,err :weight bold)))) + `(font-lock-preprocessor-face ((t (:foreground ,secondary-fixed-dim)))) + `(font-lock-negation-char-face ((t (:foreground ,tertiary-fixed)))) + + ;; Show paren + `(show-paren-match ((t (:background ,primary-container :foreground ,on-primary-container :weight bold)))) + `(show-paren-mismatch ((t (:background ,err-container :foreground ,on-err-container :weight bold)))) + + ;; Mode line - improved status bar styling + `(mode-line ((t (:background ,surface-container-high :foreground ,on-surface :box nil)))) + `(mode-line-inactive ((t (:background ,surface :foreground ,on-surface-variant :box nil)))) + `(mode-line-buffer-id ((t (:foreground ,primary :weight bold)))) + `(mode-line-emphasis ((t (:foreground ,primary :weight bold)))) + `(mode-line-highlight ((t (:foreground ,primary :box nil)))) + + ;; Improved Source blocks - make them integrated with the theme + `(org-block ((t (:background ,surface-container-low :extend t :inherit fixed-pitch)))) + `(org-block-begin-line ((t (:background ,surface-container-low :foreground ,primary-fixed-dim :extend t :slant italic :inherit fixed-pitch)))) + `(org-block-end-line ((t (:background ,surface-container-low :foreground ,primary-fixed-dim :extend t :slant italic :inherit fixed-pitch)))) + `(org-code ((t (:background ,surface-container-low :foreground ,tertiary-fixed :inherit fixed-pitch)))) + `(org-verbatim ((t (:background ,surface-container-low :foreground ,primary-fixed :inherit fixed-pitch)))) + `(org-meta-line ((t (:foreground ,outline-color :slant italic)))) + + ;; Org mode with hidden asterisks + `(org-level-1 ((t (:foreground ,primary :weight bold :height 1.2)))) + `(org-level-2 ((t (:foreground ,secondary :weight bold :height 1.1)))) + `(org-level-3 ((t (:foreground ,tertiary :weight bold)))) + `(org-level-4 ((t (:foreground ,primary :weight bold)))) + `(org-level-5 ((t (:foreground ,secondary :weight bold)))) + `(org-level-6 ((t (:foreground ,tertiary :weight bold)))) + `(org-level-7 ((t (:foreground ,primary :weight bold)))) + `(org-level-8 ((t (:foreground ,secondary :weight bold)))) + `(org-document-title ((t (:foreground ,primary :weight bold :height 1.3)))) + `(org-document-info ((t (:foreground ,primary-container)))) + `(org-todo ((t (:foreground ,err :weight bold)))) + `(org-done ((t (:foreground ,success :weight bold)))) + `(org-headline-done ((t (:foreground ,on-surface-variant)))) + `(org-hide ((t (:foreground ,bg)))) ;; Hide leading asterisks + `(org-ellipsis ((t (:foreground ,tertiary :underline nil)))) ;; Style for folded content indicator + `(org-table ((t (:foreground ,secondary-fixed :inherit fixed-pitch)))) + `(org-formula ((t (:foreground ,tertiary :inherit fixed-pitch)))) + `(org-checkbox ((t (:foreground ,primary :weight bold :inherit fixed-pitch)))) + `(org-date ((t (:foreground ,secondary-fixed :underline t)))) + `(org-special-keyword ((t (:foreground ,on-surface-variant :slant italic)))) + `(org-tag ((t (:foreground ,outline-color :weight normal)))) + + ;; Magit + `(magit-section-highlight ((t (:background ,surface-container-low)))) + `(magit-diff-hunk-heading ((t (:background ,surface-container :foreground ,on-surface-variant)))) + `(magit-diff-hunk-heading-highlight ((t (:background ,surface-container-high :foreground ,on-surface)))) + `(magit-diff-context ((t (:foreground ,on-surface-variant)))) + `(magit-diff-context-highlight ((t (:background ,surface-container-low :foreground ,on-surface)))) + `(magit-diff-added ((t (:background ,success-container :foreground ,on-success-container)))) + `(magit-diff-added-highlight ((t (:background ,success-container :foreground ,on-success-container :weight bold)))) + `(magit-diff-removed ((t (:background ,err-container :foreground ,on-err-container)))) + `(magit-diff-removed-highlight ((t (:background ,err-container :foreground ,on-err-container :weight bold)))) + `(magit-hash ((t (:foreground ,outline-color)))) + `(magit-branch-local ((t (:foreground ,tertiary :weight bold)))) + `(magit-branch-remote ((t (:foreground ,primary :weight bold)))) + + ;; Company + `(company-tooltip ((t (:background ,surface-container :foreground ,on-surface)))) + `(company-tooltip-selection ((t (:background ,primary-container :foreground ,on-primary-container)))) + `(company-tooltip-common ((t (:foreground ,primary)))) + `(company-tooltip-common-selection ((t (:foreground ,on-primary-container :weight bold)))) + `(company-tooltip-annotation ((t (:foreground ,tertiary)))) + `(company-scrollbar-fg ((t (:background ,primary)))) + `(company-scrollbar-bg ((t (:background ,surface-variant)))) + `(company-preview ((t (:foreground ,on-surface-variant :slant italic)))) + `(company-preview-common ((t (:foreground ,primary :slant italic)))) + + ;; Ido + `(ido-first-match ((t (:foreground ,primary :weight bold)))) + `(ido-only-match ((t (:foreground ,tertiary :weight bold)))) + `(ido-subdir ((t (:foreground ,secondary)))) + `(ido-indicator ((t (:foreground ,err)))) + `(ido-virtual ((t (:foreground ,outline-color)))) + + ;; Helm + `(helm-selection ((t (:background ,primary-container :foreground ,on-primary-container)))) + `(helm-match ((t (:foreground ,primary :weight bold)))) + `(helm-source-header ((t (:background ,surface-container-high :foreground ,primary :weight bold :height 1.1)))) + `(helm-candidate-number ((t (:foreground ,tertiary :weight bold)))) + `(helm-ff-directory ((t (:foreground ,primary :weight bold)))) + `(helm-ff-file ((t (:foreground ,on-surface)))) + `(helm-ff-executable ((t (:foreground ,tertiary)))) + + ;; corfu + `(corfu-default ((t (:background ,surface-container :foreground ,on-surface)))) + `(corfu-current ((t (:background ,primary-container :foreground ,on-primary-container)))) + + ;; Which-key + `(which-key-key-face ((t (:foreground ,primary :weight bold)))) + `(which-key-separator-face ((t (:foreground ,outline-variant)))) + `(which-key-command-description-face ((t (:foreground ,on-surface)))) + `(which-key-group-description-face ((t (:foreground ,secondary)))) + `(which-key-special-key-face ((t (:foreground ,tertiary :weight bold)))) + + ;; Line numbers + `(line-number ((t (:foreground ,outline-variant :inherit fixed-pitch)))) + `(line-number-current-line ((t (:foreground ,primary :weight bold :inherit fixed-pitch)))) + + ;; Parenthesis matching + `(sp-show-pair-match-face ((t (:background ,primary-container :foreground ,on-primary-container)))) + `(sp-show-pair-mismatch-face ((t (:background ,err-container :foreground ,on-err-container)))) + + ;; Rainbow delimiters + `(rainbow-delimiters-depth-1-face ((t (:foreground ,primary)))) + `(rainbow-delimiters-depth-2-face ((t (:foreground ,secondary)))) + `(rainbow-delimiters-depth-3-face ((t (:foreground ,tertiary)))) + `(rainbow-delimiters-depth-4-face ((t (:foreground ,primary-fixed)))) + `(rainbow-delimiters-depth-5-face ((t (:foreground ,secondary-fixed)))) + `(rainbow-delimiters-depth-6-face ((t (:foreground ,tertiary-fixed)))) + `(rainbow-delimiters-depth-7-face ((t (:foreground ,primary-fixed-dim)))) + `(rainbow-delimiters-depth-8-face ((t (:foreground ,secondary-fixed-dim)))) + `(rainbow-delimiters-depth-9-face ((t (:foreground ,tertiary-fixed-dim)))) + `(rainbow-delimiters-mismatched-face ((t (:foreground ,err :weight bold)))) + `(rainbow-delimiters-unmatched-face ((t (:foreground ,err :weight bold)))) + + ;; Dired + `(dired-directory ((t (:foreground ,primary :weight bold)))) + `(dired-ignored ((t (:foreground ,outline-variant)))) + `(dired-flagged ((t (:foreground ,err)))) + `(dired-marked ((t (:foreground ,tertiary :weight bold)))) + `(dired-symlink ((t (:foreground ,secondary :slant italic)))) + `(dired-header ((t (:foreground ,primary :weight bold :height 1.1)))) + + ;; Terminal colors + `(term-color-black ((t (:foreground ,term0 :background ,term0)))) + `(term-color-red ((t (:foreground ,term1 :background ,term1)))) + `(term-color-green ((t (:foreground ,term2 :background ,term2)))) + `(term-color-yellow ((t (:foreground ,term3 :background ,term3)))) + `(term-color-blue ((t (:foreground ,term4 :background ,term4)))) + `(term-color-magenta ((t (:foreground ,term5 :background ,term5)))) + `(term-color-cyan ((t (:foreground ,term6 :background ,term6)))) + `(term-color-white ((t (:foreground ,term7 :background ,term7)))) + + ;; EShell + `(eshell-prompt ((t (:foreground ,primary :weight bold)))) + `(eshell-ls-directory ((t (:foreground ,primary :weight bold)))) + `(eshell-ls-symlink ((t (:foreground ,secondary :slant italic)))) + `(eshell-ls-executable ((t (:foreground ,tertiary)))) + `(eshell-ls-archive ((t (:foreground ,on-tertiary-container)))) + `(eshell-ls-backup ((t (:foreground ,outline-variant)))) + `(eshell-ls-clutter ((t (:foreground ,err)))) + `(eshell-ls-missing ((t (:foreground ,err)))) + `(eshell-ls-product ((t (:foreground ,on-surface-variant)))) + `(eshell-ls-readonly ((t (:foreground ,on-surface-variant)))) + `(eshell-ls-special ((t (:foreground ,secondary-fixed)))) + `(eshell-ls-unreadable ((t (:foreground ,outline-variant)))) + + ;; Improved markdown mode + `(markdown-header-face ((t (:foreground ,primary :weight bold)))) + `(markdown-header-face-1 ((t (:foreground ,primary :weight bold :height 1.2)))) + `(markdown-header-face-2 ((t (:foreground ,primary-container :weight bold :height 1.1)))) + `(markdown-header-face-3 ((t (:foreground ,secondary :weight bold)))) + `(markdown-header-face-4 ((t (:foreground ,secondary-container :weight bold)))) + `(markdown-inline-code-face ((t (:foreground ,tertiary-fixed :background ,surface-container-low :inherit fixed-pitch)))) + `(markdown-code-face ((t (:background ,surface-container-low :extend t :inherit fixed-pitch)))) + `(markdown-pre-face ((t (:background ,surface-container-low :inherit fixed-pitch)))) + `(markdown-table-face ((t (:foreground ,secondary-fixed :inherit fixed-pitch)))) + + ;; Web mode + `(web-mode-html-tag-face ((t (:foreground ,primary)))) + `(web-mode-html-tag-bracket-face ((t (:foreground ,on-surface-variant)))) + `(web-mode-html-attr-name-face ((t (:foreground ,secondary)))) + `(web-mode-html-attr-value-face ((t (:foreground ,tertiary)))) + `(web-mode-css-selector-face ((t (:foreground ,primary)))) + `(web-mode-css-property-name-face ((t (:foreground ,secondary)))) + `(web-mode-css-string-face ((t (:foreground ,tertiary)))) + + ;; Flycheck + `(flycheck-error ((t (:underline (:style wave :color ,err))))) + `(flycheck-warning ((t (:underline (:style wave :color ,secondary))))) + `(flycheck-info ((t (:underline (:style wave :color ,tertiary))))) + `(flycheck-fringe-error ((t (:foreground ,err)))) + `(flycheck-fringe-warning ((t (:foreground ,secondary)))) + `(flycheck-fringe-info ((t (:foreground ,tertiary)))) + + ;; Mini-buffer customization + `(minibuffer-prompt ((t (:foreground ,primary :weight bold)))) + + ;; Improved search highlighting + `(lsp-face-highlight-textual ((t (:background ,primary-container :foreground ,on-primary-container :weight bold)))) + `(lsp-face-highlight-read ((t (:background ,secondary-container :foreground ,on-secondary-container :weight bold)))) + `(lsp-face-highlight-write ((t (:background ,tertiary-container :foreground ,on-tertiary-container :weight bold)))) + + ;; Info and help modes + `(info-title-1 ((t (:foreground ,primary :weight bold :height 1.3)))) + `(info-title-2 ((t (:foreground ,primary-container :weight bold :height 1.2)))) + `(info-title-3 ((t (:foreground ,secondary :weight bold :height 1.1)))) + `(info-title-4 ((t (:foreground ,secondary-container :weight bold)))) + `(Info-quoted ((t (:foreground ,tertiary)))) + `(info-menu-header ((t (:foreground ,primary :weight bold)))) + `(info-menu-star ((t (:foreground ,primary)))) + `(info-node ((t (:foreground ,tertiary :weight bold)))) + + ;; Tabs + `(tab-bar ((t (:background ,surface-container-high :foreground ,on-surface :box nil)))) + `(tab-bar-tab ((t (:background ,surface-container-high :foreground ,on-surface :weight bold :box nil)))) + `(tab-bar-tab-inactive ((t (:background ,surface :foreground ,on-surface-variant :box nil)))) + + `(tab-line ((t (:background ,surface-container-high :foreground ,on-surface :box nil)))) + `(tab-line-tab ((t (:background ,surface :foreground ,on-surface-variant :box nil)))) + `(tab-line-tab-current ((t (:background ,surface-container-high :foreground ,on-surface :weight bold :box nil)))) + `(tab-line-tab-inactive ((t (:background ,surface :foreground ,on-surface-variant :box nil)))) + `(tab-line-highlight ((t (:background ,surface-container-highest :foreground ,on-surface)))) + + `(centaur-tabs-default ((t (:background ,surface-container-high :foreground ,on-surface)))) + `(centaur-tabs-selected ((t (:background ,surface-container-high :foreground ,on-surface :weight bold)))) + `(centaur-tabs-unselected ((t (:background ,surface :foreground ,on-surface-variant)))) + `(centaur-tabs-selected-modified ((t (:background ,surface-container-high :foreground ,tertiary :weight bold)))) + `(centaur-tabs-unselected-modified ((t (:background ,surface :foreground ,tertiary)))) + `(centaur-tabs-active-bar-face ((t (:background ,primary)))) + + ;; Fixed-pitch faces + `(fixed-pitch ((t (:family "monospace")))) + `(fixed-pitch-serif ((t (:family "monospace serif")))) + + ;; Variable-pitch face + `(variable-pitch ((t (:family "sans serif")))) + )) + +;; Add org-mode hooks for hiding leading stars +(with-eval-after-load 'org + (setq org-hide-leading-stars t) + (setq org-startup-indented t)) + +;;;###autoload +(when load-file-name + (add-to-list 'custom-theme-load-path + (file-name-as-directory (file-name-directory load-file-name)))) + +(provide-theme 'noctalia) +;;; noctalia-theme.el ends here + diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/fuzzel.conf b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/fuzzel.conf new file mode 100644 index 0000000..dc519a8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/fuzzel.conf @@ -0,0 +1,15 @@ +# Fuzzel Colors +# Generated by Noctalia's Template Processor + +[colors] +background={{colors.background.default.hex_stripped}}CC +text={{colors.on_surface.default.hex_stripped}}ff +prompt={{colors.secondary.default.hex_stripped}}ff +placeholder={{colors.tertiary.default.hex_stripped}}ff +input={{colors.primary.default.hex_stripped}}ff +match={{colors.tertiary.default.hex_stripped}}ff +selection={{colors.primary.default.hex_stripped}}80 +selection-text={{colors.on_surface.default.hex_stripped}}ff +selection-match={{colors.on_primary.default.hex_stripped}}ff +counter={{colors.secondary.default.hex_stripped}}ff +border={{colors.primary.default.hex_stripped}}ff diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/gtk3.css b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/gtk3.css new file mode 100644 index 0000000..dd3a420 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/gtk3.css @@ -0,0 +1,53 @@ +/* +* GTK Colors (GTK3) +* Generated by Noctalia's Template Processor +* stylelint-disable at-rule-no-unknown +*/ + +@define-color accent_color {{colors.primary.default.hex}}; +@define-color accent_bg_color {{colors.primary.default.hex}}; +@define-color accent_fg_color {{colors.on_primary.default.hex}}; + +@define-color destructive_bg_color {{colors.error.default.hex}}; +@define-color destructive_fg_color {{colors.on_error.default.hex}}; + +@define-color error_bg_color {{colors.error.default.hex}}; +@define-color error_fg_color {{colors.on_error.default.hex}}; + +@define-color window_bg_color {{colors.surface.default.hex}}; +@define-color window_fg_color {{colors.on_surface.default.hex}}; + +@define-color view_bg_color {{colors.surface.default.hex}}; +@define-color view_fg_color {{colors.on_surface.default.hex}}; + +@define-color headerbar_bg_color {{colors.surface.default.hex}}; +@define-color headerbar_fg_color {{colors.on_surface.default.hex}}; +@define-color headerbar_backdrop_color @window_bg_color; + +@define-color popover_bg_color {{colors.surface_container.default.hex}}; +@define-color popover_fg_color {{colors.on_surface.default.hex}}; + +@define-color card_bg_color {{colors.surface_container.default.hex}}; +@define-color card_fg_color {{colors.on_surface.default.hex}}; + +@define-color dialog_bg_color {{colors.surface.default.hex}}; +@define-color dialog_fg_color {{colors.on_surface.default.hex}}; + +@define-color overview_bg_color {{colors.surface_container.default.hex}}; +@define-color overview_fg_color {{colors.on_surface.default.hex}}; + +@define-color sidebar_bg_color {{colors.surface_container.default.hex}}; +@define-color sidebar_fg_color {{colors.on_surface.default.hex}}; +@define-color sidebar_backdrop_color @window_bg_color; +@define-color sidebar_border_color @window_bg_color; + +@define-color secondary_sidebar_bg_color {{colors.surface.default.hex}}; +@define-color secondary_sidebar_fg_color {{colors.on_surface.default.hex}}; + +/* Backdrop/unfocused states */ +@define-color theme_unfocused_fg_color @window_fg_color; +@define-color theme_unfocused_text_color @view_fg_color; +@define-color theme_unfocused_bg_color @window_bg_color; +@define-color theme_unfocused_base_color @window_bg_color; +@define-color theme_unfocused_selected_bg_color @accent_bg_color; +@define-color theme_unfocused_selected_fg_color @accent_fg_color; diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/gtk4.css b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/gtk4.css new file mode 100644 index 0000000..7f6f4d0 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/gtk4.css @@ -0,0 +1,103 @@ +/* +* GTK Colors (GTK4) +* Generated by Noctalia's Template Processor +* stylelint-disable at-rule-no-unknown +*/ + +@define-color accent_color {{colors.primary.default.hex}}; +@define-color accent_bg_color {{colors.primary.default.hex}}; +@define-color accent_fg_color {{colors.on_primary.default.hex}}; + +@define-color destructive_bg_color {{colors.error.default.hex}}; +@define-color destructive_fg_color {{colors.on_error.default.hex}}; + +@define-color error_bg_color {{colors.error.default.hex}}; +@define-color error_fg_color {{colors.on_error.default.hex}}; + +@define-color window_bg_color {{colors.surface.default.hex}}; +@define-color window_fg_color {{colors.on_surface.default.hex}}; + +@define-color view_bg_color {{colors.surface.default.hex}}; +@define-color view_fg_color {{colors.on_surface.default.hex}}; + +@define-color headerbar_bg_color {{colors.surface.default.hex}}; +@define-color headerbar_fg_color {{colors.on_surface.default.hex}}; +@define-color headerbar_backdrop_color @window_bg_color; + +@define-color popover_bg_color {{colors.surface_container.default.hex}}; +@define-color popover_fg_color {{colors.on_surface.default.hex}}; + +@define-color card_bg_color {{colors.surface_container.default.hex}}; +@define-color card_fg_color {{colors.on_surface.default.hex}}; + +@define-color dialog_bg_color {{colors.surface.default.hex}}; +@define-color dialog_fg_color {{colors.on_surface.default.hex}}; + +@define-color overview_bg_color {{colors.surface_container.default.hex}}; +@define-color overview_fg_color {{colors.on_surface.default.hex}}; + +@define-color sidebar_bg_color {{colors.surface_container.default.hex}}; +@define-color sidebar_fg_color {{colors.on_surface.default.hex}}; +@define-color sidebar_backdrop_color @window_bg_color; +@define-color sidebar_border_color @window_bg_color; + +@define-color secondary_sidebar_bg_color {{colors.surface.default.hex}}; +@define-color secondary_sidebar_fg_color {{colors.on_surface.default.hex}}; + +/* Backdrop/unfocused states */ +@define-color theme_unfocused_fg_color @window_fg_color; +@define-color theme_unfocused_text_color @view_fg_color; +@define-color theme_unfocused_bg_color @window_bg_color; +@define-color theme_unfocused_base_color @window_bg_color; +@define-color theme_unfocused_selected_bg_color @accent_bg_color; +@define-color theme_unfocused_selected_fg_color @accent_fg_color; + +:root { + --accent-color: {{colors.primary.default.hex}}; + --accent-bg-color: {{colors.primary.default.hex}}; + --accent-fg-color: {{colors.on_primary.default.hex}}; + + --destructive-bg-color: {{colors.error.default.hex}}; + --destructive-fg-color: {{colors.on_error.default.hex}}; + + --error-bg-color: {{colors.error.default.hex}}; + --error-fg-color: {{colors.on_error.default.hex}}; + --error-color: {{colors.error.default.hex}}; + + --window-bg-color: {{colors.surface.default.hex}}; + --window-fg-color: {{colors.on_surface.default.hex}}; + + --view-bg-color: {{colors.surface.default.hex}}; + --view-fg-color: {{colors.on_surface.default.hex}}; + + --headerbar-bg-color: {{colors.surface.default.hex}}; + --headerbar-fg-color: {{colors.on_surface.default.hex}}; + --headerbar-backdrop-color: @window_bg_color; + + --popover-bg-color: {{colors.surface_container.default.hex}}; + --popover-fg-color: {{colors.on_surface.default.hex}}; + + --card-bg-color: {{colors.surface_container.default.hex}}; + --card-fg-color: {{colors.on_surface.default.hex}}; + + --dialog-bg-color: {{colors.surface.default.hex}}; + --dialog-fg-color: {{colors.on_surface.default.hex}}; + + --overview-bg-color: {{colors.surface_container.default.hex}}; + --overview-fg-color: {{colors.on_surface.default.hex}}; + + --sidebar-bg-color: {{colors.surface_container.default.hex}}; + --sidebar-fg-color: {{colors.on_surface.default.hex}}; + --sidebar-backdrop-color: @window_bg_color; + --sidebar-border-color: @window_bg_color; + + --warning-bg-color: {{colors.tertiary_container.default.hex}}; + --warning-fg-color: {{colors.on_tertiary_container.default.hex}}; + --warning-color: {{colors.tertiary.default.hex}}; + + --success-color: {{colors.secondary.default.hex}}; + --success-bg-color: {{colors.secondary_container.default.hex}}; + --success-fg-color: {{colors.on_secondary_container.default.hex}}; + + --shade-color: rgba(0, 0, 0, 0.36); +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/helix.toml b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/helix.toml new file mode 100644 index 0000000..93c6e54 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/helix.toml @@ -0,0 +1,162 @@ +# Syntax highlighting +# ------------------- +"attribute" = { fg = "primary", modifiers = ["bold"] } + +"type" = "primary" +"type.enum.variant" = "secondary" + +"constructor" = "tertiary" + +"constant" = "secondary" +"constant.character" = "secondaryContainer" +"constant.character.escape" = "tertiaryContainer" + +"string" = { fg = "tertiary", bg = "surfaceContainerHigh" } +"string.regexp" = { fg = "tertiaryContainer", bg = "surfaceContainerHigh" } +"string.special" = { fg = "tertiary", bg = "surfaceContainerHigh" } +"string.special.symbol" = { fg = "tertiary", bg = "surfaceContainerHigh" } +"string.special.path" = { fg = "secondary", underline = { color = "secondary", style = "line" } } + +"path" = { fg = "secondary", underline = { color = "secondary", style = "line" } } + +"comment" = { fg = "outline", bg = "surfaceContainerHigh", modifiers = ["italic"] } + +"variable" = "onSurface" +"variable.builtin" = "secondary" +"variable.parameter" = { fg = "onSurfaceVariant", modifiers = ["italic"] } +"variable.other.member" = "primary" + +"label" = "secondary" + +"punctuation" = "outlineVariant" +"punctuation.special" = "outline" + +"keyword" = { fg = "primary", modifiers = ["bold"] } +"keyword.control.conditional" = { fg = "tertiary", modifiers = ["italic", "bold"] } + +"operator" = { fg = "secondary", modifiers = ["bold"] } + +"function" = "tertiary" +"function.macro" = "tertiary" + +"tag" = "secondary" + +"namespace" = { fg = "secondary", modifiers = ["italic"] } + +"special" = "secondary" + +"markup.heading" = "primary" +"markup.list" = "onSurface" +"markup.bold" = { fg = "secondary", modifiers = ["bold"] } +"markup.italic" = { fg = "secondary", modifiers = ["italic"] } +"markup.link.url" = { fg = "secondary", modifiers = ["italic", "underlined"] } +"markup.link.label" = "primary" +"markup.raw" = "onSurface" +"markup.quote" = "secondary" + +"diff.plus" = "plus" +"diff.minus" = "error" +"diff.delta" = "delta" + +# User Interface +# -------------- +"ui.background" = "none" + +"ui.cursor" = { fg = "onSecondary", bg = "secondary" } +"ui.cursor.match" = { fg = "onTertiary", bg = "tertiary", modifiers = ["bold"] } +"ui.cursor.primary" = { fg = "onPrimary", bg = "primary" } + +"ui.linenr" = { fg = "onSurfaceVariant" } +"ui.linenr.selected" = { fg = "primary" } + +"ui.statusline" = { fg = "onSurface", bg = "surfaceContainerLow" } +"ui.statusline.inactive" = { fg = "onSurface", bg = "surfaceContainerLowest" } +"ui.statusline.normal" = { fg = "onPrimary", bg = "primary", modifiers = ["bold"] } +"ui.statusline.insert" = { fg = "onTertiary", bg = "tertiary", modifiers = ["bold"] } +"ui.statusline.select" = { fg = "onSecondary", bg = "secondary", modifiers = ["bold"] } + +"ui.bufferline" = { fg = "onSurface", bg = "surfaceContainerLowest" } +"ui.bufferline.active" = { fg = "onSurface", bg = "surfaceContainer", underline = { color = "primary", style = "line" } } +"ui.bufferline.background" = { bg = "surfaceContainerLowest" } + +"ui.popup" = { fg = "onSurface", bg = "surfaceContainerLow" } + +"ui.window" = { fg = "onSurface" } +"ui.help" = { fg = "onSurface", bg = "surfaceContainerLow" } + +"ui.text" = { fg = "onBackground" } +"ui.text.focus" = { fg = "onSurface", bg = "surfaceContainer", modifiers = ["bold"] } + +"ui.virtual" = "surfaceVariant" +"ui.virtual.ruler" = { bg = "surfaceContainerLow" } +"ui.virtual.indent-guide" = "surfaceVariant" +"ui.virtual.inlay-hint" = { fg = "onSurfaceVariant", bg = "surfaceContainer", modifiers = ["dim"] } +"ui.virtual.jump-label" = { fg = "primary", modifiers = ["bold"] } + +"ui.menu" = { fg = "onSurface", bg = "surfaceContainer" } +"ui.menu.selected" = { fg = "onPrimary", bg = "primary", modifiers = ["bold"] } + +"ui.selection" = { bg = "surfaceContainerHigh" } + +"ui.highlight" = { bg = "primaryContainer", modifiers = ["bold"] } + +"ui.cursorline" = { bg = "surfaceContainerLowest" } +"ui.cursorline.primary" = { bg = "surfaceContainerLow" } +"ui.cursorline.secondary" = { bg = "surfaceContainerLow" } + +error = "error" +warning = "warning" +info = "info" +hint = "hint" + +"diagnostic.hint" = { underline = { color = "hint", style = "curl" } } +"diagnostic.info" = { underline = { color = "info", style = "curl" } } +"diagnostic.warning" = { underline = { color = "warning", style = "curl" } } +"diagnostic.error" = { underline = { color = "error", style = "curl" } } +"diagnostic.unnecessary" = { modifiers = ["dim"] } + +[palette] +# Constants to be used in syntax highlighting, not meant for template processor. +warning = "#f9e2af" +info = "#89dceb" +hint = "#94e2d5" +plus = "#a6e3a1" +delta = "#89b4fa" + +# Template colors +primary = "{{colors.primary.default.hex}}" +surfaceTint = "{{colors.primary.default.hex}}" +onPrimary = "{{colors.on_primary.default.hex}}" +primaryContainer = "{{colors.primary_container.default.hex}}" +onPrimaryContainer = "{{colors.on_primary_container.default.hex}}" + +secondary = "{{colors.secondary.default.hex}}" +onSecondary = "{{colors.on_secondary.default.hex}}" +secondaryContainer = "{{colors.secondary_container.default.hex}}" +onSecondaryContainer = "{{colors.on_secondary_container.default.hex}}" + +tertiary = "{{colors.tertiary.default.hex}}" +onTertiary = "{{colors.on_tertiary.default.hex}}" +tertiaryContainer = "{{colors.tertiary_container.default.hex}}" +onTertiaryContainer = "{{colors.on_tertiary_container.default.hex}}" + +error = "{{colors.error.default.hex}}" +onError = "{{colors.on_error.default.hex}}" +errorContainer = "{{colors.error_container.default.hex}}" +onErrorContainer = "{{colors.on_error_container.default.hex}}" + +background = "{{colors.background.default.hex}}" +onBackground = "{{colors.on_background.default.hex}}" +surface = "{{colors.surface.default.hex}}" +onSurface = "{{colors.on_surface.default.hex}}" +surfaceVariant = "{{colors.surface_variant.default.hex}}" +onSurfaceVariant = "{{colors.on_surface_variant.default.hex}}" + +outline = "{{colors.outline.default.hex}}" +outlineVariant = "{{colors.outline_variant.default.hex}}" + +surfaceContainerLowest = "{{colors.surface_container_lowest.default.hex}}" +surfaceContainerLow = "{{colors.surface_container_low.default.hex}}" +surfaceContainer = "{{colors.surface_container.default.hex}}" +surfaceContainerHigh = "{{colors.surface_container_high.default.hex}}" +surfaceContainerHighest = "{{colors.surface_container_highest.default.hex}}" diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/hyprland.conf b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/hyprland.conf new file mode 100644 index 0000000..ad8d291 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/hyprland.conf @@ -0,0 +1,25 @@ +$primary = rgb({{colors.primary.default.hex_stripped}}) +$surface = rgb({{colors.surface.default.hex_stripped}}) +$secondary = rgb({{colors.secondary.default.hex_stripped}}) +$error = rgb({{colors.error.default.hex_stripped}}) +$tertiary = rgb({{colors.tertiary.default.hex_stripped}}) +$surface_lowest = rgb({{colors.surface_container_lowest.default.hex_stripped}}) + +general { + col.active_border = $primary + col.inactive_border = $surface +} + +group { + col.border_active = $secondary + col.border_inactive = $surface + col.border_locked_active = $error + col.border_locked_inactive = $surface + + groupbar { + col.active = $secondary + col.inactive = $surface + col.locked_active = $error + col.locked_inactive = $surface + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/hyprtoolkit.conf b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/hyprtoolkit.conf new file mode 100644 index 0000000..5af3568 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/hyprtoolkit.conf @@ -0,0 +1,7 @@ +background = rgba({{colors.background.default.hex_stripped}}ff) +base = rgba({{colors.surface.default.hex_stripped}}ff) +text = rgba({{colors.on_surface.default.hex_stripped}}ff) +alternate_base = rgba({{colors.surface_variant.default.hex_stripped}}ff) +bright_text = rgba({{colors.on_secondary.default.hex_stripped}}ff) +accent = rgba({{colors.primary.default.hex_stripped}}ff) +accent_secondary = rgba({{colors.secondary.default.hex_stripped}}ff) \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/kcolorscheme.colors b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/kcolorscheme.colors new file mode 100644 index 0000000..b4a61a7 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/kcolorscheme.colors @@ -0,0 +1,146 @@ +[KDE] +contrast=4 + +[General] +ColorScheme=Noctalia +Name=noctalia + +[ColorEffects:Disabled] +Color={{colors.surface_container_lowest.default.hex}} +ColorAmount=0 +ColorEffect=0 +ContrastAmount=0.65 +ContrastEffect=1 +IntensityAmount=0.1 +IntensityEffect=2 + +[ColorEffects:Inactive] +ChangeSelectionColor=true +Color={{colors.surface_variant.default.hex}} +ColorAmount=0.025 +ColorEffect=2 +ContrastAmount=0.1 +ContrastEffect=2 +Enable=false +IntensityAmount=0 +IntensityEffect=0 + +[Colors:Button] +BackgroundAlternate={{colors.surface_container_low.default.hex}} +BackgroundNormal={{colors.surface_container_high.default.hex}} +DecorationFocus={{colors.primary.default.hex}} +DecorationHover={{colors.primary.default.hex}} +ForegroundActive={{colors.primary.default.hex}} +ForegroundInactive={{colors.on_surface_variant.default.hex}} +ForegroundLink={{colors.secondary.default.hex}} +ForegroundNegative={{colors.error.default.hex}} +ForegroundNeutral={{colors.tertiary.default.hex}} +ForegroundNormal={{colors.on_surface.default.hex}} +ForegroundPositive={{colors.tertiary.default.hex}} +ForegroundVisited={{colors.on_secondary_container.default.hex}} + +[Colors:Complementary] +BackgroundAlternate={{colors.surface_container_low.default.hex}} +BackgroundNormal={{colors.surface.default.hex}} +DecorationFocus={{colors.primary.default.hex}} +DecorationHover={{colors.primary.default.hex}} +ForegroundActive={{colors.primary.default.hex}} +ForegroundInactive={{colors.on_surface_variant.default.hex}} +ForegroundLink={{colors.secondary.default.hex}} +ForegroundNegative={{colors.error.default.hex}} +ForegroundNeutral={{colors.tertiary.default.hex}} +ForegroundNormal={{colors.on_primary_container.default.hex}} +ForegroundPositive={{colors.tertiary.default.hex}} +ForegroundVisited={{colors.on_secondary_container.default.hex}} + +[Colors:Header] +BackgroundAlternate={{colors.surface.default.hex}} +BackgroundNormal={{colors.surface_container.default.hex}} +DecorationFocus={{colors.primary.default.hex}} +DecorationHover={{colors.primary.default.hex}} +ForegroundActive={{colors.primary.default.hex}} +ForegroundInactive={{colors.on_surface_variant.default.hex}} +ForegroundLink={{colors.secondary.default.hex}} +ForegroundNegative={{colors.error.default.hex}} +ForegroundNeutral={{colors.tertiary.default.hex}} +ForegroundNormal={{colors.on_surface.default.hex}} +ForegroundPositive={{colors.tertiary.default.hex}} +ForegroundVisited={{colors.on_secondary_container.default.hex}} + +[Colors:Header][Inactive] +BackgroundAlternate={{colors.surface_container.default.hex}} +BackgroundNormal={{colors.surface.default.hex}} +DecorationFocus={{colors.primary.default.hex}} +DecorationHover={{colors.primary.default.hex}} +ForegroundActive={{colors.primary.default.hex}} +ForegroundInactive={{colors.on_surface_variant.default.hex}} +ForegroundLink={{colors.secondary.default.hex}} +ForegroundNegative={{colors.error.default.hex}} +ForegroundNeutral={{colors.tertiary.default.hex}} +ForegroundNormal={{colors.on_surface.default.hex}} +ForegroundPositive={{colors.tertiary.default.hex}} +ForegroundVisited={{colors.on_secondary_container.default.hex}} + +[Colors:Selection] +BackgroundAlternate={{colors.surface_container_low.default.hex}} +BackgroundNormal={{colors.primary.default.hex}} +DecorationFocus={{colors.primary.default.hex}} +DecorationHover={{colors.primary.default.hex}} +ForegroundActive={{colors.on_primary.default.hex}} +ForegroundInactive={{colors.on_surface_variant.default.hex}} +ForegroundLink={{colors.secondary.default.hex}} +ForegroundNegative={{colors.error_container.default.hex}} +ForegroundNeutral={{colors.tertiary_container.default.hex}} +ForegroundNormal={{colors.on_primary.default.hex}} +ForegroundPositive={{colors.tertiary_container.default.hex}} +ForegroundVisited={{colors.on_secondary_container.default.hex}} + +[Colors:Tooltip] +BackgroundAlternate={{colors.surface.default.hex}} +BackgroundNormal={{colors.surface_container.default.hex}} +DecorationFocus={{colors.primary.default.hex}} +DecorationHover={{colors.primary.default.hex}} +ForegroundActive={{colors.primary.default.hex}} +ForegroundInactive={{colors.on_surface_variant.default.hex}} +ForegroundLink={{colors.secondary.default.hex}} +ForegroundNegative={{colors.error.default.hex}} +ForegroundNeutral={{colors.tertiary.default.hex}} +ForegroundNormal={{colors.on_background.default.hex}} +ForegroundPositive={{colors.tertiary.default.hex}} +ForegroundVisited={{colors.on_secondary_container.default.hex}} + +[Colors:View] +BackgroundAlternate={{colors.surface_container.default.hex}} +BackgroundNormal={{colors.background.default.hex}} +DecorationFocus={{colors.on_primary_container.default.hex}} +DecorationHover={{colors.on_primary.default.hex}} +ForegroundActive={{colors.primary.default.hex}} +ForegroundInactive={{colors.on_surface_variant.default.hex}} +ForegroundLink={{colors.secondary.default.hex}} +ForegroundNegative={{colors.error.default.hex}} +ForegroundNeutral={{colors.tertiary.default.hex}} +ForegroundNormal={{colors.on_background.default.hex}} +ForegroundPositive={{colors.tertiary.default.hex}} +ForegroundVisited={{colors.on_secondary_container.default.hex}} + +[Colors:Window] +BackgroundAlternate={{colors.primary_container.default.hex}} +BackgroundNormal={{colors.surface_container.default.hex}} +DecorationFocus={{colors.primary.default.hex}} +DecorationHover={{colors.primary.default.hex}} +ForegroundActive={{colors.primary.default.hex}} +ForegroundInactive={{colors.on_surface_variant.default.hex}} +ForegroundLink={{colors.secondary.default.hex}} +ForegroundNegative={{colors.error.default.hex}} +ForegroundNeutral={{colors.tertiary.default.hex}} +ForegroundNormal={{colors.on_background.default.hex}} +ForegroundPositive={{colors.tertiary.default.hex}} +ForegroundVisited={{colors.on_secondary_container.default.hex}} + +[WM] +activeBackground={{colors.primary_container.default.hex}} +activeBlend={{colors.on_primary_container.default.hex}} +activeForeground={{colors.on_primary_container.default.hex}} +inactiveBackground={{colors.surface.default.hex}} +inactiveBlend={{colors.on_surface_variant.default.hex}} +inactiveForeground={{colors.on_surface_variant.default.hex}} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/labwc.conf b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/labwc.conf new file mode 100644 index 0000000..82f7faa --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/labwc.conf @@ -0,0 +1,37 @@ +# window border +window.active.border.color: {{colors.primary.default.hex}} +window.inactive.border.color: {{colors.secondary.default.hex}} + +# ToggleKeybinds status indicator +window.active.indicator.toggled-keybind.color: {{colors.error.default.hex}} + +# window titlebar background +window.active.title.bg.color: {{colors.primary.default.hex}} +window.inactive.title.bg.color: {{colors.secondary.default.hex}} + +window.active.label.text.color: {{colors.on_primary.default.hex}} +window.inactive.label.text.color: {{colors.on_secondary.default.hex}} +window.label.text.justify: center + +# window button hover overlay +window.button.hover.bg.color: {{colors.hover.default.hex}} + +# window buttons +window.active.button.unpressed.image.color: {{colors.on_primary.default.hex}} +window.inactive.button.unpressed.image.color: {{colors.on_primary.default.hex}} + +# menu +menu.border.color: {{colors.on_primary.default.hex}} +menu.items.bg.color: {{colors.primary.default.hex}} +menu.items.text.color: {{colors.on_primary.default.hex}} +menu.items.active.bg.color: {{colors.secondary.default.hex}} +menu.items.active.text.color: {{colors.on_secondary.default.hex}} +menu.separator.color: {{colors.on_primary.default.hex}} +menu.title.bg.color: {{colors.primary.default.hex}} +menu.title.text.color: {{colors.on_primary.default.hex}} + +# on screen display (window-cycle dialog) +osd.bg.color: {{colors.primary.default.hex}} +osd.border.color: {{colors.on_primary.default.hex}} +osd.label.text.color: {{colors.on_primary.default.hex}} +osd.window-switcher.preview.border.color: {{colors.outline.default.hex}} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/mango.conf b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/mango.conf new file mode 100644 index 0000000..324ba48 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/mango.conf @@ -0,0 +1,12 @@ +# Noctalia Theme for Mango +# Generated automatically by Noctalia Shell + +shadowscolor = 0x{{colors.shadow.default.hex_stripped}}ff +rootcolor = 0x{{colors.surface.default.hex_stripped}}ff +bordercolor = 0x{{colors.outline.default.hex_stripped}}ff +focuscolor = 0x{{colors.primary.default.hex_stripped}}ff +maximizescreencolor = 0x{{colors.secondary.default.hex_stripped}}ff +urgentcolor = 0x{{colors.error.default.hex_stripped}}ff +scratchpadcolor = 0x{{colors.tertiary.default.hex_stripped}}ff +globalcolor = 0x{{colors.primary_container.default.hex_stripped}}ff +overlaycolor = 0x{{colors.secondary_container.default.hex_stripped}}ff diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/niri.kdl b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/niri.kdl new file mode 100644 index 0000000..c724a7c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/niri.kdl @@ -0,0 +1,35 @@ +layout { + + focus-ring { + active-color "{{colors.primary.default.hex}}" + inactive-color "{{colors.surface.default.hex}}" + urgent-color "{{colors.error.default.hex}}" + } + + border { + active-color "{{colors.primary.default.hex}}" + inactive-color "{{colors.surface.default.hex}}" + urgent-color "{{colors.error.default.hex}}" + } + + shadow { + color "{{colors.shadow.default.hex}}70" + } + + tab-indicator { + active-color "{{colors.primary.default.hex}}" + inactive-color "{{colors.primary_container.default.hex}}" + urgent-color "{{colors.error.default.hex}}" + } + + insert-hint { + color "{{colors.primary.default.hex}}80" + } +} + +recent-windows { + highlight { + active-color "{{colors.primary.default.hex}}" + urgent-color "{{colors.error.default.hex}}" + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/noctalia.json b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/noctalia.json new file mode 100644 index 0000000..1fe2097 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/noctalia.json @@ -0,0 +1,25 @@ +{ + "mPrimary": "{{colors.primary.default.hex}}", + "mOnPrimary": "{{colors.on_primary.default.hex}}", + + "mSecondary": "{{colors.secondary.default.hex}}", + "mOnSecondary": "{{colors.on_secondary.default.hex}}", + + "mTertiary": "{{colors.tertiary.default.hex}}", + "mOnTertiary": "{{colors.on_tertiary.default.hex}}", + + "mError": "{{colors.error.default.hex}}", + "mOnError": "{{colors.on_error.default.hex}}", + + "mSurface": "{{colors.surface.default.hex}}", + "mOnSurface": "{{colors.on_surface.default.hex}}", + + "mSurfaceVariant": "{{colors.surface_container.default.hex}}", + "mOnSurfaceVariant": "{{colors.on_surface_variant.default.hex}}", + + "mOutline": "{{colors.outline_variant.default.hex}}", + "mShadow": "{{colors.shadow.default.hex}}", + + "mHover": "{{colors.tertiary.default.hex}}", + "mOnHover": "{{colors.on_tertiary.default.hex}}" +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/pywalfox.json b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/pywalfox.json new file mode 100644 index 0000000..8bda0db --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/pywalfox.json @@ -0,0 +1,22 @@ +{ + "wallpaper": "{{image}}", + "alpha": "100", + "colors": { + "color0": "{{colors.surface.dark.hex}}", + "color1": "", + "color2": "", + "color3": "{{colors.primary.default.hex}}", + "color4": "", + "color5": "{{colors.tertiary.default.hex}}", + "color6": "", + "color7": "{{colors.surface_container_highest.default.hex}}", + "color8": "", + "color9": "", + "color10": "{{colors.primary.default.hex}}", + "color11": "", + "color12": "", + "color13": "{{colors.secondary.default.hex}}", + "color14": "", + "color15": "{{colors.on_background.default.hex}}" + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/qtct.conf b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/qtct.conf new file mode 100644 index 0000000..b0f5f7d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/qtct.conf @@ -0,0 +1,8 @@ +[ColorScheme] +#https://doc.qt.io/archives/qt-5.15/qpalette.html +#https://doc.qt.io/qt-6/qpalette.html - Qt6 didn't add/delete/change any of the color functions or docs +# functions that correspond to the colors definition location +# windowText,button,light,midlight,dark,mid,text,brightText,buttonText,base,window,shadow,highlight,highlightedText,link,linkVisited,alternateBase,NO_IDEA (just use some sane default),toolTipBase,toolTipText,placeholderText,accent +active_colors={{colors.on_background.default.hex}}, {{colors.surface.default.hex}}, #ffffff, #cacaca, #9f9f9f, #b8b8b8, {{colors.on_background.default.hex}}, #ffffff, {{colors.on_surface.default.hex}}, {{colors.background.default.hex}}, {{colors.background.default.hex}}, {{colors.shadow.default.hex}}, {{colors.primary_container.default.hex}}, {{colors.on_primary_container.default.hex}}, {{colors.secondary.default.hex}}, {{colors.primary.default.hex}}, {{colors.surface_variant.default.hex}}, {{colors.surface.default.hex}}, {{colors.surface_variant.default.hex}}, {{colors.on_surface.default.hex}}, {{colors.on_surface.default.hex}}, {{colors.primary.default.hex}} +disabled_colors={{colors.on_background.default.hex}}, {{colors.surface.default.hex}}, #ffffff, #cacaca, #9f9f9f, #b8b8b8, {{colors.on_background.default.hex}}, #ffffff, {{colors.on_surface.default.hex}}, {{colors.background.default.hex}}, {{colors.background.default.hex}}, {{colors.shadow.default.hex}}, {{colors.primary_container.default.hex}}, {{colors.on_primary_container.default.hex}}, {{colors.secondary.default.hex}}, {{colors.primary.default.hex}}, {{colors.surface_variant.default.hex}}, {{colors.surface.default.hex}}, {{colors.surface_variant.default.hex}}, {{colors.on_surface.default.hex}}, {{colors.on_surface.default.hex}}, {{colors.primary.default.hex}} +inactive_colors={{colors.on_background.default.hex}}, {{colors.surface.default.hex}}, #ffffff, #cacaca, #9f9f9f, #b8b8b8, {{colors.on_background.default.hex}}, #ffffff, {{colors.on_surface.default.hex}}, {{colors.background.default.hex}}, {{colors.background.default.hex}}, {{colors.shadow.default.hex}}, {{colors.primary_container.default.hex}}, {{colors.on_primary_container.default.hex}}, {{colors.secondary.default.hex}}, {{colors.primary.default.hex}}, {{colors.surface_variant.default.hex}}, {{colors.surface.default.hex}}, {{colors.surface_variant.default.hex}}, {{colors.on_surface.default.hex}}, {{colors.on_surface.default.hex}}, {{colors.primary.default.hex}} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/scroll b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/scroll new file mode 100644 index 0000000..bcd8357 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/scroll @@ -0,0 +1,22 @@ +set $primary #{{colors.primary.default.hex_stripped}} +set $on_primary #{{colors.on_primary.default.hex_stripped}} +set $secondary #{{colors.secondary.default.hex_stripped}} +set $on_secondary_transp #{{colors.on_secondary.default.hex_stripped}}AA +set $tertiary #{{colors.tertiary.default.hex_stripped}} +set $error #{{colors.error.default.hex_stripped}} +set $surface #{{colors.surface.default.hex_stripped}} +set $on_surface #{{colors.on_surface.default.hex_stripped}} +set $on_surface_variant #{{colors.on_surface_variant.default.hex_stripped}} +set $outline #{{colors.outline.default.hex_stripped}} + +## Window Colours +# class border backgr. text indicator child_border +client.focused $primary $surface $on_surface $primary $primary +client.focused_inactive $outline $surface $on_surface_variant $outline $outline +client.unfocused $outline $surface $on_surface_variant $outline $outline +client.urgent $error $surface $on_surface $error $error +client.placeholder $surface $surface $on_surface_variant $surface $surface +client.background $surface + +jump_labels_color $secondary +jump_labels_background $on_secondary_transp \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/spicetify.ini b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/spicetify.ini new file mode 100644 index 0000000..55c6865 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/spicetify.ini @@ -0,0 +1,27 @@ +[Comfy] +text = {{colors.on_surface.default.hex_stripped}} +subtext = {{colors.on_surface_variant.default.hex_stripped}} +main = {{colors.surface.default.hex_stripped}} +main-elevated = {{colors.surface.default.hex_stripped}} +main-transition = {{colors.surface_container_lowest.default.hex_stripped}} +highlight = {{colors.surface_container_low.default.hex_stripped}} +highlight-elevated = {{colors.surface_container_highest.default.hex_stripped}} +sidebar = {{colors.surface.default.hex_stripped}} +player = {{colors.surface.default.hex_stripped}} +card = {{colors.surface.default.hex_stripped}} +shadow = {{colors.surface.default.hex_stripped}} +selected-row = {{colors.on_surface.default.hex_stripped}} +button = {{colors.primary.default.hex_stripped}} +button-active = {{colors.primary.default.hex_stripped}} +button-disabled = {{colors.primary.default.hex_stripped}} +tab-active = {{colors.surface.default.hex_stripped}} +notification = {{colors.tertiary.default.hex_stripped}} +notification-error = {{colors.error.default.hex_stripped}} +misc = {{colors.surface.default.hex_stripped}} +play-button = {{colors.secondary.default.hex_stripped}} +play-button-active = {{colors.secondary.default.hex_stripped}} +progress-fg = {{colors.primary.default.hex_stripped}} +progress-bg = {{colors.surface.default.hex_stripped}} +heart = {{colors.error.default.hex_stripped}} +pagelink-active = {{colors.on_tertiary_container.default.hex_stripped}} +radio-btn-active = {{colors.on_tertiary_container.default.hex_stripped}} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/steam.css b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/steam.css new file mode 100644 index 0000000..5a7f63c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/steam.css @@ -0,0 +1,7 @@ +:root { + --theme-color: "Matugen"; + --hue-rotate: 220deg; + <* for name, value in colors *> + --md-sys-color-{{name | replace: "_", "-" }}: {{value.default.rgb}}; + <* endfor *> +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/sway b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/sway new file mode 100644 index 0000000..fa4978f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/sway @@ -0,0 +1,18 @@ +set $primary #{{colors.primary.default.hex_stripped}} +set $on_primary #{{colors.on_primary.default.hex_stripped}} +set $secondary #{{colors.secondary.default.hex_stripped}} +set $tertiary #{{colors.tertiary.default.hex_stripped}} +set $error #{{colors.error.default.hex_stripped}} +set $surface #{{colors.surface.default.hex_stripped}} +set $on_surface #{{colors.on_surface.default.hex_stripped}} +set $on_surface_variant #{{colors.on_surface_variant.default.hex_stripped}} +set $outline #{{colors.outline.default.hex_stripped}} + +## Window Colours +# class border backgr. text indicator child_border +client.focused $primary $surface $on_surface $primary $primary +client.focused_inactive $outline $surface $on_surface_variant $outline $outline +client.unfocused $outline $surface $on_surface_variant $outline $outline +client.urgent $error $surface $on_surface $error $error +client.placeholder $surface $surface $on_surface_variant $surface $surface +client.background $surface diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/telegram.tdesktop-theme b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/telegram.tdesktop-theme new file mode 100644 index 0000000..f2d9e67 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/telegram.tdesktop-theme @@ -0,0 +1,139 @@ +// Material You theme for Telegram Desktop +// Generated by Noctalia's Template Processor + +COLOR_GRAY: {{colors.outline.default.hex}}; +COLOR_DARK: {{colors.surface_variant.default.hex}}; + +windowBg: {{colors.background.default.hex}}; // Main background +windowFg: {{colors.on_background.default.hex}}; // Main text +windowBgOver: {{colors.surface_variant.default.hex}}; // Generic background on hover +windowBgRipple: {{colors.surface_variant.default.hex}}; // Ripple effect +windowFgOver: {{colors.on_surface_variant.default.hex}}; // Text on hover +windowSubTextFg: {{colors.outline.default.hex}}; // Minor text +windowSubTextFgOver: {{colors.outline.default.hex}}; // Minor text on hover +windowBoldFg: {{colors.on_background.default.hex}}; // Bold text +windowBoldFgOver: {{colors.on_surface_variant.default.hex}}; // Bold text on hover +windowBgActive: {{colors.primary.default.hex}}; // Active items background +windowFgActive: {{colors.on_primary.default.hex}}; // Active items text +windowActiveTextFg: {{colors.primary.default.hex}}; // Active items text +windowShadowFg: {{colors.shadow.default.hex}}; // Window shadow +windowShadowFgFallback: {{colors.shadow.default.hex}}; // Fallback for shadow +historyOutIconFg: {{colors.primary.default.hex}}; +historyIconFgInverted: {{colors.on_surface.default.hex}}; + +msgServiceBg: {{colors.primary_container.default.hex}}; +msgServiceFg: {{colors.on_surface.default.hex}}; +msgOutBg: {{colors.primary_container.default.hex}}; +msgOutBgSelected : {{colors.tertiary_container.default.hex}}; +msgOutServiceFg: {{colors.on_surface.default.hex}}; +msgOutDateFg: {{colors.on_surface.default.hex}}; +historySentIconFg: {{colors.on_surface.default.hex}}; +msgOutDateFgSelected: {{colors.on_surface.default.hex}}; +msgDateImgFg: {{colors.on_surface.default.hex}}; +dialogsSentIconFg: {{colors.primary.default.hex}}; +dialogsSentIconFgOver: {{colors.primary.default.hex}}; +dialogsOnlineBadgeFg: {{colors.primary.default.hex}}; + + +shadowFg: {{colors.shadow.default.hex}}; // General shadow +slideFadeOutBg: {{colors.background.default.hex}}; +slideFadeOutShadowFg: {{colors.shadow.default.hex}}; + +imageBg: {{colors.surface.default.hex}}; +imageBgTransparent: {{colors.surface.default.hex}}; + +activeButtonBg: {{colors.primary.default.hex}}; // Active button background +activeButtonBgOver: {{colors.primary_container.default.hex}}; // Active button hover background +activeButtonBgRipple: {{colors.on_primary_container.default.hex}}; // Active button ripple +activeButtonFg: {{colors.on_primary.default.hex}}; // Active button text +activeButtonFgOver: {{colors.on_primary_container.default.hex}}; // Active button hover text +activeButtonSecondaryFg: {{colors.on_primary.default.hex}}; // Active button secondary text +activeButtonSecondaryFgOver: {{colors.on_primary_container.default.hex}}; // Active button secondary hover text +activeLineFg: {{colors.on_surface.default.hex}}; +dialogsBgActive: {{colors.primary.default.hex}}; + +lightButtonBg: {{colors.surface.default.hex}}; // Light button background +lightButtonBgOver: {{colors.surface_variant.default.hex}}; // Light button hover background +lightButtonBgRipple: {{colors.primary.default.hex}}; // Light button ripple +lightButtonFg: {{colors.on_surface.default.hex}}; // Light button text +lightButtonFgOver: {{colors.on_surface_variant.default.hex}}; // Light button hover text + +attentionButtonFg: {{colors.error.default.hex}}; +attentionButtonFgOver: {{colors.error.default.hex}}; +attentionButtonBgOver: {{colors.error_container.default.hex}}; +attentionButtonBgRipple: {{colors.on_error_container.default.hex}}; + +outlineButtonBg: {{colors.surface.default.hex}}; // Outline button background +outlineButtonBgOver: {{colors.surface_variant.default.hex}}; // Outline button hover background +outlineButtonOutlineFg: {{colors.primary.default.hex}}; // Outline button color +outlineButtonBgRipple: {{colors.primary.default.hex}}; // Outline button ripple + +menuBg: {{colors.surface.default.hex}}; +menuBgOver: {{colors.surface_variant.default.hex}}; +menuBgRipple: {{colors.primary.default.hex}}; +menuIconFg: {{colors.on_surface.default.hex}}; +menuIconFgOver: {{colors.on_surface_variant.default.hex}}; +menuSubmenuArrowFg: {{colors.outline.default.hex}}; +menuFgDisabled: {{colors.outline.default.hex}}; +menuSeparatorFg: {{colors.outline.default.hex}}; + +scrollBarBg: {{colors.primary.default.hex}}40; // Scroll bar background (40% opacity) +scrollBarBgOver: {{colors.primary.default.hex}}60; // Scroll bar hover background (60% opacity) +scrollBg: {{colors.surface_variant.default.hex}}40; // Scroll bar track (40% opacity) +scrollBgOver: {{colors.surface_variant.default.hex}}60; // Scroll bar track on hover (60% opacity) + +smallCloseIconFg: {{colors.outline.default.hex}}; +smallCloseIconFgOver: {{colors.on_surface_variant.default.hex}}; + +radialFg: {{colors.primary.default.hex}}; +radialBg: {{colors.surface.default.hex}}; + +placeholderFg: {{colors.outline.default.hex}}; // Placeholder text +placeholderFgActive: {{colors.primary.default.hex}}; // Active placeholder text +inputBorderFg: {{colors.outline.default.hex}}; // Input border +filterInputBorderFg: {{colors.outline.default.hex}}; // Search input border +filterInputInactiveBg: {{colors.surface.default.hex}}; // Inactive search input background +checkboxFg: {{colors.primary.default.hex}}; // Checkbox color + +titleBg: {{colors.surface.default.hex}}; // Window title background +titleShadow: {{colors.shadow.default.hex}}; +titleButtonFg: {{colors.on_surface.default.hex}}; // Title button color +titleButtonBgOver: {{colors.surface_variant.default.hex}}; // Title button hover background +titleButtonFgOver: {{colors.on_surface_variant.default.hex}}; // Title button hover color +titleButtonCloseBgOver: {{colors.error.default.hex}}; +titleButtonCloseFgOver: {{colors.on_error.default.hex}}; +titleFgActive: {{colors.on_surface.default.hex}}; // Active title text +titleFg: {{colors.on_surface.default.hex}}; // Inactive title text + +trayCounterBg: {{colors.error.default.hex}}; // Tray counter background +trayCounterBgMute: {{colors.outline.default.hex}}; // Muted tray counter background +trayCounterFg: {{colors.on_error.default.hex}}; // Tray counter text +trayCounterBgMacInvert: {{colors.error.default.hex}}; // Mac tray counter +trayCounterFgMacInvert: {{colors.on_error.default.hex}}; // Mac tray counter text + +layerBg: {{colors.surface.default.hex}}99; // Layer background (60% opacity) + +cancelIconFg: {{colors.error.default.hex}}; // Cancel icon +cancelIconFgOver: {{colors.error.default.hex}}; // Cancel icon on hover + +boxBg: {{colors.surface.default.hex}}; // Box background +boxTextFg: {{colors.on_surface.default.hex}}; // Box text +boxTextFgGood: {{colors.primary.default.hex}}; // Box good text +boxTextFgError: {{colors.error.default.hex}}; // Box error text +boxTitleFg: {{colors.on_surface.default.hex}}; // Box title text +boxSearchBg: {{colors.surface.default.hex}}; // Box search field background +boxSearchCancelIconFg: {{colors.error.default.hex}}; // Box search cancel icon +boxSearchCancelIconFgOver: {{colors.error.default.hex}}; // Box search cancel icon on hover + +contactsBg: {{colors.surface.default.hex}}; // Contacts background +contactsBgOver: {{colors.surface_variant.default.hex}}; // Contacts background on hover +contactsNameFg: {{colors.on_surface.default.hex}}; // Contact name +contactsStatusFg: {{colors.outline.default.hex}}; // Contact status +contactsStatusFgOver: {{colors.on_surface_variant.default.hex}}; // Contact status on hover +contactsStatusFgOnline: {{colors.primary.default.hex}}; // Online contact status + +photoCropFadeBg: {{colors.surface.default.hex}}cc; // Photo crop fade background +photoCropPointFg: {{colors.primary.default.hex}}; // Photo crop points + +chat_inBubbleSelected: #313244; // inbox selected chat background +chat_outBubbleSelected: #313244; // outbox selected chat background diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/alacritty-predefined.toml b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/alacritty-predefined.toml new file mode 100644 index 0000000..a3ba707 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/alacritty-predefined.toml @@ -0,0 +1,33 @@ +# Colors (Noctalia) + +[colors.bright] +black = '{{colors.terminal_bright_black.default.hex}}' +blue = '{{colors.terminal_bright_blue.default.hex}}' +cyan = '{{colors.terminal_bright_cyan.default.hex}}' +green = '{{colors.terminal_bright_green.default.hex}}' +magenta = '{{colors.terminal_bright_magenta.default.hex}}' +red = '{{colors.terminal_bright_red.default.hex}}' +white = '{{colors.terminal_bright_white.default.hex}}' +yellow = '{{colors.terminal_bright_yellow.default.hex}}' + +[colors.cursor] +cursor = '{{colors.terminal_cursor.default.hex}}' +text = '{{colors.terminal_cursor_text.default.hex}}' + +[colors.normal] +black = '{{colors.terminal_normal_black.default.hex}}' +blue = '{{colors.terminal_normal_blue.default.hex}}' +cyan = '{{colors.terminal_normal_cyan.default.hex}}' +green = '{{colors.terminal_normal_green.default.hex}}' +magenta = '{{colors.terminal_normal_magenta.default.hex}}' +red = '{{colors.terminal_normal_red.default.hex}}' +white = '{{colors.terminal_normal_white.default.hex}}' +yellow = '{{colors.terminal_normal_yellow.default.hex}}' + +[colors.primary] +background = '{{colors.terminal_background.default.hex}}' +foreground = '{{colors.terminal_foreground.default.hex}}' + +[colors.selection] +background = '{{colors.terminal_selection_bg.default.hex}}' +text = '{{colors.terminal_selection_fg.default.hex}}' diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/alacritty.toml b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/alacritty.toml new file mode 100644 index 0000000..f0bb7cd --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/alacritty.toml @@ -0,0 +1,68 @@ +[colors.primary] +background = '{{colors.background.default.hex}}' +foreground = '{{colors.on_surface.default.hex}}' + +[colors.cursor] +text = '{{colors.on_surface.default.hex}}' +cursor = '{{colors.on_surface_variant.default.hex}}' + +[colors.vi_mode_cursor] +text = '{{colors.background.default.hex}}' +cursor = '{{colors.primary.default.hex}}' + +[colors.search.matches] +foreground = '{{colors.surface_variant.default.hex}}' +background = '{{colors.tertiary.default.hex}}' + +[colors.search.focused_match] +foreground = '{{colors.surface_variant.default.hex}}' +background = '{{colors.primary.default.hex}}' + +[colors.footer_bar] +foreground = '{{colors.surface_variant.default.hex}}' +background = '{{colors.inverse_surface.default.hex}}' + +[colors.hints.start] +foreground = '{{colors.surface_variant.default.hex}}' +background = '{{colors.secondary.default.hex}}' + +[colors.hints.end] +foreground = '{{colors.surface_variant.default.hex}}' +background = '{{colors.secondary.default.hex}}' + +[colors.selection] +text = '{{colors.background.default.hex}}' +background = '{{colors.primary.default.hex}}' + + +[colors.normal] +black = '#181818' +red = '{{colors.error.default.hex}}' +green = '{{colors.primary.default.hex}}' +yellow = '{{colors.inverse_primary.default.hex}}' +blue = '{{colors.primary.default.hex}}' +magenta = '{{colors.tertiary.default.hex}}' +cyan = '{{colors.secondary.default.hex}}' +white = '#BAC2DE' + + +[colors.bright] +black = '#585B70' +red = '#F38BA8' +green = '#A6E3A1' +yellow = '#F9E2AF' +blue = '#89B4FA' +magenta = '#F5C2E7' +cyan = '#94E2D5' +white = '#A6ADC8' + + +[colors.dim] +black = '#45475A' +red = '#F38BA8' +green = '#A6E3A1' +yellow = '#F9E2AF' +blue = '#89B4FA' +magenta = '#F5C2E7' +cyan = '#94E2D5' +white = '#BAC2DE' \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/foot b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/foot new file mode 100644 index 0000000..ee4d7b1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/foot @@ -0,0 +1,24 @@ +[colors-dark] +background = {{colors.surface.default.hex_stripped}} +foreground = {{colors.on_surface.default.hex_stripped}} + +selection-background = {{colors.tertiary.default.hex_stripped}} +selection-foreground = {{colors.on_tertiary.default.hex_stripped}} + +regular0 = {{colors.surface_variant.default.hex_stripped}} +regular1 = {{colors.error.default.hex_stripped}} +regular2 = {{colors.primary.default.hex_stripped}} +regular3 = {{colors.secondary.default.hex_stripped}} +regular4 = {{colors.tertiary.default.hex_stripped}} +regular5 = {{colors.primary_fixed_dim.default.hex_stripped}} +regular6 = {{colors.secondary_fixed_dim.default.hex_stripped}} +regular7 = {{colors.on_surface.default.hex_stripped}} + +bright0 = {{colors.on_surface_variant.default.hex_stripped}} +bright1 = {{colors.error.default.hex_stripped}} +bright2 = {{colors.primary.default.hex_stripped}} +bright3 = {{colors.secondary.default.hex_stripped}} +bright4 = {{colors.tertiary.default.hex_stripped}} +bright5 = {{colors.primary_fixed_dim.default.hex_stripped}} +bright6 = {{colors.secondary_fixed_dim.default.hex_stripped}} +bright7 = {{colors.on_surface.default.hex_stripped}} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/foot-predefined b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/foot-predefined new file mode 100644 index 0000000..ccee2e3 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/foot-predefined @@ -0,0 +1,22 @@ +[colors-dark] +foreground={{colors.terminal_foreground.default.hex_stripped}} +background={{colors.terminal_background.default.hex_stripped}} +regular0={{colors.terminal_normal_black.default.hex_stripped}} +regular1={{colors.terminal_normal_red.default.hex_stripped}} +regular2={{colors.terminal_normal_green.default.hex_stripped}} +regular3={{colors.terminal_normal_yellow.default.hex_stripped}} +regular4={{colors.terminal_normal_blue.default.hex_stripped}} +regular5={{colors.terminal_normal_magenta.default.hex_stripped}} +regular6={{colors.terminal_normal_cyan.default.hex_stripped}} +regular7={{colors.terminal_normal_white.default.hex_stripped}} +bright0={{colors.terminal_bright_black.default.hex_stripped}} +bright1={{colors.terminal_bright_red.default.hex_stripped}} +bright2={{colors.terminal_bright_green.default.hex_stripped}} +bright3={{colors.terminal_bright_yellow.default.hex_stripped}} +bright4={{colors.terminal_bright_blue.default.hex_stripped}} +bright5={{colors.terminal_bright_magenta.default.hex_stripped}} +bright6={{colors.terminal_bright_cyan.default.hex_stripped}} +bright7={{colors.terminal_bright_white.default.hex_stripped}} +selection-foreground={{colors.terminal_selection_fg.default.hex_stripped}} +selection-background={{colors.terminal_selection_bg.default.hex_stripped}} +cursor={{colors.terminal_cursor_text.default.hex_stripped}} {{colors.terminal_cursor.default.hex_stripped}} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/ghostty b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/ghostty new file mode 100644 index 0000000..131561d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/ghostty @@ -0,0 +1,22 @@ +palette = 0= {{colors.surface_variant.default.hex}} +palette = 1= {{colors.error.default.hex}} +palette = 2= {{colors.primary.default.hex}} +palette = 3= {{colors.secondary.default.hex}} +palette = 4= {{colors.tertiary.default.hex}} +palette = 5= {{colors.primary_fixed_dim.default.hex}} +palette = 6= {{colors.secondary_fixed_dim.default.hex}} +palette = 7= {{colors.on_surface.default.hex}} +palette = 8= {{colors.on_surface_variant.default.hex}} +palette = 9= {{colors.error.default.hex}} +palette = 10= {{colors.primary.default.hex}} +palette = 11= {{colors.secondary.default.hex}} +palette = 12= {{colors.tertiary.default.hex}} +palette = 13= {{colors.primary_fixed_dim.default.hex}} +palette = 14= {{colors.secondary_fixed_dim.default.hex}} +palette = 15= {{colors.on_surface.default.hex}} +background = {{colors.surface.default.hex}} +foreground = {{colors.on_surface.default.hex}} +cursor-color = {{colors.on_surface.default.hex}} +cursor-text = {{colors.surface.default.hex}} +selection-background = {{colors.tertiary.default.hex}} +selection-foreground = {{colors.on_tertiary.default.hex}} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/ghostty-predefined b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/ghostty-predefined new file mode 100644 index 0000000..6f79e9b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/ghostty-predefined @@ -0,0 +1,22 @@ +palette = 0={{colors.terminal_normal_black.default.hex}} +palette = 1={{colors.terminal_normal_red.default.hex}} +palette = 2={{colors.terminal_normal_green.default.hex}} +palette = 3={{colors.terminal_normal_yellow.default.hex}} +palette = 4={{colors.terminal_normal_blue.default.hex}} +palette = 5={{colors.terminal_normal_magenta.default.hex}} +palette = 6={{colors.terminal_normal_cyan.default.hex}} +palette = 7={{colors.terminal_normal_white.default.hex}} +palette = 8={{colors.terminal_bright_black.default.hex}} +palette = 9={{colors.terminal_bright_red.default.hex}} +palette = 10={{colors.terminal_bright_green.default.hex}} +palette = 11={{colors.terminal_bright_yellow.default.hex}} +palette = 12={{colors.terminal_bright_blue.default.hex}} +palette = 13={{colors.terminal_bright_magenta.default.hex}} +palette = 14={{colors.terminal_bright_cyan.default.hex}} +palette = 15={{colors.terminal_bright_white.default.hex}} +background = {{colors.terminal_background.default.hex}} +foreground = {{colors.terminal_foreground.default.hex}} +cursor-color = {{colors.terminal_cursor.default.hex}} +cursor-text = {{colors.terminal_cursor_text.default.hex}} +selection-background = {{colors.terminal_selection_bg.default.hex}} +selection-foreground = {{colors.terminal_selection_fg.default.hex}} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/kitty-predefined.conf b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/kitty-predefined.conf new file mode 100644 index 0000000..7b3087d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/kitty-predefined.conf @@ -0,0 +1,30 @@ +color0 {{colors.terminal_normal_black.default.hex}} +color1 {{colors.terminal_normal_red.default.hex}} +color2 {{colors.terminal_normal_green.default.hex}} +color3 {{colors.terminal_normal_yellow.default.hex}} +color4 {{colors.terminal_normal_blue.default.hex}} +color5 {{colors.terminal_normal_magenta.default.hex}} +color6 {{colors.terminal_normal_cyan.default.hex}} +color7 {{colors.terminal_normal_white.default.hex}} +color8 {{colors.terminal_bright_black.default.hex}} +color9 {{colors.terminal_bright_red.default.hex}} +color10 {{colors.terminal_bright_green.default.hex}} +color11 {{colors.terminal_bright_yellow.default.hex}} +color12 {{colors.terminal_bright_blue.default.hex}} +color13 {{colors.terminal_bright_magenta.default.hex}} +color14 {{colors.terminal_bright_cyan.default.hex}} +color15 {{colors.terminal_bright_white.default.hex}} +background {{colors.terminal_background.default.hex}} +selection_foreground {{colors.terminal_selection_fg.default.hex}} +cursor {{colors.terminal_cursor.default.hex}} +cursor_text_color {{colors.terminal_cursor_text.default.hex}} +foreground {{colors.terminal_foreground.default.hex}} +selection_background {{colors.terminal_selection_bg.default.hex}} +active_border_color {{colors.primary.default.hex}} +inactive_border_color {{colors.secondary.default.hex}} + +active_tab_foreground {{colors.on_primary.default.hex}} +active_tab_background {{colors.primary.default.hex}} +inactive_tab_foreground {{colors.on_surface_variant.default.hex}} +inactive_tab_background {{colors.surface_variant.default.hex}} +cursor_trail_color {{colors.on_surface_variant.default.hex}} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/kitty.conf b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/kitty.conf new file mode 100644 index 0000000..56eb0eb --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/kitty.conf @@ -0,0 +1,32 @@ +color0 {{colors.surface.default.hex}} +color1 {{colors.error.default.hex}} +color2 {{colors.primary.default.hex}} +color3 {{colors.secondary.default.hex}} +color4 {{colors.tertiary.default.hex}} +color5 {{colors.primary_fixed_dim.default.hex}} +color6 {{colors.secondary_fixed_dim.default.hex}} +color7 {{colors.on_surface.default.hex}} +color8 {{colors.outline.default.hex}} +color9 {{colors.error.default.hex}} +color10 {{colors.primary.default.hex}} +color11 {{colors.secondary.default.hex}} +color12 {{colors.tertiary.default.hex}} +color13 {{colors.primary_fixed_dim.default.hex}} +color14 {{colors.secondary_fixed_dim.default.hex}} +color15 {{colors.on_surface.default.hex}} + +cursor {{colors.on_surface.default.hex}} +cursor_text_color {{colors.surface.default.hex}} +background {{colors.surface.default.hex}} +foreground {{colors.on_surface.default.hex}} +selection_foreground {{colors.on_surface_variant.default.hex}} +selection_background {{colors.surface_variant.default.hex}} +active_border_color {{colors.primary.default.hex}} +inactive_border_color {{colors.surface_variant.default.hex}} +url_color {{colors.primary.default.hex}} + +active_tab_foreground {{colors.on_primary.default.hex}} +active_tab_background {{colors.primary.default.hex}} +inactive_tab_foreground {{colors.on_surface_variant.default.hex}} +inactive_tab_background {{colors.surface_variant.default.hex}} +cursor_trail_color {{colors.on_surface_variant.default.hex}} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/wezterm-predefined.toml b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/wezterm-predefined.toml new file mode 100644 index 0000000..da4c226 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/wezterm-predefined.toml @@ -0,0 +1,84 @@ +[colors] +ansi = [ + "{{colors.terminal_normal_black.default.hex}}", + "{{colors.terminal_normal_red.default.hex}}", + "{{colors.terminal_normal_green.default.hex}}", + "{{colors.terminal_normal_yellow.default.hex}}", + "{{colors.terminal_normal_blue.default.hex}}", + "{{colors.terminal_normal_magenta.default.hex}}", + "{{colors.terminal_normal_cyan.default.hex}}", + "{{colors.terminal_normal_white.default.hex}}", +] +background = "{{colors.terminal_background.default.hex}}" +brights = [ + "{{colors.terminal_bright_black.default.hex}}", + "{{colors.terminal_bright_red.default.hex}}", + "{{colors.terminal_bright_green.default.hex}}", + "{{colors.terminal_bright_yellow.default.hex}}", + "{{colors.terminal_bright_blue.default.hex}}", + "{{colors.terminal_bright_magenta.default.hex}}", + "{{colors.terminal_bright_cyan.default.hex}}", + "{{colors.terminal_bright_white.default.hex}}", +] +compose_cursor = "{{colors.terminal_cursor.default.hex}}" +cursor_bg = "{{colors.terminal_cursor.default.hex}}" +cursor_border = "{{colors.terminal_cursor.default.hex}}" +cursor_fg = "{{colors.terminal_cursor_text.default.hex}}" +foreground = "{{colors.terminal_foreground.default.hex}}" +scrollbar_thumb = "{{colors.terminal_selection_bg.default.hex}}" +selection_bg = "{{colors.terminal_selection_bg.default.hex}}" +selection_fg = "{{colors.terminal_selection_fg.default.hex}}" +split = "{{colors.terminal_bright_black.default.hex}}" +visual_bell = "{{colors.terminal_normal_black.default.hex}}" + +[colors.indexed] +16 = "{{colors.secondary.default.hex}}" +17 = "{{colors.terminal_cursor.default.hex}}" + +[colors.tab_bar] +background = "{{colors.terminal_background.default.hex | darken 0.1}}" +inactive_tab_edge = "{{colors.terminal_selection_bg.default.hex}}" + +[colors.tab_bar.active_tab] +bg_color = "{{colors.primary.default.hex}}" +fg_color = "{{colors.on_primary.default.hex}}" +intensity = "Normal" +italic = false +strikethrough = false +underline = "None" + +[colors.tab_bar.inactive_tab] +bg_color = "{{colors.terminal_background.default.hex | darken 0.05}}" +fg_color = "{{colors.terminal_foreground.default.hex}}" +intensity = "Normal" +italic = false +strikethrough = false +underline = "None" + +[colors.tab_bar.inactive_tab_hover] +bg_color = "{{colors.terminal_background.default.hex}}" +fg_color = "{{colors.terminal_foreground.default.hex}}" +intensity = "Normal" +italic = false +strikethrough = false +underline = "None" + +[colors.tab_bar.new_tab] +bg_color = "{{colors.terminal_selection_bg.default.hex}}" +fg_color = "{{colors.terminal_foreground.default.hex}}" +intensity = "Normal" +italic = false +strikethrough = false +underline = "None" + +[colors.tab_bar.new_tab_hover] +bg_color = "{{colors.terminal_bright_black.default.hex}}" +fg_color = "{{colors.terminal_foreground.default.hex}}" +intensity = "Normal" +italic = false +strikethrough = false +underline = "None" + +[metadata] +author = "Noctalia" +name = "Noctalia" diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/wezterm.toml b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/wezterm.toml new file mode 100644 index 0000000..4c73b0e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/terminal/wezterm.toml @@ -0,0 +1,31 @@ +[colors] +ansi = [ + "{{colors.surface.default.hex}}", + "{{colors.error.default.hex}}", + "{{colors.tertiary.default.hex}}", + "{{colors.secondary.default.hex}}", + "{{colors.primary.default.hex}}", + "{{colors.tertiary_fixed_dim.default.hex}}", + "{{colors.secondary_fixed_dim.default.hex}}", + "{{colors.on_surface.default.hex}}", +] +background = "{{colors.surface.default.hex}}" +brights = [ + "{{colors.outline.default.hex}}", + "{{colors.error.default.hex}}", + "{{colors.tertiary.default.hex}}", + "{{colors.secondary.default.hex}}", + "{{colors.primary.default.hex}}", + "{{colors.tertiary_fixed_dim.default.hex}}", + "{{colors.secondary_fixed_dim.default.hex}}", + "{{colors.on_surface.default.hex}}", +] +cursor_bg = "{{colors.on_surface.default.hex}}" +cursor_border = "{{colors.on_surface.default.hex}}" +cursor_fg = "{{colors.surface.default.hex}}" +foreground = "{{colors.on_surface.default.hex}}" +selection_bg = "{{colors.surface_variant.default.hex}}" +selection_fg = "{{colors.on_surface_variant.default.hex}}" + +[metadata] +name = "Noctalia" diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/vicinae.toml b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/vicinae.toml new file mode 100644 index 0000000..35c4d70 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/vicinae.toml @@ -0,0 +1,127 @@ +# Vicinae Noctalia's Theme Template + +[meta] +name = "Noctalia" +description = "Theme generated by Noctalia - {{mode}} variant" +variant = "{{mode}}" +icon = "noctalia.svg" + +# ============================================================================ +# Core Colors +# ============================================================================ + +[colors.core] +accent = "{{colors.primary.default.hex}}" +accent_foreground = "{{colors.on_primary.default.hex}}" +background = "{{colors.surface.default.hex}}" +foreground = "{{colors.on_surface.default.hex}}" +secondary_background = "{{colors.surface_container.default.hex}}" +border = "{{colors.outline_variant.default.hex}}" + +# ============================================================================ +# Window Borders +# ============================================================================ + +[colors.main_window] +border = "{{colors.outline_variant.default.hex}}" # Softer for main app + +[colors.settings_window] +border = "{{colors.outline.default.hex}}" # Stronger but not primary (less distracting) + +# ============================================================================ +# Accent Palette +# ============================================================================ + +[colors.accents] +blue = "{{colors.primary.default.hex}}" +green = "{{colors.tertiary.default.hex}}" +magenta = "{{colors.secondary.default.hex}}" +orange = { name = "{{colors.error.default.hex}}", lighter = 40 } +red = "{{colors.error.default.hex}}" +yellow = { name = "{{colors.tertiary.default.hex}}", lighter = 80 } +cyan = { name = "{{colors.primary.default.hex}}", lighter = 50 } +purple = "{{colors.secondary.default.hex}}" + +# ============================================================================ +# Text System +# ============================================================================ + +[colors.text] +default = "{{colors.on_surface.default.hex}}" +muted = "{{colors.on_surface_variant.default.hex}}" +danger = "{{colors.error.default.hex}}" +success = "{{colors.tertiary.default.hex}}" +placeholder = { name = "{{colors.on_surface_variant.default.hex}}", opacity = 0.6 } + +[colors.text.selection] +background = "{{colors.primary.default.hex}}" +foreground = "{{colors.on_primary.default.hex}}" + +[colors.text.links] +default = "{{colors.primary.default.hex}}" +visited = { name = "{{colors.tertiary.default.hex}}", darker = 20 } + +# ============================================================================ +# Input Fields +# ============================================================================ + +[colors.input] +border = "{{colors.outline.default.hex}}" +border_focus = "{{colors.primary.default.hex}}" +border_error = "{{colors.error.default.hex}}" + +# ============================================================================ +# Buttons +# ============================================================================ + +[colors.button.primary] +background = "{{colors.surface_container_high.default.hex}}" +foreground = "{{colors.on_surface.default.hex}}" + +[colors.button.primary.hover] +background = "{{colors.surface_container_highest.default.hex}}" + +[colors.button.primary.focus] +outline = "{{colors.primary.default.hex}}" + +# ============================================================================ +# Lists +# ============================================================================ + +[colors.list.item.hover] +background = { name = "{{colors.primary_container.default.hex}}", opacity = 0.25 } +foreground = "{{colors.on_surface.default.hex}}" + +[colors.list.item.selection] +background = { name = "{{colors.primary_container.default.hex}}", opacity = 0.50 } +foreground = "{{colors.on_primary_container.default.hex}}" +secondary_background = "{{colors.primary_container.default.hex}}" +secondary_foreground = "{{colors.on_primary_container.default.hex}}" + +# ============================================================================ +# Grid Items +# ============================================================================ + +[colors.grid.item] +background = "{{colors.surface_container.default.hex}}" + +[colors.grid.item.hover] +outline = { name = "{{colors.secondary.default.hex}}", opacity = 0.8 } + +[colors.grid.item.selection] +outline = { name = "{{colors.primary.default.hex}}" } + +# ============================================================================ +# Scrollbars +# ============================================================================ + +[colors.scrollbars] +background = { name = "{{colors.primary.default.hex}}", opacity = 0.2 } + +# ============================================================================ +# Loading States +# ============================================================================ + +[colors.loading] +bar = "{{colors.primary.default.hex}}" +spinner = "{{colors.primary.default.hex}}" diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/walker.css b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/walker.css new file mode 100644 index 0000000..3452f7f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/walker.css @@ -0,0 +1,189 @@ +@define-color surface {{colors.surface.default.hex}}; +@define-color surfaceVariant {{colors.surface_variant.default.hex}}; + +@define-color onSurface {{colors.on_surface.default.hex}}; +@define-color onSurfaceVariant {{colors.on_surface_variant.default.hex}}; + +@define-color primary {{colors.primary.default.hex}}; +@define-color secondary {{colors.secondary.default.hex}}; +@define-color error {{colors.error.default.hex}}; +@define-color outline {{colors.outline.default.hex}}; + +* { + all: unset; +} + +.normal-icons { + -gtk-icon-size: 16px; +} + +.large-icons { + -gtk-icon-size: 36px; +} + +scrollbar { + opacity: 0; +} + +.box-wrapper { + box-shadow: + 0 19px 38px rgba(0, 0, 0, 0.3), + 0 15px 12px rgba(0, 0, 0, 0.22); + background: @surface; + padding: 12px; + border-radius: 20px; + border: 2px solid @outline; +} + +.preview-box, +.elephant-hint, +.placeholder { + color: @onSurface; +} + +.box { + background: @surfaceVariant; + padding: 20px; + border-radius: 20px; + border: 2px solid @outline; +} + +.search-container { + border-radius: 10px; + border: 2px solid @outline; +} + +.input placeholder { + opacity: 0.5; +} + +.input { + background: @surface; + color: @onSurface; + caret-color: @onSurface; + padding: 10px; +} + +.input:focus, +.input:active { +} + +.content-container { +} + +.placeholder { +} + +.scroll { +} + +.list { + color: @onSurface; +} + +child { +} + +.item-box { + border-radius: 10px; + padding: 10px; +} + +.item-quick-activation { + background: @secondary; + border-radius: 10px; + padding: 10px; + color: @surface; +} + +child:hover .item-box, +child:selected .item-box { + background: alpha(@primary, 0.4); +} + +.item-text-box { +} + +.item-subtext { + font-size: 12px; + opacity: 0.5; +} + +.providerlist .item-subtext { + font-size: unset; + opacity: 0.75; +} + +.item-image-text { + font-size: 28px; +} + +.preview { + border-top: 1px solid @primary; + padding: 10px; + border-radius: 10px; + color: @onSurface; +} + +.calc .item-text { + font-size: 24px; +} + +.calc .item-subtext { +} + +.symbols .item-image { + font-size: 24px; +} + +.todo.done .item-text-box { + opacity: 0.25; +} + +.todo.urgent { + font-size: 24px; +} + +.todo.active { + font-weight: bold; +} + +.primarytooth.disconnected { + opacity: 0.5; +} + +.preview .large-icons { + -gtk-icon-size: 64px; +} + +.keybinds-wrapper { + border-top: 1px solid @primary; + font-size: 12px; + color: @primary; +} + +.keybinds { +} + +.keybind { + color: @onSurfaceVariant; +} + +.keybind-bind { + font-weight: bold; + text-transform: lowercase; +} + +.keybind-label { +} + +.error { + padding: 10px; + background: @error; + color: @surface; +} + +:not(.calc).current { + font-style: italic; +} + diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/yazi.toml b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/yazi.toml new file mode 100644 index 0000000..3fc26cf --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/yazi.toml @@ -0,0 +1,917 @@ +# : Manager [[[ + +[mgr] +cwd = { fg = "{{colors.on_surface.default.hex}}" } + +# Find +find_keyword = { fg = "{{colors.error.default.hex}}", bold = true, italic = true, underline = true } +find_position = { fg = "{{colors.error.default.hex}}", bold = true, italic = true } + +# Marker +marker_copied = { fg = "{{colors.tertiary_container.default.hex}}", bg = "{{colors.tertiary_container.default.hex}}" } +marker_cut = { fg = "{{colors.tertiary_container.default.hex}}", bg = "{{colors.tertiary_container.default.hex}}" } +marker_marked = { fg = "{{colors.error.default.hex}}", bg = "{{colors.error.default.hex}}" } +marker_selected = { fg = "{{colors.tertiary.default.hex}}", bg = "{{colors.tertiary.default.hex}}" } + +# Count +count_copied = { fg = "{{colors.on_tertiary_container.default.hex}}", bg = "{{colors.tertiary_container.default.hex}}" } +count_cut = { fg = "{{colors.on_tertiary_container.default.hex}}", bg = "{{colors.tertiary_container.default.hex}}" } +count_selected = { fg = "{{colors.on_primary.default.hex}}", bg = "{{colors.tertiary.default.hex}}" } + +# Border +border_style = { fg = "{{colors.primary.default.hex}}" } + +# : ]]] + + +# : Status [[[ + +[status] +overall = { fg = "{{colors.primary.default.hex}}" } +sep_left = { open = "", close = "" } +sep_right = { open = "", close = "" } + +# Progress +progress_label = { bold = true } +progress_normal = { fg = "{{colors.primary.default.hex}}", bg = "{{colors.surface.default.hex}}" } +progress_error = { fg = "{{colors.error.default.hex}}", bg = "{{colors.surface.default.hex}}" } + +# Permissions +perm_type = { fg = "{{colors.secondary.default.hex}}" } +perm_write = { fg = "{{colors.tertiary.default.hex}}" } +perm_exec = { fg = "{{colors.error.default.hex}}" } +perm_read = { fg = "{{colors.tertiary_container.default.hex}}" } +perm_sep = { fg = "{{colors.primary_container.default.hex}}" } + +# : ]]] + + +# : Mode [[[ + +[mode] + +normal_main = { bg = "{{colors.primary.default.hex}}", fg = "{{colors.on_primary.default.hex}}", bold = true } +normal_alt = { bg = "{{colors.surface_variant.default.hex}}", fg = "{{colors.on_surface_variant.default.hex}}" } + +select_main = { bg = "{{colors.secondary.default.hex}}", fg = "{{colors.on_secondary.default.hex}}", bold = true } +select_alt = { bg = "{{colors.surface_variant.default.hex}}", fg = "{{colors.on_surface_variant.default.hex}}" } + +unset_main = { bg = "{{colors.tertiary.default.hex}}", fg = "{{colors.on_tertiary.default.hex}}", bold = true } +unset_alt = { bg = "{{colors.surface_variant.default.hex}}", fg = "{{colors.on_surface_variant.default.hex}}" } + +# : ]]] + + +# : Input [[[ + +[input] +border = { fg = "{{colors.primary.default.hex}}" } +title = {} +value = { fg = "{{colors.on_surface.default.hex}}" } +selected = { reversed = true } + +# : ]]] + + +# : Tabs [[[ + +[tabs] +active = { fg = "{{colors.surface.default.hex}}", bold = true, bg = "{{colors.primary.default.hex}}" } +inactive = { fg = "{{colors.secondary.default.hex}}", bg = "{{colors.surface.default.hex}}" } +sep_inner = { open = "", close = "" } + +# : ]]] + + +# : Completion [[[ + +[cmp] +border = { fg = "{{colors.primary.default.hex}}", bg = "{{colors.on_primary.default.hex}}" } + +# : ]]] + + +# : Tasks [[[ + +[tasks] +border = { fg = "{{colors.primary.default.hex}}" } +title = {} +hovered = { fg = "{{colors.tertiary_container.default.hex}}", underline = true } + +# : ]]] + + +# : Which [[[ + +[which] +cols = 3 +mask = { bg = "{{colors.surface.default.hex}}" } +cand = { fg = "{{colors.primary.default.hex}}" } +rest = { fg = "{{colors.on_primary.default.hex}}" } +desc = { fg = "{{colors.on_surface.default.hex}}" } +separator = " ▶ " +separator_style = { fg = "{{colors.on_surface.default.hex}}" } + +# : ]]] + +# : Spotter [[[ + +[spot] +border = { fg = "{{colors.primary.default.hex}}" } +title = { fg = "{{colors.primary.default.hex}}" } +tbl_col = { fg = "{{colors.on_surface.default.hex}}" } +tbl_cell = { fg = "{{colors.on_surface.default.hex}}", bg = "{{colors.surface.default.hex}}" } + +# : ]]] + + +# : Help [[[ + +[help] +on = { fg = "{{colors.on_surface.default.hex}}" } +run = { fg = "{{colors.on_surface.default.hex}}" } +hovered = { reversed = true, bold = true } +footer = { fg = "{{colors.on_secondary.default.hex}}", bg = "{{colors.secondary.default.hex}}" } + +# : ]]] + + +# : Notify [[[ + +[notify] +title_info = { fg = "{{colors.tertiary.default.hex}}" } +title_warn = { fg = "{{colors.primary.default.hex}}" } +title_error = { fg = "{{colors.error.default.hex}}" } + +# : ]]] + + +# : File-specific styles [[[ + +[filetype] + +rules = [ + # Images + { mime = "image/*", fg = "#94e2d5" }, + + # Media + { mime = "{audio,video}/*", fg = "#f9e2af" }, + + # Archives + { mime = "application/{zip,rar,7z*,tar,gzip,xz,zstd,bzip*,lzma,compress,archive,cpio,arj,xar,ms-cab*}", fg = "#f5c2e7" }, + + # Documents + { mime = "application/{pdf,doc,rtf}", fg = "#a6e3a1" }, + + # Special files + { mime = "*", is = "orphan", fg = "{{colors.on_error_container.default.hex}}", bg = "{{colors.error_container.default.hex}}" }, + { mime = "application/*exec*", fg = "{{colors.error.default.hex}}" }, + + # Fallback + { url = "*", fg = "{{colors.on_surface.default.hex}}" }, + { url = "*/", fg = "{{colors.primary.default.hex}}" }, +] + +# : ]]] + +# : Icons {{{ + +[icon] +globs = [] +dirs = [ + { name = ".config", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".git", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".github", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".npm", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "Desktop", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "Development", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "Documents", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "Downloads", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "Library", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "Movies", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "Music", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "Pictures", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "Public", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "Videos", text = "", fg = "{{colors.primary.default.hex}}" }, +] +files = [ + { name = ".babelrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".bash_profile", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".bashrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".clang-format", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".clang-tidy", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".codespellrc", text = "󰓆", fg = "{{colors.primary.default.hex}}" }, + { name = ".condarc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".dockerignore", text = "󰡨", fg = "{{colors.primary.default.hex}}" }, + { name = ".ds_store", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".editorconfig", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".env", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".eslintignore", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".eslintrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".git-blame-ignore-revs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".gitattributes", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".gitconfig", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".gitignore", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".gitlab-ci.yml", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".gitmodules", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".gtkrc-2.0", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".gvimrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".justfile", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".luacheckrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".luaurc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".mailmap", text = "󰊢", fg = "{{colors.primary.default.hex}}" }, + { name = ".nanorc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".npmignore", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".npmrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".nuxtrc", text = "󱄆", fg = "{{colors.primary.default.hex}}" }, + { name = ".nvmrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".pnpmfile.cjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".pre-commit-config.yaml", text = "󰛢", fg = "{{colors.primary.default.hex}}" }, + { name = ".prettierignore", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".prettierrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".prettierrc.cjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".prettierrc.js", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".prettierrc.json", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".prettierrc.json5", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".prettierrc.mjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".prettierrc.toml", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".prettierrc.yaml", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".prettierrc.yml", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".pylintrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".settings.json", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".SRCINFO", text = "󰣇", fg = "{{colors.primary.default.hex}}" }, + { name = ".vimrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".Xauthority", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".xinitrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".Xresources", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".xsession", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".zprofile", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".zshenv", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = ".zshrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "_gvimrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "_vimrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "AUTHORS", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "AUTHORS.txt", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "brewfile", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "bspwmrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "build", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "build.gradle", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "build.zig.zon", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "bun.lock", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "bun.lockb", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cantorrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "checkhealth", text = "󰓙", fg = "{{colors.primary.default.hex}}" }, + { name = "cmakelists.txt", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "code_of_conduct", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "code_of_conduct.md", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "commit_editmsg", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "commitlint.config.js", text = "󰜘", fg = "{{colors.primary.default.hex}}" }, + { name = "commitlint.config.ts", text = "󰜘", fg = "{{colors.primary.default.hex}}" }, + { name = "compose.yaml", text = "󰡨", fg = "{{colors.primary.default.hex}}" }, + { name = "compose.yml", text = "󰡨", fg = "{{colors.primary.default.hex}}" }, + { name = "config", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "containerfile", text = "󰡨", fg = "{{colors.primary.default.hex}}" }, + { name = "copying", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "copying.lesser", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "Directory.Build.props", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "Directory.Build.targets", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "Directory.Packages.props", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "docker-compose.yaml", text = "󰡨", fg = "{{colors.primary.default.hex}}" }, + { name = "docker-compose.yml", text = "󰡨", fg = "{{colors.primary.default.hex}}" }, + { name = "dockerfile", text = "󰡨", fg = "{{colors.primary.default.hex}}" }, + { name = "eslint.config.cjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "eslint.config.js", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "eslint.config.mjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "eslint.config.ts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ext_typoscript_setup.txt", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "favicon.ico", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fp-info-cache", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fp-lib-table", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "FreeCAD.conf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "Gemfile", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gnumakefile", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "go.mod", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "go.sum", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "go.work", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gradle-wrapper.properties", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gradle.properties", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gradlew", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "groovy", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gruntfile.babel.js", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gruntfile.coffee", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gruntfile.js", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gruntfile.ts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gtkrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gulpfile.babel.js", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gulpfile.coffee", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gulpfile.js", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gulpfile.ts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "hypridle.conf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "hyprland.conf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "hyprlandd.conf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "hyprlock.conf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "hyprpaper.conf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "hyprsunset.conf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "i18n.config.js", text = "󰗊", fg = "{{colors.primary.default.hex}}" }, + { name = "i18n.config.ts", text = "󰗊", fg = "{{colors.primary.default.hex}}" }, + { name = "i3blocks.conf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "i3status.conf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "index.theme", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ionic.config.json", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "Jenkinsfile", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "justfile", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kalgebrarc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kdeglobals", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kdenlive-layoutsrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kdenliverc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kritadisplayrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kritarc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "license", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "license.md", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "lxde-rc.xml", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "lxqt.conf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "makefile", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "mix.lock", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "mpv.conf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "next.config.cjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "next.config.js", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "next.config.ts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "node_modules", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "nuxt.config.cjs", text = "󱄆", fg = "{{colors.primary.default.hex}}" }, + { name = "nuxt.config.js", text = "󱄆", fg = "{{colors.primary.default.hex}}" }, + { name = "nuxt.config.mjs", text = "󱄆", fg = "{{colors.primary.default.hex}}" }, + { name = "nuxt.config.ts", text = "󱄆", fg = "{{colors.primary.default.hex}}" }, + { name = "package-lock.json", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "package.json", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "PKGBUILD", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "platformio.ini", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "playwright.config.cjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "playwright.config.cts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "playwright.config.js", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "playwright.config.mjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "playwright.config.mts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "playwright.config.ts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pnpm-lock.yaml", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pnpm-workspace.yaml", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pom.xml", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "prettier.config.cjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "prettier.config.js", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "prettier.config.mjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "prettier.config.ts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "prisma.config.mts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "prisma.config.ts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "procfile", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "PrusaSlicer.ini", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "PrusaSlicerGcodeViewer.ini", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "py.typed", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "QtProject.conf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "rakefile", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "readme", text = "󰂺", fg = "{{colors.primary.default.hex}}" }, + { name = "readme.md", text = "󰂺", fg = "{{colors.primary.default.hex}}" }, + { name = "rmd", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "robots.txt", text = "󰚩", fg = "{{colors.primary.default.hex}}" }, + { name = "security", text = "󰒃", fg = "{{colors.primary.default.hex}}" }, + { name = "security.md", text = "󰒃", fg = "{{colors.primary.default.hex}}" }, + { name = "settings.gradle", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "svelte.config.js", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "sxhkdrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "sym-lib-table", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "tailwind.config.js", text = "󱏿", fg = "{{colors.primary.default.hex}}" }, + { name = "tailwind.config.mjs", text = "󱏿", fg = "{{colors.primary.default.hex}}" }, + { name = "tailwind.config.ts", text = "󱏿", fg = "{{colors.primary.default.hex}}" }, + { name = "tmux.conf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "tmux.conf.local", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "tsconfig.json", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "unlicense", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vagrantfile", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vercel.json", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vite.config.cjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vite.config.cts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vite.config.js", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vite.config.mjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vite.config.mts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vite.config.ts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vitest.config.cjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vitest.config.cts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vitest.config.js", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vitest.config.mjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vitest.config.mts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vitest.config.ts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vlcrc", text = "󰕼", fg = "{{colors.primary.default.hex}}" }, + { name = "webpack", text = "󰜫", fg = "{{colors.primary.default.hex}}" }, + { name = "weston.ini", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "workspace", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "wrangler.jsonc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "wrangler.toml", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "xdph.conf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "xmobarrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "xmobarrc.hs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "xmonad.hs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "xorg.conf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "xsettingsd.conf", text = "", fg = "{{colors.primary.default.hex}}" }, +] +exts = [ + { name = "3gp", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "3mf", text = "󰆧", fg = "{{colors.primary.default.hex}}" }, + { name = "7z", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "a", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "aac", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ada", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "adb", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ads", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ai", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "aif", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "aiff", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "android", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ape", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "apk", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "apl", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "app", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "applescript", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "asc", text = "󰦝", fg = "{{colors.primary.default.hex}}" }, + { name = "asm", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ass", text = "󰨖", fg = "{{colors.primary.default.hex}}" }, + { name = "astro", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "avif", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "awk", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "azcli", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "bak", text = "󰁯", fg = "{{colors.primary.default.hex}}" }, + { name = "bash", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "bat", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "bazel", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "bib", text = "󱉟", fg = "{{colors.primary.default.hex}}" }, + { name = "bicep", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "bicepparam", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "bin", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "blade.php", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "blend", text = "󰂫", fg = "{{colors.primary.default.hex}}" }, + { name = "blp", text = "󰺾", fg = "{{colors.primary.default.hex}}" }, + { name = "bmp", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "bqn", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "brep", text = "󰻫", fg = "{{colors.primary.default.hex}}" }, + { name = "bz", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "bz2", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "bz3", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "bzl", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "c", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "c++", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cache", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cast", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cbl", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ccm", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cfc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cfg", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cfm", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "clj", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cljc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cljd", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cljs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cmake", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cob", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cobol", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "coffee", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "conda", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "conf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "config.ru", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cow", text = "󰆚", fg = "{{colors.primary.default.hex}}" }, + { name = "cp", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cpp", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cppm", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cpy", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cr", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "crdownload", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cs", text = "󰌛", fg = "{{colors.primary.default.hex}}" }, + { name = "csh", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cshtml", text = "󱦗", fg = "{{colors.primary.default.hex}}" }, + { name = "cson", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "csproj", text = "󰪮", fg = "{{colors.primary.default.hex}}" }, + { name = "css", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "csv", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cu", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cue", text = "󰲹", fg = "{{colors.primary.default.hex}}" }, + { name = "cuh", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cxx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "cxxm", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "d", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "d.ts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "dart", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "db", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "dconf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "desktop", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "diff", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "dll", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "doc", text = "󰈬", fg = "{{colors.primary.default.hex}}" }, + { name = "Dockerfile", text = "󰡨", fg = "{{colors.primary.default.hex}}" }, + { name = "dockerignore", text = "󰡨", fg = "{{colors.primary.default.hex}}" }, + { name = "docx", text = "󰈬", fg = "{{colors.primary.default.hex}}" }, + { name = "dot", text = "󱁉", fg = "{{colors.primary.default.hex}}" }, + { name = "download", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "drl", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "dropbox", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "dump", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "dwg", text = "󰻫", fg = "{{colors.primary.default.hex}}" }, + { name = "dxf", text = "󰻫", fg = "{{colors.primary.default.hex}}" }, + { name = "ebook", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ebuild", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "edn", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "eex", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ejs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "el", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "elc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "elf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "elm", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "eln", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "env", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "eot", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "epp", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "epub", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "erb", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "erl", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ex", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "exe", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "exs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "f#", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "f3d", text = "󰻫", fg = "{{colors.primary.default.hex}}" }, + { name = "f90", text = "󱈚", fg = "{{colors.primary.default.hex}}" }, + { name = "fbx", text = "󰆧", fg = "{{colors.primary.default.hex}}" }, + { name = "fcbak", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fcmacro", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fcmat", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fcparam", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fcscript", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fcstd", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fcstd1", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fctb", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fctl", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fdmdownload", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "feature", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fish", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "flac", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "flc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "flf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fnl", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fodg", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fodp", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fods", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fodt", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "frag", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fsi", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fsscript", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "fsx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gcode", text = "󰐫", fg = "{{colors.primary.default.hex}}" }, + { name = "gd", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gemspec", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "geom", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gif", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "git", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "glb", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gleam", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "glsl", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gnumakefile", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "go", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "godot", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gpr", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gql", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gradle", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "graphql", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gresource", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "gv", text = "󱁉", fg = "{{colors.primary.default.hex}}" }, + { name = "gz", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "h", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "haml", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "hbs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "heex", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "hex", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "hh", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "hpp", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "hrl", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "hs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "htm", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "html", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "http", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "huff", text = "󰡘", fg = "{{colors.primary.default.hex}}" }, + { name = "hurl", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "hx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "hxx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ical", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "icalendar", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ico", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ics", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ifb", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ifc", text = "󰻫", fg = "{{colors.primary.default.hex}}" }, + { name = "ige", text = "󰻫", fg = "{{colors.primary.default.hex}}" }, + { name = "iges", text = "󰻫", fg = "{{colors.primary.default.hex}}" }, + { name = "igs", text = "󰻫", fg = "{{colors.primary.default.hex}}" }, + { name = "image", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "img", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "import", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "info", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ini", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ino", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ipynb", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "iso", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ixx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "jar", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "java", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "jl", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "jpeg", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "jpg", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "js", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "json", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "json5", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "jsonc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "jsx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "jwmrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "jxl", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kbx", text = "󰯄", fg = "{{colors.primary.default.hex}}" }, + { name = "kdb", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kdbx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kdenlive", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kdenlivetitle", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kicad_dru", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kicad_mod", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kicad_pcb", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kicad_prl", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kicad_pro", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kicad_sch", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kicad_sym", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kicad_wks", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ko", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kpp", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kra", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "krz", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ksh", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kt", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "kts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "lck", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "leex", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "less", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "lff", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "lhs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "lib", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "license", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "liquid", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "lock", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "log", text = "󰌱", fg = "{{colors.primary.default.hex}}" }, + { name = "lrc", text = "󰨖", fg = "{{colors.primary.default.hex}}" }, + { name = "lua", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "luac", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "luau", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "m", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "m3u", text = "󰲹", fg = "{{colors.primary.default.hex}}" }, + { name = "m3u8", text = "󰲹", fg = "{{colors.primary.default.hex}}" }, + { name = "m4a", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "m4v", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "magnet", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "makefile", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "markdown", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "material", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "md", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "md5", text = "󰕥", fg = "{{colors.primary.default.hex}}" }, + { name = "mdx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "mint", text = "󰌪", fg = "{{colors.primary.default.hex}}" }, + { name = "mjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "mk", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "mkv", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ml", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "mli", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "mm", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "mo", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "mobi", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "mojo", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "mov", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "mp3", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "mp4", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "mpp", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "msf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "mts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "mustache", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "nfo", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "nim", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "nix", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "norg", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "nswag", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "nu", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "o", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "obj", text = "󰆧", fg = "{{colors.primary.default.hex}}" }, + { name = "odf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "odg", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "odin", text = "󰟢", fg = "{{colors.primary.default.hex}}" }, + { name = "odp", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ods", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "odt", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "oga", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ogg", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ogv", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ogx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "opus", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "org", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "otf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "out", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "part", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "patch", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pck", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pcm", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pdf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "php", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pl", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pls", text = "󰲹", fg = "{{colors.primary.default.hex}}" }, + { name = "ply", text = "󰆧", fg = "{{colors.primary.default.hex}}" }, + { name = "pm", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "png", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "po", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pot", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pp", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ppt", text = "󰈧", fg = "{{colors.primary.default.hex}}" }, + { name = "pptx", text = "󰈧", fg = "{{colors.primary.default.hex}}" }, + { name = "prisma", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pro", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ps1", text = "󰨊", fg = "{{colors.primary.default.hex}}" }, + { name = "psb", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "psd", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "psd1", text = "󰨊", fg = "{{colors.primary.default.hex}}" }, + { name = "psm1", text = "󰨊", fg = "{{colors.primary.default.hex}}" }, + { name = "pub", text = "󰷖", fg = "{{colors.primary.default.hex}}" }, + { name = "pxd", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pxi", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "py", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pyc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pyd", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pyi", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pyo", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pyw", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "pyx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "qm", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "qml", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "qrc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "qss", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "query", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "R", text = "󰟔", fg = "{{colors.primary.default.hex}}" }, + { name = "r", text = "󰟔", fg = "{{colors.primary.default.hex}}" }, + { name = "rake", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "rar", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "rasi", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "razor", text = "󱦘", fg = "{{colors.primary.default.hex}}" }, + { name = "rb", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "res", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "resi", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "rlib", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "rmd", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "rproj", text = "󰗆", fg = "{{colors.primary.default.hex}}" }, + { name = "rs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "rss", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "s", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "sass", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "sbt", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "sc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "scad", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "scala", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "scm", text = "󰘧", fg = "{{colors.primary.default.hex}}" }, + { name = "scss", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "sh", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "sha1", text = "󰕥", fg = "{{colors.primary.default.hex}}" }, + { name = "sha224", text = "󰕥", fg = "{{colors.primary.default.hex}}" }, + { name = "sha256", text = "󰕥", fg = "{{colors.primary.default.hex}}" }, + { name = "sha384", text = "󰕥", fg = "{{colors.primary.default.hex}}" }, + { name = "sha512", text = "󰕥", fg = "{{colors.primary.default.hex}}" }, + { name = "sig", text = "󰘧", fg = "{{colors.primary.default.hex}}" }, + { name = "signature", text = "󰘧", fg = "{{colors.primary.default.hex}}" }, + { name = "skp", text = "󰻫", fg = "{{colors.primary.default.hex}}" }, + { name = "sldasm", text = "󰻫", fg = "{{colors.primary.default.hex}}" }, + { name = "sldprt", text = "󰻫", fg = "{{colors.primary.default.hex}}" }, + { name = "slim", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "sln", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "slnx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "slvs", text = "󰻫", fg = "{{colors.primary.default.hex}}" }, + { name = "sml", text = "󰘧", fg = "{{colors.primary.default.hex}}" }, + { name = "so", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "sol", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "spec.js", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "spec.jsx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "spec.ts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "spec.tsx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "spx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "sql", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "sqlite", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "sqlite3", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "srt", text = "󰨖", fg = "{{colors.primary.default.hex}}" }, + { name = "ssa", text = "󰨖", fg = "{{colors.primary.default.hex}}" }, + { name = "ste", text = "󰻫", fg = "{{colors.primary.default.hex}}" }, + { name = "step", text = "󰻫", fg = "{{colors.primary.default.hex}}" }, + { name = "stl", text = "󰆧", fg = "{{colors.primary.default.hex}}" }, + { name = "stories.js", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "stories.jsx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "stories.mjs", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "stories.svelte", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "stories.ts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "stories.tsx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "stories.vue", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "stp", text = "󰻫", fg = "{{colors.primary.default.hex}}" }, + { name = "strings", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "styl", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "sub", text = "󰨖", fg = "{{colors.primary.default.hex}}" }, + { name = "sublime", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "suo", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "sv", text = "󰍛", fg = "{{colors.primary.default.hex}}" }, + { name = "svelte", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "svg", text = "󰜡", fg = "{{colors.primary.default.hex}}" }, + { name = "svgz", text = "󰜡", fg = "{{colors.primary.default.hex}}" }, + { name = "svh", text = "󰍛", fg = "{{colors.primary.default.hex}}" }, + { name = "swift", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "t", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "tbc", text = "󰛓", fg = "{{colors.primary.default.hex}}" }, + { name = "tcl", text = "󰛓", fg = "{{colors.primary.default.hex}}" }, + { name = "templ", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "terminal", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "test.js", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "test.jsx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "test.ts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "test.tsx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "tex", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "tf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "tfvars", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "tgz", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "tmpl", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "tmux", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "toml", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "torrent", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "tres", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ts", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "tscn", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "tsconfig", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "tsx", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ttf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "twig", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "txt", text = "󰈙", fg = "{{colors.primary.default.hex}}" }, + { name = "txz", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "typ", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "typoscript", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "ui", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "v", text = "󰍛", fg = "{{colors.primary.default.hex}}" }, + { name = "vala", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vert", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vh", text = "󰍛", fg = "{{colors.primary.default.hex}}" }, + { name = "vhd", text = "󰍛", fg = "{{colors.primary.default.hex}}" }, + { name = "vhdl", text = "󰍛", fg = "{{colors.primary.default.hex}}" }, + { name = "vi", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vim", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vsh", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vsix", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "vue", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "wasm", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "wav", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "webm", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "webmanifest", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "webp", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "webpack", text = "󰜫", fg = "{{colors.primary.default.hex}}" }, + { name = "wma", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "wmv", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "woff", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "woff2", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "wrl", text = "󰆧", fg = "{{colors.primary.default.hex}}" }, + { name = "wrz", text = "󰆧", fg = "{{colors.primary.default.hex}}" }, + { name = "wv", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "wvc", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "x", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "xaml", text = "󰙳", fg = "{{colors.primary.default.hex}}" }, + { name = "xcf", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "xcplayground", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "xcstrings", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "xls", text = "󰈛", fg = "{{colors.primary.default.hex}}" }, + { name = "xlsx", text = "󰈛", fg = "{{colors.primary.default.hex}}" }, + { name = "xm", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "xml", text = "󰗀", fg = "{{colors.primary.default.hex}}" }, + { name = "xpi", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "xslt", text = "󰗀", fg = "{{colors.primary.default.hex}}" }, + { name = "xul", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "xz", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "yaml", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "yml", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "zig", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "zip", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "zsh", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "zst", text = "", fg = "{{colors.primary.default.hex}}" }, + { name = "🔥", text = "", fg = "{{colors.primary.default.hex}}" }, +] +conds = [ + # Special files + { if = "orphan", text = "", fg = "{{colors.primary.default.hex}}" }, + { if = "link", text = "", fg = "{{colors.primary.default.hex}}" }, + { if = "block", text = "", fg = "{{colors.primary.default.hex}}" }, + { if = "char", text = "", fg = "{{colors.primary.default.hex}}" }, + { if = "fifo", text = "", fg = "{{colors.primary.default.hex}}" }, + { if = "sock", text = "", fg = "{{colors.primary.default.hex}}" }, + { if = "sticky", text = "", fg = "{{colors.primary.default.hex}}" }, + { if = "dummy", text = "", fg = "{{colors.primary.default.hex}}" }, + + # Fallback + { if = "dir", text = "", fg = "{{colors.primary.default.hex}}" }, + { if = "exec", text = "", fg = "{{colors.primary.default.hex}}" }, + { if = "!dir", text = "", fg = "{{colors.primary.default.hex}}" }, +] + +# : }}} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/zathurarc b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/zathurarc new file mode 100644 index 0000000..248a52c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/zathurarc @@ -0,0 +1,31 @@ +set default-bg "{{colors.surface.default.rgba | set_alpha: 0.8}}" +set default-fg "{{colors.on_surface.default.hex}}" +set recolor-lightcolor "rgba(0,0,0,0)" +set recolor-darkcolor "{{colors.on_surface.default.hex}}" + +set statusbar-bg "{{colors.on_primary.default.hex}}" +set statusbar-fg "{{colors.primary.default.hex}}" +set inputbar-bg "{{colors.on_primary.default.hex}}" +set inputbar-fg "{{colors.primary.default.hex}}" + +set notification-bg "{{colors.on_primary.default.hex}}" +set notification-fg "{{colors.primary.default.hex}}" +set notification-error-bg "{{colors.on_error.default.hex}}" +set notification-error-fg "{{colors.error.default.hex}}" +set notification-warning-bg "{{colors.primary_fixed.default.hex}}" +set notification-warning-fg "{{colors.error_container.default.hex}}" + +set index-bg "rgba(0,0,0,0)" +set index-fg "{{colors.on_surface.default.hex}}" +set index-active-bg "{{colors.primary.default.hex}}" +set index-active-fg "{{colors.on_primary.default.hex}}" +set highlight-color "{{colors.primary_fixed.default.rgba | set_alpha: 0.5}}" +set highlight-active-color "{{colors.primary_fixed_dim.default.rgba | set_alpha: 0.5}}" +set highlight-fg "{{colors.on_primary.default.hex}}" + +set completion-bg "{{colors.on_primary.default.hex}}" +set completion-fg "{{colors.primary.default.hex}}" +set completion-group-bg "{{colors.on_primary.default.hex}}" +set completion-group-fg "{{colors.primary.default.hex}}" +set completion-highlight-fg "{{colors.on_primary.default.hex}}" +set completion-highlight-bg "{{colors.primary.default.hex}}" diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/zed.json b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/zed.json new file mode 100644 index 0000000..b44d9dd --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/zed.json @@ -0,0 +1,1175 @@ +{ + "$schema": "https://zed.dev/schema/themes/v0.2.0.json", + "name": "Noctalia", + "author": "Noctalia", + "themes": [ + { + "name": "Noctalia Dark", + "appearance": "dark", + "style": { + "accents": [ + "{{colors.primary.dark.hex}}", + "{{colors.secondary.dark.hex}}", + "{{colors.tertiary.dark.hex}}" + ], + "background.appearance": "opaque", + "border": "{{colors.outline_variant.dark.hex}}", + "border.variant": "{{colors.outline.dark.hex}}", + "border.focused": "{{colors.primary.dark.hex}}", + "border.selected": "{{colors.primary.dark.hex}}", + "border.transparent": "{{colors.outline_variant.dark.hex}}40", + "border.disabled": "{{colors.outline_variant.dark.hex}}60", + "elevated_surface.background": "{{colors.surface_container_high.dark.hex}}", + "surface.background": "{{colors.surface.dark.hex}}", + "background": "{{colors.background.dark.hex}}", + "element.background": "{{colors.surface_container.dark.hex}}", + "element.hover": "{{colors.surface_container_high.dark.hex}}", + "element.active": "{{colors.surface_container_highest.dark.hex}}", + "element.selected": "{{colors.secondary_container.dark.hex}}", + "element.disabled": "{{colors.surface_variant.dark.hex}}", + "drop_target.background": "{{colors.primary_container.dark.hex}}80", + "ghost_element.background": null, + "ghost_element.hover": "{{colors.surface_container.dark.hex}}80", + "ghost_element.active": "{{colors.surface_container_high.dark.hex}}", + "ghost_element.selected": "{{colors.secondary_container.dark.hex}}80", + "ghost_element.disabled": "{{colors.surface_variant.dark.hex}}60", + "text": "{{colors.on_surface.dark.hex}}", + "text.muted": "{{colors.on_surface_variant.dark.hex}}", + "text.placeholder": "{{colors.on_surface_variant.dark.hex}}99", + "text.disabled": "{{colors.on_surface.dark.hex}}60", + "text.accent": "{{colors.primary.dark.hex}}", + "icon": "{{colors.on_surface.dark.hex}}", + "icon.muted": "{{colors.on_surface_variant.dark.hex}}", + "icon.disabled": "{{colors.on_surface.dark.hex}}60", + "icon.placeholder": "{{colors.on_surface_variant.dark.hex}}80", + "icon.accent": "{{colors.primary.dark.hex}}", + "status_bar.background": "{{colors.surface.dark.hex}}", + "title_bar.background": "{{colors.surface.dark.hex}}", + "title_bar.inactive_background": "{{colors.surface_dim.dark.hex}}", + "toolbar.background": "{{colors.surface_container_low.dark.hex}}", + "tab_bar.background": "{{colors.surface_container.dark.hex}}", + "tab.inactive_background": "{{colors.surface_container_low.dark.hex}}", + "tab.active_background": "{{colors.surface_container_high.dark.hex}}", + "search.match_background": "{{colors.tertiary_container.dark.hex}}80", + "panel.background": "{{colors.surface_container_low.dark.hex}}", + "panel.focused_border": "{{colors.primary.dark.hex}}", + "pane.focused_border": "{{colors.primary.dark.hex}}", + "scrollbar.thumb.background": "{{colors.on_surface_variant.dark.hex}}80", + "scrollbar.thumb.hover_background": "{{colors.on_surface_variant.dark.hex}}c0", + "scrollbar.thumb.border": "{{colors.outline_variant.dark.hex}}40", + "scrollbar.track.background": "{{colors.surface_container.dark.hex}}", + "scrollbar.track.border": "{{colors.outline_variant.dark.hex}}20", + "editor.foreground": "{{colors.on_surface.dark.hex}}", + "editor.background": "{{colors.surface_container_low.dark.hex}}", + "editor.gutter.background": "{{colors.surface_container_low.dark.hex}}", + "editor.subheader.background": "{{colors.surface_container.dark.hex}}", + "editor.indent_guide": "{{colors.outline_variant.dark.hex}}60", + "editor.indent_guide_active": "{{colors.outline.dark.hex}}", + "editor.active_line.background": "{{colors.surface_container_high.dark.hex}}80", + "editor.highlighted_line.background": "{{colors.surface_container_high.dark.hex}}60", + "editor.line_number": "{{colors.on_surface_variant.dark.hex}}", + "editor.active_line_number": "{{colors.primary.dark.hex}}", + "editor.invisible": "{{colors.outline_variant.dark.hex}}80", + "editor.wrap_guide": "{{colors.outline_variant.dark.hex}}40", + "editor.active_wrap_guide": "{{colors.outline.dark.hex}}80", + "editor.document_highlight.read_background": "{{colors.primary_container.dark.hex}}60", + "editor.document_highlight.write_background": "{{colors.secondary_container.dark.hex}}80", + "terminal.background": "{{colors.surface_container_low.dark.hex}}", + "terminal.foreground": "{{colors.on_surface.dark.hex}}", + "terminal.bright_foreground": "{{colors.on_surface.dark.hex}}", + "terminal.dim_foreground": "{{colors.on_surface_variant.dark.hex}}", + "terminal.ansi.black": "{{colors.surface_dim.dark.hex}}", + "terminal.ansi.bright_black": "{{colors.surface_container_high.dark.hex}}", + "terminal.ansi.dim_black": "{{colors.surface.dark.hex}}", + "terminal.ansi.red": "{{colors.error.dark.hex}}", + "terminal.ansi.bright_red": "{{colors.error.dark.hex}}", + "terminal.ansi.dim_red": "{{colors.on_error_container.dark.hex}}", + "terminal.ansi.green": "{{colors.tertiary.dark.hex}}", + "terminal.ansi.bright_green": "{{colors.tertiary.dark.hex}}", + "terminal.ansi.dim_green": "{{colors.tertiary_fixed_dim.dark.hex}}", + "terminal.ansi.yellow": "{{colors.tertiary_fixed_dim.dark.hex}}", + "terminal.ansi.bright_yellow": "{{colors.tertiary_fixed.dark.hex}}", + "terminal.ansi.dim_yellow": "{{colors.on_tertiary_fixed.dark.hex}}", + "terminal.ansi.blue": "{{colors.primary.dark.hex}}", + "terminal.ansi.bright_blue": "{{colors.primary.dark.hex}}", + "terminal.ansi.dim_blue": "{{colors.on_primary_container.dark.hex}}", + "terminal.ansi.magenta": "{{colors.secondary.dark.hex}}", + "terminal.ansi.bright_magenta": "{{colors.secondary.dark.hex}}", + "terminal.ansi.dim_magenta": "{{colors.on_secondary_container.dark.hex}}", + "terminal.ansi.cyan": "{{colors.primary_fixed_dim.dark.hex}}", + "terminal.ansi.bright_cyan": "{{colors.primary_fixed.dark.hex}}", + "terminal.ansi.dim_cyan": "{{colors.on_primary_fixed_variant.dark.hex}}", + "terminal.ansi.white": "{{colors.on_surface.dark.hex}}", + "terminal.ansi.bright_white": "{{colors.on_surface.dark.hex}}", + "terminal.ansi.dim_white": "{{colors.on_surface_variant.dark.hex}}", + "link_text.hover": "{{colors.primary.dark.hex}}", + "conflict": "{{colors.error.dark.hex}}", + "conflict.background": "{{colors.error_container.dark.hex}}80", + "conflict.border": "{{colors.on_error_container.dark.hex}}", + "created": "{{colors.tertiary.dark.hex}}", + "created.background": "{{colors.tertiary_container.dark.hex}}80", + "created.border": "{{colors.on_tertiary_container.dark.hex}}", + "deleted": "{{colors.error.dark.hex}}", + "deleted.background": "{{colors.error_container.dark.hex}}80", + "deleted.border": "{{colors.on_error_container.dark.hex}}", + "error": "{{colors.error.dark.hex}}", + "error.background": "{{colors.error_container.dark.hex}}", + "error.border": "{{colors.on_error_container.dark.hex}}", + "hidden": "{{colors.outline_variant.dark.hex}}", + "hidden.border": "{{colors.outline_variant.dark.hex}}60", + "hint": "{{colors.primary.dark.hex}}", + "hint.background": "{{colors.primary_container.dark.hex}}80", + "hint.border": "{{colors.on_primary_container.dark.hex}}", + "ignored": "{{colors.on_surface_variant.dark.hex}}60", + "ignored.background": "{{colors.surface_variant.dark.hex}}40", + "ignored.border": "{{colors.outline_variant.dark.hex}}40", + "info": "{{colors.primary.dark.hex}}", + "info.background": "{{colors.primary_container.dark.hex}}80", + "info.border": "{{colors.on_primary_container.dark.hex}}", + "modified": "{{colors.secondary.dark.hex}}", + "modified.background": "{{colors.secondary_container.dark.hex}}80", + "modified.border": "{{colors.on_secondary_container.dark.hex}}", + "predictive": "{{colors.on_surface_variant.dark.hex}}80", + "predictive.border": "{{colors.outline.dark.hex}}", + "predictive.background": "{{colors.surface_container_highest.dark.hex}}80", + "renamed": "{{colors.secondary.dark.hex}}", + "renamed.border": "{{colors.on_secondary_container.dark.hex}}", + "renamed.background": "{{colors.secondary_container.dark.hex}}80", + "success": "{{colors.tertiary.dark.hex}}", + "success.background": "{{colors.tertiary_container.dark.hex}}80", + "success.border": "{{colors.on_tertiary_container.dark.hex}}", + "unreachable": "{{colors.on_surface_variant.dark.hex}}60", + "unreachable.background": "{{colors.surface_variant.dark.hex}}40", + "unreachable.border": "{{colors.outline_variant.dark.hex}}60", + "warning": "{{colors.tertiary_fixed_dim.dark.hex}}", + "warning.background": "{{colors.tertiary_container.dark.hex}}80", + "warning.border": "{{colors.on_tertiary_container.dark.hex}}", + "players": [ + { + "cursor": "{{colors.primary.dark.hex}}", + "background": "{{colors.primary_container.dark.hex}}80", + "selection": "{{colors.primary_container.dark.hex}}60" + }, + { + "cursor": "{{colors.secondary.dark.hex}}", + "background": "{{colors.secondary_container.dark.hex}}80", + "selection": "{{colors.secondary_container.dark.hex}}60" + } + ], + "syntax": { + "boolean": { + "color": "{{colors.tertiary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "comment": { + "color": "{{colors.on_surface_variant.dark.hex}}", + "font_style": "italic", + "font_weight": null + }, + "comment.doc": { + "color": "{{colors.on_surface_variant.dark.hex}}", + "font_style": "italic", + "font_weight": null + }, + "constant": { + "color": "{{colors.tertiary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "constructor": { + "color": "{{colors.secondary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "emphasis": { + "color": "{{colors.primary.dark.hex}}", + "font_style": "italic", + "font_weight": null + }, + "emphasis.strong": { + "color": "{{colors.primary.dark.hex}}", + "font_style": null, + "font_weight": 700 + }, + "function": { + "color": "{{colors.primary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "keyword": { + "color": "{{colors.secondary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "number": { + "color": "{{colors.tertiary_fixed.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "operator": { + "color": "{{colors.on_surface_variant.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "property": { + "color": "{{colors.on_surface.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation": { + "color": "{{colors.on_surface_variant.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation.bracket": { + "color": "{{colors.secondary_fixed.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation.delimiter": { + "color": "{{colors.on_surface_variant.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation.list_marker": { + "color": "{{colors.on_surface_variant.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation.special": { + "color": "{{colors.secondary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "string": { + "color": "{{colors.tertiary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "string.escape": { + "color": "{{colors.tertiary_fixed_dim.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "string.regex": { + "color": "{{colors.tertiary_fixed.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "string.special": { + "color": "{{colors.secondary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "string.special.symbol": { + "color": "{{colors.tertiary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "tag": { + "color": "{{colors.secondary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "text.literal": { + "color": "{{colors.tertiary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "type": { + "color": "{{colors.primary_fixed.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "variable": { + "color": "{{colors.on_surface.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "variable.special": { + "color": "{{colors.primary.dark.hex}}", + "font_style": null, + "font_weight": null + } + } + } + }, + { + "name": "Noctalia Light", + "appearance": "light", + "style": { + "accents": [ + "{{colors.primary.light.hex}}", + "{{colors.secondary.light.hex}}", + "{{colors.tertiary.light.hex}}" + ], + "background.appearance": "opaque", + "border": "{{colors.outline_variant.light.hex}}", + "border.variant": "{{colors.outline.light.hex}}", + "border.focused": "{{colors.primary.light.hex}}", + "border.selected": "{{colors.primary.light.hex}}", + "border.transparent": "{{colors.outline_variant.light.hex}}40", + "border.disabled": "{{colors.outline_variant.light.hex}}60", + "elevated_surface.background": "{{colors.surface_container_high.light.hex}}", + "surface.background": "{{colors.surface.light.hex}}", + "background": "{{colors.background.light.hex}}", + "element.background": "{{colors.surface_container.light.hex}}", + "element.hover": "{{colors.surface_container_high.light.hex}}", + "element.active": "{{colors.surface_container_highest.light.hex}}", + "element.selected": "{{colors.secondary_container.light.hex}}", + "element.disabled": "{{colors.surface_variant.light.hex}}", + "drop_target.background": "{{colors.primary_container.light.hex}}80", + "ghost_element.background": null, + "ghost_element.hover": "{{colors.surface_container.light.hex}}80", + "ghost_element.active": "{{colors.surface_container_high.light.hex}}", + "ghost_element.selected": "{{colors.secondary_container.light.hex}}80", + "ghost_element.disabled": "{{colors.surface_variant.light.hex}}60", + "text": "{{colors.on_surface.light.hex}}", + "text.muted": "{{colors.on_surface_variant.light.hex}}", + "text.placeholder": "{{colors.on_surface_variant.light.hex}}99", + "text.disabled": "{{colors.on_surface.light.hex}}60", + "text.accent": "{{colors.primary.light.hex}}", + "icon": "{{colors.on_surface.light.hex}}", + "icon.muted": "{{colors.on_surface_variant.light.hex}}", + "icon.disabled": "{{colors.on_surface.light.hex}}60", + "icon.placeholder": "{{colors.on_surface_variant.light.hex}}80", + "icon.accent": "{{colors.primary.light.hex}}", + "status_bar.background": "{{colors.surface.light.hex}}", + "title_bar.background": "{{colors.surface.light.hex}}", + "title_bar.inactive_background": "{{colors.surface_dim.light.hex}}", + "toolbar.background": "{{colors.surface_container_low.light.hex}}", + "tab_bar.background": "{{colors.surface_container.light.hex}}", + "tab.inactive_background": "{{colors.surface_container_low.light.hex}}", + "tab.active_background": "{{colors.surface_container_high.light.hex}}", + "search.match_background": "{{colors.tertiary_container.light.hex}}80", + "panel.background": "{{colors.surface_container_low.light.hex}}", + "panel.focused_border": "{{colors.primary.light.hex}}", + "pane.focused_border": "{{colors.primary.light.hex}}", + "scrollbar.thumb.background": "{{colors.on_surface_variant.light.hex}}80", + "scrollbar.thumb.hover_background": "{{colors.on_surface_variant.light.hex}}c0", + "scrollbar.thumb.border": "{{colors.outline_variant.light.hex}}40", + "scrollbar.track.background": "{{colors.surface_container.light.hex}}", + "scrollbar.track.border": "{{colors.outline_variant.light.hex}}20", + "editor.foreground": "{{colors.on_surface.light.hex}}", + "editor.background": "{{colors.surface_container_low.light.hex}}", + "editor.gutter.background": "{{colors.surface_container_low.light.hex}}", + "editor.subheader.background": "{{colors.surface_container.light.hex}}", + "editor.indent_guide": "{{colors.outline_variant.light.hex}}60", + "editor.indent_guide_active": "{{colors.outline.light.hex}}", + "editor.active_line.background": "{{colors.surface_container_high.light.hex}}80", + "editor.highlighted_line.background": "{{colors.surface_container_high.light.hex}}60", + "editor.line_number": "{{colors.on_surface_variant.light.hex}}", + "editor.active_line_number": "{{colors.primary.light.hex}}", + "editor.invisible": "{{colors.outline_variant.light.hex}}80", + "editor.wrap_guide": "{{colors.outline_variant.light.hex}}40", + "editor.active_wrap_guide": "{{colors.outline.light.hex}}80", + "editor.document_highlight.read_background": "{{colors.primary_container.light.hex}}60", + "editor.document_highlight.write_background": "{{colors.secondary_container.light.hex}}80", + "terminal.background": "{{colors.surface_container_low.light.hex}}", + "terminal.foreground": "{{colors.on_surface.light.hex}}", + "terminal.bright_foreground": "{{colors.on_surface.light.hex}}", + "terminal.dim_foreground": "{{colors.on_surface_variant.light.hex}}", + "terminal.ansi.black": "{{colors.surface_dim.light.hex}}", + "terminal.ansi.bright_black": "{{colors.surface_container_high.light.hex}}", + "terminal.ansi.dim_black": "{{colors.surface.light.hex}}", + "terminal.ansi.red": "{{colors.error.light.hex}}", + "terminal.ansi.bright_red": "{{colors.error.light.hex}}", + "terminal.ansi.dim_red": "{{colors.on_error_container.light.hex}}", + "terminal.ansi.green": "{{colors.tertiary.light.hex}}", + "terminal.ansi.bright_green": "{{colors.tertiary.light.hex}}", + "terminal.ansi.dim_green": "{{colors.tertiary_fixed_dim.light.hex}}", + "terminal.ansi.yellow": "{{colors.tertiary_fixed_dim.light.hex}}", + "terminal.ansi.bright_yellow": "{{colors.tertiary_fixed.light.hex}}", + "terminal.ansi.dim_yellow": "{{colors.on_tertiary_fixed.light.hex}}", + "terminal.ansi.blue": "{{colors.primary.light.hex}}", + "terminal.ansi.bright_blue": "{{colors.primary.light.hex}}", + "terminal.ansi.dim_blue": "{{colors.on_primary_container.light.hex}}", + "terminal.ansi.magenta": "{{colors.secondary.light.hex}}", + "terminal.ansi.bright_magenta": "{{colors.secondary.light.hex}}", + "terminal.ansi.dim_magenta": "{{colors.on_secondary_container.light.hex}}", + "terminal.ansi.cyan": "{{colors.primary_fixed_dim.light.hex}}", + "terminal.ansi.bright_cyan": "{{colors.primary_fixed.light.hex}}", + "terminal.ansi.dim_cyan": "{{colors.on_primary_fixed_variant.light.hex}}", + "terminal.ansi.white": "{{colors.on_surface.light.hex}}", + "terminal.ansi.bright_white": "{{colors.on_surface.light.hex}}", + "terminal.ansi.dim_white": "{{colors.on_surface_variant.light.hex}}", + "link_text.hover": "{{colors.primary.light.hex}}", + "conflict": "{{colors.error.light.hex}}", + "conflict.background": "{{colors.error_container.light.hex}}80", + "conflict.border": "{{colors.on_error_container.light.hex}}", + "created": "{{colors.tertiary.light.hex}}", + "created.background": "{{colors.tertiary_container.light.hex}}80", + "created.border": "{{colors.on_tertiary_container.light.hex}}", + "deleted": "{{colors.error.light.hex}}", + "deleted.background": "{{colors.error_container.light.hex}}80", + "deleted.border": "{{colors.on_error_container.light.hex}}", + "error": "{{colors.error.light.hex}}", + "error.background": "{{colors.error_container.light.hex}}", + "error.border": "{{colors.on_error_container.light.hex}}", + "hidden": "{{colors.outline_variant.light.hex}}", + "hidden.border": "{{colors.outline_variant.light.hex}}60", + "hint": "{{colors.primary.light.hex}}", + "hint.background": "{{colors.primary_container.light.hex}}80", + "hint.border": "{{colors.on_primary_container.light.hex}}", + "ignored": "{{colors.on_surface_variant.light.hex}}60", + "ignored.background": "{{colors.surface_variant.light.hex}}40", + "ignored.border": "{{colors.outline_variant.light.hex}}40", + "info": "{{colors.primary.light.hex}}", + "info.background": "{{colors.primary_container.light.hex}}80", + "info.border": "{{colors.on_primary_container.light.hex}}", + "modified": "{{colors.secondary.light.hex}}", + "modified.background": "{{colors.secondary_container.light.hex}}80", + "modified.border": "{{colors.on_secondary_container.light.hex}}", + "predictive": "{{colors.on_surface_variant.light.hex}}80", + "predictive.border": "{{colors.outline.light.hex}}", + "predictive.background": "{{colors.surface_container_highest.light.hex}}80", + "renamed": "{{colors.secondary.light.hex}}", + "renamed.border": "{{colors.on_secondary_container.light.hex}}", + "renamed.background": "{{colors.secondary_container.light.hex}}80", + "success": "{{colors.tertiary.light.hex}}", + "success.background": "{{colors.tertiary_container.light.hex}}80", + "success.border": "{{colors.on_tertiary_container.light.hex}}", + "unreachable": "{{colors.on_surface_variant.light.hex}}60", + "unreachable.background": "{{colors.surface_variant.light.hex}}40", + "unreachable.border": "{{colors.outline_variant.light.hex}}60", + "warning": "{{colors.tertiary_fixed_dim.light.hex}}", + "warning.background": "{{colors.tertiary_container.light.hex}}80", + "warning.border": "{{colors.on_tertiary_container.light.hex}}", + "players": [ + { + "cursor": "{{colors.primary.light.hex}}", + "background": "{{colors.primary_container.light.hex}}80", + "selection": "{{colors.primary_container.light.hex}}60" + }, + { + "cursor": "{{colors.secondary.light.hex}}", + "background": "{{colors.secondary_container.light.hex}}80", + "selection": "{{colors.secondary_container.light.hex}}60" + } + ], + "syntax": { + "boolean": { + "color": "{{colors.tertiary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "comment": { + "color": "{{colors.on_surface_variant.light.hex}}", + "font_style": "italic", + "font_weight": null + }, + "comment.doc": { + "color": "{{colors.on_surface_variant.light.hex}}", + "font_style": "italic", + "font_weight": null + }, + "constant": { + "color": "{{colors.tertiary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "constructor": { + "color": "{{colors.secondary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "emphasis": { + "color": "{{colors.primary.light.hex}}", + "font_style": "italic", + "font_weight": null + }, + "emphasis.strong": { + "color": "{{colors.primary.light.hex}}", + "font_style": null, + "font_weight": 700 + }, + "function": { + "color": "{{colors.primary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "keyword": { + "color": "{{colors.secondary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "number": { + "color": "{{colors.tertiary_fixed.light.hex}}", + "font_style": null, + "font_weight": null + }, + "operator": { + "color": "{{colors.on_surface_variant.light.hex}}", + "font_style": null, + "font_weight": null + }, + "property": { + "color": "{{colors.on_surface.light.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation": { + "color": "{{colors.on_surface_variant.light.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation.bracket": { + "color": "{{colors.secondary_fixed.light.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation.delimiter": { + "color": "{{colors.on_surface_variant.light.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation.list_marker": { + "color": "{{colors.on_surface_variant.light.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation.special": { + "color": "{{colors.secondary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "string": { + "color": "{{colors.tertiary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "string.escape": { + "color": "{{colors.tertiary_fixed_dim.light.hex}}", + "font_style": null, + "font_weight": null + }, + "string.regex": { + "color": "{{colors.tertiary_fixed.light.hex}}", + "font_style": null, + "font_weight": null + }, + "string.special": { + "color": "{{colors.secondary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "string.special.symbol": { + "color": "{{colors.tertiary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "tag": { + "color": "{{colors.secondary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "text.literal": { + "color": "{{colors.tertiary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "type": { + "color": "{{colors.primary_fixed.light.hex}}", + "font_style": null, + "font_weight": null + }, + "variable": { + "color": "{{colors.on_surface.light.hex}}", + "font_style": null, + "font_weight": null + }, + "variable.special": { + "color": "{{colors.primary.light.hex}}", + "font_style": null, + "font_weight": null + } + } + } + }, + { + "name": "Noctalia Dark Transparent", + "appearance": "dark", + "style": { + "accents": [ + "{{colors.primary.dark.hex}}", + "{{colors.secondary.dark.hex}}", + "{{colors.tertiary.dark.hex}}" + ], + "background.appearance": "opaque", + "border": "{{colors.outline_variant.dark.hex}}", + "border.variant": "{{colors.outline.dark.hex}}", + "border.focused": "{{colors.primary.dark.hex}}", + "border.selected": "{{colors.primary.dark.hex}}", + "border.transparent": "{{colors.outline_variant.dark.hex}}40", + "border.disabled": "{{colors.outline_variant.dark.hex}}60", + "elevated_surface.background": "{{colors.surface_container_high.dark.hex}}", + "surface.background": "{{colors.surface.dark.hex}}", + "background": "{{colors.background.dark.hex}}B6", + "element.background": "{{colors.surface_container.dark.hex}}", + "element.hover": "{{colors.surface_container_high.dark.hex}}", + "element.active": "{{colors.surface_container_highest.dark.hex}}", + "element.selected": "{{colors.secondary_container.dark.hex}}", + "element.disabled": "{{colors.surface_variant.dark.hex}}", + "drop_target.background": "{{colors.primary_container.dark.hex}}80", + "ghost_element.background": null, + "ghost_element.hover": "{{colors.surface_container.dark.hex}}80", + "ghost_element.active": "{{colors.surface_container_high.dark.hex}}", + "ghost_element.selected": "{{colors.secondary_container.dark.hex}}80", + "ghost_element.disabled": "{{colors.surface_variant.dark.hex}}60", + "text": "{{colors.on_surface.dark.hex}}", + "text.muted": "{{colors.on_surface_variant.dark.hex}}", + "text.placeholder": "{{colors.on_surface_variant.dark.hex}}99", + "text.disabled": "{{colors.on_surface.dark.hex}}60", + "text.accent": "{{colors.primary.dark.hex}}", + "icon": "{{colors.on_surface.dark.hex}}", + "icon.muted": "{{colors.on_surface_variant.dark.hex}}", + "icon.disabled": "{{colors.on_surface.dark.hex}}60", + "icon.placeholder": "{{colors.on_surface_variant.dark.hex}}80", + "icon.accent": "{{colors.primary.dark.hex}}", + "status_bar.background": "{{colors.surface.dark.hex}}B6", + "title_bar.background": "{{colors.surface.dark.hex}}B6", + "title_bar.inactive_background": "{{colors.surface_dim.dark.hex}}B6", + "toolbar.background": "#00000000", + "tab_bar.background": "#00000000", + "tab.inactive_background": "{{colors.surface_container_low.dark.hex}}B6", + "tab.active_background": "{{colors.surface_container_high.dark.hex}}B6", + "search.match_background": "{{colors.tertiary_container.dark.hex}}80", + "panel.background": "#00000000", + "panel.focused_border": "{{colors.primary.dark.hex}}", + "pane.focused_border": "{{colors.primary.dark.hex}}", + "scrollbar.thumb.background": "{{colors.on_surface_variant.dark.hex}}80", + "scrollbar.thumb.hover_background": "{{colors.on_surface_variant.dark.hex}}c0", + "scrollbar.thumb.border": "{{colors.outline_variant.dark.hex}}40", + "scrollbar.track.background": "#00000000", + "scrollbar.track.border": "{{colors.outline_variant.dark.hex}}20", + "editor.foreground": "{{colors.on_surface.dark.hex}}", + "editor.background": "#00000000", + "editor.gutter.background": "#00000000", + "editor.subheader.background": "#00000000", + "editor.indent_guide": "{{colors.outline_variant.dark.hex}}60", + "editor.indent_guide_active": "{{colors.outline.dark.hex}}", + "editor.active_line.background": "{{colors.surface_container_high.dark.hex}}80", + "editor.highlighted_line.background": "{{colors.surface_container_high.dark.hex}}60", + "editor.line_number": "{{colors.on_surface_variant.dark.hex}}", + "editor.active_line_number": "{{colors.primary.dark.hex}}", + "editor.invisible": "{{colors.outline_variant.dark.hex}}80", + "editor.wrap_guide": "{{colors.outline_variant.dark.hex}}40", + "editor.active_wrap_guide": "{{colors.outline.dark.hex}}80", + "editor.document_highlight.read_background": "{{colors.primary_container.dark.hex}}60", + "editor.document_highlight.write_background": "{{colors.secondary_container.dark.hex}}80", + "terminal.background": "#00000000", + "terminal.foreground": "{{colors.on_surface.dark.hex}}", + "terminal.bright_foreground": "{{colors.on_surface.dark.hex}}", + "terminal.dim_foreground": "{{colors.on_surface_variant.dark.hex}}", + "terminal.ansi.black": "{{colors.surface_dim.dark.hex}}", + "terminal.ansi.bright_black": "{{colors.surface_container_high.dark.hex}}", + "terminal.ansi.dim_black": "{{colors.surface.dark.hex}}", + "terminal.ansi.red": "{{colors.error.dark.hex}}", + "terminal.ansi.bright_red": "{{colors.error.dark.hex}}", + "terminal.ansi.dim_red": "{{colors.on_error_container.dark.hex}}", + "terminal.ansi.green": "{{colors.tertiary.dark.hex}}", + "terminal.ansi.bright_green": "{{colors.tertiary.dark.hex}}", + "terminal.ansi.dim_green": "{{colors.tertiary_fixed_dim.dark.hex}}", + "terminal.ansi.yellow": "{{colors.tertiary_fixed_dim.dark.hex}}", + "terminal.ansi.bright_yellow": "{{colors.tertiary_fixed.dark.hex}}", + "terminal.ansi.dim_yellow": "{{colors.on_tertiary_fixed.dark.hex}}", + "terminal.ansi.blue": "{{colors.primary.dark.hex}}", + "terminal.ansi.bright_blue": "{{colors.primary.dark.hex}}", + "terminal.ansi.dim_blue": "{{colors.on_primary_container.dark.hex}}", + "terminal.ansi.magenta": "{{colors.secondary.dark.hex}}", + "terminal.ansi.bright_magenta": "{{colors.secondary.dark.hex}}", + "terminal.ansi.dim_magenta": "{{colors.on_secondary_container.dark.hex}}", + "terminal.ansi.cyan": "{{colors.primary_fixed_dim.dark.hex}}", + "terminal.ansi.bright_cyan": "{{colors.primary_fixed.dark.hex}}", + "terminal.ansi.dim_cyan": "{{colors.on_primary_fixed_variant.dark.hex}}", + "terminal.ansi.white": "{{colors.on_surface.dark.hex}}", + "terminal.ansi.bright_white": "{{colors.on_surface.dark.hex}}", + "terminal.ansi.dim_white": "{{colors.on_surface_variant.dark.hex}}", + "link_text.hover": "{{colors.primary.dark.hex}}", + "conflict": "{{colors.error.dark.hex}}", + "conflict.background": "{{colors.error_container.dark.hex}}80", + "conflict.border": "{{colors.on_error_container.dark.hex}}", + "created": "{{colors.tertiary.dark.hex}}", + "created.background": "{{colors.tertiary_container.dark.hex}}80", + "created.border": "{{colors.on_tertiary_container.dark.hex}}", + "deleted": "{{colors.error.dark.hex}}", + "deleted.background": "{{colors.error_container.dark.hex}}80", + "deleted.border": "{{colors.on_error_container.dark.hex}}", + "error": "{{colors.error.dark.hex}}", + "error.background": "{{colors.error_container.dark.hex}}", + "error.border": "{{colors.on_error_container.dark.hex}}", + "hidden": "{{colors.outline_variant.dark.hex}}", + "hidden.border": "{{colors.outline_variant.dark.hex}}60", + "hint": "{{colors.primary.dark.hex}}", + "hint.background": "{{colors.primary_container.dark.hex}}80", + "hint.border": "{{colors.on_primary_container.dark.hex}}", + "ignored": "{{colors.on_surface_variant.dark.hex}}60", + "ignored.background": "{{colors.surface_variant.dark.hex}}40", + "ignored.border": "{{colors.outline_variant.dark.hex}}40", + "info": "{{colors.primary.dark.hex}}", + "info.background": "{{colors.primary_container.dark.hex}}80", + "info.border": "{{colors.on_primary_container.dark.hex}}", + "modified": "{{colors.secondary.dark.hex}}", + "modified.background": "{{colors.secondary_container.dark.hex}}80", + "modified.border": "{{colors.on_secondary_container.dark.hex}}", + "predictive": "{{colors.on_surface_variant.dark.hex}}80", + "predictive.border": "{{colors.outline.dark.hex}}", + "predictive.background": "{{colors.surface_container_highest.dark.hex}}80", + "renamed": "{{colors.secondary.dark.hex}}", + "renamed.border": "{{colors.on_secondary_container.dark.hex}}", + "renamed.background": "{{colors.secondary_container.dark.hex}}80", + "success": "{{colors.tertiary.dark.hex}}", + "success.background": "{{colors.tertiary_container.dark.hex}}80", + "success.border": "{{colors.on_tertiary_container.dark.hex}}", + "unreachable": "{{colors.on_surface_variant.dark.hex}}60", + "unreachable.background": "{{colors.surface_variant.dark.hex}}40", + "unreachable.border": "{{colors.outline_variant.dark.hex}}60", + "warning": "{{colors.tertiary_fixed_dim.dark.hex}}", + "warning.background": "{{colors.tertiary_container.dark.hex}}80", + "warning.border": "{{colors.on_tertiary_container.dark.hex}}", + "players": [ + { + "cursor": "{{colors.primary.dark.hex}}", + "background": "{{colors.primary_container.dark.hex}}80", + "selection": "{{colors.primary_container.dark.hex}}60" + }, + { + "cursor": "{{colors.secondary.dark.hex}}", + "background": "{{colors.secondary_container.dark.hex}}80", + "selection": "{{colors.secondary_container.dark.hex}}60" + } + ], + "syntax": { + "boolean": { + "color": "{{colors.tertiary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "comment": { + "color": "{{colors.on_surface_variant.dark.hex}}", + "font_style": "italic", + "font_weight": null + }, + "comment.doc": { + "color": "{{colors.on_surface_variant.dark.hex}}", + "font_style": "italic", + "font_weight": null + }, + "constant": { + "color": "{{colors.tertiary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "constructor": { + "color": "{{colors.secondary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "emphasis": { + "color": "{{colors.primary.dark.hex}}", + "font_style": "italic", + "font_weight": null + }, + "emphasis.strong": { + "color": "{{colors.primary.dark.hex}}", + "font_style": null, + "font_weight": 700 + }, + "function": { + "color": "{{colors.primary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "keyword": { + "color": "{{colors.secondary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "number": { + "color": "{{colors.tertiary_fixed.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "operator": { + "color": "{{colors.on_surface_variant.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "property": { + "color": "{{colors.on_surface.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation": { + "color": "{{colors.on_surface_variant.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation.bracket": { + "color": "{{colors.secondary_fixed.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation.delimiter": { + "color": "{{colors.on_surface_variant.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation.list_marker": { + "color": "{{colors.on_surface_variant.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation.special": { + "color": "{{colors.secondary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "string": { + "color": "{{colors.tertiary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "string.escape": { + "color": "{{colors.tertiary_fixed_dim.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "string.regex": { + "color": "{{colors.tertiary_fixed.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "string.special": { + "color": "{{colors.secondary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "string.special.symbol": { + "color": "{{colors.tertiary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "tag": { + "color": "{{colors.secondary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "text.literal": { + "color": "{{colors.tertiary.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "type": { + "color": "{{colors.primary_fixed.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "variable": { + "color": "{{colors.on_surface.dark.hex}}", + "font_style": null, + "font_weight": null + }, + "variable.special": { + "color": "{{colors.primary.dark.hex}}", + "font_style": null, + "font_weight": null + } + } + } + }, + { + "name": "Noctalia Light Transparent", + "appearance": "light", + "style": { + "accents": [ + "{{colors.primary.light.hex}}", + "{{colors.secondary.light.hex}}", + "{{colors.tertiary.light.hex}}" + ], + "background.appearance": "opaque", + "border": "{{colors.outline_variant.light.hex}}", + "border.variant": "{{colors.outline.light.hex}}", + "border.focused": "{{colors.primary.light.hex}}", + "border.selected": "{{colors.primary.light.hex}}", + "border.transparent": "{{colors.outline_variant.light.hex}}40", + "border.disabled": "{{colors.outline_variant.light.hex}}60", + "elevated_surface.background": "{{colors.surface_container_high.light.hex}}", + "surface.background": "{{colors.surface.light.hex}}", + "background": "{{colors.background.light.hex}}B6", + "element.background": "{{colors.surface_container.light.hex}}", + "element.hover": "{{colors.surface_container_high.light.hex}}", + "element.active": "{{colors.surface_container_highest.light.hex}}", + "element.selected": "{{colors.secondary_container.light.hex}}", + "element.disabled": "{{colors.surface_variant.light.hex}}", + "drop_target.background": "{{colors.primary_container.light.hex}}80", + "ghost_element.background": null, + "ghost_element.hover": "{{colors.surface_container.light.hex}}80", + "ghost_element.active": "{{colors.surface_container_high.light.hex}}", + "ghost_element.selected": "{{colors.secondary_container.light.hex}}80", + "ghost_element.disabled": "{{colors.surface_variant.light.hex}}60", + "text": "{{colors.on_surface.light.hex}}", + "text.muted": "{{colors.on_surface_variant.light.hex}}", + "text.placeholder": "{{colors.on_surface_variant.light.hex}}99", + "text.disabled": "{{colors.on_surface.light.hex}}60", + "text.accent": "{{colors.primary.light.hex}}", + "icon": "{{colors.on_surface.light.hex}}", + "icon.muted": "{{colors.on_surface_variant.light.hex}}", + "icon.disabled": "{{colors.on_surface.light.hex}}60", + "icon.placeholder": "{{colors.on_surface_variant.light.hex}}80", + "icon.accent": "{{colors.primary.light.hex}}", + "status_bar.background": "{{colors.surface.light.hex}}B6", + "title_bar.background": "{{colors.surface.light.hex}}B6", + "title_bar.inactive_background": "{{colors.surface_dim.light.hex}}B6", + "toolbar.background": "#00000000", + "tab_bar.background": "#00000000", + "tab.inactive_background": "{{colors.surface_container_low.light.hex}}B6", + "tab.active_background": "{{colors.surface_container_high.light.hex}}B6", + "search.match_background": "{{colors.tertiary_container.light.hex}}80", + "panel.background": "#00000000", + "panel.focused_border": "{{colors.primary.light.hex}}", + "pane.focused_border": "{{colors.primary.light.hex}}", + "scrollbar.thumb.background": "{{colors.on_surface_variant.light.hex}}80", + "scrollbar.thumb.hover_background": "{{colors.on_surface_variant.light.hex}}c0", + "scrollbar.thumb.border": "{{colors.outline_variant.light.hex}}40", + "scrollbar.track.background": "#00000000", + "scrollbar.track.border": "{{colors.outline_variant.light.hex}}20", + "editor.foreground": "{{colors.on_surface.light.hex}}", + "editor.background": "#00000000", + "editor.gutter.background": "#00000000", + "editor.subheader.background": "#00000000", + "editor.indent_guide": "{{colors.outline_variant.light.hex}}60", + "editor.indent_guide_active": "{{colors.outline.light.hex}}", + "editor.active_line.background": "{{colors.surface_container_high.light.hex}}80", + "editor.highlighted_line.background": "{{colors.surface_container_high.light.hex}}60", + "editor.line_number": "{{colors.on_surface_variant.light.hex}}", + "editor.active_line_number": "{{colors.primary.light.hex}}", + "editor.invisible": "{{colors.outline_variant.light.hex}}80", + "editor.wrap_guide": "{{colors.outline_variant.light.hex}}40", + "editor.active_wrap_guide": "{{colors.outline.light.hex}}80", + "editor.document_highlight.read_background": "{{colors.primary_container.light.hex}}60", + "editor.document_highlight.write_background": "{{colors.secondary_container.light.hex}}80", + "terminal.background": "#00000000", + "terminal.foreground": "{{colors.on_surface.light.hex}}", + "terminal.bright_foreground": "{{colors.on_surface.light.hex}}", + "terminal.dim_foreground": "{{colors.on_surface_variant.light.hex}}", + "terminal.ansi.black": "{{colors.surface_dim.light.hex}}", + "terminal.ansi.bright_black": "{{colors.surface_container_high.light.hex}}", + "terminal.ansi.dim_black": "{{colors.surface.light.hex}}", + "terminal.ansi.red": "{{colors.error.light.hex}}", + "terminal.ansi.bright_red": "{{colors.error.light.hex}}", + "terminal.ansi.dim_red": "{{colors.on_error_container.light.hex}}", + "terminal.ansi.green": "{{colors.tertiary.light.hex}}", + "terminal.ansi.bright_green": "{{colors.tertiary.light.hex}}", + "terminal.ansi.dim_green": "{{colors.tertiary_fixed_dim.light.hex}}", + "terminal.ansi.yellow": "{{colors.tertiary_fixed_dim.light.hex}}", + "terminal.ansi.bright_yellow": "{{colors.tertiary_fixed.light.hex}}", + "terminal.ansi.dim_yellow": "{{colors.on_tertiary_fixed.light.hex}}", + "terminal.ansi.blue": "{{colors.primary.light.hex}}", + "terminal.ansi.bright_blue": "{{colors.primary.light.hex}}", + "terminal.ansi.dim_blue": "{{colors.on_primary_container.light.hex}}", + "terminal.ansi.magenta": "{{colors.secondary.light.hex}}", + "terminal.ansi.bright_magenta": "{{colors.secondary.light.hex}}", + "terminal.ansi.dim_magenta": "{{colors.on_secondary_container.light.hex}}", + "terminal.ansi.cyan": "{{colors.primary_fixed_dim.light.hex}}", + "terminal.ansi.bright_cyan": "{{colors.primary_fixed.light.hex}}", + "terminal.ansi.dim_cyan": "{{colors.on_primary_fixed_variant.light.hex}}", + "terminal.ansi.white": "{{colors.on_surface.light.hex}}", + "terminal.ansi.bright_white": "{{colors.on_surface.light.hex}}", + "terminal.ansi.dim_white": "{{colors.on_surface_variant.light.hex}}", + "link_text.hover": "{{colors.primary.light.hex}}", + "conflict": "{{colors.error.light.hex}}", + "conflict.background": "{{colors.error_container.light.hex}}80", + "conflict.border": "{{colors.on_error_container.light.hex}}", + "created": "{{colors.tertiary.light.hex}}", + "created.background": "{{colors.tertiary_container.light.hex}}80", + "created.border": "{{colors.on_tertiary_container.light.hex}}", + "deleted": "{{colors.error.light.hex}}", + "deleted.background": "{{colors.error_container.light.hex}}80", + "deleted.border": "{{colors.on_error_container.light.hex}}", + "error": "{{colors.error.light.hex}}", + "error.background": "{{colors.error_container.light.hex}}", + "error.border": "{{colors.on_error_container.light.hex}}", + "hidden": "{{colors.outline_variant.light.hex}}", + "hidden.border": "{{colors.outline_variant.light.hex}}60", + "hint": "{{colors.primary.light.hex}}", + "hint.background": "{{colors.primary_container.light.hex}}80", + "hint.border": "{{colors.on_primary_container.light.hex}}", + "ignored": "{{colors.on_surface_variant.light.hex}}60", + "ignored.background": "{{colors.surface_variant.light.hex}}40", + "ignored.border": "{{colors.outline_variant.light.hex}}40", + "info": "{{colors.primary.light.hex}}", + "info.background": "{{colors.primary_container.light.hex}}80", + "info.border": "{{colors.on_primary_container.light.hex}}", + "modified": "{{colors.secondary.light.hex}}", + "modified.background": "{{colors.secondary_container.light.hex}}80", + "modified.border": "{{colors.on_secondary_container.light.hex}}", + "predictive": "{{colors.on_surface_variant.light.hex}}80", + "predictive.border": "{{colors.outline.light.hex}}", + "predictive.background": "{{colors.surface_container_highest.light.hex}}80", + "renamed": "{{colors.secondary.light.hex}}", + "renamed.border": "{{colors.on_secondary_container.light.hex}}", + "renamed.background": "{{colors.secondary_container.light.hex}}80", + "success": "{{colors.tertiary.light.hex}}", + "success.background": "{{colors.tertiary_container.light.hex}}80", + "success.border": "{{colors.on_tertiary_container.light.hex}}", + "unreachable": "{{colors.on_surface_variant.light.hex}}60", + "unreachable.background": "{{colors.surface_variant.light.hex}}40", + "unreachable.border": "{{colors.outline_variant.light.hex}}60", + "warning": "{{colors.tertiary_fixed_dim.light.hex}}", + "warning.background": "{{colors.tertiary_container.light.hex}}80", + "warning.border": "{{colors.on_tertiary_container.light.hex}}", + "players": [ + { + "cursor": "{{colors.primary.light.hex}}", + "background": "{{colors.primary_container.light.hex}}80", + "selection": "{{colors.primary_container.light.hex}}60" + }, + { + "cursor": "{{colors.secondary.light.hex}}", + "background": "{{colors.secondary_container.light.hex}}80", + "selection": "{{colors.secondary_container.light.hex}}60" + } + ], + "syntax": { + "boolean": { + "color": "{{colors.tertiary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "comment": { + "color": "{{colors.on_surface_variant.light.hex}}", + "font_style": "italic", + "font_weight": null + }, + "comment.doc": { + "color": "{{colors.on_surface_variant.light.hex}}", + "font_style": "italic", + "font_weight": null + }, + "constant": { + "color": "{{colors.tertiary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "constructor": { + "color": "{{colors.secondary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "emphasis": { + "color": "{{colors.primary.light.hex}}", + "font_style": "italic", + "font_weight": null + }, + "emphasis.strong": { + "color": "{{colors.primary.light.hex}}", + "font_style": null, + "font_weight": 700 + }, + "function": { + "color": "{{colors.primary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "keyword": { + "color": "{{colors.secondary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "number": { + "color": "{{colors.tertiary_fixed.light.hex}}", + "font_style": null, + "font_weight": null + }, + "operator": { + "color": "{{colors.on_surface_variant.light.hex}}", + "font_style": null, + "font_weight": null + }, + "property": { + "color": "{{colors.on_surface.light.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation": { + "color": "{{colors.on_surface_variant.light.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation.bracket": { + "color": "{{colors.secondary_fixed.light.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation.delimiter": { + "color": "{{colors.on_surface_variant.light.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation.list_marker": { + "color": "{{colors.on_surface_variant.light.hex}}", + "font_style": null, + "font_weight": null + }, + "punctuation.special": { + "color": "{{colors.secondary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "string": { + "color": "{{colors.tertiary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "string.escape": { + "color": "{{colors.tertiary_fixed_dim.light.hex}}", + "font_style": null, + "font_weight": null + }, + "string.regex": { + "color": "{{colors.tertiary_fixed.light.hex}}", + "font_style": null, + "font_weight": null + }, + "string.special": { + "color": "{{colors.secondary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "string.special.symbol": { + "color": "{{colors.tertiary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "tag": { + "color": "{{colors.secondary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "text.literal": { + "color": "{{colors.tertiary.light.hex}}", + "font_style": null, + "font_weight": null + }, + "type": { + "color": "{{colors.primary_fixed.light.hex}}", + "font_style": null, + "font_weight": null + }, + "variable": { + "color": "{{colors.on_surface.light.hex}}", + "font_style": null, + "font_weight": null + }, + "variable.special": { + "color": "{{colors.primary.light.hex}}", + "font_style": null, + "font_weight": null + } + } + } + } + ] +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/zen-browser/zen-userChrome.css b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/zen-browser/zen-userChrome.css new file mode 100644 index 0000000..f909038 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/zen-browser/zen-userChrome.css @@ -0,0 +1,177 @@ +* { + --base: {{colors.surface_container_lowest.default.hex}}; + --surface: {{colors.surface_container_low.default.hex}}; + --overlay: {{colors.surface_container.default.hex}}; + --muted: {{colors.surface_container_high.default.hex}}; + --subtle: {{colors.surface_container_highest.default.hex}}; + --text: {{colors.on_surface.default.hex}}; + --secondary: {{colors.secondary.default.hex}}; + --primary: {{colors.primary.default.hex}}; + --on_primary: {{colors.on_primary.default.hex}}; +} + +:root { + /* --- Core Palette Overrides --- */ + --zen-colors-primary: var(--base) !important; + --zen-primary-color: var(--base) !important; + --zen-colors-secondary: var(--surface) !important; + --zen-colors-tertiary: var(--overlay) !important; + --zen-colors-border: var(--primary) !important; + + /* --- Internal Zen Color Variables --- */ + --zen-primary-color: var(--primary) !important; + --zen-colors-primary: var(--base) !important; + --zen-colors-secondary: var(--surface) !important; + --zen-colors-tertiary: var(--base) !important; + + /* --- Backgrounds --- */ + --toolbar-bgcolor: var(--base) !important; + --newtab-background-color: var(--overlay) !important; + --zen-themed-toolbar-bg: var(--base) !important; + --zen-main-browser-background: var(--base) !important; + --toolbox-bgcolor-inactive: var(--base) !important; + --zen-urlbar-background: var(--surface) !important; + --zen-colors-input-bg: var(--surface) !important; + --zen-themed-toolbar-bg-transparent: transparent !important; + + /* --- Text & Icons --- */ + --lwt-text-color: var(--text) !important; + --toolbarbutton-icon-fill: var(--primary) !important; + --toolbar-field-color: var(--text) !important; + --toolbar-field-focus-color: var(--text) !important; + --toolbar-color: var(--text) !important; + --toolbox-textcolor: var(--text) !important; + --toolbox-textcolor-inactive: var(--text) !important; + --tab-selected-textcolor: var(--primary) !important; + --newtab-text-primary-color: var(--text) !important; + + /* --- Popups, Menus & Sidebars --- */ + --arrowpanel-color: var(--text) !important; + --arrowpanel-background: var(--overlay) !important; + --sidebar-text-color: var(--text) !important; + --lwt-sidebar-text-color: var(--text) !important; + --lwt-sidebar-background-color: var(--base) !important; +} + +/* --- Sidebar & Layout --- */ + +#navigator-toolbox, +#zen-tabbox-wrapper, +.sidebar-panel { + background-color: var(--base) !important; +} + +#zen-sidebar-splitter { + background-color: var(--base) !important; + opacity: 1 !important; +} + +.sidebar-placesTree { + background-color: var(--surface) !important; +} + +#zen-workspaces-button { + background-color: var(--surface) !important; +} + +#zen-appcontent-navbar-container { + background-color: var(--base) !important; +} + +/* --- Navigation Controls --- */ + +#back-button .toolbarbutton-icon, +#forward-button .toolbarbutton-icon, +#reload-button .toolbarbutton-icon, +#stop-button .toolbarbutton-icon, +#PanelUI-menu-button .toolbarbutton-icon, +#unified-extensions-button .toolbarbutton-icon, +#nav-bar-overflow-button .toolbarbutton-icon, +#zen-toggle-compact-mode .toolbarbutton-icon { + fill: var(--primary) !important; + color: var(--primary) !important; +} + +#back-button[disabled] .toolbarbutton-icon, +#forward-button[disabled] .toolbarbutton-icon { + opacity: 0.3 !important; +} + +toolbar .toolbarbutton-1 { + &:not([disabled]) { + &:is([open], [checked]) > :is( + .toolbarbutton-icon, + .toolbarbutton-text, + .toolbarbutton-badge-stack + ) { + fill: var(--base); + } + } +} + +/* --- URL Bar --- */ + +#urlbar-input { + color: var(--text) !important; +} + +#urlbar-input::selection { + background-color: var(--primary) !important; + color: var(--on_primary) !important; +} + +.urlbar-background { + background-color: var(--base) !important; +} + +#urlbar-input-container { + background-color: var(--base) !important; +} + +.urlbarView-url { + color: var(--primary) !important; +} + +.urlbarView-favicon { + background-color: var(--base) !important; +} + +#urlbar-results { + background-color: var(--overlay) !important; + color: var(--text) !important; +} + +/* --- UI Components --- */ + +#zen-toast-container { + color: var(--primary) !important; +} + +#PanelUI-menu-button[open] .toolbarbutton-icon { + fill: var(--primary) !important; +} + +#permissions-granted-icon { + color: var(--base) !important; +} + +.tab-background[selected=""] { + background: var(--overlay) !important; +} + +.content-shortcuts { + background-color: var(--surface) !important; + border-color: var(--secondary) !important; +} + +#zenEditBookmarkPanelFaviconContainer { + background: var(--base) !important; +} + +#TabsToolbar { + background-color: var(--base) !important; +} + +hbox#titlebar { + background-color: var(--base) !important; +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Templates/zen-browser/zen-userContent.css b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/zen-browser/zen-userContent.css new file mode 100644 index 0000000..dc75d17 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Templates/zen-browser/zen-userContent.css @@ -0,0 +1,83 @@ +/* Common variables affecting all pages */ + @-moz-document url-prefix("about:") { + :root { + --in-content-page-color: {{colors.secondary.default.hex}} !important; + --color-accent-primary: {{colors.primary.default.hex}} !important; + /* --color-accent-primary-hover: rgb(249, 217, 240) !important; + --color-accent-primary-active: rgb(245, 195, 219) !important; */ + background-color: {{colors.surface_container.default.hex}} !important; + --in-content-page-background: {{colors.surface_container.default.hex}} !important; + } + + } + + /* Variables and styles specific to about:newtab and about:home */ + @-moz-document url("about:newtab"), url("about:home") { + + :root { + --newtab-background-color: {{colors.surface_container.default.hex}} !important; + --newtab-background-color-secondary: {{colors.surface_container_high.default.hex}} !important; + --newtab-element-hover-color: {{colors.surface_container_high.default.hex}} !important; + --newtab-text-primary-color: {{colors.secondary.default.hex}} !important; + --newtab-wordmark-color: {{colors.secondary.default.hex}} !important; + --newtab-primary-action-background: {{colors.primary.default.hex}} !important; + } + + .icon { + color: {{colors.primary.default.hex}} !important; + } + + .card-outer:is(:hover, :focus, .active):not(.placeholder) .card-title { + color: {{colors.primary.default.hex}} !important; + } + } + + /* Variables and styles specific to about:preferences */ + @-moz-document url-prefix("about:preferences") { + :root { + --zen-colors-tertiary: {{colors.surface_container_low.default.hex}} !important; + --in-content-text-color: {{colors.secondary.default.hex}} !important; + --link-color: {{colors.primary.default.hex}} !important; + /* --link-color-hover: rgb(249, 217, 240) !important; */ + --zen-colors-primary: {{colors.surface_container_high.default.hex}} !important; + --in-content-box-background: {{colors.surface_container_high.default.hex}} !important; + --zen-primary-color: {{colors.primary.default.hex}} !important; + } + + button, + groupbox menulist { + background: {{colors.surface_container_high.default.hex}} !important; + color: {{colors.secondary.default.hex}} !important; + } + + .main-content { + background-color: {{colors.surface_container_lowest.default.hex}} !important; + } + } + + /* Variables and styles specific to about:addons */ + @-moz-document url-prefix("about:addons") { + :root { + --zen-dark-color-mix-base: {{colors.surface_container_low.default.hex}} !important; + --background-color-box: {{colors.surface_container.default.hex}} !important; + } + } + + /* Variables and styles specific to about:protections */ + @-moz-document url-prefix("about:protections") { + :root { + --zen-primary-color: {{colors.surface_container.default.hex}} !important; + --in-content-primary-button-text-color-hover: {{colors.secondary.default.hex}} !important; + --in-content-primary-button-background: {{colors.surface_container.default.hex}} !important; + --in-content-primary-button-text-color: {{colors.secondary.default.hex}} !important; + } + + .card { + background-color: {{colors.surface_container_high.default.hex}} !important; + } + } + /* Pane UI button visibility issue fix */ + #PanelUI-menu-button[open] .toolbarbutton-icon { + fill: {{colors.primary.default.hex}} !important; + } +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/cs.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/cs.json new file mode 100644 index 0000000..fec5e20 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/cs.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "Vymazat historii", + "close-app": "Zavřít {app}", + "connect-vpn": "Připojit se k {name}", + "cycle-visualizer": "Přepnout vizualizér", + "disable-bluetooth": "Vypnout Bluetooth", + "disable-dnd": "Vypnout Nerušit", + "disable-wifi": "Vypnout Wi-Fi", + "disconnect-vpn": "Odpojit {name}", + "dock-settings": "Nastavení docku", + "enable-bluetooth": "Zapnout Bluetooth", + "enable-dnd": "Zapnout Nerušit", + "enable-wifi": "Zapnout Wi-Fi", + "launcher-settings": "Nastavení spouštěče", + "lower-to-bottom": "Přesunout na spodek", + "open-calendar": "Otevřít kalendář", + "open-display-settings": "Nastavení displeje", + "open-launcher": "Otevřít spouštěč", + "open-settings": "Otevřít nastavení", + "raise-to-top": "Přesunout na vrchol", + "random-wallpaper": "Náhodná tapeta", + "run-custom-command": "Spustit vlastní příkaz", + "show-active-only": "Zobrazit jen aktivní", + "show-all": "Zobrazit vše", + "toggle-mute": "Přepnout ztlumení", + "widget-settings": "Nastavení widgetu" + }, + "authentication": { + "error": "Chyba ověření", + "failed": "Ověření selhalo" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Použít barvy motivu na ikonu aktivního okna.", + "hide-mode-description": "Určuje chování widgetu, když není žádné okno aktivní.", + "scrolling-mode-description": "Řídí, kdy je povoleno posouvání textu pro dlouhé názvy oken.", + "show-app-icon-description": "Zobrazit ikonu aplikace vedle názvu okna.", + "show-app-icon-label": "Zobrazit ikonu aplikace", + "show-app-text-description": "Zobrazit název aktivního okna.", + "show-app-text-label": "Zobrazit název okna" + }, + "audio-visualizer": { + "color-name-description": "Vybrat barvu vizualizéru.", + "color-name-label": "Barva výplně", + "height-description": "Vlastní výška komponenty.", + "hide-when-idle-description": "Pokud je povoleno, vizualizér je skrytý, dokud nepřehrává žádný přehrávač.", + "hide-when-idle-label": "Skrýt při nečinnosti přehrávače", + "width-description": "Vlastní šířka komponenty." + }, + "battery": { + "device-default": "Výchozí (zobrazovací zařízení)", + "device-description": "Vybrat, které bateriové zařízení zobrazit.", + "device-label": "Bateriové zařízení", + "display-mode-description": "Vybrat způsob zobrazení baterie v panelu.", + "display-mode-graphic": "Grafická baterie", + "display-mode-graphic-clean": "Grafická baterie (bez %)", + "display-mode-icon-always": "Ikona – vždy zobrazit %", + "display-mode-icon-hover": "Ikona – zobrazit při najetí myší", + "display-mode-icon-only": "Jen ikona", + "hide-if-idle-description": "Skrýt widget, pokud se baterie nenabíjí ani nevybíjí.", + "hide-if-idle-label": "Skrýt při nečinnosti", + "hide-if-not-detected-description": "Skrýt widget, pokud není detekována žádná baterie.", + "hide-if-not-detected-label": "Skrýt pokud není detekována", + "low-battery-threshold-label": "Práh upozornění na nízký stav baterie", + "show-noctalia-performance-description": "Zobrazit přepínač výkonnostního režimu Noctalia uvnitř panelu baterie.
Vypne stíny a animace v Noctalia pro snížení spotřeby zdrojů.", + "show-noctalia-performance-label": "Zobrazit přepínač výkonu Noctalia", + "show-power-profile-description": "Zobrazit výběr profilu napájení uvnitř panelu baterie.", + "show-power-profile-label": "Zobrazit ovládání profilu napájení" + }, + "brightness": { + "apply-all-description": "Změnit jas pro každý monitor při použití kolečka myši.", + "apply-all-label": "Použít změny kolečkem na všechny monitory" + }, + "clock": { + "clock-display-description": "Přizpůsobit zobrazení hodin přidáním tokenů ze seznamu níže. Pro 12hodinový formát musíte zahrnout token 'AP'.", + "clock-display-label": "Zobrazení hodin", + "custom-font-description": "Vybrat vlastní písmo pro zobrazení hodin.", + "custom-font-label": "Vlastní písmo", + "custom-font-placeholder": "Vybrat vlastní písmo...", + "custom-font-search-placeholder": "Hledat písma...", + "horizontal-bar-description": "Tip: Použijte \\n pro odřádkování.", + "horizontal-bar-label": "Vodorovný panel", + "preview": "Náhled", + "tooltip-format-description": "Formátovací řetězec pro tooltip zobrazený při najetí na hodiny. Ponechte prázdné pro výchozí tooltip.", + "tooltip-format-label": "Formát tooltipu", + "use-custom-font-description": "Nahradit výchozí výběr písma vlastním písmem pro hodiny.", + "use-custom-font-label": "Použít vlastní písmo", + "use-monospaced-font-description": "Pokud je povoleno, hodiny použijí neproporcionální písmo.", + "use-monospaced-font-label": "Použít neproporcionální písmo", + "vertical-bar-description": "Oddělte jednotlivé části mezerou pro zobrazení na novém řádku.", + "vertical-bar-label": "Svislý panel" + }, + "control-center": { + "browse-file": "Procházet soubor", + "browse-library": "Procházet knihovnu", + "color-selection-description": "Použít barvy motivu na ikony.", + "enable-colorization-description": "Povolit barevné zvýraznění ikony s použitím barev motivu.", + "icon-description": "Vybrat ikonu z knihovny nebo vlastního souboru.", + "select-custom-icon": "Vybrat vlastní ikonu", + "use-distro-logo-description": "Použít logo vaší distribuce místo vlastní ikony.", + "use-distro-logo-label": "Použít logo distribuce místo ikony" + }, + "custom-button": { + "collapse-condition-description": "Pokud výstup příkazu odpovídá této hodnotě, tlačítko se sbalí.", + "collapse-condition-label": "Podmínka sbalení", + "color-selection-description": "Použít barvy motivu na ikonu a text.", + "default-tooltip": "Vlastní tlačítko, nastavte v nastavení", + "display-command-output-description": "Zadejte příkaz, který se spustí v pravidelných intervalech. První řádek jeho výstupu bude zobrazen jako text.", + "display-command-output-label": "Zobrazit výstup příkazu", + "display-command-output-stream-description": "Zadejte příkaz, který poběží nepřetržitě.", + "dynamic-text": "Dynamický text", + "enable-colorization-description": "Povolit barevné zvýraznění ikony a textu vlastního tlačítka s použitím barev motivu.", + "enable-colorization-label": "Povolit barevné zvýraznění", + "general-tooltip-text-description": "Vlastní text zobrazený v tooltipu tlačítka.", + "general-tooltip-text-label": "Vlastní text tooltipu", + "hide-mode-always-expanded": "Vždy rozbaleno", + "hide-mode-description": "Řídí viditelnost widgetu, když příkaz nemá výstup.", + "hide-mode-expand-with-output": "Rozbalit při výstupu", + "hide-mode-label": "Režim skrytí", + "hide-mode-max-transparent": "Maximálně rozbaleno, ale průhledné", + "icon-description": "Vybrat ikonu z knihovny.", + "ipc-identifier-description": "Jedinečný identifikátor pro příkazy IPC. Použijte tento identifikátor s 'qs -c noctalia-shell ipc call cb [action] [identifier]' pro ovládání tlačítka přes IPC.", + "ipc-identifier-label": "Identifikátor IPC", + "left-click-description": "Příkaz spuštěný při kliknutí levým tlačítkem myši.", + "left-click-label": "Levé kliknutí", + "left-click-update-text": "Aktualizovat zobrazený text při levém kliknutí", + "max-text-length-horizontal-description": "Maximální počet znaků zobrazených ve vodorovném panelu (0 = skrýt text).", + "max-text-length-horizontal-label": "Max. délka textu (vodorovně)", + "max-text-length-vertical-description": "Maximální počet znaků zobrazených ve svislém panelu (0 = skrýt text).", + "max-text-length-vertical-label": "Max. délka textu (svisle)", + "middle-click-description": "Příkaz spuštěný při kliknutí prostředním tlačítkem myši.", + "middle-click-label": "Prostřední kliknutí", + "middle-click-update-text": "Aktualizovat zobrazený text při prostředním kliknutí", + "parse-json-description": "Zpracovat výstup příkazu jako JSON objekt pro dynamické nastavení textu a ikony.", + "parse-json-label": "Zpracovat výstup jako JSON", + "refresh-interval-description": "Interval v milisekundách.", + "refresh-interval-label": "Interval obnovení", + "right-click-description": "Příkaz spuštěný při kliknutí pravým tlačítkem myši.", + "right-click-label": "Pravé kliknutí", + "right-click-update-text": "Aktualizovat zobrazený text při pravém kliknutí", + "show-exec-tooltip-description": "Zobrazit tooltipy s podrobnostmi o příkazech (levé/pravé/prostřední kliknutí, kolečko).", + "show-exec-tooltip-label": "Zobrazit tooltipy příkazů", + "show-icon-description": "Přepíná viditelnost ikony widgetu.", + "show-icon-label": "Zobrazit ikonu", + "show-text-tooltip-description": "Zobrazit tooltipy s výstupem textového příkazu.", + "show-text-tooltip-label": "Zobrazit tooltipy dynamického textu", + "text-stream-description": "Streamované řádky z příkazu budou zobrazeny jako text na tlačítku.", + "text-stream-label": "Proud dat", + "wheel-description": "Příkaz spuštěný při použití kolečka myši.
Použijte $delta pro delta hodnotu kolečka v příkazu.", + "wheel-down": "Posouvání dolů", + "wheel-down-description": "Příkaz spuštěný při posouvání kolečkem dolů.", + "wheel-down-label": "Příkaz kolečka dolů", + "wheel-label": "Kolečko myši", + "wheel-mode-separate-description": "Povolit samostatné příkazy pro kolečko nahoru a dolů.", + "wheel-mode-separate-label": "Oddělené příkazy kolečka", + "wheel-up": "Posouvání nahoru", + "wheel-up-description": "Příkaz spuštěný při posouvání kolečkem nahoru.", + "wheel-up-label": "Příkaz kolečka nahoru", + "wheel-update-text": "Aktualizovat zobrazený text při posouvání" + }, + "keyboard-layout": { + "show-icon-description": "Zobrazit ikonu rozložení klávesnice." + }, + "lock-keys": { + "hide-when-off-description": "Skrýt indikátor, pokud klávesa není aktivní.", + "hide-when-off-label": "Skrýt při vypnutí", + "show-caps-lock-description": "Zobrazit stav Caps Lock.", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "Zobrazit stav Num Lock.", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "Zobrazit stav Scroll Lock.", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "Povolit úsporné rozložení panelu přehrávače médií.", + "compact-mode-label": "Kompaktní režim", + "hide-mode-description": "Řídí chování widgetu, když nehrají žádná média.", + "max-width-description": "Nastaví maximální vodorovnou velikost widgetu. Widget se zmenší na kratší obsah.", + "no-active-player": "Žádný aktivní přehrávač", + "panel-section-description": "Nastavit vzhled a chování panelu přehrávače médií.", + "panel-section-label": "Panel přehrávače médií", + "scrolling-mode-description": "Řídí, kdy je povoleno posouvání textu pro dlouhé názvy stop.", + "scrolling-mode-label": "Režim posouvání", + "show-album-art-description": "Zobrazit obal alba pro aktuálně přehrávanou stopu.", + "show-album-art-label": "Zobrazit obal alba", + "show-artist-first-description": "Zobrazit interpreta – název místo název – interpret.", + "show-artist-first-label": "Zobrazit interpreta jako první", + "show-progress-ring-description": "Zobrazit kruhový indikátor průběhu přehrávání stopy.", + "show-progress-ring-label": "Zobrazit kruhový průběh", + "show-visualizer-description": "Zobrazit audio vizualizér při přehrávání hudby.", + "show-visualizer-label": "Zobrazit vizualizér", + "use-fixed-width-description": "Pokud je povoleno, widget vždy použije maximální šířku místo dynamického přizpůsobení obsahu.", + "use-fixed-width-label": "Použít pevnou šířku", + "visualizer-type-description": "Vybrat styl audio vizualizéru.", + "visualizer-type-label": "Typ vizualizéru" + }, + "notification-history": { + "hide-widget-when-zero-description": "Skrýt ikonu oznámení, pokud nejsou žádná oznámení.", + "hide-widget-when-zero-label": "Skrýt ikonu bez oznámení", + "hide-widget-when-zero-unread-description": "Skrýt ikonu oznámení, pokud nejsou žádná nepřečtená oznámení.", + "hide-widget-when-zero-unread-label": "Skrýt ikonu bez nepřečtených oznámení", + "show-unread-badge-description": "Zobrazit odznak s počtem nepřečtených oznámení.", + "show-unread-badge-label": "Zobrazit odznak nepřečtených", + "unread-badge-color-description": "Vybrat barvu odznaku nepřečtených oznámení.", + "unread-badge-color-label": "Barva odznaku nepřečtených" + }, + "section-editor": { + "placeholder": "Vyberte widget k přidání...", + "search-placeholder": "Hledat widget..." + }, + "spacer": { + "width-description": "Šířka mezery v pixelech." + }, + "system-monitor": { + "compact-mode-description": "Zobrazit statistiky jako mini sloupcové grafy místo textových hodnot. Zabraňuje posunu rozložení.", + "compact-mode-label": "Kompaktní režim", + "cpu-cores-description": "Zobrazit využití jednotlivých jader CPU.", + "cpu-cores-label": "Jádra CPU", + "cpu-frequency-description": "Zobrazit aktuální takt procesoru v GHz.", + "cpu-frequency-label": "Zobrazit frekvenci CPU", + "cpu-temperature-description": "Zobrazit teplotu CPU, pokud je dostupná.", + "cpu-temperature-label": "Teplota CPU", + "cpu-usage-description": "Zobrazit aktuální procentuální využití CPU.", + "cpu-usage-label": "Využití CPU", + "disk-path-description": "Vybrat přípojný bod disku ke sledování.", + "disk-path-label": "Cesta k disku", + "gpu-temperature-description": "Zobrazit teplotu GPU, pokud je dostupná.", + "load-average-description": "Zobrazit průměrné zatížení systému.", + "load-average-label": "Průměrné zatížení", + "memory-percentage-description": "Zobrazit využití paměti v procentech místo absolutních hodnot.", + "memory-percentage-label": "Paměť v procentech", + "memory-usage-description": "Zobrazit aktuální informace o využití RAM.", + "memory-usage-label": "Využití paměti", + "network-traffic-description": "Zobrazit rychlosti odesílání a stahování sítě.", + "network-traffic-label": "Síťový provoz", + "storage-as-percentage-description": "Zobrazit místo na disku v procentech místo absolutních hodnot.", + "storage-as-percentage-label": "Disk v procentech", + "storage-available-description": "Zobrazuje, kolik místa na disku je dostupné, místo kolik je využito.", + "storage-available-label": "Dostupné místo na disku", + "storage-usage-description": "Zobrazit informace o využití místa na disku.", + "storage-usage-label": "Využití úložiště", + "swap-usage-description": "Zobrazit využití odkládacího prostoru.", + "swap-usage-label": "Využití swapu", + "use-monospace-font-description": "Použít neproporcionální písmo pro konzistentní šířku znaků.", + "use-monospace-font-label": "Neproporcionální písmo", + "use-padding-description": "Doplní textové hodnoty mezerami, aby se zabránilo posunu rozložení.", + "use-padding-description-disabled-monospace-font": "Pro tuto funkci je potřeba neproporcionální písmo.", + "use-padding-description-disabled-vertical": "Svislý panel úloh nepodporuje doplňování textu.", + "use-padding-label": "Doplnit text" + }, + "taskbar": { + "colorize-icons-description": "Použít barvy motivu na ikony panelu úloh.", + "hide-mode-description": "Řídí chování widgetu, když nejsou žádná odpovídající okna.", + "hide-mode-label": "Režim skrytí", + "icon-scale-description": "Nastaví faktor škálování ikon panelu úloh.", + "icon-scale-label": "Škálování ikon", + "max-width-description": "Maximální šířka panelu úloh jako procento šířky obrazovky.", + "max-width-label": "Maximální šířka", + "only-active-workspaces-description": "Zobrazit pouze aplikace z aktivních pracovních ploch.", + "only-active-workspaces-label": "Jen z aktivních pracovních ploch", + "only-same-monitor-description": "Zobrazit pouze aplikace z monitoru, kde se panel nachází.", + "only-same-monitor-label": "Jen ze stejného monitoru", + "show-pinned-apps-description": "Zobrazit připnuté aplikace z docku v panelu úloh.", + "show-pinned-apps-label": "Zobrazit připnuté aplikace", + "show-title-description": "Zobrazit názvy oken v panelu úloh.", + "show-title-description-disabled": "Svislý panel úloh nepodporuje zobrazení názvů.", + "show-title-label": "Zobrazit název", + "smart-width-description": "Automaticky upravit šířku položek podle jejich počtu.", + "smart-width-label": "Chytrá šířka", + "title-width-description": "Nastavit šířku názvů oken v panelu úloh (v pixelech).", + "title-width-label": "Šířka názvu", + "title-width-reset-tooltip": "Obnovit šířku názvu" + }, + "tray": { + "chevron-color-description": "Použít barvy motivu na ikonu chevron zásuvky.", + "chevron-color-label": "Barva chevron", + "colorize-icons-description": "Použít barvy motivu na ikony zásobníku.", + "colorize-icons-label": "Obarvit ikony", + "drawer-enabled-description": "Pokud je povoleno, nepřipnuté položky zásobníku jsou zobrazeny v zásuvkovém panelu.
Pokud je vypnuto, všechny položky zásobníku jsou zobrazeny přímo.", + "drawer-enabled-label": "Povolit zásuvku", + "hide-passive-description": "Pokud je povoleno, položky zásobníku se stavem 'Pasivní' budou skryty.", + "hide-passive-label": "Skrýt pasivní položky" + }, + "volume": { + "display-mode-description": "Zvolte, jak chcete tuto hodnotu zobrazit." + }, + "workspace": { + "character-count-description": "Počet znaků zobrazených z názvů pracovních ploch (1–10).", + "character-count-label": "Počet znaků", + "empty-color-description": "Nastavit barvu pozadí prázdných pracovních ploch.", + "empty-color-label": "Barva prázdné pracovní plochy", + "enable-scrollwheel-description": "Přepínat mezi pracovními plochami pomocí kolečka myši.", + "enable-scrollwheel-label": "Kolečkem přepínat pracovní plochy", + "focused-color-description": "Nastavit barvu pozadí aktivní pracovní plochy.", + "focused-color-label": "Barva aktivní pracovní plochy", + "follow-focused-screen-description": "Zobrazit pracovní plochy z aktuálně aktivní obrazovky, nikoli z té, kde se panel nachází.", + "follow-focused-screen-label": "Sledovat aktivní obrazovku", + "font-weight-description": "Nastavit tloušťku písma textu v pracovní ploše.", + "font-weight-label": "Tloušťka písma", + "grouped-border-opacity-description": "Nastavit průhlednost okrajů kontejneru pracovní plochy.", + "grouped-border-opacity-label": "Průhlednost okraje", + "hide-unoccupied-description": "Nezobrazovat pracovní plochy bez oken.", + "hide-unoccupied-label": "Skrýt prázdné", + "label-mode-description": "Vybrat způsob zobrazení popisků pracovních ploch.", + "label-mode-label": "Režim popisků", + "occupied-color-description": "Nastavit barvu pozadí obsazených pracovních ploch.", + "occupied-color-label": "Barva obsazené pracovní plochy", + "pill-size-description": "Upravit velikost pilulek pracovní plochy.", + "pill-size-label": "Velikost pilulky", + "reverse-scrolling-description": "Obrátit směr přepínání pracovních ploch při posouvání.", + "reverse-scrolling-label": "Obrácené posouvání", + "show-applications-description": "Zobrazit ikony aplikací uvnitř každé pracovní plochy.", + "show-applications-hover-description": "Zobrazit ikony aplikací uvnitř pracovní plochy při najetí myší.", + "show-applications-hover-label": "Zobrazit aplikace při najetí", + "show-applications-label": "Zobrazit aplikace", + "show-badge-description": "Zobrazit odznak čísla pracovní plochy ve skupinovém režimu.", + "show-badge-label": "Zobrazit odznak pracovní plochy", + "show-labels-only-when-occupied-description": "Zobrazovat popisky pracovních ploch jen pokud obsahují okna.", + "show-labels-only-when-occupied-label": "Popisky jen při obsazení", + "unfocused-icons-opacity-description": "Nastavit průhlednost ikon neaktivních aplikací.", + "unfocused-icons-opacity-label": "Průhlednost neaktivních ikon" + } + }, + "battery": { + "all-batteries": "Všechny baterie (kombinovaně)", + "battery-health": "Zdraví baterie", + "battery-level": "Stav baterie", + "capacity-level": "Kapacita: {level}", + "charging-rate": "Rychlost nabíjení: {rate} W", + "discharging-rate": "Rychlost vybíjení: {rate} W", + "health": "Zdraví: {percent}%", + "inhibit-idle-description": "Udržuje systém v chodu.", + "no-battery-detected": "Nebyla detekována žádná baterie", + "plugged-in": "Připojen napájecí kabel", + "power-profile": "Profil napájení", + "time-left": "Zbývající čas: {time}", + "time-until-full": "Čas do úplného nabití: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Dostupná zařízení", + "blocked": "Blokováno", + "connected-devices": "Připojená zařízení", + "device-address": "Adresa zařízení", + "disabled": "Bluetooth je vypnuto", + "discoverable": "Viditelné", + "enable-message": "Zapněte Bluetooth pro zobrazení dostupných zařízení.", + "known-devices": "Známá zařízení", + "no-devices": "Žádná dostupná zařízení", + "paired-devices": "Spárovaná zařízení", + "pairing-mode": "Ujistěte se, že je vaše zařízení v režimu párování.", + "scanning": "Vyhledávání zařízení...", + "signal-text-excellent": "Signál: Výborný", + "signal-text-fair": "Signál: Přijatelný", + "signal-text-good": "Signál: Dobrý", + "signal-text-poor": "Signál: Slabý", + "signal-text-unknown": "Signál: Neznámý", + "signal-text-very-poor": "Signál: Velmi slabý" + } + }, + "changelog": { + "error": { + "fetch-failed": "Nelze načíst data changelogu. Zkuste to prosím znovu.", + "rate-limit": "Byl překročen limit požadavků na GitHub. Zkuste to znovu za několik minut." + }, + "panel": { + "buttons-discord": "Přidat se na Discord", + "buttons-dismiss": "OK", + "buttons-feedback": "Poskytnout zpětnou vazbu", + "empty": "Poznámky k vydání zatím nejsou dostupné.", + "highlight-title": "Hlavní novinky", + "released": "Vydáno {date}", + "subtitle-fresh": "Děkujeme za instalaci Noctalia! Zde je obsah tohoto buildu.", + "subtitle-updated": "Aktualizováno z {previousVersion}", + "title": "Co je nového ve verzi {version}", + "version": "Verze {version}", + "version-new-user": "Čerstvá instalace" + } + }, + "common": { + "actions": "Akce", + "active": "Aktivní", + "add": "Přidat", + "appearance": "Vzhled", + "apply": "Použít", + "auto-connect": "Automatické připojení", + "automation": "Automatizace", + "available": "Dostupné", + "back": "Zpět", + "battery": "Baterie", + "behavior": "Chování", + "bluetooth": "Bluetooth", + "brightness": "Jas", + "browse": "Procházet", + "calendar": "Kalendář", + "calendar-panel": "Panel kalendáře", + "cancel": "Zrušit", + "cards": "Karty", + "charging": "Nabíjení", + "clear": "Vymazat", + "clipboard": "Schránka", + "close": "Zavřít", + "color-muted": "Tlumená", + "colors": "Barvy", + "command": "Příkaz", + "confirm": "Potvrdit", + "connect": "Připojit", + "connected": "Připojeno", + "connecting": "Připojování...", + "continue": "Pokračovat", + "contributors": "Přispěvatelé", + "copied-to-clipboard": "Zkopírováno do schránky", + "countdown": "Odpočet", + "customize": "Přizpůsobit", + "date": "Datum", + "default": "Výchozí", + "delete": "Smazat", + "devices": "Zařízení", + "disabled": "Vypnuto", + "discharging": "Vybíjení", + "disconnect": "Odpojit", + "disconnected": "Odpojeno", + "disconnecting": "Odpojování...", + "display-mode": "Režim zobrazení", + "documentation": "Dokumentace", + "download": "Stáhnout", + "duration": "Trvání", + "dysfunctional": "Dysfunkční", + "edit": "Upravit", + "enabled": "Zapnuto", + "error": "Chyba", + "ethernet": "Ethernet", + "events": "Události", + "execute": "Spustit", + "faithful": "Věrný", + "focus": "Zaměření", + "font-weight-bold": "Tučné", + "font-weight-light": "Tenké", + "font-weight-medium": "Střední", + "font-weight-regular": "Normální", + "font-weight-semibold": "Polotučné", + "frequency": "Frekvence", + "gateway": "Brána", + "general": "Obecné", + "height": "Výška", + "hibernate": "Hibernace", + "history": "Historie", + "icon": "Ikona", + "idle": "Nečinnost", + "info": "Informace", + "input": "Vstup", + "install": "Nainstalovat", + "installed": "Nainstalováno", + "interface": "Rozhraní", + "internet": "Internet", + "keybind": "Klávesová zkratka", + "language": "Jazyk", + "loading": "Načítání...", + "local": "Místní", + "location": "Poloha", + "lock": "Zamknout", + "logout": "Odhlásit", + "look": "Vzhled", + "margins": "Okraje", + "media": "Média", + "media-player": "Přehrávač médií", + "memory": "Paměť", + "monitors": "Monitory", + "move-down": "Posunout dolů", + "move-up": "Posunout nahoru", + "network": "Síť", + "next": "Další", + "night-light": "Noční světlo", + "no": "Ne", + "no-results": "Žádné výsledky", + "no-summary": "Bez shrnutí", + "none": "Nic", + "not-found": "Nenalezeno", + "notifications": "Oznámení", + "official": "Oficiální", + "on-surface": "Na povrchu", + "output": "Výstup", + "pair": "Spárovat", + "paired": "Spárováno", + "pairing": "Párování...", + "panels": "Panely", + "password": "Heslo", + "pause": "Pozastavit", + "performance": "Výkon", + "pin": "Připnout", + "play": "Přehrát", + "polling": "Dotazování", + "position": "Pozice", + "previous": "Předchozí", + "primary": "Primární", + "random": "Náhodné", + "reboot": "Restartovat", + "reboot-to-uefi": "Restartovat do UEFI", + "record": "Zahájit nahrávání", + "refresh": "Obnovit", + "required": "(povinné)", + "reset": "Obnovit výchozí", + "reset-to-default": "Obnovit výchozí", + "result-count": "{count} výsledek", + "result-count-plural": "{count} výsledků", + "resume": "Obnovit", + "save": "Uložit", + "scanning": "Vyhledávání...", + "screen-corners": "Rohy obrazovky", + "search": "Hledat", + "second": "{count} sekunda", + "second-plural": "{count} sekund", + "secondary": "Sekundární", + "security": "Zabezpečení", + "select": "Vybrat", + "select-color": "Vybrat barvu", + "select-color-description": "Použít barvy motivu pro zvýraznění.", + "select-icon-color": "Vybrat barvu ikony", + "settings": "Nastavení", + "shortcuts": "Zkratky", + "shutdown": "Vypnout", + "signal": "Signál", + "sound": "Zvuk", + "sources": "Zdroje", + "start": "Spustit", + "stop": "Zastavit", + "supporters": "Podporovatelé", + "suspend": "Uspat", + "templates": "Šablony", + "tertiary": "Terciární", + "test": "Testovat", + "thresholds": "Prahové hodnoty", + "title": "Název", + "toast": "Vyskakovací oznámení", + "trusted": "Důvěryhodné", + "uninstall": "Odinstalovat", + "unknown": "Neznámé", + "unpair": "Zrušit párování", + "unpin": "Odepnout", + "update": "Aktualizovat", + "upload": "Nahrát", + "username": "Uživatelské jméno", + "userspace-reboot": "Restart uživatelského prostoru", + "version": "Verze", + "vibrant": "Živé", + "visualizer": "Vizualizér", + "volume": "Hlasitost", + "volumes": "Hlasitosti", + "wallpaper": "Tapeta", + "weather": "Počasí", + "weather-loading": "Načítání počasí...", + "weather-no-location": "Nastavte polohu v Nastavení, abyste viděli počasí", + "week": "Týden", + "widgets": "Widgety", + "width": "Šířka", + "wifi": "Wi-Fi", + "windows": "Okna", + "yes": "Ano" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Profil napájení", + "tooltip-disabled": "Pro použití profilů napájení nainstalujte power-profiles-daemon" + }, + "wifi": { + "label-disconnected": "Wi-Fi odpojeno", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Vždy skrýt", + "always-show": "Vždy zobrazit", + "force-open": "Vynutit otevření", + "on-hover": "Při najetí myší" + }, + "hide-modes": { + "auto-hide": "Automaticky skrýt", + "hidden": "Skrýt když prázdný", + "idle": "Skrýt při nečinnosti", + "non-exclusive": "Neexkluzivní", + "transparent": "Průhledný když prázdný", + "visible": "Vždy viditelný" + }, + "launcher": { + "categories": { + "all": "Vše", + "audiovideo": "Audio a video", + "chat": "Chat", + "development": "Vývoj", + "education": "Vzdělávání", + "emoji-activity": "Aktivita", + "emoji-animals": "Zvířata", + "emoji-flags": "Vlajky", + "emoji-food": "Jídlo a pití", + "emoji-nature": "Příroda", + "emoji-objects": "Objekty", + "emoji-people": "Lidé a tělo", + "emoji-recent": "Nedávné", + "emoji-symbols": "Symboly", + "emoji-travel": "Cestování a místa", + "game": "Hry", + "graphics": "Grafika", + "misc": "Různé", + "office": "Kancelář", + "pinned": "Připnuté", + "system": "Systém", + "webbrowser": "Webový prohlížeč" + }, + "providers": { + "applications": "Aplikace", + "calculator": "Kalkulačka", + "calculator-deprecated": "Příkaz >calc je zastaralý a brzy bude odebrán. Zadávejte matematické výrazy přímo do vyhledávání.", + "calculator-description": "Kalkulačka – vyhodnocení matematických výrazů", + "calculator-enter-expression": "Zadejte matematický výraz", + "calculator-press-enter-to-copy": "Stiskněte Enter pro zkopírování výsledku", + "clipboard": "Historie schránky", + "clipboard-clear-description": "Vymazat celou historii schránky", + "clipboard-clear-description-full": "Odebrat všechny položky z historie schránky", + "clipboard-clear-history": "Vymazat historii schránky", + "clipboard-delete": "Smazat položku schránky", + "clipboard-history-disabled": "Historie schránky je vypnuta", + "clipboard-history-disabled-description": "Povolte historii schránky v nastavení nebo nainstalujte cliphist", + "clipboard-loading": "Načítání historie schránky...", + "clipboard-search-description": "Prohledat historii schránky", + "command-description": "Spouštět příkazy shellu", + "emoji": "Výběr emoji", + "emoji-loading": "Načítání emoji...", + "emoji-loading-description": "Čekejte prosím", + "emoji-no-recent": "Zatím žádná nedávná emoji", + "emoji-search-description": "Vyhledat a zkopírovat emoji", + "settings-search-description": "Vyhledat a přejít na nastavení", + "windows-search-description": "Vyhledat a aktivovat otevřená okna" + } + }, + "lock-screen": { + "authenticating": "Ověřování...", + "password": "Zadejte heslo...", + "restart": "Restartovat", + "shut-down": "Vypnout" + }, + "notifications": { + "panel": { + "click-to-collapse": "Kliknutím sbalit", + "click-to-expand": "Kliknutím rozbalit", + "description": "Vaše oznámení se zobrazí zde po příchodu.", + "no-notifications": "Žádná oznámení" + }, + "range": { + "earlier": "Dříve", + "today": "Dnes", + "yesterday": "Včera" + }, + "time": { + "diff-d": "před 1 dnem", + "diff-dd": "před {diff} dny", + "diff-h": "před 1 hodinou", + "diff-hh": "před {diff} hodinami", + "diff-m": "před 1 minutou", + "diff-mm": "před {diff} minutami", + "now": "právě teď" + } + }, + "options": { + "bar": { + "density-comfortable": "Pohodlné", + "density-compact": "Kompaktní", + "density-default": "Výchozí", + "density-mini": "Mini", + "density-spacious": "Prostorné", + "display-mode-always-visible": "Vždy viditelný", + "type-floating": "Plovoucí", + "type-framed": "Orámovaný", + "type-simple": "Jednoduchý" + }, + "control-center": { + "quick-settings-style-classic": "Klasický", + "quick-settings-style-modern": "Moderní" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "Pohodlné", + "compact": "Kompaktní", + "default": "Výchozí" + }, + "launcher-view-mode": { + "grid": "Mřížka", + "list": "Seznam" + }, + "notification-density": { + "compact": "Kompaktní", + "default": "Výchozí" + }, + "scrolling-modes": { + "always": "Vždy posouvat", + "hover": "Posouvat při najetí", + "never": "Nikdy neposouvat" + }, + "session-menu-grid-layout": { + "grid": "Mřížka", + "single-row": "Jeden řádek" + }, + "settings-panel-mode": { + "attached": "Panel připojen k lištu", + "centered": "Vystředěný panel", + "window": "Samostatné okno" + }, + "visualizer-types": { + "linear": "Lineární", + "mirrored": "Zrcadlený", + "wave": "Vlna" + }, + "visualizer-visibility": { + "with-background": "Jen s pozadím" + }, + "workspace-labels": { + "index": "Index", + "index-and-name": "Index a název", + "name": "Název" + } + }, + "panels": { + "about": { + "become-supporter": "Stát se podporovatelem", + "changelog": "Zobrazit changelog", + "changelog-on-startup": "Zobrazit changelog při aktualizaci", + "changelog-on-startup-desc": "Automaticky zobrazit changelog při aktualizaci Noctalia", + "contributors-description": "Díky našemu {count} skvělému přispěvateli!", + "contributors-description-plural": "Díky našim {count} skvělým přispěvatelům!", + "copy-info": "Zkopírovat informace", + "debug-disabled": "Ladicí režim vypnut", + "debug-enabled": "Ladicí režim zapnut", + "info-copied": "Informace zkopírovány do schránky", + "noctalia-available": "Dostupné:", + "noctalia-desc": "Elegantní a minimalistický desktopový shell navržený pro Wayland, postavený na Quickshell.", + "noctalia-git-commit": "Git commit:", + "noctalia-installed-version": "Nainstalovaná verze:", + "noctalia-latest-version": "Nejnovější verze:", + "noctalia-qs-version": "Verze Noctalia QS:", + "noctalia-title": "Noctalia Shell", + "privacy-policy": "Zásady ochrany soukromí", + "support": "Podpořte nás", + "supporter-badge": "Podporovatel", + "supporters-desc": "Velké díky našemu úžasnému podporovateli!", + "supporters-desc-plural": "Velké díky našim {count} úžasným podporovatelům!", + "supporters-loading": "Načítání podporovatelů...", + "system-board": "Základní deska:", + "system-cpu": "CPU:", + "system-disk": "Disk:", + "system-gpu": "GPU:", + "system-host": "Hostitel:", + "system-install-hint": "Nainstalujte fastfetch pro zobrazení systémových informací", + "system-kernel": "Jádro:", + "system-loading": "Načítání systémových informací...", + "system-memory": "Paměť:", + "system-monitor": "Monitor:", + "system-not-installed": "fastfetch není nainstalován", + "system-os": "OS:", + "system-packages": "Balíčky:", + "system-product": "Produkt:", + "system-title": "Systémové informace", + "system-uptime": "Provozní doba:", + "system-wm": "Správce oken:", + "telemetry-data-copied": "Data telemetrie zkopírována do schránky", + "telemetry-desc": "Pomozte zlepšit Noctalia sdílením anonymních systémových informací (rozlišení obrazovky, compositor, distribuce). Odesláno jednou při startu, bez sledování, data automaticky smazána po 30 dnech.", + "telemetry-enabled": "Odesílat anonymní systémové informace", + "telemetry-show-data": "Zobrazit data", + "telemetry-title": "Soukromí", + "title": "O aplikaci", + "up-to-date": "Máte nejnovější verzi!", + "update-available": "Dostupná aktualizace", + "view-commit": "Zobrazit commit na GitHubu" + }, + "audio": { + "devices-desc": "Nastavit dostupná zvuková vstupní a výstupní zařízení.", + "devices-input-device-description": "Vybrat požadované zvukové vstupní zařízení.", + "devices-input-device-label": "Vstupní zařízení", + "devices-output-device-description": "Vybrat požadované zvukové výstupní zařízení.", + "devices-output-device-label": "Výstupní zařízení", + "devices-title": "Zvuková zařízení", + "external-mixer-description": "Zadejte příkaz nebo cestu k aplikaci pro spuštění externího audio mixéru.", + "external-mixer-label": "Příkaz externího audio mixéru", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Nastavit preferované a ignorované mediální aplikace.", + "media-excluded-player-description": "Přidat klíčová slova přehrávačů, které má systém ignorovat. Každé klíčové slovo na nový řádek.", + "media-excluded-player-label": "Vyloučený přehrávač", + "media-excluded-player-placeholder": "zadejte podřetězec a stiskněte +", + "media-frame-rate-description": "Vyšší snímkový kmitočet je plynulejší, ale využívá více zdrojů.", + "media-frame-rate-label": "Snímkový kmitočet", + "media-primary-player-description": "Zadejte klíčové slovo pro identifikaci hlavního přehrávače.", + "media-primary-player-label": "Primární přehrávač", + "media-primary-player-placeholder": "např. spotify, vlc, mpv", + "media-scrolling-speed-description": "Čas v sekundách pro posouvání názvu od začátku do konce.", + "media-scrolling-speed-label": "Rychlost posouvání", + "media-scrolling-title-description": "Povolit kontinuální posouvání pro dlouhé názvy médií.", + "media-scrolling-title-label": "Posouvání názvu", + "media-title": "Přehrávače médií", + "on-middle-clicked-description": "Příkaz spuštěný při kliknutí prostředním tlačítkem myši.", + "panel-applications-empty": "Žádné aplikace momentálně nepřehrávají zvuk", + "spectrum-mirrored-description": "Zrcadlit spektrum tak, aby nízké frekvence byly uprostřed. Při vypnutí jsou nízké frekvence vlevo a vysoké vpravo.", + "spectrum-mirrored-label": "Zrcadlit spektrum", + "title": "Zvuk", + "visualizer-type-description": "Vybrat typ vizualizace pro přehrávání médií.", + "visualizer-type-label": "Typ vizualizace", + "volumes-desc": "Upravit ovládání hlasitosti a úrovně zvuku.", + "volumes-feedback-sound-file-description": "Cesta ke zvukovému souboru přehrávanému při změně hlasitosti.", + "volumes-feedback-sound-file-label": "Zvukový soubor zpětné vazby hlasitosti", + "volumes-feedback-sound-file-select-title": "Vybrat zvukový soubor zpětné vazby hlasitosti", + "volumes-input-volume-description": "Úroveň hlasitosti mikrofonu.", + "volumes-mute-input-description": "Ztlumit výchozí zvukový vstup (mikrofon).", + "volumes-mute-input-label": "Ztlumit zvukový vstup", + "volumes-mute-output-description": "Ztlumit hlavní zvukový výstup systému.", + "volumes-mute-output-label": "Ztlumit zvukový výstup", + "volumes-output-volume-description": "Úroveň hlasitosti celého systému.", + "volumes-step-size-description": "Upravit velikost kroku pro změny hlasitosti (kolečko myši, klávesové zkratky).", + "volumes-step-size-label": "Velikost kroku hlasitosti", + "volumes-volume-feedback-description": "Přehrát zpětnovazební zvuk při změně hlasitosti.", + "volumes-volume-feedback-label": "Přehrát zvuk zpětné vazby hlasitosti", + "volumes-volume-overdrive-description": "Umožnit zvýšení hlasitosti nad 100 %. Nemusí být podporováno všemi zařízeními.", + "volumes-volume-overdrive-label": "Povolit překročení hlasitosti" + }, + "bar": { + "appearance-auto-hide-delay-description": "Čas před skrytím lišty po opuštění myší", + "appearance-auto-hide-delay-label": "Zpoždění skrytí", + "appearance-auto-hide-exclusive-note": "Poznámka: Exkluzivní zóna je automaticky zakázána při aktivním automatickém skrývání", + "appearance-auto-show-delay-description": "Čas před zobrazením lišty při najetí myší na okraj", + "appearance-auto-show-delay-label": "Zpoždění zobrazení", + "appearance-background-opacity-description": "Nastavit průhlednost pozadí lišty.", + "appearance-background-opacity-label": "Průhlednost pozadí lišty", + "appearance-capsule-color-description": "Vybrat barvu pro kapsle lišty, nebo ponechat výchozí barvu povrchu.", + "appearance-capsule-color-label": "Barva kapsle", + "appearance-capsule-opacity-description": "Nastavit průhlednost pozadí widgetů při zobrazení kapsle.", + "appearance-capsule-opacity-label": "Průhlednost kapsle", + "appearance-content-padding-description": "Upravit odsazení mezi okraji lišty a widgety.", + "appearance-content-padding-label": "Odsazení obsahu", + "appearance-density-description": "Upravit odsazení lišty pro kompaktní nebo prostorný vzhled.", + "appearance-density-label": "Hustota lišty", + "appearance-desc": "Přizpůsobit vzhled a pozici lišty.", + "appearance-display-mode-description": "Vybrat, kdy je lišta viditelná.", + "appearance-enable-exclusion-zone-inset-description": "Zmenšit exkluzivní zónu o 1 fyzický pixel, aby okna zarovnaná na okraj přesně podtékala pod okraj lišty.", + "appearance-enable-exclusion-zone-inset-label": "Vložit exkluzivní zónu", + "appearance-floating-description": "Zobrazit lištu jako plovoucí 'pilulku'.", + "appearance-floating-label": "Plovoucí lišta", + "appearance-font-scale-description": "Upravit měřítko velikosti písma textu zobrazeného v liště.", + "appearance-font-scale-label": "Měřítko písma", + "appearance-frame-radius": "Vnitřní poloměr", + "appearance-frame-settings-description": "Upravit tloušťku rámu a poloměr vnitřních rohů", + "appearance-frame-settings-label": "Nastavení rámu", + "appearance-frame-thickness": "Tloušťka", + "appearance-hide-on-overview-description": "Skrýt lištu a zavřít panely při aktivním přehledu compositoru.", + "appearance-hide-on-overview-label": "Skrýt lištu v přehledu", + "appearance-margins-description": "Upravit okraje kolem plovoucí lišty.", + "appearance-margins-horizontal": "Vodorovný okraj", + "appearance-margins-vertical": "Svislý okraj", + "appearance-outer-corners-description": "Zobrazit vnější zaoblené rohy na liště.", + "appearance-outer-corners-label": "Vnější rohy", + "appearance-position-description": "Vybrat umístění lišty na obrazovce.", + "appearance-position-label": "Pozice lišty", + "appearance-show-capsule-description": "Zobrazit pozadí widgetů.", + "appearance-show-capsule-label": "Zobrazit kapsle", + "appearance-show-on-workspace-switch-description": "Automaticky krátce zobrazit lištu při přepnutí pracovní plochy.", + "appearance-show-on-workspace-switch-label": "Zobrazit lištu při přepnutí pracovní plochy", + "appearance-show-outline-description": "Zobrazit viditelný okraj kolem každého widgetu.", + "appearance-show-outline-label": "Zobrazit obrysy widgetů", + "appearance-type-description": "Vybrat styl lišty: Jednoduchý, Plovoucí nebo Orámovaný.", + "appearance-type-label": "Typ lišty", + "appearance-use-separate-opacity-description": "Povolit použití samostatné hodnoty průhlednosti pro pozadí lišty.", + "appearance-use-separate-opacity-label": "Použít samostatnou průhlednost lišty", + "appearance-widget-spacing-description": "Upravit mezery mezi widgety v liště.", + "appearance-widget-spacing-label": "Mezery widgetů", + "behavior-middle-click-command-description": "Příkaz spuštěný při středním kliknutí.", + "behavior-middle-click-command-label": "Příkaz středního kliknutí", + "behavior-middle-click-command-placeholder": "niri msg action toggle-overview", + "behavior-middle-click-description": "Vybrat akci středního kliknutí na prázdné oblasti lišty.", + "behavior-middle-click-follow-mouse-description": "Otevřít vybraný panel středního kliknutí na pozici kurzoru.", + "behavior-middle-click-follow-mouse-label": "Střední kliknutí sleduje myš", + "behavior-middle-click-label": "Akce středního kliknutí na lištu", + "behavior-right-click-command-description": "Příkaz spuštěný při pravém kliknutí.", + "behavior-right-click-command-label": "Příkaz pravého kliknutí", + "behavior-right-click-command-placeholder": "notify-send \"Pravé kliknutí\"", + "behavior-right-click-description": "Vybrat akci pravého kliknutí na prázdné oblasti lišty.", + "behavior-right-click-follow-mouse-description": "Otevřít vybraný panel pravého kliknutí na pozici kurzoru.", + "behavior-right-click-follow-mouse-label": "Pravé kliknutí sleduje myš", + "behavior-right-click-label": "Akce pravého kliknutí na lištu", + "behavior-wheel-wrap-description": "Pokud je povoleno, posouvání pokračuje od poslední položky na první.", + "behavior-wheel-wrap-label": "Přetočení", + "behavior-workspace-scroll-description": "Vybrat akci kolečka myši na prázdné oblasti lišty.", + "behavior-workspace-scroll-label": "Akce kolečka myši na lištu", + "behavior-workspace-scroll-option-content": "Obsah", + "behavior-workspace-scroll-option-workspace": "Pracovní plocha", + "monitor-configure-widgets": "Konfigurovat widgety", + "monitor-override-settings": "Přepsat globální nastavení", + "monitor-override-settings-description": "Použít vlastní nastavení pro tento monitor.", + "monitor-reset-all": "Obnovit vše", + "monitor-widgets-title": "Konfigurace widgetů – {monitor}", + "monitors-desc": "Zobrazit lištu na konkrétních monitorech. Výchozí je zobrazení na všech.", + "monitors-desc-new": "Nastavit, na kterých monitorech se lišta zobrazuje, a přizpůsobit nastavení pro každý monitor.", + "monitors-title": "Zobrazení na monitorech", + "title": "Lišta", + "tray-blacklist-description": "Přidat pravidla výjimek zásobníku, podporuje zástupné znaky (*).", + "tray-blacklist-label": "Černá listina", + "tray-blacklist-placeholder": "např. nm-applet, Fcitx*", + "tray-pin-application": "Připnout aplikaci", + "tray-unpin-application": "Odepnout aplikaci", + "use-global-widgets": "Použít globální widgety", + "widgets-desc": "Přetáhněte widgety pro změnu jejich pořadí. Pomocí kontextového menu je přesuňte mezi sekcemi nebo odeberte.", + "widgets-title": "Umístění widgetů" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Generovat barevná schémata z vaší tapety. Automaticky extrahuje barvy pro vytvoření soudržného motivu.", + "color-source-use-wallpaper-colors-label": "Použít barvy tapety", + "dark-mode-mode-description": "Povolí automatické přepínání mezi světlým a tmavým režimem.", + "dark-mode-mode-label": "Rozvrh tmavého režimu", + "dark-mode-mode-manual": "Ručně", + "dark-mode-mode-off": "Vypnuto", + "dark-mode-switch-description": "Přepne na tmavší motiv pro pohodlnější noční sledování.", + "delete-error-description": "Nepodařilo se smazat {scheme}", + "delete-error-title": "Mazání selhalo", + "delete-success-description": "Úspěšně smazáno {scheme}", + "delete-success-title": "Barevné schéma smazáno", + "download-button": "Stáhnout další", + "download-downloading": "Stahování...", + "download-empty": "Žádná barevná schémata nejsou dostupná", + "download-error-api-error": "Chyba API: {status}", + "download-error-description": "Nepodařilo se stáhnout {scheme}", + "download-error-download-failed": "Stahování selhalo s kódem: {code}", + "download-error-invalid-response": "Neplatný formát odpovědi API", + "download-error-no-files": "Pro schéma nebyly nalezeny žádné soubory", + "download-error-parse-failed": "Nepodařilo se zpracovat odpověď API: {error}", + "download-error-rate-limit": "Překročen limit požadavků GitHub API", + "download-error-title": "Stahování selhalo", + "download-fetching": "Načítání dostupných barevných schémat...", + "download-success-description": "Úspěšně staženo {scheme}", + "download-success-title": "Barevné schéma staženo", + "download-title": "Stáhnout barevná schémata", + "method-description": { + "content": "Schéma Material Design s vysokou věrností extrakce barev, které úzce odpovídá skutečným barvám zdrojového obsahu.", + "dysfunctional": "Jako Věrný, ale jako primární vybere druhou nejdominantnější rodinu barev.", + "faithful": "Snaží se zůstat blízko zdrojové barvě při generování harmonické palety.", + "fruit-salad": "Schéma Material Design produkující hravou, živou paletu s různorodými odstíny.", + "monochrome": "Schéma Material Design používající jednobarevnou šedou škálu s minimálním chromatickým obsahem.", + "muted": "Generuje měkké, nenasycené tóny pro klidný a nenápadný vzhled.", + "rainbow": "Schéma Material Design generující barvy napříč celým spektrem pro maximální barevnou rozmanitost.", + "tonal-spot": "Výchozí algoritmus Material Design. Vytváří vyvážené palety pomocí tónových variací zdrojové barvy.", + "vibrant": "Maximalizuje sytost barev pro výrazné, vizuálně silné palety s živými tóny." + }, + "predefined-desc": "Vybrat z kolekce předdefinovaných barevných schémat.", + "predefined-generate-templates-label": "Generovat šablony pro předdefinovaná schémata", + "predefined-title": "Předdefinovaná barevná schémata", + "sync-gsettings-description": "Přizpůsobit systémové téma aktivní světlé nebo tmavé variantě.", + "sync-gsettings-label": "Synchronizovat téma systému", + "templates-desc": "Aplikovat barvy na externí aplikace.", + "templates-filter-description": "Zobrazit šablony z konkrétní kategorie.", + "templates-filter-label": "Filtrovat podle kategorie", + "templates-misc-description": "Vytvořte vlastní šablony.", + "templates-misc-label": "Pokročilé", + "templates-misc-user-templates-description": "Povolte jen pokud víte, co děláte — viz online dokumentace.", + "templates-misc-user-templates-label": "Povolit uživatelské šablony", + "templates-none-detected": "Nic nedetekováno", + "templates-write-path": "Zapisuje do: {filepath}", + "title": "Barevné schéma", + "wallpaper-method-description": "Vybrat oblíbenou metodu generování palety.", + "wallpaper-method-label": "Metoda generování palety", + "wallpaper-monitor-source-description": "Vybrat monitor pro extrakci barev tapety.", + "wallpaper-monitor-source-label": "Zdroj pro generování barev" + }, + "connections": { + "authentication-required": "Vyžadováno ověření", + "bluetooth-auto-connect-description": "Automaticky se připojit k důvěryhodným spárovaným zařízením při zapnutí Bluetooth.", + "bluetooth-auto-connect-label": "Automaticky připojit spárovaná zařízení", + "bluetooth-devices-unnamed": "Nepojmenovaná zařízení nejsou zobrazena.", + "bluetooth-discoverable": "Toto zařízení je viditelné jako {hostName}, dokud je tato záložka nastavení otevřena.", + "bluetooth-rssi-polling-description": "Pravidelně vzorkovat RSSI připojených zařízení přes bluetoothctl. Nemusí být dostupné pro všechna zařízení; při zapnutí využívá minimální prostředky.", + "bluetooth-rssi-polling-interval-description": "Nastavit, jak často se aktualizuje síla signálu připojených zařízení.", + "bluetooth-rssi-polling-interval-label": "Interval dotazování", + "bluetooth-rssi-polling-label": "Dotazování na signál Bluetooth", + "disable-discoverability-description": "Skrýt zařízení před okolními Bluetooth zařízeními.", + "disable-discoverability-label": "Vypnout viditelnost zařízení", + "hide-unnamed-devices-description": "Skrýt zařízení, která se zobrazují pouze jako Bluetooth adresy.", + "hide-unnamed-devices-label": "Skrýt nepojmenovaná zařízení", + "pin-instructions": "Zadejte prosím PIN kód zobrazený na vašem zařízení.", + "title": "Připojení", + "wifi-header-text": "Použijte Wi-Fi pro zobrazení dostupných sítí a připojení k internetu." + }, + "control-center": { + "cards-desc": "Přizpůsobit, které ovládací prvky se zobrazují v řídicím centru a v jakém pořadí.", + "desc": "Nastavit pozici a chování panelu řídicího centra.", + "open-at-mouse-description": "Při pravém kliknutí na prázdnou oblast lišty otevřít řídicí centrum u kurzoru. Při vypnutí se použije nastavení pozice výše.", + "open-at-mouse-label": "Otevřít u kurzoru při pravém kliknutí", + "position-description": "Vybrat, kde se panel řídicího centra zobrazí při otevření.", + "shortcuts-custom-button-command-description": "Příkaz spuštěný při kliknutí na tlačítko.", + "shortcuts-custom-button-enable-on-state-logic-description": "Povolit druhou ikonu a stav 'zapnuto' na základě příkazu kontroly.", + "shortcuts-custom-button-enable-on-state-logic-label": "Povolit logiku stavu zapnuto", + "shortcuts-custom-button-icon-description": "Vybrat ikonu z knihovny.", + "shortcuts-custom-button-on-clicked-label": "Příkaz levého kliknutí", + "shortcuts-custom-button-on-middle-clicked-description": "Příkaz spuštěný při kliknutí prostředním tlačítkem myši.", + "shortcuts-custom-button-on-state-command-description": "Příkaz pro kontrolu, zda má být tlačítko ve stavu 'zapnuto'. Vrátí 0 pro zapnuto, nenulové pro vypnuto.", + "shortcuts-custom-button-on-state-command-label": "Příkaz kontroly stavu zapnuto", + "shortcuts-custom-button-on-state-icon-description": "Ikona tlačítka ve stavu 'zapnuto'.", + "shortcuts-custom-button-on-state-icon-label": "Ikona stavu zapnuto", + "shortcuts-custom-button-state-checks-add": "Přidat kontrolu stavu", + "shortcuts-custom-button-state-checks-command": "Příkaz pro tuto kontrolu stavu", + "shortcuts-custom-button-state-checks-label": "Kontroly stavu", + "shortcuts-custom-button-state-checks-remove": "Odebrat", + "shortcuts-custom-button-tooltip-description": "Tooltip zobrazený při najetí na tlačítko.", + "shortcuts-custom-button-tooltip-label": "Tooltip", + "shortcuts-title": "Widgety zkratek", + "system-monitor-disk-path-description": "Vybrat přípojný bod disku, který má sledovat karta systémového monitoru v řídicím centru.", + "system-monitor-disk-path-label": "Cesta k disku systémového monitoru", + "title": "Řídicí centrum" + }, + "desktop-widgets": { + "clock-enabled-description": "Zobrazit widget hodin na ploše.", + "clock-enabled-label": "Povolit widget hodin", + "clock-format-label": "Formát", + "clock-height-description": "Výška widgetu hodin v pixelech.", + "clock-height-label": "Výška", + "clock-minimal-mode-description": "Použít minimální zobrazení hodin s časem a datem.", + "clock-minimal-mode-label": "Minimální režim", + "clock-rounded-corners-description": "Použít zaoblené rohy pro pozadí widgetu.", + "clock-rounded-corners-label": "Zaoblené rohy", + "clock-show-background-description": "Zobrazit kontejner pozadí pro widget hodin.", + "clock-show-background-label": "Zobrazit pozadí", + "clock-show-date-description": "Zobrazit aktuální datum pod časem.", + "clock-show-date-label": "Zobrazit datum", + "clock-show-seconds-description": "Zobrazit sekundy v čase.", + "clock-show-seconds-label": "Zobrazit sekundy", + "clock-style-analog": "Analogový", + "clock-style-binary": "Binární", + "clock-style-description": "Vybrat styl zobrazení hodin.", + "clock-style-digital": "Digitální", + "clock-style-label": "Styl hodin", + "clock-style-minimal": "Minimální", + "clock-width-description": "Šířka widgetu hodin v pixelech.", + "cpu-intensive-note": "Widgety označené ! využívají více CPU a měly by být povoleny uvážlivě.", + "edit-mode-button-label": "Vstoupit do režimu úprav", + "edit-mode-controls-explanation": "Levé kliknutí a přetažení: Přesunout nebo změnit velikost widgetu.\nPravé kliknutí: Otevřít možnosti kontextového menu.", + "edit-mode-description": "Povolit režim úprav pro přesouvání a přemísťování widgetů plochy. Při aktivaci widgety zobrazí obrys pro přetažení.", + "edit-mode-exit-button": "Opustit režim úprav", + "edit-mode-grid-snap-label": "Přichytit k mřížce", + "edit-mode-grid-snap-scale-label": "Měřítko přichycení k mřížce", + "edit-mode-label": "Režim úprav", + "enabled-description": "Povolit nebo zakázat widgety plochy úplně.", + "enabled-label": "Povolit widgety plochy", + "general-desc": "Nastavit widgety zobrazené na ploše.", + "general-title": "Widgety plochy", + "media-player-enabled-description": "Zobrazit widget přehrávače médií na ploše.", + "media-player-enabled-label": "Povolit widget přehrávače médií", + "media-player-rounded-corners-description": "Povolit zaoblené rohy na okrajích widgetu.", + "media-player-show-album-art-description": "Zobrazit obal alba a informace o stopě (název a interpret).", + "media-player-show-album-art-label": "Zobrazit obal alba a název", + "media-player-show-background-description": "Zobrazit kontejner pozadí.", + "media-player-show-buttons-description": "Zobrazit tlačítka ovládání médií (přehrát/pozastavit, předchozí, další).", + "media-player-show-buttons-label": "Zobrazit tlačítka", + "media-player-show-visualizer-description": "Zobrazit překryv audio vizualizéru.", + "media-player-visualizer-type-description": "Vybrat typ vizualizace.", + "overview-enabled-description": "Zobrazit widgety plochy při otevřeném přehledu compositoru.", + "overview-enabled-label": "Zobrazit v přehledu", + "system-stat-layout-bottom": "Dole", + "system-stat-layout-description": "Vybrat způsob zobrazení legendy vůči grafu.", + "system-stat-layout-label": "Rozložení", + "system-stat-layout-side": "Po straně", + "system-stat-rounded-corners-description": "Použít zaoblené rohy pro pozadí widgetu.", + "system-stat-rounded-corners-label": "Zaoblené rohy", + "system-stat-show-background-description": "Zobrazit kontejner pozadí pro widget systémových statistik.", + "system-stat-show-background-label": "Zobrazit pozadí", + "system-stat-stat-type-description": "Vybrat, která systémová statistika se zobrazí.", + "system-stat-stat-type-label": "Typ statistiky", + "title": "Widgety plochy", + "weather-enabled-description": "Zobrazit widget počasí na ploše.", + "weather-enabled-label": "Povolit widget počasí", + "weather-show-background-description": "Zobrazit kontejner pozadí pro widget počasí." + }, + "display": { + "monitors-backlight-device-auto-option": "Výchozí", + "monitors-backlight-device-description": "Vybrat zařízení podsvícení pro tento výstup.", + "monitors-backlight-device-label": "Zařízení podsvícení", + "monitors-brightness-step-description": "Upravit velikost kroku pro změny jasu (kolečko myši a klávesové zkratky).", + "monitors-brightness-step-label": "Velikost kroku jasu", + "monitors-brightness-unavailable-ddc-disabled": "Ovládání jasu není dostupné. Povolte Podpora externího jasu pro ovládání jasu tohoto displeje.", + "monitors-brightness-unavailable-generic": "Ovládání jasu pro tento displej není dostupné.", + "monitors-desc": "Upravit nastavení jasu pro každý displej.", + "monitors-enforce-minimum-description": "Řeší problém s úplným vypnutím podsvícení na některých displejích při 0 % jasu.", + "monitors-enforce-minimum-label": "Vynutit minimální jas (1 %)", + "monitors-external-brightness-description": "Povolit podporu DDCUtil pro ovládání jasu externích displejů přes protokol DDC/CI.", + "monitors-external-brightness-label": "Podpora externího jasu", + "monitors-global-brightness-description": "Upravit jas všech monitorů najednou.", + "monitors-global-brightness-label": "Všechny monitory", + "monitors-title": "Nastavení pro každý monitor", + "night-light-auto-schedule-description": "Na základě času západu a východu slunce v {location} – doporučeno.", + "night-light-auto-schedule-label": "Automatický rozvrh", + "night-light-desc": "Snižte emisi modrého světla pro lepší spánek a méně namáhané oči.", + "night-light-enable-description": "Aplikovat teplý barevný filtr pro snížení emise modrého světla.", + "night-light-enable-label": "Povolit noční světlo", + "night-light-force-activation-description": "Ignoruje rozvrh a okamžitě aplikuje noční filtr.", + "night-light-force-activation-label": "Vynutit aktivaci", + "night-light-manual-schedule-description": "Nastavit vlastní časy východu a západu slunce.", + "night-light-manual-schedule-label": "Ruční rozvrh", + "night-light-manual-schedule-select-start": "Vybrat čas začátku", + "night-light-manual-schedule-select-stop": "Vybrat čas konce", + "night-light-manual-schedule-sunrise": "Čas východu slunce", + "night-light-manual-schedule-sunset": "Čas západu slunce", + "night-light-temperature-day": "Den", + "night-light-temperature-day-description": "Řídí teplotu barvy během dne.", + "night-light-temperature-description": "Nastavit barevnou teplotu pro noc a den.", + "night-light-temperature-label": "Barevná teplota", + "night-light-temperature-night": "Noc", + "night-light-temperature-night-description": "Řídí teplotu barvy během noci.", + "title": "Displej" + }, + "dock": { + "appearance-background-opacity-description": "Upravit průhlednost pozadí docku.", + "appearance-border-radius-description": "Upravit poloměr rohů docku.", + "appearance-border-radius-label": "Poloměr rohů", + "appearance-colorize-icons-description": "Použít barvy motivu na ikony aplikací docku (pouze neaktivní aplikace).", + "appearance-colorize-icons-label": "Obarvit ikony", + "appearance-dead-opacity-description": "Upravit průhlednost ikon aplikací, které nejsou spuštěny.", + "appearance-dead-opacity-label": "Průhlednost neaktivních", + "appearance-desc": "Přizpůsobit chování a vzhled docku.", + "appearance-display-auto-hide": "Automaticky skrýt", + "appearance-display-description": "Vybrat způsob chování docku.", + "appearance-display-exclusive": "Exkluzivní", + "appearance-dock-indicator-description": "Zobrazit malý indikátor při skrytém docku.", + "appearance-dock-indicator-label": "Indikátor docku", + "appearance-floating-distance-description": "Nastavit vzdálenost mezi dockem a okrajem obrazovky.", + "appearance-floating-distance-label": "Plovoucí vzdálenost docku", + "appearance-group-apps-description": "Seskupit více oken stejné aplikace do jedné položky docku.", + "appearance-group-apps-label": "Seskupit stejné aplikace", + "appearance-group-click-action-cycle": "Cyklovat okna", + "appearance-group-click-action-description": "Vybrat akci levého kliknutí pro seskupené aplikace.", + "appearance-group-click-action-label": "Akce kliknutí na skupinu", + "appearance-group-click-action-list": "Otevřít seznam oken", + "appearance-group-context-menu-mode-description": "Vybrat chování kontextového menu pro seskupené aplikace.", + "appearance-group-context-menu-mode-extended": "Rozšířené", + "appearance-group-context-menu-mode-label": "Režim menu skupiny", + "appearance-group-context-menu-mode-list": "Seznam oken", + "appearance-group-indicator-style-description": "Vybrat způsob zobrazení indikátorů seskupených spuštěných aplikací.", + "appearance-group-indicator-style-dots": "Tečky", + "appearance-group-indicator-style-label": "Styl indikátoru skupiny", + "appearance-group-indicator-style-number": "Číslo", + "appearance-hide-show-speed-description": "Upravit rychlost animace skrytí/zobrazení docku.", + "appearance-hide-show-speed-label": "Rychlost skrytí/zobrazení", + "appearance-icon-size-description": "Upravit celkovou velikost docku.", + "appearance-icon-size-label": "Velikost docku", + "appearance-inactive-indicators-description": "Zobrazit indikační pilulky pro všechny aplikace, nejen pro aktivní.", + "appearance-inactive-indicators-label": "Indikátory spuštěných", + "appearance-indicator-color-description": "Vybrat barvu indikátoru skrytého docku.", + "appearance-indicator-color-label": "Barva indikátoru", + "appearance-indicator-opacity-description": "Upravit průhlednost indikátoru skrytého docku.", + "appearance-indicator-opacity-label": "Průhlednost indikátoru", + "appearance-indicator-thickness-description": "Přepnout na silnější indikátor skrytého docku (6 px místo 3 px).", + "appearance-indicator-thickness-label": "Silnější indikátor", + "appearance-launcher-icon-description": "Vybrat ikonu použitou pro spouštěč docku.", + "appearance-launcher-icon-label": "Ikona spouštěče", + "appearance-launcher-position-description": "Vybrat, kde se ikona spouštěče zobrazí v docku.", + "appearance-launcher-position-end": "Konec", + "appearance-launcher-position-label": "Pozice spouštěče", + "appearance-launcher-position-start": "Začátek", + "appearance-launcher-use-distro-logo-description": "Použít logo vaší distribuce jako ikonu spouštěče docku.", + "appearance-launcher-use-distro-logo-label": "Použít logo distribuce místo ikony", + "appearance-pinned-static-description": "Vždy posunout ikony připnutých aplikací vlevo ve statickém pořadí.", + "appearance-pinned-static-label": "Statické připnuté aplikace", + "appearance-position-description": "Vybrat, kde se dock zobrazí na obrazovce.", + "appearance-position-label": "Pozice", + "appearance-show-launcher-icon-description": "Zobrazit ikonu spouštěče aplikací v docku.", + "appearance-show-launcher-icon-label": "Zobrazit spouštěč aplikací", + "appearance-sit-on-frame-description": "Zarovnat dock uvnitř okraje rámu místo umístění nad ním.", + "appearance-sit-on-frame-label": "Dock na rámu", + "appearance-type-attached": "Připojen", + "appearance-type-description": "Vybrat mezi plovoucí pilulkou nebo statickou lištou připojenou k okraji.", + "appearance-type-floating": "Plovoucí", + "appearance-type-label": "Styl docku", + "enabled-description": "Zobrazit nebo skrýt dock úplně.", + "enabled-label": "Povolit dock", + "monitors-desc": "Zobrazit dock na konkrétních monitorech. Výchozí je zobrazení na všech.", + "monitors-only-same-monitor-description": "Zobrazit pouze aplikace z monitoru, kde se dock nachází.", + "monitors-only-same-monitor-label": "Pouze aplikace ze stejného monitoru", + "monitors-title": "Zobrazení na monitorech", + "title": "Dock" + }, + "general": { + "copy-settings": "Zkopírovat nastavení", + "fonts-default-description": "Hlavní písmo používané v celém rozhraní.", + "fonts-default-label": "Výchozí písmo", + "fonts-default-placeholder": "Vybrat výchozí písmo...", + "fonts-default-scale-description": "Zvětšit nebo zmenšit velikost standardního textu.", + "fonts-default-scale-label": "Velikost výchozího písma", + "fonts-default-search-placeholder": "Hledat písmo...", + "fonts-desc": "Vybrat písma používaná v celém rozhraní.", + "fonts-monospace-description": "Neproporcionální písmo používané pro čísla a statistiky.", + "fonts-monospace-label": "Neproporcionální písmo", + "fonts-monospace-placeholder": "Vybrat neproporcionální písmo...", + "fonts-monospace-scale-description": "Zvětšit nebo zmenšit velikost neproporcionálního textu.", + "fonts-monospace-scale-label": "Velikost neproporcionálního písma", + "fonts-monospace-search-placeholder": "Hledat neproporcionální písmo...", + "fonts-title": "Písma", + "keybinds-conflict-description": "Kombinace kláves je již přiřazena k {action}.", + "keybinds-conflict-title": "Konflikt klávesové zkratky", + "keybinds-description": "Nastavit globální navigační klávesy pro panely a spouštěč.", + "keybinds-down": "Přesunout dolů", + "keybinds-enter": "Potvrdit / Akce", + "keybinds-escape": "Zavřít / Zpět", + "keybinds-left": "Přesunout vlevo", + "keybinds-modifier-description": "Zkratky musí obsahovat modifikační klávesu (Ctrl nebo Alt).", + "keybinds-modifier-title": "Vyžadován modifikátor", + "keybinds-remove": "Odebrat / Smazat", + "keybinds-right": "Přesunout vpravo", + "keybinds-title": "Navigační klávesové zkratky", + "keybinds-up": "Přesunout nahoru", + "language-desc": "Vybrat preferovaný jazyk aplikace.", + "language-select-auto-detect": "Automaticky", + "language-select-description": "Vybrat jazyk používaný v rozhraní aplikace.", + "language-select-label": "Jazyk aplikace", + "launch-setup-wizard": "Průvodce nastavením", + "profile-desc": "Upravit uživatelské údaje a avatar.", + "profile-picture-description": "Váš profilový obrázek zobrazovaný v celém rozhraní.", + "profile-picture-label": "Profilový obrázek", + "profile-select-avatar": "Vybrat obrázek avatara", + "profile-title": "Profil", + "profile-tooltip": "Profilový obrázek", + "reverse-scrolling-description": "Obrátit interpretovaný směr posouvání", + "reverse-scrolling-label": "Obrácené posouvání", + "screen-corners-desc": "Přizpůsobit zaoblení rohů obrazovky a vizuální efekty.", + "screen-corners-radius-description": "Upravit zaoblení rohů obrazovky.", + "screen-corners-radius-label": "Poloměr rohů obrazovky", + "screen-corners-radius-reset": "Obnovit poloměr rohů obrazovky", + "screen-corners-show-corners-description": "Zobrazit zaoblené rohy na okrajích obrazovky.", + "screen-corners-show-corners-label": "Zobrazit rohy obrazovky", + "screen-corners-solid-black-description": "Použít plnou černou místo barvy pozadí lišty.", + "screen-corners-solid-black-label": "Plně černé rohy", + "screen-corners-title": "Rohy obrazovky", + "settings-copied": "Nastavení zkopírováno do schránky", + "smooth-scrolling-description": "Animovat posouvání seznamů pro plynulejší zážitek s kolečkem.", + "smooth-scrolling-label": "Plynulé posouvání", + "tab-basics": "Základy", + "tab-keybinds": "Klávesové zkratky" + }, + "hooks": { + "color-generation-description": "Příkaz spuštěný po dokončení generování barev (např. po motivu na základě tapety).", + "color-generation-label": "Barvy vygenerovány", + "color-generation-placeholder": "např. notify-send \"Barvy\" \"Vygenerovány\"", + "info-command-info-description": "• Příkazy jsou spouštěny přes shell (sh -lc)
• Příkazy běží na pozadí (odpojen)
• Tlačítka testu spustí s aktuálními hodnotami", + "info-parameters-description": "• Hook tapety: $1 = cesta k tapetě, $2 = název obrazovky, $3 = motiv (dark/light)
• Hook generování barev: $1 = motiv (dark/light)
• Hook přepnutí motivu: $1 = true/false (stav tmavého režimu)
• Hooky zamknutí/odemknutí: $1 = lock/unlock (stav uzamčení)
• Hooky výkonnostního režimu: Bez parametrů
• Hook relace: $1 = akce (shutdown/reboot)", + "info-parameters-label": "Dostupné parametry", + "noctalia-started-description": "Příkaz spuštěný po dokončení načítání Noctalia.", + "noctalia-started-label": "Noctalia spuštěna", + "noctalia-started-placeholder": "např. notify-send 'Noctalia načtena'", + "performance-mode-disabled-description": "Příkaz spuštěný při vypnutí výkonnostního režimu Noctalia.", + "performance-mode-disabled-label": "Výkonnostní režim vypnut", + "performance-mode-disabled-placeholder": "např. notify-send \"Výkon\" \"Režim vypnut\"", + "performance-mode-enabled-description": "Příkaz spuštěný při zapnutí výkonnostního režimu Noctalia.", + "performance-mode-enabled-label": "Výkonnostní režim zapnut", + "performance-mode-enabled-placeholder": "např. notify-send \"Výkon\" \"Režim zapnut\"", + "screen-lock-description": "Příkaz spuštěný při zamknutí obrazovky.", + "screen-lock-label": "Obrazovka zamknuta", + "screen-lock-placeholder": "např. notify-send \"Obrazovka\" \"Zamknuta\"", + "screen-unlock-description": "Příkaz spuštěný při odemknutí zamykací obrazovky.", + "screen-unlock-label": "Obrazovka odemknuta", + "screen-unlock-placeholder": "např. notify-send \"Obrazovka\" \"Odemknuta\"", + "session-description": "Příkaz spuštěný před vypnutím nebo restartem. Přijímá typ akce jako $1 (shutdown/reboot).", + "session-label": "Konec relace", + "session-placeholder": "např. notify-send \"Relace\" \"$1\"", + "system-hooks-desc": "Nastavit příkazy spouštěné při systémových událostech.", + "system-hooks-enable-description": "Povolit nebo zakázat všechny hookové příkazy.", + "system-hooks-enable-label": "Povolit hooky", + "system-hooks-title": "Systémové hooky", + "theme-changed-description": "Příkaz spuštěný při přepnutí motivu mezi tmavým a světlým režimem.", + "theme-changed-label": "Motiv změněn", + "theme-changed-placeholder": "např. notify-send \"Motiv\" \"Přepnut\"", + "title": "Hooky", + "wallpaper-changed-description": "Příkaz spuštěný při změně tapety.", + "wallpaper-changed-label": "Tapeta změněna", + "wallpaper-changed-placeholder": "např. notify-send \"Tapeta\" \"Změněna\"" + }, + "idle": { + "custom-add": "Přidat příkaz", + "custom-description": "Spustit příkaz shellu po období nečinnosti.", + "custom-entry-command": "Příkaz", + "custom-entry-delete": "Smazat", + "custom-entry-edit": "Upravit vlastní příkaz", + "custom-entry-name": "Název", + "custom-entry-name-placeholder": "např. Vypnout světla", + "custom-entry-new": "Nový vlastní příkaz", + "custom-entry-timeout": "Čas nečinnosti", + "custom-entry-timeout-format": "{count} sekunda", + "custom-entry-timeout-format-plural": "{count} sekund", + "custom-entry-unnamed": "Nepojmenovaný příkaz", + "custom-label": "Vlastní příkazy nečinnosti", + "enable-description": "Automaticky vypnout obrazovku, zamknout nebo uspat po období nečinnosti.", + "enable-label": "Povolit správu nečinnosti", + "fade-duration-description": "Sekundy animace zatmění před spuštěním každé akce. Jakýkoli pohyb myší animaci zruší.", + "fade-duration-label": "Trvání zatmění", + "lock-description": "Sekundy nečinnosti před aktivací zamykací obrazovky.", + "lock-label": "Zamykací obrazovka", + "resume-command-label": "Příkaz obnovení", + "screen-off-description": "Sekundy nečinnosti před vypnutím monitorů.", + "screen-off-label": "Vypnout obrazovku", + "status-description": "Čas nečinnosti hlášený compositorem.", + "status-label": "Čas nečinnosti", + "suspend-description": "Sekundy nečinnosti před uspáním systému.", + "tab-behavior": "Chování", + "tab-custom": "Vlastní", + "timeouts-description": "Nastavte na 0 pro zakázání fáze. Časovače jsou pozastaveny při aktivním Udržet vzhůru.", + "timeouts-label": "Časové limity", + "title": "Nečinnost", + "unavailable": "Nativní sledování nečinnosti není na tomto compositoru dostupné." + }, + "indicator": { + "default-value": "Výchozí: {value}", + "system-default": "Výchozí systému" + }, + "launcher": { + "clipboard-desc": "Přistupovat a spravovat historii schránky ze spouštěče.", + "execute-desc": "Nastavit způsob spouštění aplikací.", + "execute-title": "Spouštění", + "settings-annotation-tool-description": "Příkaz spuštěný při kliknutí na tlačítko anotace v historii schránky. Obrázek bude předán tomuto příkazu.", + "settings-annotation-tool-label": "Nástroj anotace", + "settings-annotation-tool-placeholder": "např. 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Automaticky vložit vybranou položku schránky. Vyžaduje wtype.", + "settings-auto-paste-label": "Automatické vložení", + "settings-clip-chips-description": "Zobrazit panel záložek pro filtrování historie schránky podle typu (Obrázky, Odkazy, Soubory, Kód atd.).", + "settings-clip-chips-label": "Povolit kategorie", + "settings-clip-preview-description": "Zobrazit náhled obsahu schránky při použití příkazu >clip.", + "settings-clip-preview-label": "Povolit náhled schránky", + "settings-clip-smart-icons-description": "Zobrazit specifické ikony pro odkazy, soubory, barvy a kód místo obecné ikony schránky.", + "settings-clip-smart-icons-label": "Povolit chytré ikony", + "settings-clip-wrap-text-description": "Zalamovat text v seznamu schránky místo ořezávání.", + "settings-clip-wrap-text-label": "Zalamovat text schránky", + "settings-clipboard-history-description": "Přistupovat k dříve zkopírovaným položkám ze spouštěče.", + "settings-clipboard-history-label": "Povolit historii schránky", + "settings-clipboard-watch-image-description": "Úplný řetězec příkazu předávaný wl-paste pro změny obrázků. (vyžaduje restart)", + "settings-clipboard-watch-image-label": "Příkaz sledování obrázků", + "settings-clipboard-watch-text-description": "Úplný řetězec příkazu předávaný wl-paste pro změny textu. (vyžaduje restart)", + "settings-clipboard-watch-text-label": "Příkaz sledování textu", + "settings-custom-launch-prefix-description": "Přidat vlastní prefix k příkazům spouštění (např. 'runapp' pro integraci systemd).", + "settings-custom-launch-prefix-enabled-description": "Použít vlastní prefix pro spouštění aplikací místo výchozí metody.", + "settings-custom-launch-prefix-enabled-label": "Povolit vlastní prefix spouštění", + "settings-custom-launch-prefix-label": "Vlastní prefix spouštění", + "settings-density-description": "Upravit velikost ikon aplikací a hustotu spouštěče.", + "settings-density-label": "Hustota", + "settings-desc": "Přizpůsobit chování a vzhled spouštěče.", + "settings-enable-session-search-description": "Zobrazit akce relace (zamknout, vypnout, restartovat atd.) při vyhledávání ve spouštěči.", + "settings-enable-session-search-label": "Povolit vyhledávání relace", + "settings-enable-settings-search-description": "Zobrazit výsledky nastavení při vyhledávání ve spouštěči.", + "settings-enable-settings-search-label": "Povolit vyhledávání nastavení", + "settings-enable-windows-search-description": "Vyhledat a aktivovat aktivní okna.", + "settings-enable-windows-search-label": "Povolit vyhledávání oken", + "settings-grid-view-description": "Zobrazit položky v mřížkovém rozložení místo seznamu.", + "settings-icon-mode-description": "Použít nativní systémové ikony místo ikon Tabler.", + "settings-icon-mode-label": "Použít nativní ikony", + "settings-ignore-mouse-input-description": "Zakázat interakci myší a kolečka ve spouštěči.", + "settings-ignore-mouse-input-label": "Ignorovat vstup myši", + "settings-overlay-layer-description": "Zobrazit spouštěč na překryvné vrstvě, nad celooobrazovkovými okny. Při zapnutí se spouštěč nepřipojí k liště.", + "settings-overlay-layer-label": "Zobrazit nad celou obrazovkou", + "settings-position-description": "Vybrat, kde se panel spouštěče zobrazí.", + "settings-show-categories-description": "Zobrazit záložky kategorií pro filtrování aplikací.", + "settings-show-categories-label": "Zobrazit kategorie", + "settings-show-icon-background-description": "Zobrazit zaoblené obdélníkové pozadí za ikonami.", + "settings-show-icon-background-label": "Zobrazit pozadí ikony", + "settings-sort-by-usage-description": "Pokud je povoleno, často spouštěné aplikace se zobrazí jako první.", + "settings-sort-by-usage-label": "Řadit podle nejpoužívanějších", + "settings-terminal-command-description": "Příkaz pro spuštění terminálu. Např. 'kitty -e' nebo 'gnome-terminal --'.", + "settings-terminal-command-label": "Příkaz terminálu", + "settings-use-app2unit-description": "Používá alternativní metodu spouštění pro lepší správu procesů aplikací a prevenci problémů.", + "settings-use-app2unit-label": "Použít App2Unit pro spouštění aplikací", + "settings-view-mode-description": "Vybrat rozložení položek spouštěče.", + "settings-view-mode-label": "Režim zobrazení", + "title": "Spouštěč" + }, + "location": { + "auto-locate-description": "Automaticky detekovat vaši polohu pravidelně pomocí vaší IP adresy.", + "auto-locate-label": "Automatické Zjištění", + "calendar-cards-desc": "Organizovat a povolit/zakázat karty v panelu kalendáře.", + "calendar-cards-title": "Karty kalendáře", + "calendar-header-label": "Záhlaví kalendáře", + "calendar-month-label": "Měsíc kalendáře", + "date-time-12hour-format-description": "Zobrazit čas ve 12hodinovém formátu na zamykací obrazovce a v kalendáři. Hodiny v liště mají vlastní nastavení.", + "date-time-12hour-format-label": "Použít 12hodinový formát", + "date-time-desc": "Přizpůsobit způsob zobrazení data a času.", + "date-time-first-day-of-week-automatic": "Automaticky (použít národní prostředí systému)", + "date-time-first-day-of-week-description": "Vybrat, který den začíná týden v kalendáři.", + "date-time-first-day-of-week-label": "První den v týdnu", + "date-time-show-events-description": "Zobrazit události v panelu kalendáře.", + "date-time-show-events-label": "Zobrazit události kalendáře", + "date-time-title": "Datum a čas", + "date-time-use-analog-description": "Zobrazit analogové hodiny v okně kalendáře.", + "date-time-use-analog-label": "Použít analogový styl hodin", + "date-time-week-numbers-description": "Zobrazit číslo týdne v roce (např. týden 38) v kalendáři.", + "date-time-week-numbers-label": "Zobrazit čísla týdnů", + "geolocate-now-button": "Najít nyní", + "location-desc": "Získejte přesné počasí a rozvrh nočního světla nastavením polohy.", + "location-search-description": "např. Praha, CZ", + "location-search-label": "Hledat polohu", + "location-search-placeholder": "Zadejte název místa", + "location-title": "Vaše poloha", + "weather-desc": "Vybrat preferovanou jednotku teploty.", + "weather-enabled-description": "Zobrazit informace o počasí v celém rozhraní a načítat data počasí. Při vypnutí budou skryty všechny prvky počasí a nebudou prováděny žádné síťové požadavky.", + "weather-enabled-label": "Povolit počasí", + "weather-fahrenheit-description": "Zobrazit teplotu ve Fahrenheitech místo Celsia.", + "weather-fahrenheit-label": "Zobrazit teplotu ve Fahrenheitech (°F)", + "weather-hide-city-description": "Skrýt název města z zobrazení počasí v celém rozhraní.", + "weather-hide-city-label": "Skrýt název města", + "weather-hide-timezone-description": "Skrýt zkratku časového pásma ze zobrazení počasí v celém rozhraní.", + "weather-hide-timezone-label": "Skrýt časové pásmo", + "weather-show-effects-description": "Zobrazit další vizuální efekty (jako déšť, sníh nebo blesk) na kartě počasí.", + "weather-show-effects-label": "Zobrazit efekty počasí", + "weather-show-in-calendar-description": "Zobrazit denní předpověď počasí přímo v zobrazení kalendáře.", + "weather-show-in-calendar-label": "Zobrazit počasí v kalendáři", + "weather-talia-mascot-always-description": "Vždy používejte Talia emojis namísto tabler icons.", + "weather-talia-mascot-always-label": "Vždy zobrazovat emotikony počasí Talia" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "Pokud je aktivní fprintd (otisky prstů), tato možnost umožňuje přihlásit se heslem místo otisku prstu.", + "allow-password-with-fprintd-label": "Povolit přihlášení heslem s fprintd", + "auto-start-auth-description": "Automaticky zahájí ověření otiskem prstu bez nutnosti stisknout klávesu nebo tlačítko.", + "auto-start-auth-label": "Automatické spuštění ověření", + "clock-format-description": "Přizpůsobit formát hodin pomocí tokenů syntaxe data/času.", + "clock-format-label": "Formát hodin", + "clock-style-analog": "Analogový", + "clock-style-custom": "Vlastní", + "clock-style-description": "Vybrat vizuální styl hodin na zamykací obrazovce.", + "clock-style-digital": "Digitální", + "clock-style-label": "Styl hodin", + "compact-lockscreen-description": "Zobrazit pouze vstup pro přihlášení a systémové ovládací prvky, skrýt widgety počasí a médií.", + "compact-lockscreen-label": "Kompaktní zamykací obrazovka", + "enable-lockscreen-media-controls-description": "Zobrazit interaktivní ovládání přehrávání médií na zamykací obrazovce.", + "enable-lockscreen-media-controls-label": "Ovládání médií na zamykací obrazovce", + "lock-on-suspend-description": "Automaticky zamknout obrazovku při uspání systému.", + "lock-on-suspend-label": "Zamknout při uspání", + "lock-screen-animations-description": "Povolit nebo zakázat animace zamykací obrazovky.", + "lock-screen-animations-label": "Animace zamykací obrazovky", + "lock-screen-blur-strength-description": "Aplikovat efekt rozmazání na tapetu zamykací obrazovky.", + "lock-screen-blur-strength-label": "Síla rozmazání zamykací obrazovky", + "lock-screen-tint-strength-description": "Aplikovat barevný překryv na tapetu zamykací obrazovky.", + "lock-screen-tint-strength-label": "Síla tónu zamykací obrazovky", + "monitors-desc": "Zobrazit zamykací obrazovku na konkrétních monitorech. Výchozí je zobrazení na všech.", + "password-chars-description": "Roztomilé ikony pro skrytí vašeho hesla.", + "password-chars-label": "Náhodné ikony hesla", + "show-hibernate-description": "Zobrazit možnost 'hibernace' v ovládání napájení.", + "show-hibernate-label": "Zobrazit hibernaci", + "show-session-buttons-description": "Umožnit přístup k nastavení napájení ze zamykací obrazovky.", + "show-session-buttons-label": "Ovládání napájení", + "title": "Zamykací obrazovka" + }, + "network": { + "bluetooth-description": "Aktivovat správu Bluetooth.", + "bluetooth-rssi-polling-description": "Pravidelně vzorkovat RSSI připojených zařízení přes bluetoothctl. Nemusí být dostupné pro všechna zařízení; při zapnutí využívá minimální prostředky.", + "bluetooth-rssi-polling-label": "Dotazování na signál Bluetooth", + "desc": "Spravovat Wi-Fi a Bluetooth připojení.", + "wifi-description": "Spravovat bezdrátové sítě (vyžaduje NetworkManager)." + }, + "notifications": { + "duration-critical-urgency-description": "Jak dlouho zůstávají viditelná kritická oznámení.", + "duration-critical-urgency-label": "Kritická naléhavost", + "duration-desc": "Nastavit, jak dlouho zůstávají oznámení viditelná podle jejich naléhavosti.", + "duration-low-urgency-description": "Jak dlouho zůstávají viditelná oznámení s nízkou prioritou.", + "duration-low-urgency-label": "Nízká naléhavost", + "duration-normal-urgency-description": "Jak dlouho zůstávají viditelná normální oznámení.", + "duration-normal-urgency-label": "Normální naléhavost", + "duration-respect-expire-description": "Použít čas vypršení nastavený v oznámení.", + "duration-respect-expire-label": "Respektovat čas vypršení", + "duration-title": "Trvání oznámení", + "history-clear-dismiss-description": "Smazat oznámení z historie při zavření.", + "history-clear-dismiss-label": "Smazat při zavření", + "history-critical-urgency-description": "Ukládat kritická oznámení do historie.", + "history-critical-urgency-label": "Ukládat kritická do historie", + "history-desc": "Řídit, která oznámení se ukládají do historie podle jejich naléhavosti.", + "history-low-urgency-description": "Ukládat oznámení s nízkou prioritou do historie.", + "history-low-urgency-label": "Ukládat nízká do historie", + "history-normal-urgency-description": "Ukládat normální oznámení do historie.", + "history-normal-urgency-label": "Ukládat normální do historie", + "monitors-desc": "Zobrazit oznámení na konkrétních monitorech. Výchozí je zobrazení na všech.", + "rules-action-block": "Blokovat", + "rules-action-block-desc": "Úplně přeskočí.", + "rules-action-hide": "Skrýt", + "rules-action-hide-desc": "Žádné vyskakovací okno, žádný zvuk, přidá do historie.", + "rules-action-label": "Akce", + "rules-action-mute": "Ztlumit", + "rules-action-mute-desc": "Žádný zvuk, stále zobrazí vyskakovací okno a přidá do historie.", + "rules-add": "Přidat pravidlo", + "rules-delete": "Smazat pravidlo", + "rules-description": "Porovnat název aplikace nebo obsah. Platí první shoda.", + "rules-edit": "Upravit pravidlo", + "rules-label": "Pravidla filtrování", + "rules-pattern-label": "Vzor", + "rules-pattern-placeholder": "firefox, discord, nebo /regex/", + "rules-tab": "Pravidla", + "settings-always-on-top-description": "Zobrazit oznámení nad celooobrazovkovými okny a ostatními vrstvami.", + "settings-background-opacity-description": "Upravit průhlednost pozadí oznámení.", + "settings-density-description": "Vybrat hustotu karet oznámení.", + "settings-density-label": "Hustota", + "settings-desc": "Nastavit vzhled a chování oznámení.", + "settings-do-not-disturb-description": "Zakáže všechna vyskakovací oznámení při zapnutí.", + "settings-enabled-description": "Povolit nebo zakázat démona oznámení, vyžaduje restart shellu Noctalia.", + "settings-enabled-label": "Povolit oznámení", + "settings-location-description": "Kde se oznámení zobrazují na obrazovce.", + "settings-markdown-description": "Vykreslovat obsah oznámení pomocí formátování Markdown.", + "settings-markdown-label": "Povolit Markdown", + "sounds-desc": "Nastavit zvukové efekty oznámení a hlasitost.", + "sounds-enabled-description": "Povolit zvukové efekty pro příchozí oznámení.", + "sounds-enabled-label": "Povolit zvuky oznámení", + "sounds-excluded-apps-description": "Přeskočit přehrávání nastaveného zvuku oznámení pro konkrétní aplikace, které mají vlastní zvuky.", + "sounds-excluded-apps-label": "Vyloučené aplikace", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Cesta ke zvukovému souboru přehrávanému pro kritická oznámení.", + "sounds-files-critical-label": "Zvuk kritické naléhavosti", + "sounds-files-critical-select-title": "Vybrat zvukový soubor pro kritická oznámení", + "sounds-files-desc": "Nastavit vlastní zvukové soubory pro různé úrovně naléhavosti oznámení.", + "sounds-files-low-description": "Cesta ke zvukovému souboru přehrávanému pro oznámení s nízkou prioritou.", + "sounds-files-low-label": "Zvuk nízké naléhavosti", + "sounds-files-low-select-title": "Vybrat zvukový soubor pro nízká oznámení", + "sounds-files-normal-description": "Cesta ke zvukovému souboru přehrávanému pro normální oznámení.", + "sounds-files-normal-label": "Zvuk normální naléhavosti", + "sounds-files-normal-select-title": "Vybrat zvukový soubor pro normální oznámení", + "sounds-files-placeholder": "Zadejte cestu ke zvukovému souboru", + "sounds-files-select-file": "Vybrat zvukový soubor", + "sounds-files-title": "Zvukové soubory", + "sounds-files-unified-description": "Cesta ke zvukovému souboru přehrávanému pro oznámení.", + "sounds-files-unified-label": "Zvuk oznámení", + "sounds-files-unified-select-title": "Vybrat zvukový soubor pro oznámení", + "sounds-separate-description": "Použít různé zvukové soubory pro oznámení s nízkou, normální a kritickou prioritou.", + "sounds-separate-label": "Použít různé zvuky podle priority", + "sounds-title": "Nastavení zvuku", + "sounds-unavailable-description": "Nainstalujte Qt6 Multimedia pro povolení zvuků oznámení.", + "sounds-unavailable-label": "Zvuky oznámení nejsou dostupné", + "sounds-volume-description": "Upravit úroveň hlasitosti zvuků oznámení.", + "sounds-volume-label": "Hlasitost zvuku", + "toast-battery-description": "Zobrazit upozornění, když stav baterie klesne pod toto procento.", + "toast-battery-label": "Upozornění na baterii", + "toast-desc": "Nastavit vzhled a chování vyskakovacích oznámení.", + "toast-keyboard-description": "Zobrazit vyskakovací oznámení při změně rozložení klávesnice.", + "toast-keyboard-label": "Rozložení klávesnice", + "toast-media-description": "Zobrazit vyskakovací oznámení při změně stavu přehrávání médií.", + "toast-media-label": "Média" + }, + "osd": { + "always-on-top-description": "Zobrazit OSD nad celooobrazovkovými okny a ostatními vrstvami.", + "always-on-top-label": "Vždy nahoře", + "background-opacity-description": "Řídí průhlednost pozadí OSD.", + "background-opacity-label": "Průhlednost pozadí", + "description": "Nastavit indikátory na obrazovce, jako jsou překryvy hlasitosti a jasu.", + "duration-auto-hide-description": "Upravit čas před zmizením OSD.", + "duration-auto-hide-label": "Automaticky skrýt po", + "duration-desc": "Jak dlouho OSD zůstane viditelné před automatickým skrytím.", + "duration-title": "Časový limit automatického skrytí", + "enabled-description": "Zobrazit změny hlasitosti a jasu v reálném čase.", + "enabled-label": "Povolit OSD", + "events-desc": "Vybrat, které události spustí OSD.", + "general-desc": "Nastavit viditelnost a chování OSD.", + "location-description": "Kde se OSD zobrazuje na obrazovce.", + "monitors-desc": "Zobrazit OSD na konkrétních monitorech. Výchozí je zobrazení na všech.", + "title": "Zobrazení na obrazovce (OSD)", + "types-brightness-description": "Zobrazit OSD při změně jasu obrazovky.", + "types-brightness-label": "Jas", + "types-custom-text-description": "Zobrazit OSD pro vlastní textové zprávy z IPC.", + "types-custom-text-label": "Vlastní text", + "types-desc": "Vybrat události, které spustí OSD.", + "types-input-volume-description": "Zobrazit OSD při změně hlasitosti mikrofonu.", + "types-input-volume-label": "Vstupní hlasitost", + "types-lockkey-description": "Zobrazit OSD při přepnutí Caps Lock, Num Lock nebo Scroll Lock.", + "types-lockkey-label": "Zamykací klávesy", + "types-media-description": "Zobrazit OSD při změně stavu přehrávání médií (přehrát, pozastavit, přeskočit).", + "types-media-label": "Přehrávání médií", + "types-title": "Události spouštějící OSD", + "types-volume-description": "Zobrazit OSD při změně hlasitosti zvukového výstupu.", + "types-volume-label": "Výstupní hlasitost" + }, + "plugins": { + "auto-update": "Automaticky aktualizovat pluginy", + "auto-update-description": "Automaticky aktualizovat všechny pluginy při spuštění shellu.", + "available-description": "Procházet a instalovat pluginy z nakonfigurovaných zdrojů.", + "available-label": "Dostupné pluginy", + "available-no-plugins-description": "Zkontrolujte zdroje pluginů nebo obnovte seznam.", + "available-no-plugins-label": "Žádné dostupné pluginy", + "check-for-updates": "Zkontrolovat aktualizace", + "checking-for-updates": "Kontrola aktualizací...", + "collision-already-installed": "Tento plugin je již nainstalován", + "collision-custom-version-exists": "Vlastní verze ze zdroje \"{source}\" je již nainstalována", + "collision-official-version-exists": "Oficiální verze tohoto pluginu je již nainstalována", + "development-disable": "Zakázat vývojový režim pro tento plugin", + "development-enable": "Povolit vývojový režim pro tento plugin", + "filter-downloaded": "Stažené", + "filter-not-downloaded": "Nestažené", + "filter-tags-description": "Filtrovat pluginy podle kategorie nebo stavu stažení.", + "filter-tags-label": "Štítky", + "hot-reload-description": "Automaticky znovu načíst pluginy při změně jejich souborů. Užitečné pro vývoj pluginů.", + "hot-reload-label": "Horké načtení (vývojový režim)", + "hot-reloaded": "Plugin znovu načten: {name}", + "install-error": "Instalace selhala: {error}", + "install-incompatible": "{plugin} vyžaduje Noctalia v{version} nebo novější", + "install-success": "Úspěšně nainstalován {plugin}", + "installed-description": "Spravovat a konfigurovat všechny místně nainstalované pluginy.", + "installed-label": "Nainstalované pluginy", + "installed-no-plugins-description": "Nainstalujte pluginy ze sekce Dostupné.", + "installed-no-plugins-label": "Žádné nainstalované pluginy", + "installing": "Instalace {plugin}...", + "notify-updates": "Oznámení o aktualizacích pluginů", + "notify-updates-description": "Zobrazit vyskakovací oznámení při dostupnosti nových aktualizací pluginů.", + "open-plugin-page": "Otevřít stránku pluginu", + "open-plugins-tab": "Otevřít nastavení pluginů", + "plugin-settings-title": "Nastavení {plugin}", + "refresh-refreshing": "Obnovování seznamu pluginů...", + "refresh-tooltip": "Obnovit dostupné pluginy", + "settings-error-not-loaded": "Plugin není načten", + "settings-saved": "Nastavení pluginu uloženo", + "settings-tooltip": "Nastavení pluginu", + "source-custom": "Vlastní zdroj", + "sources-add-custom": "Přidat vlastní repozitář", + "sources-add-dialog-description": "Přidat repozitář GitHub jako zdroj pluginů.", + "sources-add-dialog-error": "Přidání zdroje pluginu selhalo", + "sources-add-dialog-name": "Název repozitáře", + "sources-add-dialog-name-placeholder": "Moje vlastní pluginy", + "sources-add-dialog-success": "Zdroj pluginu úspěšně přidán", + "sources-add-dialog-title": "Přidat zdroj pluginu", + "sources-add-dialog-url": "URL repozitáře", + "sources-description": "Spravovat repozitáře pluginů.", + "sources-placeholder": "Můj skvělý repozitář", + "sources-remove-tooltip": "Odebrat zdroj pluginu", + "title": "Pluginy", + "translations-reloaded": "Překlady znovu načteny: {name}", + "uninstall-dialog-description": "Opravdu chcete odinstalovat {plugin}? Tím se odstraní všechna data pluginu.", + "uninstall-dialog-title": "Odinstalovat plugin", + "uninstall-error": "Odinstalování selhalo: {error}", + "uninstall-success": "Úspěšně odinstalován {plugin}", + "uninstalling": "Odinstalování {plugin}...", + "update-all": "Aktualizovat vše ({count})", + "update-all-success": "Všechny pluginy úspěšně aktualizovány", + "update-available": "Dostupná nová aktualizace pluginu", + "update-available-plural": "Dostupné aktualizace pluginů ({count})", + "update-error": "Aktualizace pluginu selhala: {plugin}: {error}", + "update-pending": "v{current} → v{new} (vyžaduje Noctalia v{required})", + "update-success": "Plugin {plugin} aktualizován na v{version}", + "update-version": "v{current} → v{new}", + "updating": "Aktualizování..." + }, + "region": { + "title": "Region" + }, + "session-menu": { + "countdown-duration-description": "Nastavit délku odpočtu před provedením akcí napájení.", + "countdown-duration-label": "Délka odpočtu", + "enable-countdown-description": "Zobrazit odpočet před provedením akcí napájení.", + "enable-countdown-label": "Povolit odpočet", + "entries-desc": "Přizpůsobit, které akce napájení se zobrazují v menu relace a v jakém pořadí.", + "entries-title": "Akce napájení", + "entry-settings-command-description": "Vlastní příkaz pro tuto akci. Ponechte prázdné pro použití výchozího systémového příkazu.", + "entry-settings-command-placeholder": "např. systemctl poweroff", + "entry-settings-default-command-lock": "Interní zamykací obrazovka (bez příkazu)", + "entry-settings-default-command-logout": "Interní odhlášení (bez příkazu)", + "entry-settings-default-info-description": "Pokud není zadán vlastní příkaz, použije se výchozí systémový příkaz.", + "entry-settings-default-info-label": "Výchozí příkaz", + "entry-settings-keybind-description": "Přiřadit kombinaci kláves pro spuštění této akce při otevřeném menu relace.", + "entry-settings-keybind-placeholder": "Stiskněte Nahrát pro přiřazení klávesy...", + "entry-settings-keybind-recording": "Nahrávání... Stiskněte libovolnou kombinaci kláves", + "entry-settings-title": "Konfigurovat {entry}", + "entry-settings-tooltip": "Konfigurovat příkaz", + "general-desc": "Nastavit chování a vzhled panelu menu relace.", + "large-buttons-layout-description": "Vybrat způsob zobrazení tlačítek menu relace.", + "large-buttons-layout-label": "Rozložení velkých tlačítek", + "large-buttons-style-description": "Zobrazit menu relace s velkými tlačítky v mřížkovém rozložení.", + "large-buttons-style-label": "Styl velkých tlačítek", + "position-description": "Vybrat, kde se panel menu relace zobrazí při otevření.", + "show-header-description": "Zobrazit název a tlačítko zavření v horní části menu relace.", + "show-header-label": "Zobrazit záhlaví", + "show-keybinds-description": "Zobrazit nápovědu klávesových zkratek u možností relace.", + "show-keybinds-label": "Zobrazit klávesové zkratky" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Udržet widgety plochy viditelné při zapnutém výkonnostním režimu Noctalia.", + "noctalia-performance-disable-desktop-widgets-label": "Povolit widgety plochy ve výkonnostním režimu", + "noctalia-performance-disable-wallpaper-description": "Udržet tapety plochy, přehledu a zamykací obrazovky viditelné při zapnutém výkonnostním režimu Noctalia.", + "noctalia-performance-disable-wallpaper-label": "Povolit vykreslování tapety ve výkonnostním režimu", + "title": "Systém" + }, + "system-monitor": { + "critical-color-label": "Kritická barva", + "custom-highlight-colors-title-label": "Vlastní barvy zvýraznění", + "disk-available-label": "Dostupné místo na disku", + "disk-section-label": "Využití disku", + "enable-dgpu-monitoring-description": "Upozornění: Toto probudí vaši diskrétní GPU (NVIDIA/AMD), což může výrazně ovlivnit výdrž baterie na laptopech s hybridní grafikou.", + "enable-dgpu-monitoring-label": "Povolit sledování diskrétní GPU", + "external-monitor-description": "Zadejte příkaz nebo cestu k aplikaci pro spuštění externího systémového monitoru.", + "external-monitor-label": "Příkaz externího systémového monitoru", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "Nastavit chování a vzhled systémového monitoru.", + "gpu-section-label": "Teplota GPU", + "highlight-colors-section-label": "Barvy zvýraznění", + "polling-interval-label": "Interval dotazování", + "polling-section-description": "Nastavit, jak často se aktualizuje každá systémová metrika.", + "polling-section-label": "Intervaly dotazování", + "threshold-critical": "Kritická prahová hodnota", + "threshold-warning": "Prahová hodnota varování", + "thresholds-section-description": "Upravit prahové hodnoty varování/kritické pro každou systémovou metriku.", + "use-custom-highlight-colors-description": "Při vypnutí se použijí výchozí barvy zvýraznění motivu.", + "use-custom-highlight-colors-label": "Použít vlastní barvy zvýraznění", + "warning-color-label": "Barva varování" + }, + "user-interface": { + "allow-panels-without-bar-description": "Pokud je povoleno, panely se mohou otevřít na libovolné obrazovce. Při vypnutí se panely otevřou pouze na obrazovkách s lištou, což může snížit spotřebu paměti.", + "allow-panels-without-bar-label": "Povolit panely na obrazovkách bez lišty", + "animation-disable-description": "Zakázat všechny animace pro rychlejší a responzivnější zážitek.", + "animation-disable-label": "Zakázat animace UI", + "animation-speed-description": "Upravit globální rychlost animací.", + "animation-speed-label": "Rychlost animací", + "animation-speed-reset": "Obnovit rychlost animací", + "appearance-desc": "Přizpůsobit vizuální prvky, jako jsou tooltips, okraje a stíny.", + "blur-behind-description": "Rozmazává oblast za lištami a panely pomocí protocolu blur compositoru.", + "blur-behind-label": "Rozmazání za objekty", + "box-border-description": "Zobrazit obrys kolem obsahových oblastí.", + "box-border-label": "Obrys kontejneru", + "box-border-radius-description": "Upravuje zaoblení rohů hlavních částí rozložení, jako jsou postranní panely, karty a obsahové panely.", + "box-border-radius-label": "Poloměr kontejneru", + "control-border-radius-description": "Řídí zaoblení interaktivních prvků, včetně tlačítek, přepínačů a textových polí.", + "control-border-radius-label": "Poloměr vstupů", + "desc": "Přizpůsobit vzhled, pocit a chování rozhraní.", + "dim-desktop-description": "Ztmavit plochu při otevřených panelech nebo menu.", + "dim-desktop-label": "Ztmavit plochu", + "dimmer-opacity-description": "Nastavit úroveň průhlednosti ztmavení plochy.", + "dimmer-opacity-label": "Průhlednost ztmavené plochy", + "panel-background-opacity-description": "Nastavit průhlednost pozadí pro všechny panely (lišta, spouštěč, nastavení atd.).", + "panel-background-opacity-label": "Průhlednost pozadí panelů", + "panels-attached-to-bar-description": "Panely se uzamknou k liště a okrajům obrazovky, čímž vytvoří plynulý vzhled se stylovými zaoblenými rohy.", + "panels-attached-to-bar-label": "Přichytit panely k okrajům", + "panels-overlay-description": "Zajišťuje, aby panely a lišta zůstaly viditelné i nad celooobrazovkovými aplikacemi.", + "panels-overlay-label": "Udržet panely a lištu nahoře", + "scaling-description": "Mění velikost obecného uživatelského rozhraní, s výjimkou lišty.", + "scaling-label": "Škálování rozhraní", + "scrollbar-always-visible-description": "Udržet posuvníky vždy viditelné, když lze obsah posouvat, místo zobrazení jen při najetí.", + "scrollbar-always-visible-label": "Vždy zobrazit posuvníky", + "settings-panel-header": "Panel nastavení", + "settings-panel-mode-description": "Vybrat rozložení nastavení (může vyžadovat znovu otevření).", + "settings-panel-mode-label": "Režim panelu nastavení", + "settings-panel-sidebar-card-style-description": "Zabalí postranní panel nastavení do vyplněného pozadí se zaoblenými rohy.", + "settings-panel-sidebar-card-style-label": "Styl karty postranního panelu", + "shadows-description": "Povolí vrhání stínů pod lišty a panely.", + "shadows-direction-description": "Vybrat směr vrhání stínu.", + "shadows-direction-label": "Směr stínu", + "shadows-label": "Vrhání stínů", + "title": "Uživatelské rozhraní", + "tooltips-description": "Povolit nebo zakázat tooltips v celém rozhraní.", + "tooltips-label": "Zobrazit tooltips", + "translucent-widgets-description": "Udělat tlačítka, záložky a další widgety uvnitř panelů poloprůhledné.", + "translucent-widgets-label": "Poloprůhledné widgety" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Abecedně", + "automation-change-mode-description": "Vybrat způsob výběru tapet při automatické změně.", + "automation-change-mode-label": "Režim změny", + "automation-custom-interval-description": "Zadejte čas ve formátu HH:MM (např. 01:30).", + "automation-custom-interval-label": "Vlastní interval", + "automation-interval-description": "Jak často automaticky měnit tapety.", + "automation-interval-label": "Interval tapety", + "automation-random-wallpaper-description": "Naplánovat náhodné změny tapety v pravidelných intervalech.", + "automation-scheduled-change-description": "Automaticky měnit tapety v pravidelných intervalech.", + "automation-scheduled-change-label": "Plánovaná změna", + "look-feel-edge-smoothness-description": "Aplikuje měkký, rozostřený efekt na okraj přechodů.", + "look-feel-edge-smoothness-label": "Změkčit okraj přechodu", + "look-feel-fill-color-description": "Vybrat barvu výplně, která se může zobrazit za tapetou.", + "look-feel-fill-mode-description": "Vybrat způsob škálování obrázku pro přizpůsobení rozlišení monitoru.", + "look-feel-fill-mode-label": "Režim vyplnění", + "look-feel-skip-startup-transition-description": "Přeskočit animaci tapety při spuštění shellu.", + "look-feel-skip-startup-transition-label": "Přeskočit přechod při spuštění", + "look-feel-title": "Vzhled a pocit", + "look-feel-transition-duration-description": "Trvání animací přechodu v sekundách.", + "look-feel-transition-duration-label": "Trvání přechodu", + "look-feel-transition-type-description": "Typ animace při přepínání tapety.", + "look-feel-transition-type-label": "Typ přechodu", + "settings-clear-cache-button": "Vymazat mezipaměť", + "settings-clear-cache-description": "Vymazat mezipaměť změněných velikostí tapet pro uvolnění místa na disku.", + "settings-clear-cache-label": "Mezipaměť tapety", + "settings-clear-cache-toast": "Mezipaměť tapety vymazána", + "settings-desc": "Řídit správu a zobrazení tapet.", + "settings-enable-management-description": "Spravovat tapety přes Noctalia. Odškrtněte, pokud preferujete jinou aplikaci.", + "settings-enable-management-label": "Povolit správu tapety", + "settings-enable-overview-description": "Aplikuje rozmazanou a ztmavenou tapetu na obrazovku přehledu.", + "settings-enable-overview-label": "Povolit tapetu přehledu", + "settings-folder-description": "Cesta k hlavní složce tapet.", + "settings-folder-label": "Složka tapet", + "settings-hide-wallpaper-filenames-tooltip-hide": "Skrýt názvy souborů", + "settings-hide-wallpaper-filenames-tooltip-show": "Zobrazit názvy souborů", + "settings-monitor-specific-description": "Nastavit jinou složku tapet pro každý monitor.", + "settings-monitor-specific-label": "Složky pro každý monitor", + "settings-monitor-specific-tooltip": "Složka tapet monitoru", + "settings-overview-blur-strength-description": "Aplikuje sílu rozmazání na přehled.", + "settings-overview-blur-strength-label": "Síla rozmazání přehledu", + "settings-overview-tint-description": "Aplikuje sílu tónu na přehled.", + "settings-overview-tint-label": "Síla tónu přehledu", + "settings-recursive-search-description": "Hledat tapety také v podsložkách složky tapet.", + "settings-recursive-search-label": "Prohledávat podsložky", + "settings-select-monitor-folder": "Vybrat složku tapet monitoru", + "settings-selector-description": "Vybrat tapetu.", + "settings-selector-position-description": "Vybrat, kde se panel výběru tapety zobrazí.", + "settings-show-hidden-files-tooltip-hide": "Skrýt skryté soubory", + "settings-show-hidden-files-tooltip-show": "Zobrazit skryté soubory", + "settings-title": "Nastavení tapety", + "settings-use-original-images-description": "Přeskočit změnu velikosti tapet před zobrazením. Šetří místo na disku a snižuje využití CPU při změně tapety, ale může využívat více paměti pro velmi velké obrázky.", + "settings-use-original-images-label": "Použít originální obrázky", + "settings-view-mode-description": "Vybrat způsob zobrazení tapet z vaší složky.", + "settings-view-mode-label": "Režim zobrazení", + "view-mode-browse": "Procházet složky", + "view-mode-cycle-tooltip": "Režim zobrazení: {mode} (kliknutím změnit)", + "view-mode-recursive": "Sloučené podsložky", + "view-mode-single": "Kořenová složka" + } + }, + "placeholders": { + "add-new-keybind": "Přidat novou klávesovou zkratku", + "command-example": "echo \"Ahoj světe\"", + "enter-command": "Zadejte příkaz (aplikace nebo vlastní skript)", + "enter-ipc-identifier": "Zadejte jedinečný identifikátor pro příkazy IPC", + "enter-path": "Zadejte cestu...", + "enter-text-to-collapse": "např. 'nic nehraje'. Použijte /regex/ pro vzory.", + "enter-tooltip": "Zadejte tooltip", + "enter-width-pixels": "Zadejte šířku v pixelech", + "keybind-recording": "Nahrávání zkratky...", + "search": "Hledat...", + "search-icons": "např. noctalia, niri, baterie, oblak", + "search-launcher": "Hledat... nebo použijte > pro příkazy", + "search-wallhaven": "Hledat na Wallhaven...", + "search-wallpapers": "Zadejte pro filtrování tapet..." + }, + "positions": { + "bottom": "Dole", + "bottom-center": "Dole uprostřed", + "bottom-left": "Dole vlevo", + "bottom-right": "Dole vpravo", + "center": "Uprostřed", + "center-left": "Vlevo uprostřed", + "center-right": "Vpravo uprostřed", + "close-to-bar": "Blízko tlačítka lišty", + "follow-bar": "Sledovat lištu", + "left": "Vlevo", + "right": "Vpravo", + "top": "Nahoře", + "top-center": "Nahoře uprostřed", + "top-left": "Nahoře vlevo", + "top-right": "Nahoře vpravo" + }, + "session-menu": { + "action-in-seconds": "{action} za {seconds} sekund...", + "cancel-timer": "Zrušit odpočet", + "title": "Menu relace" + }, + "setup": { + "all-done": "Hotovo!", + "appearance": { + "subheader": "Vyberte tmavý režim a zdroje barev (tapeta nebo předdefinované)." + }, + "customize": { + "header": "Přizpůsobte si zážitek", + "subheader": "Upravte pozici lišty, hustotu, škálování a další." + }, + "skip-setup": "Přeskočit nastavení", + "telemetry-wizard-done": "Rozumím!", + "telemetry-wizard-note": "Máte plnou kontrolu – kdykoli to zapněte nebo vypněte v nastavení", + "telemetry-wizard-subtitle": "Přidali jsme anonymní analytiku pro zlepšení Noctalia", + "telemetry-wizard-title": "Aktualizace soukromí", + "wallpaper": { + "choose-dir": "Vyberte složku obsahující obrázky tapet", + "dir-browse": "Procházet složku tapet", + "dir-description": "Vyberte složku obsahující vaše tapety.", + "dir-label": "Složka tapet", + "dir-select-title": "Vybrat složku tapet", + "header": "Vyberte tapetu", + "no-dir": "Není vybrána žádná složka tapet", + "no-valid": "Ve složce nebyly nalezeny žádné platné obrázky: {dir}", + "none-in-dir": "Ve složce nebyly nalezeny žádné tapety", + "preview-error": "Nepodařilo se načíst obrázek", + "select-prompt": "Vyberte tapetu níže", + "subheader": "Nastavte náladu s krásným pozadím." + }, + "welcome": "Vítejte", + "welcome-note": "Jen pár základů pro začátek – úplné možnosti najdete v nastavení", + "welcome-subtitle": "Pojďme udělat váš desktop jedinečným", + "welcome-title": "Vítejte v Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "Žádný přehrávač médií nenalezen", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "Neznámá aplikace", + "uptime": "Provozní doba: {uptime}", + "user-requested": "Požadavek uživatele", + "welcome-back": "Vítej zpět,", + "widget-settings-title": "Nastavení {widget}" + }, + "system-monitor": { + "core-usage": "Využití jádra {id}", + "cpu-temp": "Teplota CPU", + "cpu-usage": "Využití CPU", + "disk": "Disk", + "download-speed": "Rychlost stahování", + "gpu-temp": "Teplota GPU", + "load-average": "Průměrné zatížení", + "title": "Systémový monitor", + "upload-speed": "Rychlost odesílání" + }, + "toast": { + "airplane-mode": { + "description": "Zakázat veškerou bezdrátovou komunikaci.", + "title": "Režim letadlo" + }, + "battery": { + "critical": "Kritický stav baterie", + "critical-desc": "Baterie je na {percent}% – okamžitě připojte nabíječku", + "low": "Nízký stav baterie", + "low-desc": "Baterie je na {percent}% – připojte nabíječku" + }, + "bluetooth": { + "address-copied": "Adresa zkopírována do schránky", + "auto-connect-disabled": "Automatické připojení zakázáno", + "auto-connect-enabled": "Automatické připojení povoleno", + "auto-connecting": "Připojování k {count} zařízení(m)...", + "confirm-code": "Potvrďte kód {value} na druhém zařízení.", + "connect-failed": "Připojení k zařízení selhalo", + "disconnect-failed": "Odpojení od zařízení selhalo", + "discoverable-change-failed": "Změna stavu viditelnosti selhala", + "discoverable-disabled": "Viditelnost zakázána", + "discoverable-enabled": "Viditelnost povolena", + "display-code": "Zadejte kód {value} na druhém zařízení.", + "forget-failed": "Zapomenutí zařízení selhalo", + "pair-failed": "Párování zařízení selhalo", + "passkey-required": "Zařízení vyžaduje přístupový klíč.", + "pincode-required": "Zařízení vyžaduje PIN kód.", + "state-change-failed": "Změna stavu Bluetooth selhala" + }, + "clipboard": { + "long-text": "Dlouhý text", + "unavailable": "Historie schránky není dostupná", + "unavailable-desc": "Aplikace 'cliphist' není nainstalována. Nainstalujte ji pro použití funkcí historie schránky" + }, + "custom-command-failed": { + "description": "Příkaz selhal: {command}\nKód ukončení: {code}", + "title": "Vlastní příkaz selhal" + }, + "do-not-disturb": { + "disabled": "Nerušit zakázáno", + "disabled-desc": "Zobrazuji všechna oznámení", + "enabled": "Nerušit povoleno", + "enabled-desc": "Tato oznámení najdete v historii" + }, + "donation-opened": "Stránka pro podporu otevřena v prohlížeči", + "internet-limited": "Připojeno bez internetu", + "keyboard-layout": { + "changed": "Rozložení klávesnice změněno na {layout}", + "title": "Klávesnice" + }, + "missing-control-center": { + "description": "Widget řídicího centra byl odebrán z lišty. Pro přístup z lišty znovu přidejte widget. Můžete ho otevřít také pravým kliknutím na lištu.", + "label": "Poslední widget řídicího centra odebrán" + }, + "night-light": { + "forced": "Vynucená aktivace", + "normal": "Normální režim", + "not-installed": "wlsunset není nainstalován" + }, + "noctalia-performance": { + "disabled": "Výkonnostní režim zakázán", + "enabled": "Výkonnostní režim povolen", + "label": "Výkon Noctalia" + }, + "power-profile": { + "changed": "Profil napájení změněn", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "Při zpracování šablon nastala chyba", + "title": "Zpracování šablon selhalo" + }, + "vpn": { + "connected": "Připojeno k '{name}'", + "disconnected": "Odpojeno od '{name}'" + }, + "wallpaper-colors": { + "disabled": "Barvy tapety zakázány", + "enabled": "Barvy tapety povoleny", + "label": "Barvy tapety" + }, + "wifi": { + "connected": "Připojeno k '{ssid}'", + "connection-failed": "Připojení selhalo", + "connection-timeout": "Časový limit připojení", + "disconnected": "Odpojeno od '{ssid}'", + "incorrect-password": "Nesprávné heslo", + "network-not-found": "Síť nenalezena" + } + }, + "tooltips": { + "add-widget": "Přidat widget", + "auto-connect": "Přepnout automatické připojení pro toto zařízení", + "bluetooth-auto-connect-off": "Automatické připojení je vypnuto", + "bluetooth-auto-connect-on": "Automatické připojení je zapnuto", + "bluetooth-devices": "Bluetooth zařízení", + "brightness-at": "Jas: {brightness}%", + "click-to-start-recording": "Záznam obrazovky (zahájit nahrávání)", + "click-to-stop-recording": "Záznam obrazovky (zastavit nahrávání)", + "collapse": "Sbalit postranní panel", + "copy-address": "Zkopírovat adresu", + "delete-notification": "Smazat oznámení", + "dismiss-notification": "Zavřít oznámení", + "do-not-disturb-enabled": "Nerušit", + "expand": "Rozbalit postranní panel", + "forget-network": "Zapomenout síť", + "grid-view": "Mřížkové zobrazení", + "hidden-files-hide": "Skryté soubory", + "hide-unnamed-devices": "Skrýt nepojmenovaná zařízení", + "home": "Domovský adresář", + "input-muted": "Přepnout ztlumení vstupu", + "keep-awake": "Udržet vzhůru", + "keyboard-layout": "Rozložení klávesnice {layout}", + "list-view": "Zobrazení seznamu", + "manage-vpn": "VPN připojení", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "Dosažen maximální počet widgetů", + "microphone-volume-at": "Hlasitost mikrofonu: {volume}%", + "move-to-section": "Přesunout do {section}", + "mute": "Ztlumit", + "next-media": "Další stopa", + "next-month": "Další měsíc", + "night-light-not-installed": "Noční světlo (nedostupné)", + "noctalia-performance-enabled": "Výkonnostní režim Noctalia", + "open-annotation-tool": "Otevřít s nástrojem anotace", + "open-control-center": "Řídicí centrum", + "open-notification-history-enable-dnd": "Historie oznámení", + "open-settings": "Nastavení", + "open-tray-dropdown": "Systémový zásobník", + "output-muted": "Přepnout ztlumení výstupu", + "power-profile": "Profil napájení {profile}", + "previous-media": "Předchozí stopa", + "previous-month": "Předchozí měsíc", + "refresh-devices": "Obnovit zařízení", + "refresh-wallhaven": "Obnovit výsledky Wallhaven", + "refresh-wallpaper-list": "Obnovit seznam tapet", + "remove-widget": "Odebrat widget", + "search-close": "Zavřít hledání", + "session-menu": "Menu relace", + "show-all-devices": "Zobrazit všechna zařízení", + "switch-to-dark-mode": "Tmavý režim", + "switch-to-light-mode": "Světlý režim", + "unmute": "Zrušit ztlumení", + "up": "Nadřazená složka", + "volume-at": "Výstupní hlasitost: {volume}%", + "wallpaper-selector": "Výběr tapety" + }, + "wallpaper": { + "browse": { + "empty-directory": "Tato složka je prázdná.", + "go-root": "Přejít do kořenové složky tapet", + "go-up": "Přejít do nadřazené složky", + "go-up-hint": "Pro přechod nahoru použijte tlačítko zpět." + }, + "configure-directory": "Nastavte složku tapet s obrázky.", + "fill-modes": { + "crop": "Oříznout (vyplnit)", + "fit": "Přizpůsobit (obsahovat)", + "repeat": "Opakovat (dlaždice)", + "stretch": "Roztáhnout" + }, + "no-match": "Žádná shoda nenalezena.", + "no-wallpaper": "Žádná tapeta nenalezena.", + "panel": { + "apikey-help": "Pro přístup k obsahu NSFW je vyžadován klíč API.", + "apikey-label": "Klíč API", + "apikey-managed-by-env": "Spravováno přes proměnnou prostředí NOCTALIA_WALLHAVEN_API_KEY.", + "apikey-placeholder": "Zadejte váš klíč Wallhaven API", + "appearance-dark-tab": "Tmavý", + "appearance-light-tab": "Světlý", + "categories-anime": "Anime", + "categories-label": "Kategorie", + "categories-people": "Lidé", + "color-extraction-disabled": "Použít extrakci barev tapety", + "color-extraction-enabled": "Použít předdefinovaná barevná schémata", + "header-devices-apply-all-tooltip": "Vybrat tapetu pro každý monitor", + "header-devices-per-monitor-tooltip": "Použít na všechny monitory", + "header-link-light-dark-tooltip": "Zachovat tapetu při změně motivu", + "header-separate-light-dark-tooltip": "Automaticky přepínat tapetu s tématem", + "header-sun-linked-tooltip": "Oddělit světlé a tmavé tapety", + "header-sun-separate-tooltip": "Stejná tapeta pro světlý a tmavý režim", + "order-asc": "Vzestupně", + "order-desc": "Sestupně", + "order-label": "Pořadí", + "purity-label": "Filtr obsahu", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Pochybné", + "ratios-all-portrait": "Vše na výšku", + "ratios-all-wide": "Vše na šířku", + "ratios-any": "Libovolný", + "ratios-label": "Poměr stran", + "resolution-atleast": "Alespoň", + "resolution-exact": "Přesně", + "resolution-label": "Rozlišení", + "resolution-mode-label": "Režim", + "solid-color-tooltip": "Jednobarevné pozadí", + "sort-date-asc": "Řadit od nejstaršího", + "sort-date-desc": "Řadit od nejnovějšího", + "sort-name-asc": "Řadit podle názvu (A–Z)", + "sort-name-desc": "Řadit podle názvu (Z–A)", + "sort-random": "Náhodné pořadí", + "sorting-date-added": "Datum přidání", + "sorting-favorites": "Oblíbené", + "sorting-label": "Řadit podle", + "sorting-relevance": "Relevance", + "sorting-toplist": "Žebříček", + "sorting-views": "Zobrazení", + "source-label": "Zdroj", + "source-wallhaven": "Wallhaven", + "title": "Výběr tapety", + "wallhaven-settings-title": "Nastavení Wallhaven" + }, + "transitions": { + "disc": "Disk", + "fade": "Přechod", + "honeycomb": "Plástev", + "pixelate": "Pixelizace", + "stripes": "Pruhy", + "wipe": "Stírání" + }, + "try-different-search": "Zkuste jiný vyhledávací dotaz.", + "wallhaven": { + "loading": "Načítání tapet...", + "no-results": "Žádné tapety nenalezeny. Zkuste jiný vyhledávací dotaz.", + "page": "{current} z {total}", + "page-prefix": "Stránka", + "page-suffix": "z {total}" + } + }, + "weather": { + "clear-sky": "Jasná obloha", + "drizzle": "Mrholení", + "fog": "Mlha", + "mainly-clear": "Převážně jasno", + "overcast": "Zataženo", + "partly-cloudy": "Částečně oblačno", + "rain-showers": "Přeháňky", + "snow": "Sníh", + "thunderstorm": "Bouřka" + }, + "widgets": { + "color-picker": { + "palette-description": "Vybírejte z široké škály předdefinovaných barev.", + "palette-label": "Paleta", + "palette-theme-colors": "Rychlý přístup k barevné paletě vašeho motivu.", + "title": "Výběr barvy" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm malými písmeny", + "ampm-uppercase": "AM/PM velkými písmeny", + "common-12hour-time-minutes": "12hodinový čas s minutami", + "common-24hour-time-minutes": "24hodinový čas s minutami", + "common-24hour-time-seconds": "24hodinový čas s sekundami", + "common-european-date": "Evropský formát data", + "common-iso-date": "ISO formát data", + "common-us-date": "Americký formát data", + "common-weekday-date": "Den v týdnu s datem", + "common-weekday-month-day": "Den v týdnu, měsíc a den", + "day-abbreviated": "Zkrácený název dne", + "day-full": "Plný název dne", + "day-leading-zero": "Den s vedoucí nulou (01–31)", + "day-no-leading-zero": "Den bez vedoucí nuly (1–31)", + "hour-leading-zero": "Hodina s vedoucí nulou (00–23) – 24hodinový formát", + "hour-no-leading-zero": "Hodina bez vedoucí nuly (0–23) – 24hodinový formát", + "minute-leading-zero": "Minuta s vedoucí nulou (00–59)", + "minute-no-leading-zero": "Minuta bez vedoucí nuly (0–59)", + "month-abbreviated": "Zkrácený název měsíce", + "month-full": "Plný název měsíce", + "month-number-leading-zero": "Měsíc jako číslo s vedoucí nulou (01–12)", + "month-number-no-zero": "Měsíc jako číslo bez vedoucí nuly (1–12)", + "second-leading-zero": "Sekunda s vedoucí nulou (00–59)", + "second-no-leading-zero": "Sekunda bez vedoucí nuly (0–59)", + "timezone-abbreviation": "Zkratka časového pásma", + "year-four-digit": "Rok jako čtyřciferné číslo", + "year-two-digit": "Rok jako dvouciferné číslo (00–99)" + }, + "file-picker": { + "item": "položka", + "items": "položek", + "search-placeholder": "Hledat soubory a složky...", + "select-current": "Vybrat aktuální", + "select-file": "Vybrat soubor", + "select-folder": "Vybrat složku", + "selected": "Vybráno:", + "title": "Výběr souboru" + }, + "file-picker-title": "Vybrat soubor", + "icon-picker": { + "title": "Výběr ikony" + }, + "tooltip-placeholder": "Zástupný symbol" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "Anonymní identita", + "ca-cert": "Certifikát CA", + "eap-method": "Metoda EAP", + "password": "Heslo uživatele", + "phase2-auth": "Ověření fáze 2", + "username": "Identita / Uživatelské jméno" + }, + "panel": { + "action-required": "Vyžadována akce", + "add-hidden-network": "Přidat skrytou síť", + "add-network": "Přidat síť", + "available-interfaces": "Dostupná rozhraní", + "available-networks": "Dostupné sítě", + "disabled": "Wi‑Fi je vypnuto", + "disconnecting": "Odpojování...", + "dns": "DNS", + "enable-message": "Zapněte Wi‑Fi pro zobrazení dostupných sítí.", + "enter-password": "Zadejte heslo Wi-Fi...", + "forget": "Zapomenout", + "forget-network": "Zapomenout tuto síť", + "forgetting": "Zapomínání...", + "hidden-network": "Skrytá síť", + "interface": "Síťové rozhraní", + "internet-connected": "Internet připojen", + "internet-limited": "Bez internetu", + "internet-status": "Stav internetu", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "Známé sítě", + "link-speed": "Rychlost připojení", + "network-name-ssid": "Název sítě (SSID)", + "no-ethernet-devices": "Žádná ethernetová zařízení nenalezena", + "no-networks": "Žádné Wi‑Fi sítě nenalezeny", + "saved": "Uloženo", + "scan-again": "Vyhledat znovu", + "searching": "Vyhledávání sítí...", + "security-open": "Otevřená", + "security-wep": "WEP", + "security-wpa": "WPA", + "security-wpa-ent": "WPA Enterprise", + "security-wpa2-ent": "WPA2 Enterprise", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 Enterprise", + "show-password": "Zobrazit heslo", + "title": "Wi‑Fi" + }, + "signal": { + "excellent": "Výborný", + "fair": "Přijatelný", + "good": "Dobrý", + "poor": "Slabý", + "weak": "Velmi slabý" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/de.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/de.json new file mode 100644 index 0000000..3b8819e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/de.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "Verlauf löschen", + "close-app": "{app} schließen", + "connect-vpn": "Mit {name} verbinden", + "cycle-visualizer": "Zyklus-Visualisierer", + "disable-bluetooth": "Bluetooth deaktivieren", + "disable-dnd": "'Nicht stören' deaktivieren", + "disable-wifi": "WLAN deaktivieren", + "disconnect-vpn": "Verbindung zu {name} trennen", + "dock-settings": "Dock-Einstellungen", + "enable-bluetooth": "Bluetooth aktivieren", + "enable-dnd": "'Nicht stören' aktivieren", + "enable-wifi": "WLAN aktivieren", + "launcher-settings": "Launcher-Einstellungen", + "lower-to-bottom": "Nach unten verschieben", + "open-calendar": "Kalender öffnen", + "open-display-settings": "Anzeigeeinstellungen", + "open-launcher": "Launcher öffnen", + "open-settings": "Einstellungen öffnen", + "raise-to-top": "Nach oben verschieben", + "random-wallpaper": "Zufälliges Hintergrundbild", + "run-custom-command": "Benutzerdefinierten Befehl ausführen", + "show-active-only": "Nur aktive anzeigen", + "show-all": "Alle anzeigen", + "toggle-mute": "Stummschaltung umschalten", + "widget-settings": "Widget-Einstellungen" + }, + "authentication": { + "error": "Authentifizierungsfehler", + "failed": "Authentifizierung fehlgeschlagen" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Themenfarben auf das aktive Fenster-Symbol anwenden.", + "hide-mode-description": "Steuert das Verhalten des Widgets, wenn kein Fenster aktiv ist.", + "scrolling-mode-description": "Steuern, wann Textscrolling für lange Fenstertitel aktiviert ist.", + "show-app-icon-description": "Anwendungssymbol neben dem Fenstertitel anzeigen.", + "show-app-icon-label": "App-Symbol anzeigen", + "show-app-text-description": "Den Titel des aktiven Fensters anzeigen.", + "show-app-text-label": "Fenstertitel anzeigen" + }, + "audio-visualizer": { + "color-name-description": "Wählen Sie die Farbe für den Visualisierer aus.", + "color-name-label": "Füllfarbe", + "height-description": "Benutzerdefinierte Komponentenbreite.", + "hide-when-idle-description": "Wenn aktiviert, wird der Visualizer ausgeblendet, sofern keine Wiedergabe läuft.", + "hide-when-idle-label": "Ausblenden, wenn keine Medien wiedergegeben werden", + "width-description": "Benutzerdefinierte Komponentenbreite." + }, + "battery": { + "device-default": "Standard (Anzeigegerät)", + "device-description": "Wählen Sie das anzuzeigende Akkugerät aus.", + "device-label": "Akkugerät", + "display-mode-description": "Wählen Sie, wie der Akku in der Leiste angezeigt wird.", + "display-mode-graphic": "Grafischer Akku", + "display-mode-graphic-clean": "Grafischer Akku (ohne %)", + "display-mode-icon-always": "Symbol - Immer % anzeigen", + "display-mode-icon-hover": "Symbol - Beim Hover anzeigen", + "display-mode-icon-only": "Nur Symbol", + "hide-if-idle-description": "Das Widget ausblenden, wenn der Akku nicht geladen oder entladen wird.", + "hide-if-idle-label": "Ausblenden, wenn im Leerlauf", + "hide-if-not-detected-description": "Widget ausblenden, wenn kein Akku im System erkannt wird.", + "hide-if-not-detected-label": "Ausblenden, wenn nicht erkannt", + "low-battery-threshold-label": "Schwellenwert für niedrigen Akkustand", + "show-noctalia-performance-description": "Zeige den Noctalia-Leistungsmodus-Schalter im Akku-Panel an.
Deaktiviert Schatten und Animationen in Noctalia, um Ressourcen zu sparen.", + "show-noctalia-performance-label": "Noctalia-Performance-Schalter anzeigen", + "show-power-profile-description": "Zeige die Auswahl des Energieprofils im Akku-Panel an.", + "show-power-profile-label": "Energieprofil-Steuerung anzeigen" + }, + "brightness": { + "apply-all-description": "Helligkeit für alle Monitore ändern, wenn das Scrollrad verwendet wird.", + "apply-all-label": "Änderungen beim Scrollen auf alle Monitore anwenden" + }, + "clock": { + "clock-display-description": "Passen Sie die Anzeige Ihrer Uhr an, indem Sie Token aus der Liste unten hinzufügen. Um das 12-Stunden-Format zu verwenden, müssen Sie das 'AP'-Token einschließen.", + "clock-display-label": "Uhrenanzeige", + "custom-font-description": "Wählen Sie eine benutzerdefinierte Schriftart für die Uhrenanzeige aus.", + "custom-font-label": "Benutzerdefinierte Schriftart", + "custom-font-placeholder": "Benutzerdefinierte Schriftart auswählen...", + "custom-font-search-placeholder": "Schriftarten suchen...", + "horizontal-bar-description": "Tipp: Verwenden Sie \\n, um einen Zeilenumbruch zu erstellen.", + "horizontal-bar-label": "Horizontale Leiste", + "preview": "Vorschau", + "tooltip-format-description": "Formatzeichenfolge für den Tooltip, der beim Bewegen des Mauszeigers über die Uhr angezeigt wird. Leer lassen, um den Standard-Tooltip zu verwenden.", + "tooltip-format-label": "Tooltip-Format", + "use-custom-font-description": "Überschreibt die Standard-Schriftart mit einer benutzerdefinierten Schriftart für die Uhr.", + "use-custom-font-label": "Benutzerdefinierte Schriftart verwenden", + "use-monospaced-font-description": "Wenn aktiviert, verwendet die Uhr die Monospace-Schriftart.", + "use-monospaced-font-label": "Monospace-Schriftart verwenden", + "vertical-bar-description": "Verwenden Sie ein Leerzeichen, um jeden Teil in eine neue Zeile zu trennen.", + "vertical-bar-label": "Vertikale Leiste" + }, + "control-center": { + "browse-file": "Datei durchsuchen", + "browse-library": "Bibliothek durchsuchen", + "color-selection-description": "Wendet Themenfarben auf Symbole an.", + "enable-colorization-description": "Farbgebung für das Symbol aktivieren, Designfarben anwenden.", + "icon-description": "Symbol aus der Bibliothek oder eine benutzerdefinierte Datei auswählen.", + "select-custom-icon": "Benutzerdefiniertes Symbol auswählen", + "use-distro-logo-description": "Logo Ihrer Distribution anstelle eines benutzerdefinierten Symbols verwenden.", + "use-distro-logo-label": "Distro-Logo anstelle von Symbol verwenden" + }, + "custom-button": { + "collapse-condition-description": "Wenn der Ausgabetext diesem Wert entspricht, wird die Schaltfläche eingeklappt.", + "collapse-condition-label": "Einklappbedingung", + "color-selection-description": "Themenfarben auf Symbol und Text anwenden.", + "default-tooltip": "Benutzerdefinierte Schaltfläche, in den Einstellungen konfigurieren", + "display-command-output-description": "Befehl eingeben, der in regelmäßigen Abständen ausgeführt wird. Die erste Zeile seiner Ausgabe wird als Text angezeigt.", + "display-command-output-label": "Befehlsausgabe anzeigen", + "display-command-output-stream-description": "Geben Sie einen Befehl ein, der kontinuierlich ausgeführt werden soll.", + "dynamic-text": "Dynamischer Text", + "enable-colorization-description": "Farbgebung durch Themenfarben für das benutzerdefinierte Schaltflächensymbol und den Text aktivieren.", + "enable-colorization-label": "Farbgebung aktivieren", + "general-tooltip-text-description": "Benutzerdefinierter Text, der im Tooltip der Schaltfläche angezeigt wird.", + "general-tooltip-text-label": "Benutzerdefinierter Tooltip-Text", + "hide-mode-always-expanded": "Immer erweitert", + "hide-mode-description": "Steuert die Sichtbarkeit des Widgets, wenn der Befehl keine Ausgabe hat.", + "hide-mode-expand-with-output": "Mit Ausgabe erweitern", + "hide-mode-label": "Ausblendmodus", + "hide-mode-max-transparent": "Maximal erweitert aber transparent", + "icon-description": "Symbol aus der Bibliothek auswählen.", + "ipc-identifier-description": "Eindeutige Kennung für IPC-Befehle. Verwenden Sie diese Kennung mit 'qs -c noctalia-shell ipc call cb [Aktion] [Kennung]', um diese Schaltfläche über IPC zu steuern.", + "ipc-identifier-label": "IPC-Kennung", + "left-click-description": "Befehl, der ausgeführt wird, wenn die Schaltfläche links angeklickt wird.", + "left-click-label": "Linksklick", + "left-click-update-text": "Text auf Linksklick aktualisieren", + "max-text-length-horizontal-description": "Maximale Anzahl an Zeichen, die in horizontaler Leiste angezeigt werden (0 zum Ausblenden des Textes).", + "max-text-length-horizontal-label": "Max. Textlänge (horizontal)", + "max-text-length-vertical-description": "Maximale Anzahl an Zeichen, die in vertikaler Leiste angezeigt werden (0 zum Ausblenden des Textes).", + "max-text-length-vertical-label": "Max. Textlänge (vertikal)", + "middle-click-description": "Befehl, der ausgeführt wird, wenn die Schaltfläche mit der mittleren Maustaste angeklickt wird.", + "middle-click-label": "Mittelklick", + "middle-click-update-text": "Text auf Mittelklick aktualisieren", + "parse-json-description": "Die Befehlsausgabe als JSON-Objekt parsen, um Text und Symbol dynamisch festzulegen.", + "parse-json-label": "Ausgabe als JSON parsen", + "refresh-interval-description": "Intervall in Millisekunden.", + "refresh-interval-label": "Aktualisierungsintervall", + "right-click-description": "Befehl, der ausgeführt wird, wenn die Schaltfläche rechts angeklickt wird.", + "right-click-label": "Rechtsklick", + "right-click-update-text": "Text beim Rechtsklick aktualisieren", + "show-exec-tooltip-description": "Tooltips mit Befehlsdetails anzeigen (Linksklick, Rechtsklick, Mittelklick, Mausrad).", + "show-exec-tooltip-label": "Befehls-Tooltips anzeigen", + "show-icon-description": "Schaltet die Sichtbarkeit des Widget-Symbols um.", + "show-icon-label": "Symbol anzeigen", + "show-text-tooltip-description": "Zeige Tooltips mit der Ausgabe des Textbefehls.", + "show-text-tooltip-label": "Dynamische Text-Tooltips anzeigen", + "text-stream-description": "Gestreamte Zeilen aus dem Befehl werden als Text auf der Schaltfläche angezeigt.", + "text-stream-label": "Stream", + "wheel-description": "Befehl, der bei Verwendung des Scrollrads ausgeführt wird.
Verwenden Sie $delta für die Scrollrad-Delta im Befehl.", + "wheel-down": "Scrollrad runter", + "wheel-down-description": "Befehl, der ausgeführt wird, wenn das Scrollrad heruntergescrollt wird.", + "wheel-down-label": "Scrollrad runter Befehl", + "wheel-label": "Scrollrad", + "wheel-mode-separate-description": "Separate Befehle für Scrollrad hoch und runter aktivieren.", + "wheel-mode-separate-label": "Separate Scrollrad-Befehle", + "wheel-up": "Scrollrad hoch", + "wheel-up-description": "Befehl, der ausgeführt wird, wenn das Scrollrad hochgescrollt wird.", + "wheel-up-label": "Scrollrad hoch Befehl", + "wheel-update-text": "Anzeigetext beim Scrollen aktualisieren" + }, + "keyboard-layout": { + "show-icon-description": "Das Tastaturlayout-Symbol anzeigen." + }, + "lock-keys": { + "hide-when-off-description": "Verstecke die Anzeige, wenn die Taste nicht aktiv ist.", + "hide-when-off-label": "Ausblenden, wenn deaktiviert", + "show-caps-lock-description": "Caps Lock-Status anzeigen.", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "Num Lock-Status anzeigen.", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "Scroll Lock-Status anzeigen.", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "Aktiviere ein platzsparendes Layout für das Medienplayer-Panel.", + "compact-mode-label": "Kompaktmodus", + "hide-mode-description": "Steuert das Verhalten des Widgets, wenn keine Medien abgespielt werden.", + "max-width-description": "Stellt die maximale Horizontalgröße des Widgets ein. Das Widget wird sich an kürzere Inhalte anpassen.", + "no-active-player": "Kein aktiver Player", + "panel-section-description": "Konfigurieren Sie das Aussehen und Verhalten des Medienplayer-Panels.", + "panel-section-label": "Medienplayer-Panel", + "scrolling-mode-description": "Steuern, wann Textscrolling für lange Track-Titel aktiviert ist.", + "scrolling-mode-label": "Scrollmodus", + "show-album-art-description": "Albumcover des aktuell abgespielten Tracks anzeigen.", + "show-album-art-label": "Albumcover anzeigen", + "show-artist-first-description": "Künstler - Titel anstatt Titel - Künstler anzeigen.", + "show-artist-first-label": "Künstler zuerst anzeigen", + "show-progress-ring-description": "Runden Fortschrittsindikator anzeigen, der den Titelfortschritt anzeigt.", + "show-progress-ring-label": "Fortschrittsring anzeigen", + "show-visualizer-description": "Audio-Visualizer anzeigen, wenn Musik abgespielt wird.", + "show-visualizer-label": "Visualizer anzeigen", + "use-fixed-width-description": "Wenn aktiviert, verwendet das Widget immer die maximale Breite, anstatt sich dynamisch an den Inhalt anzupassen.", + "use-fixed-width-label": "Feste Breite verwenden", + "visualizer-type-description": "Stil des Audio-Visualizers auswählen.", + "visualizer-type-label": "Visualizer-Typ" + }, + "notification-history": { + "hide-widget-when-zero-description": "Benachrichtigungssymbol ausblenden, wenn keine Benachrichtigungen vorhanden sind.", + "hide-widget-when-zero-label": "Symbol bei keinen Benachrichtigungen ausblenden", + "hide-widget-when-zero-unread-description": "Benachrichtigungssymbol ausblenden, wenn keine ungelesenen Benachrichtigungen vorhanden sind.", + "hide-widget-when-zero-unread-label": "Symbol bei keinen ungelesenen Benachrichtigungen ausblenden", + "show-unread-badge-description": "Abzeichen mit der Anzahl ungelesener Benachrichtigungen anzeigen.", + "show-unread-badge-label": "Ungelesen-Abzeichen anzeigen", + "unread-badge-color-description": "Wählen Sie die Farbe für das Abzeichen ungelesener Benachrichtigungen.", + "unread-badge-color-label": "Farbe des Ungelesen-Abzeichens" + }, + "section-editor": { + "placeholder": "Widget auswählen...", + "search-placeholder": "Widgets suchen..." + }, + "spacer": { + "width-description": "Abstandsbreite in Pixeln." + }, + "system-monitor": { + "compact-mode-description": "Statistiken als Mini-Balkendiagramme anstelle von Textwerten anzeigen. Verhindert Layoutverschiebungen.", + "compact-mode-label": "Kompaktmodus", + "cpu-cores-description": "Die Nutzung der CPU-Kerne einzeln anzeigen.", + "cpu-cores-label": "CPU-Kerne", + "cpu-frequency-description": "Die aktuelle CPU-Taktrate in GHz anzeigen.", + "cpu-frequency-label": "CPU-Frequenz anzeigen", + "cpu-temperature-description": "CPU-Temperaturwerte anzeigen, falls verfügbar.", + "cpu-temperature-label": "CPU-Temperatur", + "cpu-usage-description": "Aktuelle CPU-Auslastung in Prozent anzeigen.", + "cpu-usage-label": "CPU-Auslastung", + "disk-path-description": "Wählen Sie den Festplatten-Mountpunkt aus, der überwacht werden soll.", + "disk-path-label": "Festplattenpfad", + "gpu-temperature-description": "Zeige GPU-Temperaturwerte an, falls verfügbar.", + "load-average-description": "Systemlastdurchschnitt anzeigen.", + "load-average-label": "Lastdurchschnitt", + "memory-percentage-description": "Speicherverbrauch als Prozentsatz statt absolute Werte anzeigen.", + "memory-percentage-label": "Speicher als Prozentsatz", + "memory-usage-description": "Aktuelle RAM-Nutzungsinformationen anzeigen.", + "memory-usage-label": "Speicherauslastung", + "network-traffic-description": "Upload- und Download-Geschwindigkeiten anzeigen.", + "network-traffic-label": "Netzwerkverkehr", + "storage-as-percentage-description": "Festplattenspeicher als Prozentsatz statt absoluter Werte anzeigen.", + "storage-as-percentage-label": "Festplatte als Prozentsatz", + "storage-available-description": "Zeigt an, wie viel Speicherplatz verfügbar ist, anstatt wie viel belegt ist.", + "storage-available-label": "Verfügbarer Speicherplatz", + "storage-usage-description": "Festplattenspeicher-Nutzungsinformationen anzeigen.", + "storage-usage-label": "Datenträgerauslastung", + "swap-usage-description": "Swap-Speichernutzung anzeigen.", + "swap-usage-label": "Swap-Auslastung", + "use-monospace-font-description": "Verwende eine Monospace-Schriftart für eine konsistente Zeichenbreite.", + "use-monospace-font-label": "Schriftart mit fester Breite", + "use-padding-description": "Füllt die Textwerte mit Leerzeichen auf. Verhindert Layoutverschiebungen.", + "use-padding-description-disabled-monospace-font": "Eine Monospace-Schriftart ist notwendig, um dieses Option wählen zu können.", + "use-padding-description-disabled-vertical": "Vertikale Taskleiste unterstützt keine Titelanzeige.", + "use-padding-label": "Text mit führenden Leerzeichen auffüllen" + }, + "taskbar": { + "colorize-icons-description": "Themenfarben auf Taskbar-Symbole anwenden.", + "hide-mode-description": "Steuert das Verhalten des Widgets, wenn keine übereinstimmenden Fenster vorhanden sind.", + "hide-mode-label": "Ausblendmodus", + "icon-scale-description": "Legt den Skalierungsfaktor für Taskleistensymbole fest.", + "icon-scale-label": "Symbolskalierung", + "max-width-description": "Maximale Breite der Taskleiste als Prozentsatz der Bildschirmbreite.", + "max-width-label": "Maximale Breite", + "only-active-workspaces-description": "Zeige nur Apps von aktiven Arbeitsflächen.", + "only-active-workspaces-label": "Nur von aktiven Arbeitsflächen", + "only-same-monitor-description": "Zeige nur Apps von dem Monitor an, auf dem sich die Leiste befindet.", + "only-same-monitor-label": "Nur vom selben Monitor", + "show-pinned-apps-description": "Angeheftete Apps aus dem Dock in der Taskleiste anzeigen.", + "show-pinned-apps-label": "Angeheftete Apps anzeigen", + "show-title-description": "Fenstertitel in der Taskleiste anzeigen.", + "show-title-description-disabled": "Vertikale Taskleiste unterstützt keine Titelanzeige.", + "show-title-label": "Titel anzeigen", + "smart-width-description": "Breite der Einträge automatisch basierend auf der Anzahl der Einträge anpassen.", + "smart-width-label": "Intelligente Breite", + "title-width-description": "Breite der Fenstertitel in der Taskleiste festlegen (in Pixel).", + "title-width-label": "Titelbreite", + "title-width-reset-tooltip": "Titelbreite zurücksetzen" + }, + "tray": { + "chevron-color-description": "Wendet die Themenfarben auf das Pfeilsymbol des Drawers an.", + "chevron-color-label": "Pfeil-Farbe", + "colorize-icons-description": "Themenfarben auf Tray-Symbole anwenden.", + "colorize-icons-label": "Symbole einfärben", + "drawer-enabled-description": "Wenn aktiviert, werden nicht angeheftete Tray-Elemente in einem Drawer-Panel angezeigt.
Wenn deaktiviert, werden alle Tray-Elemente inline angezeigt.", + "drawer-enabled-label": "Drawer aktivieren", + "hide-passive-description": "Wenn aktiviert, werden Tray-Elemente mit dem Status 'Passiv' ausgeblendet.", + "hide-passive-label": "Passive Elemente ausblenden" + }, + "volume": { + "display-mode-description": "Wählen Sie, wie dieser Wert angezeigt werden soll." + }, + "workspace": { + "character-count-description": "Anzahl der Zeichen, die von Arbeitsflächennamen angezeigt werden (1-10).", + "character-count-label": "Zeichenanzahl", + "empty-color-description": "Lege die Hintergrundfarbe für leere Arbeitsflächen fest.", + "empty-color-label": "Farbe für leere Arbeitsflächen", + "enable-scrollwheel-description": "Zwischen Arbeitsflächen mit dem Mausrad wechseln.", + "enable-scrollwheel-label": "Zum Wechseln der Arbeitsflächen scrollen", + "focused-color-description": "Lege die Hintergrundfarbe für die fokussierte Arbeitsfläche fest.", + "focused-color-label": "Farbe der fokussierten Arbeitsfläche", + "follow-focused-screen-description": "Zeige Arbeitsflächen vom aktuell fokussierten Bildschirm an, statt vom Bildschirm, auf dem sich die Leiste befindet.", + "follow-focused-screen-label": "Fokussiertem Bildschirm folgen", + "font-weight-description": "Visuelles Gewicht für Text im Arbeitsbereich festlegen.", + "font-weight-label": "Schriftstärke", + "grouped-border-opacity-description": "Legen Sie die Deckkraft für Arbeitsflächen-Container-Rahmen fest.", + "grouped-border-opacity-label": "Rahmen-Deckkraft", + "hide-unoccupied-description": "Arbeitsflächen ohne Fenster nicht anzeigen.", + "hide-unoccupied-label": "Unbesetzte ausblenden", + "label-mode-description": "Wählen Sie, wie Arbeitsflächen-Beschriftungen angezeigt werden.", + "label-mode-label": "Beschriftungsmodus", + "occupied-color-description": "Lege die Hintergrundfarbe für belegte Arbeitsflächen fest.", + "occupied-color-label": "Farbe für belegte Arbeitsfläche", + "pill-size-description": "Passen Sie die Größe der Arbeitsbereichs-Pillen an (50%-100%).", + "pill-size-label": "Pillengröße", + "reverse-scrolling-description": "Die Richtung des Arbeitsflächenwechsels beim Scrollen umkehren.", + "reverse-scrolling-label": "Scrollen umkehren", + "show-applications-description": "Anwendungssymbole in jeder Arbeitsfläche anzeigen.", + "show-applications-hover-description": "Anwendungssymbole in jedem Arbeitsbereich anzeigen, wenn der Arbeitsbereich überfahren wird.", + "show-applications-hover-label": "Anwendungen beim Hovern anzeigen", + "show-applications-label": "Anwendungen anzeigen", + "show-badge-description": "Zeige die Arbeitsflächennummer als Abzeichen im gruppierten Modus an.", + "show-badge-label": "Arbeitsflächen-Abzeichen anzeigen", + "show-labels-only-when-occupied-description": "Arbeitsflächenbezeichnungen nur anzeigen, wenn sie Fenster enthalten.", + "show-labels-only-when-occupied-label": "Beschriftungen nur anzeigen, wenn belegt", + "unfocused-icons-opacity-description": "Transparenzgrad für nicht fokussierte App-Symbole festlegen.", + "unfocused-icons-opacity-label": "Deckkraft unscharfer Symbole" + } + }, + "battery": { + "all-batteries": "Alle Akkus (kombiniert)", + "battery-health": "Akkuzustand", + "battery-level": "Ladezustand", + "capacity-level": "Kapazität: {level}", + "charging-rate": "Laderate: {rate} W", + "discharging-rate": "Entladerate: {rate} W", + "health": "Zustand: {percent}%", + "inhibit-idle-description": "Hält das System wach.", + "no-battery-detected": "Kein Akku erkannt", + "plugged-in": "Angeschlossen", + "power-profile": "Energieprofil", + "time-left": "Verbleibende Zeit: {time}", + "time-until-full": "Verbleibende Ladezeit: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Verfügbare Geräte", + "blocked": "Blockiert", + "connected-devices": "Verbundene Geräte", + "device-address": "Geräteadresse", + "disabled": "Bluetooth ist deaktiviert", + "discoverable": "Sichtbar", + "enable-message": "Aktivieren Sie Bluetooth, um verfügbare Geräte zu sehen.", + "known-devices": "Bekannte Geräte", + "no-devices": "Keine Geräte verfügbar", + "paired-devices": "Gekoppelte Geräte", + "pairing-mode": "Stellen Sie sicher, dass sich Ihr Gerät im Kopplungsmodus befindet.", + "scanning": "Scannen nach Geräten...", + "signal-text-excellent": "Signal: Ausgezeichnet", + "signal-text-fair": "Signal: Schwach", + "signal-text-good": "Signal: Gut", + "signal-text-poor": "Signal: Schlecht", + "signal-text-unknown": "Signal: Unbekannt", + "signal-text-very-poor": "Signal: Sehr schlecht" + } + }, + "changelog": { + "error": { + "fetch-failed": "Changelog-Daten konnten nicht geladen werden. Bitte versuche es später erneut.", + "rate-limit": "GitHub-Limit erreicht. Bitte versuche es in ein paar Minuten erneut." + }, + "panel": { + "buttons-discord": "Unserem Discord beitreten", + "buttons-dismiss": "OK", + "buttons-feedback": "Feedback geben", + "empty": "Es sind noch keine Versionshinweise verfügbar.", + "highlight-title": "Highlights", + "released": "Veröffentlicht am {date}", + "subtitle-fresh": "Danke, dass du Noctalia installiert hast! Das ist in diesem Build enthalten.", + "subtitle-updated": "Aktualisiert von {previousVersion}", + "title": "Was ist neu in {version}", + "version": "Version {version}", + "version-new-user": "Neuinstallation" + } + }, + "common": { + "actions": "Aktionen", + "active": "Aktiv", + "add": "Hinzufügen", + "appearance": "Aussehen", + "apply": "Anwenden", + "auto-connect": "Automatisches Verbinden", + "automation": "Automatisierung", + "available": "Verfügbar", + "back": "Zurück", + "battery": "Akku", + "behavior": "Verhalten", + "bluetooth": "Bluetooth", + "brightness": "Helligkeit", + "browse": "Durchsuchen", + "calendar": "Kalender", + "calendar-panel": "Kalender-Panel", + "cancel": "Abbrechen", + "cards": "Karten", + "charging": "Laden", + "clear": "Löschen", + "clipboard": "Zwischenablage", + "close": "Schließen", + "color-muted": "Gedämpft", + "colors": "Farben", + "command": "Befehl", + "confirm": "Bestätigen", + "connect": "Verbinden", + "connected": "Verbunden", + "connecting": "Verbinden...", + "continue": "Fortsetzen", + "contributors": "Mitwirkende", + "copied-to-clipboard": "In die Zwischenablage kopiert", + "countdown": "Countdown", + "customize": "Anpassen", + "date": "Datum", + "default": "Standard", + "delete": "Löschen", + "devices": "Geräte", + "disabled": "Deaktiviert", + "discharging": "Entladen", + "disconnect": "Trennen", + "disconnected": "Getrennt", + "disconnecting": "Verbindung wird getrennt...", + "display-mode": "Anzeigemodus", + "documentation": "Dokumentation", + "download": "Herunterladen", + "duration": "Dauer", + "dysfunctional": "Dysfunktional", + "edit": "Bearbeiten", + "enabled": "Aktiviert", + "error": "Fehler", + "ethernet": "Ethernet", + "events": "Ereignisse", + "execute": "Ausführen", + "faithful": "Originalgetreu", + "focus": "Fokus", + "font-weight-bold": "Fett", + "font-weight-light": "Leicht", + "font-weight-medium": "Mittel", + "font-weight-regular": "Regulär", + "font-weight-semibold": "Halbfett", + "frequency": "Frequenz", + "gateway": "Gateway", + "general": "Allgemein", + "height": "Höhe", + "hibernate": "Ruhezustand", + "history": "Verlauf", + "icon": "Symbol", + "idle": "Leerlauf", + "info": "Info", + "input": "Eingabe", + "install": "Installieren", + "installed": "Installiert", + "interface": "Schnittstelle", + "internet": "Internet", + "keybind": "Tastenbelegung", + "language": "Sprache", + "loading": "Wird geladen...", + "local": "Lokal", + "location": "Standort", + "lock": "Sperren", + "logout": "Abmelden", + "look": "Aussehen", + "margins": "Ränder", + "media": "Medien", + "media-player": "Medienplayer", + "memory": "Arbeitsspeicher", + "monitors": "Monitore", + "move-down": "Nach unten verschieben", + "move-up": "Hoch", + "network": "Netzwerk", + "next": "Weiter", + "night-light": "Nachtlicht", + "no": "Nein", + "no-results": "Keine Ergebnisse", + "no-summary": "Keine Zusammenfassung", + "none": "Keine", + "not-found": "Nicht gefunden", + "notifications": "Benachrichtigungen", + "official": "Offiziell", + "on-surface": "Auf Oberfläche", + "output": "Ausgabe", + "pair": "Koppeln", + "paired": "Gekoppelt", + "pairing": "Koppeln...", + "panels": "Panels", + "password": "Passwort", + "pause": "Pause", + "performance": "Leistung", + "pin": "Anpinnen", + "play": "Wiedergabe", + "polling": "Abfrageintervall", + "position": "Position", + "previous": "Zurück", + "primary": "Primär", + "random": "Zufällig", + "reboot": "Neu starten", + "reboot-to-uefi": "In UEFI neu starten", + "record": "Aufnahme starten", + "refresh": "Aktualisieren", + "required": "(erforderlich)", + "reset": "Zurücksetzen", + "reset-to-default": "Auf Standard zurücksetzen", + "result-count": "{count} Ergebnis", + "result-count-plural": "{count} Ergebnisse", + "resume": "Fortsetzen", + "save": "Speichern", + "scanning": "Scannen...", + "screen-corners": "Bildschirmecken", + "search": "Suche", + "second": "{count} Sekunde", + "second-plural": "{count} Sekunden", + "secondary": "Sekundär", + "security": "Sicherheit", + "select": "Auswählen", + "select-color": "Farbe auswählen", + "select-color-description": "Wendet Themenfarben zur Hervorhebung an.", + "select-icon-color": "Symbolfarbe auswählen", + "settings": "Einstellungen", + "shortcuts": "Kurzbefehle", + "shutdown": "Herunterfahren", + "signal": "Signal", + "sound": "Ton", + "sources": "Quellen", + "start": "Start", + "stop": "Stopp", + "supporters": "Unterstützer", + "suspend": "Energie sparen", + "templates": "Vorlagen", + "tertiary": "Tertiär", + "test": "Test", + "thresholds": "Schwellenwerte", + "title": "Titel", + "toast": "Toast", + "trusted": "Vertrauenswürdig", + "uninstall": "Deinstallieren", + "unknown": "Unbekannt", + "unpair": "Entkoppeln", + "unpin": "Loslösen", + "update": "Aktualisieren", + "upload": "Hochladen", + "username": "Benutzername", + "userspace-reboot": "Userspace neu starten", + "version": "Version", + "vibrant": "Lebhaft", + "visualizer": "Visualisierer", + "volume": "Lautstärke", + "volumes": "Lautstärken", + "wallpaper": "Hintergrundbild", + "weather": "Wetter", + "weather-loading": "Wetter wird geladen...", + "weather-no-location": "Lege einen Standort in den Einstellungen fest, um das Wetter zu sehen", + "week": "Woche", + "widgets": "Widgets", + "width": "Breite", + "wifi": "Wi-Fi", + "windows": "Fenster", + "yes": "Ja" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Energieprofil", + "tooltip-disabled": "Installieren Sie power-profiles-daemon, um Energieprofile zu verwenden" + }, + "wifi": { + "label-disconnected": "WLAN getrennt", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Immer verstecken", + "always-show": "Immer anzeigen", + "force-open": "Aufbrechen", + "on-hover": "Beim Hover" + }, + "hide-modes": { + "auto-hide": "Automatisch ausblenden", + "hidden": "Ausblenden, wenn leer", + "idle": "Ausblenden, wenn inaktiv", + "non-exclusive": "Nicht-exklusiv", + "transparent": "Transparent, wenn leer", + "visible": "Immer sichtbar" + }, + "launcher": { + "categories": { + "all": "Alle", + "audiovideo": "Audio & Video", + "chat": "Chat", + "development": "Entwicklung", + "education": "Bildung", + "emoji-activity": "Aktivität", + "emoji-animals": "Tiere", + "emoji-flags": "Flaggen", + "emoji-food": "Essen & Trinken", + "emoji-nature": "Natur", + "emoji-objects": "Objekte", + "emoji-people": "Menschen & Körper", + "emoji-recent": "Kürzlich", + "emoji-symbols": "Symbole", + "emoji-travel": "Reisen & Orte", + "game": "Spiele", + "graphics": "Grafiken", + "misc": "Verschiedenes", + "office": "Büro", + "pinned": "Angeheftet", + "system": "System", + "webbrowser": "Webbrowser" + }, + "providers": { + "applications": "Anwendungen", + "calculator": "Rechner", + "calculator-deprecated": "Der Befehl >calc ist veraltet und wird bald entfernt. Bitte geben Sie stattdessen mathematische Ausdrücke direkt in die Suche ein.", + "calculator-description": "Rechner — mathematische Ausdrücke auswerten", + "calculator-enter-expression": "Mathematischen Ausdruck eingeben", + "calculator-press-enter-to-copy": "Drücken Sie die Eingabetaste, um das Ergebnis zu kopieren", + "clipboard": "Zwischenablageverlauf", + "clipboard-clear-description": "Gesamten Zwischenablageverlauf löschen", + "clipboard-clear-description-full": "Alle Elemente aus dem Zwischenablageverlauf entfernen", + "clipboard-clear-history": "Zwischenablageverlauf löschen", + "clipboard-delete": "Zwischenablageeintrag löschen", + "clipboard-history-disabled": "Zwischenablageverlauf deaktiviert", + "clipboard-history-disabled-description": "Zwischenablageverlauf in den Einstellungen aktivieren oder cliphist installieren", + "clipboard-loading": "Lade Zwischenablageverlauf...", + "clipboard-search-description": "Zwischenablageverlauf durchsuchen", + "command-description": "Shell-Befehle ausführen", + "emoji": "Emoji-Auswahl", + "emoji-loading": "Lade Emojis...", + "emoji-loading-description": "Bitte warten", + "emoji-no-recent": "Noch keine kürzlich verwendeten Emojis", + "emoji-search-description": "Emojis suchen und kopieren", + "settings-search-description": "Suchen und zu den Einstellungen navigieren", + "windows-search-description": "Offene Fenster suchen und fokussieren" + } + }, + "lock-screen": { + "authenticating": "Authentifizierung...", + "password": "Passwort eingeben...", + "restart": "Neu starten", + "shut-down": "Herunterfahren" + }, + "notifications": { + "panel": { + "click-to-collapse": "Klicken zum Einklappen", + "click-to-expand": "Zum Erweitern klicken", + "description": "Ihre Benachrichtigungen werden hier angezeigt, sobald sie eintreffen.", + "no-notifications": "Keine Benachrichtigungen" + }, + "range": { + "earlier": "Älter", + "today": "Heute", + "yesterday": "Gestern" + }, + "time": { + "diff-d": "vor 1 Tag", + "diff-dd": "vor {diff} Tagen", + "diff-h": "vor 1 Stunde", + "diff-hh": "vor {diff} Stunden", + "diff-m": "vor 1 Minute", + "diff-mm": "vor {diff} Minuten", + "now": "jetzt" + } + }, + "options": { + "bar": { + "density-comfortable": "Bequem", + "density-compact": "Kompakt", + "density-default": "Standard", + "density-mini": "Mini", + "density-spacious": "Geräumig", + "display-mode-always-visible": "Immer sichtbar", + "type-floating": "Schwebend", + "type-framed": "Gerahmt", + "type-simple": "Einfach" + }, + "control-center": { + "quick-settings-style-classic": "Klassisch", + "quick-settings-style-modern": "Modern" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "Bequem", + "compact": "Kompakt", + "default": "Standard" + }, + "launcher-view-mode": { + "grid": "Gitter", + "list": "Liste" + }, + "notification-density": { + "compact": "Kompakt", + "default": "Standard" + }, + "scrolling-modes": { + "always": "Immer scrollen", + "hover": "Beim Hover scrollen", + "never": "Nie scrollen" + }, + "session-menu-grid-layout": { + "grid": "Raster", + "single-row": "Einzelne Reihe" + }, + "settings-panel-mode": { + "attached": "Panel an der Leiste angeheftet", + "centered": "Zentriertes Feld", + "window": "Eigenes Fenster" + }, + "visualizer-types": { + "linear": "Linear", + "mirrored": "Gespiegelt", + "wave": "Welle" + }, + "visualizer-visibility": { + "with-background": "Nur mit Hintergrund" + }, + "workspace-labels": { + "index": "Index", + "index-and-name": "Index und Name", + "name": "Name" + } + }, + "panels": { + "about": { + "become-supporter": "Werde Unterstützer", + "changelog": "Änderungsprotokoll anzeigen", + "changelog-on-startup": "Änderungsprotokoll bei Update anzeigen", + "changelog-on-startup-desc": "Automatisch den Changelog anzeigen, wenn Noctalia aktualisiert wird.", + "contributors-description": "Ein Dankeschön an unseren {count} großartigen Mitwirkenden!", + "contributors-description-plural": "Ein Dankeschön an unsere {count} großartigen Mitwirkenden!", + "copy-info": "Informationen kopieren", + "debug-disabled": "Debug-Modus deaktiviert", + "debug-enabled": "Debug-Modus aktiviert", + "info-copied": "Info in die Zwischenablage kopiert", + "noctalia-available": "Verfügbar:", + "noctalia-desc": "Eine elegante und minimalistische Desktop-Shell, sorgfältig für Wayland entwickelt, gebaut mit Quickshell.", + "noctalia-git-commit": "Git-Commit:", + "noctalia-installed-version": "Installierte Version:", + "noctalia-latest-version": "Neueste Version:", + "noctalia-qs-version": "Noctalia QS Version:", + "noctalia-title": "Noctalia Shell", + "privacy-policy": "Datenschutzbestimmungen", + "support": "Unterstütze uns", + "supporter-badge": "Unterstützer", + "supporters-desc": "Ein riesiges Dankeschön an unseren großartigen Unterstützer!", + "supporters-desc-plural": "Ein riesiges Dankeschön an unsere {count} großartigen Unterstützer!", + "supporters-loading": "Supporter werden geladen...", + "system-board": "Motherboard:", + "system-cpu": "CPU:", + "system-disk": "Festplatte:", + "system-gpu": "GPU:", + "system-host": "Host:", + "system-install-hint": "Installieren Sie fastfetch, um Systeminformationen anzuzeigen", + "system-kernel": "Kernel:", + "system-loading": "Systeminformationen werden geladen...", + "system-memory": "Speicher:", + "system-monitor": "Monitor:", + "system-not-installed": "fastfetch ist nicht installiert", + "system-os": "Betriebssystem:", + "system-packages": "Pakete:", + "system-product": "Produkt:", + "system-title": "Systeminformationen", + "system-uptime": "Betriebszeit:", + "system-wm": "WM:", + "telemetry-data-copied": "Telemetriedaten in die Zwischenablage kopiert", + "telemetry-desc": "Hilf mit, Noctalia zu verbessern, indem du anonyme Systeminformationen (Bildschirmauflösung, Compositor, Distribution) teilst. Einmaliger Versand beim Start, kein Tracking, Daten werden nach 30 Tagen automatisch gelöscht.", + "telemetry-enabled": "Anonyme Systeminformationen senden", + "telemetry-show-data": "Daten anzeigen", + "telemetry-title": "Datenschutz", + "title": "Über", + "up-to-date": "Auf dem neuesten Stand!", + "update-available": "Update verfügbar", + "view-commit": "Commit auf GitHub ansehen" + }, + "audio": { + "devices-desc": "Verfügbare Audio-Ein- und Ausgabegeräte konfigurieren.", + "devices-input-device-description": "Gewünschtes Audio-Eingabegerät auswählen.", + "devices-input-device-label": "Eingabegerät", + "devices-output-device-description": "Gewünschtes Audio-Ausgabegerät auswählen.", + "devices-output-device-label": "Ausgabegerät", + "devices-title": "Audio-Geräte", + "external-mixer-description": "Geben Sie den Befehl oder den Anwendungspfad ein, der beim Aktivieren der externen Audiomixer-Funktion gestartet werden soll.", + "external-mixer-label": "Externer Audio-Mixer-Befehl", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Bevorzugte und ignorierte Medienanwendungen festlegen.", + "media-excluded-player-description": "Stichwörter für Player hinzufügen, die das System ignorieren soll. Jedes Stichwort sollte in einer neuen Zeile stehen.", + "media-excluded-player-label": "Ausgeschlossener Player", + "media-excluded-player-placeholder": "Teilstring eingeben und + drücken", + "media-frame-rate-description": "Höhere Raten sind flüssiger, verbrauchen aber mehr Ressourcen.", + "media-frame-rate-label": "Bildrate", + "media-primary-player-description": "Stichwort eingeben, um den Hauptplayer zu identifizieren.", + "media-primary-player-label": "Hauptplayer", + "media-primary-player-placeholder": "z.B. spotify, vlc, mpv", + "media-scrolling-speed-description": "Zeit in Sekunden, die der Titel zum Scrollen vom Anfang bis zum Ende benötigt.", + "media-scrolling-speed-label": "Scrollgeschwindigkeit", + "media-scrolling-title-description": "Kontinuierliches Scrollen für lange Medientitel aktivieren.", + "media-scrolling-title-label": "Scrollender Titel", + "media-title": "Medienplayer", + "on-middle-clicked-description": "Befehl, der ausgeführt wird, wenn die Schaltfläche mit der mittleren Maustaste angeklickt wird.", + "panel-applications-empty": "Derzeit geben keine Anwendungen Audio wieder", + "spectrum-mirrored-description": "Spiegele das Spektrum, sodass tiefe Frequenzen in der Mitte sind. Wenn ausgeschaltet, sind tiefe Frequenzen links und hohe rechts.", + "spectrum-mirrored-label": "Spektrum spiegeln", + "title": "Audio", + "visualizer-type-description": "Wählen Sie einen Visualisierungstyp für die Medienwiedergabe.", + "visualizer-type-label": "Visualisierungsart", + "volumes-desc": "Lautstärkeregler und Audiopegel anpassen.", + "volumes-feedback-sound-file-description": "Pfad zur Sounddatei, die beim Ändern der Lautstärke abgespielt wird.", + "volumes-feedback-sound-file-label": "Audiodatei für Lautstärke-Feedback", + "volumes-feedback-sound-file-select-title": "Audiodatei für Lautstärkerückmeldung auswählen", + "volumes-input-volume-description": "Mikrofon-Eingabe-Lautstärke.", + "volumes-mute-input-description": "Die Standard-Audio-Eingabe (Mikrofon) stummschalten.", + "volumes-mute-input-label": "Audio-Eingabe stummschalten", + "volumes-mute-output-description": "Die Haupt-Audio-Ausgabe des Systems stummschalten.", + "volumes-mute-output-label": "Audio-Ausgabe stummschalten", + "volumes-output-volume-description": "Systemweite Lautstärke.", + "volumes-step-size-description": "Schrittgröße für Lautstärkeänderungen anpassen (Mausrad, Tastenkürzel).", + "volumes-step-size-label": "Lautstärke-Schrittgröße", + "volumes-volume-feedback-description": "Einen Feedback-Sound abspielen, wenn die Lautstärke angepasst wird.", + "volumes-volume-feedback-label": "Lautstärke-Feedback-Sound abspielen", + "volumes-volume-overdrive-description": "Erlaubt das Anheben der Lautstärke über 100%. Wird möglicherweise nicht von aller Hardware unterstützt.", + "volumes-volume-overdrive-label": "Lautstärke-Übersteuerung erlauben" + }, + "bar": { + "appearance-auto-hide-delay-description": "Zeit, bis die Bar ausgeblendet wird, nachdem die Maus sie verlassen hat.", + "appearance-auto-hide-delay-label": "Ausblende-Verzögerung", + "appearance-auto-hide-exclusive-note": "Hinweis: Die exklusive Zone wird automatisch deaktiviert, wenn Auto-Hide aktiv ist.", + "appearance-auto-show-delay-description": "Zeit, bevor die Bar angezeigt wird, wenn die Maus den Rand betritt.", + "appearance-auto-show-delay-label": "Anzeigeverzögerung", + "appearance-background-opacity-description": "Legen Sie die Hintergrundtransparenz speziell für die Leiste fest.", + "appearance-background-opacity-label": "Leisten-Hintergrundtransparenz", + "appearance-capsule-color-description": "Wählen Sie eine Farbe für Leistenkapseln oder keine, um die Standard-Oberflächenfarbe zu verwenden.", + "appearance-capsule-color-label": "Kapselfarbe", + "appearance-capsule-opacity-description": "Deckkraft der Widget-Hintergründe festlegen, wenn Kapsel angezeigt wird.", + "appearance-capsule-opacity-label": "Kapsel-Deckkraft", + "appearance-content-padding-description": "Passen Sie den Abstand zwischen Leistenrändern und Widgets an.", + "appearance-content-padding-label": "Inhaltsabstand", + "appearance-density-description": "Innenabstand der Leiste für kompaktes oder geräumiges Aussehen anpassen.", + "appearance-density-label": "Leistendichte", + "appearance-desc": "Erscheinungsbild und Position der Leiste anpassen.", + "appearance-display-mode-description": "Wählen Sie, wann die Leiste sichtbar ist.", + "appearance-enable-exclusion-zone-inset-description": "Reduziere die Ausschlusszone um 1 physisches Pixel, damit bündige Fenster perfekt unter den Rand der Leiste überlappen.", + "appearance-enable-exclusion-zone-inset-label": "Innenabstand der Ausschlusszone", + "appearance-floating-description": "Leiste als schwebende 'Pille' anzeigen.", + "appearance-floating-label": "Schwebende Leiste", + "appearance-font-scale-description": "Die Skalierung der Schriftgröße für Text in der Leiste anpassen.", + "appearance-font-scale-label": "Schriftskalierung", + "appearance-frame-radius": "Innenradius", + "appearance-frame-settings-description": "Rahmenstärke und inneren Eckenradius anpassen", + "appearance-frame-settings-label": "Rahmeneinstellungen", + "appearance-frame-thickness": "Stärke", + "appearance-hide-on-overview-description": "Die Leiste ausblenden und Panels schließen, wenn die Compositor-Übersicht aktiv ist.", + "appearance-hide-on-overview-label": "Leiste in der Übersicht ausblenden", + "appearance-margins-description": "Ränder um die schwebende Leiste anpassen.", + "appearance-margins-horizontal": "Horizontaler Rand", + "appearance-margins-vertical": "Vertikaler Rand", + "appearance-outer-corners-description": "Nach außen gewölbte Ecken auf der Leiste anzeigen.", + "appearance-outer-corners-label": "Äußere Ecken", + "appearance-position-description": "Wählen Sie, wo die Leiste auf dem Bildschirm platziert werden soll.", + "appearance-position-label": "Leistenposition", + "appearance-show-capsule-description": "Widget-Hintergründe anzeigen.", + "appearance-show-capsule-label": "Kapsel anzeigen", + "appearance-show-on-workspace-switch-description": "Die Bar automatisch kurz einblenden, wenn der Arbeitsbereich wechselt.", + "appearance-show-on-workspace-switch-label": "Leiste bei Arbeitsflächenwechsel anzeigen", + "appearance-show-outline-description": "Zeige eine sichtbare Umrandung um jedes Widget an.", + "appearance-show-outline-label": "Widget-Umrisse anzeigen", + "appearance-type-description": "Wählen Sie den Stil der Leiste: Einfach, Schwebend oder Gerahmt.", + "appearance-type-label": "Leistentyp", + "appearance-use-separate-opacity-description": "Ermöglicht die Verwendung eines separaten Deckkraftwerts für den Leistenhintergrund.", + "appearance-use-separate-opacity-label": "Separate Leisten-Deckkraft", + "appearance-widget-spacing-description": "Den Abstand zwischen jedem Widget in der Leiste anpassen.", + "appearance-widget-spacing-label": "Widget-Abstand", + "behavior-middle-click-command-description": "Befehl, der bei Mittelklick ausgeführt wird.", + "behavior-middle-click-command-label": "Mittelklick-Befehl", + "behavior-middle-click-command-placeholder": "niri msg action Übersicht umschalten", + "behavior-middle-click-description": "Wählen Sie, was ein mittlerer Klick auf leeren Bereichen der Leiste bewirkt.", + "behavior-middle-click-follow-mouse-description": "Öffne das ausgewählte Mittelklick-Panel an der Cursorposition.", + "behavior-middle-click-follow-mouse-label": "Mittelklick folgt Maus", + "behavior-middle-click-label": "Aktion bei Mittelklick auf die Leiste", + "behavior-right-click-command-description": "Befehl, der beim Rechtsklick ausgeführt wird.", + "behavior-right-click-command-label": "Rechtsklick-Befehl", + "behavior-right-click-command-placeholder": "notify-send \"Rechtsklick\"", + "behavior-right-click-description": "Wählen Sie, was ein Rechtsklick auf leere Bereiche der Leiste bewirkt.", + "behavior-right-click-follow-mouse-description": "Öffne das ausgewählte Rechtsklick-Panel an der Cursorposition.", + "behavior-right-click-follow-mouse-label": "Rechtsklick folgt Maus", + "behavior-right-click-label": "Aktion bei Rechtsklick auf die Leiste", + "behavior-wheel-wrap-description": "Wenn aktiviert, wird das Scrollen vom letzten zum ersten Element fortgesetzt.", + "behavior-wheel-wrap-label": "Umlaufend", + "behavior-workspace-scroll-description": "Wählen Sie, was das Mausrad auf leeren Bereichen der Leiste tut.", + "behavior-workspace-scroll-label": "Mausrad-Aktion der Leiste", + "behavior-workspace-scroll-option-content": "Inhalt", + "behavior-workspace-scroll-option-workspace": "Arbeitsbereich", + "monitor-configure-widgets": "Widgets konfigurieren", + "monitor-override-settings": "Globale Einstellungen überschreiben", + "monitor-override-settings-description": "Benutzerdefinierte Einstellungen für diesen Monitor verwenden.", + "monitor-reset-all": "Alles zurücksetzen", + "monitor-widgets-title": "Widget-Konfiguration - {monitor}", + "monitors-desc": "Leiste auf bestimmten Monitoren anzeigen. Standardmäßig auf allen.", + "monitors-desc-new": "Konfiguriere, welche Monitore die Bar anzeigen und passe die Einstellungen pro Monitor an.", + "monitors-title": "Monitor-Anzeige", + "title": "Leiste", + "tray-blacklist-description": "Ausschlussregeln für die Tray-Symbolleiste hinzufügen, unterstützt Platzhalter (*).", + "tray-blacklist-label": "Ausschlussliste", + "tray-blacklist-placeholder": "z.B. nm-applet, Fcitx*", + "tray-pin-application": "Anwendung anheften", + "tray-unpin-application": "Anheftung aufheben", + "use-global-widgets": "Globale Widgets verwenden", + "widgets-desc": "Ziehen Sie Widgets, um ihre Reihenfolge zu ändern. Verwenden Sie das Rechtsklick-Menü, um Widgets zwischen Abschnitten zu verschieben oder sie zu entfernen.", + "widgets-title": "Widget-Positionierung" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Generiere Farbschemata aus deinem Hintergrundbild. Extrahiert automatisch Farben, um ein stimmiges Theme zu erstellen.", + "color-source-use-wallpaper-colors-label": "Hintergrundbild-Farben verwenden", + "dark-mode-mode-description": "Ermöglicht einen automatischen Wechsel zwischem hellen und dunklen Modus.", + "dark-mode-mode-label": "Automatischer Dunkelmodus", + "dark-mode-mode-manual": "Manuell", + "dark-mode-mode-off": "Aus", + "dark-mode-switch-description": "Wechselt zu einem dunkleren Theme für einfachere Betrachtung bei Nacht.", + "delete-error-description": "Fehler beim Löschen von {scheme}", + "delete-error-title": "Löschen fehlgeschlagen", + "delete-success-description": "{scheme} wurde erfolgreich gelöscht", + "delete-success-title": "Farbschema gelöscht", + "download-button": "Mehr herunterladen", + "download-downloading": "Wird heruntergeladen...", + "download-empty": "Keine Farbschemata verfügbar", + "download-error-api-error": "API-Fehler: {status}", + "download-error-description": "Fehler beim Herunterladen von {scheme}", + "download-error-download-failed": "Download fehlgeschlagen mit Exit-Code: {code}", + "download-error-invalid-response": "Ungültiges API-Antwortformat", + "download-error-no-files": "Keine Dateien für Schema gefunden", + "download-error-parse-failed": "Fehler beim Parsen der API-Antwort: {error}", + "download-error-rate-limit": "GitHub API-Ratenlimit überschritten", + "download-error-title": "Download fehlgeschlagen", + "download-fetching": "Verfügbare Farbschemata werden abgerufen...", + "download-success-description": "{scheme} wurde erfolgreich heruntergeladen", + "download-success-title": "Farbschema heruntergeladen", + "download-title": "Farbschemata herunterladen", + "method-description": { + "content": "Material Design Schema mit hochauflösender Farbextraktion, das die tatsächlichen Farben des Quellinhalts genau wiedergibt.", + "dysfunctional": "Wie Faithful, wählt aber die zweitdominanteste Farbfamilie als primär.", + "faithful": "Versucht, nah an der Ausgangsfarbe zu bleiben, während gleichzeitig eine harmonische Palette erzeugt wird.", + "fruit-salad": "Material Design-Schema, das eine spielerische, lebendige Palette mit vielfältigen Farbtönen erzeugt.", + "monochrome": "Material Design-Schema mit einer einfarbigen Grauskala und minimalem Farbanteil.", + "muted": "Erzeugt weichere, entsättigte Farbtöne für ein ruhiges und dezentes Erscheinungsbild.", + "rainbow": "Material Design Schema, das Farben über das gesamte Spektrum erzeugt, um maximale Farbvielfalt zu erzielen.", + "tonal-spot": "Standardmäßiger Material Design Algorithmus. Erstellt ausgewogene Paletten mithilfe von tonalen Variationen der Quellfarbe.", + "vibrant": "Maximiert die Farbsättigung für kräftige, auffällige Paletten mit lebendigen Tönen." + }, + "predefined-desc": "Wählen Sie aus einer Sammlung vordefinierter Farbschemata.", + "predefined-generate-templates-label": "Templates für vordefinierte Schemata generieren", + "predefined-title": "Vordefinierte Farbschemata", + "sync-gsettings-description": "Das Systemdesign an die aktive helle oder dunkle Variante anpassen.", + "sync-gsettings-label": "Systemdesign synchronisieren", + "templates-desc": "Farben auf externe Anwendungen anwenden.", + "templates-filter-description": "Vorlagen aus einer bestimmten Kategorie anzeigen.", + "templates-filter-label": "Nach Kategorie filtern", + "templates-misc-description": "Erstellen Sie Ihre eigenen Vorlagen.", + "templates-misc-label": "Erweitert", + "templates-misc-user-templates-description": "Nur aktivieren, wenn Sie wissen, was Sie tun — weitere Informationen finden Sie in unserer Online-Dokumentation.", + "templates-misc-user-templates-label": "Benutzer-Vorlagen aktivieren", + "templates-none-detected": "Keine erkannt", + "templates-write-path": "Schreibt: {filepath}", + "title": "Farbschema", + "wallpaper-method-description": "Wählen Sie Ihre bevorzugte Methode zur Palettengenerierung.", + "wallpaper-method-label": "Methode zur Palettengenerierung", + "wallpaper-monitor-source-description": "Wählen Sie den Monitor aus, der für die Extraktion der Wallpaper-Farben verwendet werden soll.", + "wallpaper-monitor-source-label": "Quelle der Farbgenerierung" + }, + "connections": { + "authentication-required": "Authentifizierung erforderlich", + "bluetooth-auto-connect-description": "Automatisch mit vertrauenswürdigen gekoppelten Geräten verbinden, wenn Bluetooth aktiviert ist.", + "bluetooth-auto-connect-label": "Gekoppelte Geräte automatisch verbinden", + "bluetooth-devices-unnamed": "Unbenannte Geräte werden nicht angezeigt.", + "bluetooth-discoverable": "Dieses Gerät ist auffindbar als {hostName}, solange dieser Einstellungs-Tab geöffnet ist.", + "bluetooth-rssi-polling-description": "Periodisch RSSI für verbundene Geräte über bluetoothctl abfragen. Möglicherweise nicht für alle Geräte verfügbar; verbraucht bei Aktivierung minimale Ressourcen.", + "bluetooth-rssi-polling-interval-description": "Konfigurieren, wie oft die Signalstärke für verbundene Geräte aktualisiert werden soll.", + "bluetooth-rssi-polling-interval-label": "Abfrageintervall", + "bluetooth-rssi-polling-label": "Bluetooth-Signalabfrage", + "disable-discoverability-description": "Gerät vor Bluetooth-Geräten in der Nähe verbergen.", + "disable-discoverability-label": "Gerätesichtbarkeit deaktivieren", + "hide-unnamed-devices-description": "Verstecke Geräte, die nur als Bluetooth-Adressen erscheinen.", + "hide-unnamed-devices-label": "Unbenannte Geräte ausblenden", + "pin-instructions": "Bitte geben Sie den auf Ihrem Gerät angezeigten PIN-Code ein.", + "title": "Verbindungen", + "wifi-header-text": "Verwenden Sie Wi-Fi, um verfügbare Netzwerke anzuzeigen und sich mit dem Internet zu verbinden." + }, + "control-center": { + "cards-desc": "Passen Sie an, welche Steuerelemente im Kontrollzentrum angezeigt werden und in welcher Reihenfolge.", + "desc": "Konfigurieren Sie die Positionierung und das Verhalten des Kontrollzentrum-Panels.", + "open-at-mouse-description": "Beim Rechtsklick auf einen leeren Bereich der Leiste, öffne das Kontrollzentrum nahe dem Cursor. Wenn deaktiviert, wird stattdessen die obige Positionseinstellung verwendet.", + "open-at-mouse-label": "Am Mauszeiger bei Rechtsklick öffnen", + "position-description": "Wählen Sie aus, wo das Kontrollzentrum-Panel angezeigt wird, wenn es geöffnet wird.", + "shortcuts-custom-button-command-description": "Der Befehl, der ausgeführt werden soll, wenn die Schaltfläche geklickt wird.", + "shortcuts-custom-button-enable-on-state-logic-description": "Aktiviert ein zweites Symbol und 'Aktiv'-Zustand basierend auf einem Prüfbefehl.", + "shortcuts-custom-button-enable-on-state-logic-label": "Aktiv-Zustand-Logik aktivieren", + "shortcuts-custom-button-icon-description": "Wählen Sie ein Symbol aus der Bibliothek.", + "shortcuts-custom-button-on-clicked-label": "Linksklick-Befehl", + "shortcuts-custom-button-on-middle-clicked-description": "Befehl, der ausgeführt wird, wenn die Schaltfläche mit der mittleren Maustaste angeklickt wird.", + "shortcuts-custom-button-on-state-command-description": "Befehl, der ausgeführt wird, um zu prüfen, ob sich die Schaltfläche im 'Aktiv'-Zustand befinden soll. Gibt 0 für aktiv, ungleich 0 für inaktiv zurück.", + "shortcuts-custom-button-on-state-command-label": "Befehl für Aktiv-Zustand-Prüfung", + "shortcuts-custom-button-on-state-icon-description": "Das Symbol für die Schaltfläche, wenn sie sich im 'Aktiv'-Zustand befindet.", + "shortcuts-custom-button-on-state-icon-label": "Symbol im Aktiv-Zustand", + "shortcuts-custom-button-state-checks-add": "Zustandsprüfung hinzufügen", + "shortcuts-custom-button-state-checks-command": "Befehl, der für diese Zustandsprüfung ausgeführt werden soll", + "shortcuts-custom-button-state-checks-label": "Zustandsprüfungen", + "shortcuts-custom-button-state-checks-remove": "Entfernen", + "shortcuts-custom-button-tooltip-description": "Der Tooltip, der beim Bewegen des Mauszeigers über die Schaltfläche angezeigt wird.", + "shortcuts-custom-button-tooltip-label": "Tooltip", + "shortcuts-title": "Widgets für Kurzbefehle", + "system-monitor-disk-path-description": "Wählen Sie, welchen Festplatten-Mountpunkt die Systemmonitor-Karte im Kontrollzentrum überwachen soll.", + "system-monitor-disk-path-label": "Systemmonitor Festplattenpfad", + "title": "Kontrollzentrum" + }, + "desktop-widgets": { + "clock-enabled-description": "Eine Uhr als Widget auf dem Desktop anzeigen.", + "clock-enabled-label": "Uhr-Widget aktivieren", + "clock-format-label": "Format", + "clock-height-description": "Höhe des Uhr-Widgets in Pixeln.", + "clock-height-label": "Höhe", + "clock-minimal-mode-description": "Verwende eine minimale Uhr-Anzeige mit Zeit und Datum.", + "clock-minimal-mode-label": "Minimaler Modus", + "clock-rounded-corners-description": "Verwende abgerundete Ecken für den Widget-Hintergrund.", + "clock-rounded-corners-label": "Abgerundete Ecken", + "clock-show-background-description": "Zeige den Hintergrundcontainer für das Uhr-Widget an.", + "clock-show-background-label": "Hintergrund anzeigen", + "clock-show-date-description": "Zeige das aktuelle Datum unter der Uhrzeit an.", + "clock-show-date-label": "Datum anzeigen", + "clock-show-seconds-description": "Sekunden in der Zeit anzeigen.", + "clock-show-seconds-label": "Sekunden anzeigen", + "clock-style-analog": "Analog", + "clock-style-binary": "Binär", + "clock-style-description": "Wählen Sie den Anzeigestil der Uhr.", + "clock-style-digital": "Digital", + "clock-style-label": "Uhrstil", + "clock-style-minimal": "Minimal", + "clock-width-description": "Breite des Uhr-Widgets in Pixeln.", + "cpu-intensive-note": "Widgets, die mit ! markiert sind, verbrauchen mehr CPU und sollten bewusst aktiviert werden.", + "edit-mode-button-label": "Bearbeitungsmodus aktivieren", + "edit-mode-controls-explanation": "Linksklick & Ziehen: Widget verschieben oder Größe ändern.\nRechtsklick: Kontextmenü-Optionen öffnen.", + "edit-mode-description": "Aktiviere den Bearbeitungsmodus, um Desktop-Widgets zu verschieben und neu zu positionieren. Im aktivierten Zustand zeigen Widgets eine Ziehumrandung und können neu positioniert werden.", + "edit-mode-exit-button": "Bearbeitungsmodus verlassen", + "edit-mode-grid-snap-label": "Raster einrasten", + "edit-mode-grid-snap-scale-label": "Rasterfangskala", + "edit-mode-label": "Bearbeitungsmodus", + "enabled-description": "Desktop-Widgets vollständig aktivieren oder deaktivieren.", + "enabled-label": "Desktop-Widgets aktivieren", + "general-desc": "Konfigurieren Sie Widgets, die auf Ihrem Desktop angezeigt werden.", + "general-title": "Desktop-Widgets", + "media-player-enabled-description": "Zeige ein Medienplayer-Widget auf dem Desktop an.", + "media-player-enabled-label": "Medienplayer-Widget aktivieren", + "media-player-rounded-corners-description": "Aktiviere abgerundete Ecken an den Widget-Kanten.", + "media-player-show-album-art-description": "Zeige das Albumcover und die Titelinformationen (Titel und Künstler).", + "media-player-show-album-art-label": "Albumcover & Titel anzeigen", + "media-player-show-background-description": "Zeige den Hintergrundcontainer für das Medienplayer-Widget an.", + "media-player-show-buttons-description": "Mediensteuerungstasten (Wiedergabe/Pause, Zurück, Weiter) im Medienplayer-Widget anzeigen.", + "media-player-show-buttons-label": "Tasten anzeigen", + "media-player-show-visualizer-description": "Audiovisualisierungs-Overlay anzeigen.", + "media-player-visualizer-type-description": "Wählen Sie einen Visualisierungstyp für den Hintergrund des Desktop-Medienplayers.", + "overview-enabled-description": "Desktop-Widgets anzeigen, während die Compositor-Übersicht geöffnet ist.", + "overview-enabled-label": "In Übersicht anzeigen", + "system-stat-layout-bottom": "Unten", + "system-stat-layout-description": "Wählen Sie, wie die Legende relativ zum Diagramm angezeigt wird.", + "system-stat-layout-label": "Layout", + "system-stat-layout-side": "Seite", + "system-stat-rounded-corners-description": "Abgerundete Ecken für den Widget-Hintergrund verwenden.", + "system-stat-rounded-corners-label": "Abgerundete Ecken", + "system-stat-show-background-description": "Zeige den Hintergrundcontainer für das Systemstatistik-Widget.", + "system-stat-show-background-label": "Hintergrund anzeigen", + "system-stat-stat-type-description": "Wählen Sie, welche Systemstatistik angezeigt werden soll.", + "system-stat-stat-type-label": "Statistiktyp", + "title": "Desktop-Widgets", + "weather-enabled-description": "Zeige ein Wetter-Widget auf dem Desktop an.", + "weather-enabled-label": "Wetter-Widget aktivieren", + "weather-show-background-description": "Zeige den Hintergrundcontainer für das Wetter-Widget an." + }, + "display": { + "monitors-backlight-device-auto-option": "Standard", + "monitors-backlight-device-description": "Wählen Sie ein Gerät für die Hintergrundbeleuchtung für diesen Ausgang.", + "monitors-backlight-device-label": "Hintergrundbeleuchtungsgerät", + "monitors-brightness-step-description": "Schrittgröße für Helligkeitsänderungen anpassen (Mausrad und Tastenkürzel).", + "monitors-brightness-step-label": "Helligkeits-Schrittgröße", + "monitors-brightness-unavailable-ddc-disabled": "Helligkeitssteuerung nicht verfügbar. Aktivieren Sie \"Externe Helligkeitsunterstützung\", um die Helligkeit dieses Displays zu steuern.", + "monitors-brightness-unavailable-generic": "Die Helligkeitssteuerung ist für dieses Display nicht verfügbar.", + "monitors-desc": "Helligkeitseinstellungen für jedes Display anpassen.", + "monitors-enforce-minimum-description": "Löst das Problem, dass die Hintergrundbeleuchtung auf einigen Displays bei 0% Helligkeit vollständig ausgeschaltet wird.", + "monitors-enforce-minimum-label": "Mindesthelligkeit erzwingen (1%)", + "monitors-external-brightness-description": "DDCUtil-Unterstützung zum Steuern der Helligkeit externer Displays über das DDC/CI-Protokoll aktivieren.", + "monitors-external-brightness-label": "Externe Helligkeitsunterstützung", + "monitors-global-brightness-description": "Helligkeit für alle Monitore gleichzeitig anpassen.", + "monitors-global-brightness-label": "Alle Monitore", + "monitors-title": "Monitor-spezifische Einstellungen", + "night-light-auto-schedule-description": "Basierend auf Sonnenuntergang und Sonnenaufgang in {location} — empfohlen.", + "night-light-auto-schedule-label": "Automatische Planung", + "night-light-desc": "Blaulicht-Emission reduzieren für besseren Schlaf und weniger Augenbelastung.", + "night-light-enable-description": "Warmen Farbfilter anwenden, um Blaulicht-Emission zu reduzieren.", + "night-light-enable-label": "Nachtlicht aktivieren", + "night-light-force-activation-description": "Ignoriert die Planung und wendet den Nachtfilter sofort an.", + "night-light-force-activation-label": "Aktivierung erzwingen", + "night-light-manual-schedule-description": "Benutzerdefinierte Zeiten für Sonnenaufgang und Sonnenuntergang festlegen.", + "night-light-manual-schedule-label": "Manuelle Planung", + "night-light-manual-schedule-select-start": "Startzeit auswählen", + "night-light-manual-schedule-select-stop": "Endzeit auswählen", + "night-light-manual-schedule-sunrise": "Sonnenaufgang", + "night-light-manual-schedule-sunset": "Sonnenuntergang", + "night-light-temperature-day": "Tag", + "night-light-temperature-day-description": "Steuert die Farbtemperatur tagsüber.", + "night-light-temperature-description": "Farbwärme für Nacht- und Tageszeit einstellen.", + "night-light-temperature-label": "Farbtemperatur", + "night-light-temperature-night": "Nacht", + "night-light-temperature-night-description": "Steuert die Farbtemperatur nachts.", + "title": "Anzeige" + }, + "dock": { + "appearance-background-opacity-description": "Hintergrundtransparenz des Docks anpassen.", + "appearance-border-radius-description": "Den Radius der Dock-Umrandung anpassen.", + "appearance-border-radius-label": "Eckenradius", + "appearance-colorize-icons-description": "Themenfarben auf Dock-App-Symbole anwenden (nur nicht fokussierte Apps).", + "appearance-colorize-icons-label": "Symbole einfärben", + "appearance-dead-opacity-description": "Die Deckkraft von App-Symbolen anpassen, die nicht ausgeführt werden.", + "appearance-dead-opacity-label": "Tote Opazität", + "appearance-desc": "Verhalten und Erscheinungsbild des Docks anpassen.", + "appearance-display-auto-hide": "Automatisch ausblenden", + "appearance-display-description": "Wählen Sie, wie sich das Dock verhält.", + "appearance-display-exclusive": "Exklusiv", + "appearance-dock-indicator-description": "Zeige eine kleine Anzeige, wenn das Dock ausgeblendet ist.", + "appearance-dock-indicator-label": "Dock-Indikator", + "appearance-floating-distance-description": "Schwebeabstand vom Bildschirmrand anpassen.", + "appearance-floating-distance-label": "Dock-Schwebeabstand", + "appearance-group-apps-description": "Gruppiere mehrere Fenster derselben App zu einem Dock-Eintrag.", + "appearance-group-apps-label": "Gleiche Apps gruppieren", + "appearance-group-click-action-cycle": "Fenster durchwechseln", + "appearance-group-click-action-description": "Wählen Sie, was ein Linksklick für gruppierte Apps tut.", + "appearance-group-click-action-label": "Aktion beim Klick auf gruppierte App", + "appearance-group-click-action-list": "Fensterliste öffnen", + "appearance-group-context-menu-mode-description": "Wählen Sie, wie sich das Kontextmenü für gruppierte Apps verhält.", + "appearance-group-context-menu-mode-extended": "Erweitert", + "appearance-group-context-menu-mode-label": "Gruppierter App-Menü-Modus", + "appearance-group-context-menu-mode-list": "Fensterliste", + "appearance-group-indicator-style-description": "Wählen Sie, wie gruppierte laufende Indikatoren den Zustand des fokussierten Fensters anzeigen.", + "appearance-group-indicator-style-dots": "Punkte", + "appearance-group-indicator-style-label": "Gruppierter Anzeigestil", + "appearance-group-indicator-style-number": "Nummer", + "appearance-hide-show-speed-description": "Geschwindigkeit der Ein-/Ausblend-Animation des Docks anpassen.", + "appearance-hide-show-speed-label": "Ein-/Ausblend-Geschwindigkeit", + "appearance-icon-size-description": "Gesamtgröße des Docks anpassen.", + "appearance-icon-size-label": "Dock-Größe", + "appearance-inactive-indicators-description": "Anzeige von Indikator-Pillen für alle Apps, nicht nur für die aktuell aktive.", + "appearance-inactive-indicators-label": "Laufende Indikatoren", + "appearance-indicator-color-description": "Wählen Sie die Farbe des Indikators für das versteckte Dock.", + "appearance-indicator-color-label": "Indikatorfarbe", + "appearance-indicator-opacity-description": "Passen Sie die Deckkraft des Indikators für das versteckte Dock an.", + "appearance-indicator-opacity-label": "Indikator-Deckkraft", + "appearance-indicator-thickness-description": "Aktivieren Sie einen dickeren Indikator für das versteckte Dock (6px statt 3px).", + "appearance-indicator-thickness-label": "Dickerer Indikator", + "appearance-launcher-icon-description": "Wähle das Symbol, das für den Dock Launcher verwendet wird.", + "appearance-launcher-icon-label": "Launcher-Symbol", + "appearance-launcher-position-description": "Wählen Sie, wo das Launcher-Symbol im Dock erscheint.", + "appearance-launcher-position-end": "Ende", + "appearance-launcher-position-label": "Position des Starters", + "appearance-launcher-position-start": "Start", + "appearance-launcher-use-distro-logo-description": "Verwende das Logo deiner Distribution als Dock-Launcher-Symbol.", + "appearance-launcher-use-distro-logo-label": "Distro-Logo anstelle des Icons verwenden", + "appearance-pinned-static-description": "Gepinnte App-Symbole immer in statischer Reihenfolge nach links schieben.", + "appearance-pinned-static-label": "Statische angeheftete Apps", + "appearance-position-description": "Wählen Sie, wo das Dock auf dem Bildschirm angezeigt wird.", + "appearance-position-label": "Position", + "appearance-show-launcher-icon-description": "Das Anwendungsstarter-Symbol im Dock anzeigen.", + "appearance-show-launcher-icon-label": "App-Launcher anzeigen", + "appearance-sit-on-frame-description": "Richten Sie das Dock innerhalb des Rahmenrands aus, anstatt es oben zu platzieren.", + "appearance-sit-on-frame-label": "Dock sitzt am Rahmen", + "appearance-type-attached": "Angedockt", + "appearance-type-description": "Wählen Sie zwischen einer schwebenden Pillenform oder einer statischen, am Rand befestigten Leiste.", + "appearance-type-floating": "Schwebend", + "appearance-type-label": "Dock-Stil", + "enabled-description": "Dock vollständig anzeigen oder ausblenden.", + "enabled-label": "Dock aktivieren", + "monitors-desc": "Dock auf bestimmten Monitoren anzeigen. Standardmäßig werden alle angezeigt, wenn keine ausgewählt wurden.", + "monitors-only-same-monitor-description": "Zeige nur Apps von dem Monitor an, auf dem sich das Dock befindet.", + "monitors-only-same-monitor-label": "Nur Apps vom selben Monitor", + "monitors-title": "Monitor-Anzeige", + "title": "Dock" + }, + "general": { + "copy-settings": "Einstellungen kopieren", + "fonts-default-description": "Hauptschriftart für die gesamte Benutzeroberfläche.", + "fonts-default-label": "Standard-Schriftart", + "fonts-default-placeholder": "Standard-Schriftart auswählen...", + "fonts-default-scale-description": "Ändern Sie die Größe des Standardtextes.", + "fonts-default-scale-label": "Standardmäßige Schriftgröße", + "fonts-default-search-placeholder": "Schriftarten suchen...", + "fonts-desc": "Wählen Sie die Schriftart der Benutzeroberfläche.", + "fonts-monospace-description": "Monospace-Schriftart für Zahlen und Statistik-Anzeigen.", + "fonts-monospace-label": "Monospace-Schriftart", + "fonts-monospace-placeholder": "Monospace-Schriftart auswählen...", + "fonts-monospace-scale-description": "Ändern Sie die Größe des Monotextes.", + "fonts-monospace-scale-label": "Schriftgröße mit fester Breite", + "fonts-monospace-search-placeholder": "Monospace-Schriftarten suchen...", + "fonts-title": "Schriftarten", + "keybinds-conflict-description": "Die Tastenkombination ist bereits {action} zugewiesen.", + "keybinds-conflict-title": "Tastenbelegungskonflikt", + "keybinds-description": "Globale Navigationstasten für Panels und Launcher konfigurieren.", + "keybinds-down": "Nach unten", + "keybinds-enter": "Bestätigen / Aktion", + "keybinds-escape": "Schließen / Zurück", + "keybinds-left": "Nach links", + "keybinds-modifier-description": "Tastenkürzel müssen eine Modifikatortaste (Strg oder Alt) enthalten.", + "keybinds-modifier-title": "Modifikator erforderlich", + "keybinds-remove": "Entfernen / Löschen", + "keybinds-right": "Nach rechts", + "keybinds-title": "Navigations-Tastenkombinationen", + "keybinds-up": "Nach oben", + "language-desc": "Ändern Sie die Sprache von Noctalia.", + "language-select-auto-detect": "Automatisch", + "language-select-description": "Wählen Sie die Sprache der Anwendungsoberfläche.", + "language-select-label": "Anwendungssprache", + "launch-setup-wizard": "Setup-Assistent", + "profile-desc": "Ändern Sie Ihren Avatar.", + "profile-picture-description": "Der Avatar, der in der gesamten Benutzeroberfläche angezeigt wird.", + "profile-picture-label": "Profilbild", + "profile-select-avatar": "Avatar auswählen", + "profile-title": "Profil", + "profile-tooltip": "Profilbild", + "reverse-scrolling-description": "Die interpretierte Scrollrichtung umkehren.", + "reverse-scrolling-label": "Scrollen umkehren", + "screen-corners-desc": "Bildschirmecken-Rundung und visuelle Effekte anpassen.", + "screen-corners-radius-description": "Rundung der Bildschirmecken anpassen.", + "screen-corners-radius-label": "Bildschirmecken-Radius", + "screen-corners-radius-reset": "Eckenradius des Bildschirms zurücksetzen", + "screen-corners-show-corners-description": "Gerundete Ecken am Rand des Bildschirms anzeigen.", + "screen-corners-show-corners-label": "Bildschirmecken anzeigen", + "screen-corners-solid-black-description": "Schwarze Ecken anstelle der Leisten-Hintergrundfarbe verwenden.", + "screen-corners-solid-black-label": "Schwarze Ecken", + "screen-corners-title": "Bildschirmecken", + "settings-copied": "Einstellungen in die Zwischenablage kopiert", + "smooth-scrolling-description": "Listen-Scrolling animieren für ein flüssigeres Mausrad-Erlebnis.", + "smooth-scrolling-label": "Sanftes Scrollen", + "tab-basics": "Grundlagen", + "tab-keybinds": "Tastenkombinationen" + }, + "hooks": { + "color-generation-description": "Befehl, der ausgeführt wird, wenn die Farberzeugung abgeschlossen ist (z.B. nach der Hintergrundbild-basierten Thematisierung).", + "color-generation-label": "Farben generiert", + "color-generation-placeholder": "z.B. notify-send \"Farben\" \"Generiert\"", + "info-command-info-description": "• Befehle werden über Shell ausgeführt (sh -lc)
• Befehle laufen im Hintergrund (getrennt)
• Test-Buttons führen mit aktuellen Werten aus", + "info-parameters-description": "• Wallpaper hook: $1 = Hintergrundbildpfad, $2 = Bildschirmname, $3 = Thema (dunkel/hell)
• Theme toggle hook: $1 = true/false (Dunkelmodus-Status)
• Screen lock/unlock hooks: $1 = lock/unlock (Bildschirmsperrstatus)
• Performance mode hooks: Keine Parameter
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "Verfügbare Parameter", + "noctalia-started-description": "Befehl, der ausgeführt wird, wenn Noctalia vollständig geladen wurde.", + "noctalia-started-label": "Noctalia gestartet", + "noctalia-started-placeholder": "z.B. notify-send 'Noctalia geladen'", + "performance-mode-disabled-description": "Befehl, der ausgeführt wird, wenn der Noctalia-Leistungsmodus deaktiviert wird.", + "performance-mode-disabled-label": "Leistungsmodus deaktiviert", + "performance-mode-disabled-placeholder": "z.B. notify-send \"Leistung\" \"Modus deaktiviert\"", + "performance-mode-enabled-description": "Befehl, der ausgeführt wird, wenn der Noctalia-Leistungsmodus aktiviert wird.", + "performance-mode-enabled-label": "Leistungsmodus aktiviert", + "performance-mode-enabled-placeholder": "z.B. notify-send \"Leistung\" \"Modus aktiviert\"", + "screen-lock-description": "Befehl, der ausgeführt wird, wenn der Bildschirm gesperrt wird.", + "screen-lock-label": "Bildschirm gesperrt", + "screen-lock-placeholder": "z.B. notify-send \"Bildschirm\" \"Gesperrt\"", + "screen-unlock-description": "Befehl, der ausgeführt wird, wenn der Bildschirm entsperrt wird.", + "screen-unlock-label": "Bildschirm entsperrt", + "screen-unlock-placeholder": "z.B. notify-send \"Bildschirm\" \"Entsperrt\"", + "session-description": "Befehl, der vor dem Herunterfahren oder Neustart ausgeführt werden soll. Empfängt den Aktionstyp als $1 (Herunterfahren/Neustart).", + "session-label": "Sitzungsende", + "session-placeholder": "z.B. notify-send \"Session\" \"$1\"", + "system-hooks-desc": "Befehle konfigurieren, die bei Systemereignissen ausgeführt werden.", + "system-hooks-enable-description": "Alle Hook-Befehle aktivieren oder deaktivieren.", + "system-hooks-enable-label": "Hooks aktivieren", + "system-hooks-title": "System-Hooks", + "theme-changed-description": "Befehl, der ausgeführt wird, wenn das Thema zwischen dunklem und hellem Modus wechselt.", + "theme-changed-label": "Thema geändert", + "theme-changed-placeholder": "z.B. notify-send \"Thema\" \"Gewechselt\"", + "title": "Hooks", + "wallpaper-changed-description": "Befehl, der ausgeführt wird, wenn sich das Hintergrundbild ändert.", + "wallpaper-changed-label": "Hintergrundbild geändert", + "wallpaper-changed-placeholder": "z.B. notify-send \"Hintergrundbild\" \"Geändert\"" + }, + "idle": { + "custom-add": "Befehl hinzufügen", + "custom-description": "Führe einen Shell-Befehl nach einer Zeit der Inaktivität aus.", + "custom-entry-command": "Befehl", + "custom-entry-delete": "Löschen", + "custom-entry-edit": "Benutzerdefinierten Befehl bearbeiten", + "custom-entry-name": "Name", + "custom-entry-name-placeholder": "z.B. Lichter ausschalten", + "custom-entry-new": "Neuer Benutzerdefinierter Befehl", + "custom-entry-timeout": "Inaktivitätszeit", + "custom-entry-timeout-format": "{count} Sekunde", + "custom-entry-timeout-format-plural": "{count} Sekunden", + "custom-entry-unnamed": "Unbenannter Befehl", + "custom-label": "Benutzerdefinierte Inaktivitätsbefehle", + "enable-description": "Automatisch den Bildschirm ausschalten, sperren oder in den Energiesparmodus versetzen nach einer Zeit der Inaktivität.", + "enable-label": "Inaktivitätsverwaltung aktivieren", + "fade-duration-description": "Sekunden für die Schwarzblende-Animation, bevor jede Aktion ausgelöst wird. Jede Mausbewegung bricht die Überblendung ab.", + "fade-duration-label": "Einblenddauer", + "lock-description": "Sekunden der Inaktivität, bevor der Sperrbildschirm aktiviert wird.", + "lock-label": "Bildschirmsperre", + "resume-command-label": "Befehl fortsetzen", + "screen-off-description": "Sekunden der Inaktivität, bevor die Monitore ausgeschaltet werden.", + "screen-off-label": "Bildschirm ausschalten", + "status-description": "Inaktivitätszeit, wie vom Compositor gemeldet.", + "status-label": "Inaktivitätszeit", + "suspend-description": "Sekunden der Inaktivität, bevor das System in den Energiesparmodus geht.", + "tab-behavior": "Verhalten", + "tab-custom": "Benutzerdefiniert", + "timeouts-description": "Auf 0 setzen, um eine Stufe zu deaktivieren. Timeouts werden pausiert, während 'Wach halten' aktiv ist.", + "timeouts-label": "Timeouts", + "title": "Inaktivität", + "unavailable": "Native Inaktivitätsüberwachung ist auf diesem Compositor nicht verfügbar." + }, + "indicator": { + "default-value": "Standard: {value}", + "system-default": "Systemstandard" + }, + "launcher": { + "clipboard-desc": "Greifen Sie über den Launcher auf Ihren Zwischenablageverlauf zu und verwalten Sie ihn.", + "execute-desc": "Konfigurieren Sie, wie Anwendungen gestartet werden.", + "execute-title": "Ausführung", + "settings-annotation-tool-description": "Befehl, der beim Klicken auf die Annotieren-Schaltfläche im Zwischenablageverlauf ausgeführt wird. Das Bild wird an diesen Befehl weitergeleitet.", + "settings-annotation-tool-label": "Annotationstool", + "settings-annotation-tool-placeholder": "z.B. 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Automatisches Einfügen des ausgewählten Zwischenablageinhalts. Benötigt wtype.", + "settings-auto-paste-label": "Automatisches Einfügen", + "settings-clip-chips-description": "Eine Registerkartenleiste anzeigen, um die Zwischenablagehistorie nach Typ zu filtern (Bilder, Links, Dateien, Code usw.).", + "settings-clip-chips-label": "Kategorie-Chips aktivieren", + "settings-clip-preview-description": "Zeige eine Vorschau des Inhalts der Zwischenablage an, wenn der Befehl >clip verwendet wird.", + "settings-clip-preview-label": "Clip-Vorschau aktivieren", + "settings-clip-smart-icons-description": "Spezifische Symbole für Links, Dateien, Farben und Code anzeigen, anstatt eines generischen Zwischenablagesymbols.", + "settings-clip-smart-icons-label": "Intelligente Symbole aktivieren", + "settings-clip-wrap-text-description": "Text in der Zwischenablage-Liste umbrechen, anstatt ihn abzuschneiden.", + "settings-clip-wrap-text-label": "Zwischenablage-Text umbrechen", + "settings-clipboard-history-description": "Zugriff auf zuvor kopierte Elemente über den Launcher.", + "settings-clipboard-history-label": "Zwischenablageverlauf aktivieren", + "settings-clipboard-watch-image-description": "Vollständiger Befehlsstring, der an wl-paste für Bildänderungen übergeben wird (Neustart erforderlich).", + "settings-clipboard-watch-image-label": "Bildüberwachungsbefehl", + "settings-clipboard-watch-text-description": "Vollständiger Befehlsstring, der an wl-paste für Textänderungen übergeben wird (Neustart erforderlich).", + "settings-clipboard-watch-text-label": "Textüberwachungsbefehl", + "settings-custom-launch-prefix-description": "Befehle mit einem benutzerdefinierten Launcher präfixieren (z.B. 'runapp' für systemd-Integration).", + "settings-custom-launch-prefix-enabled-description": "Verwenden Sie ein benutzerdefiniertes Präfix zum Starten von Anwendungen anstelle der Standardmethode.", + "settings-custom-launch-prefix-enabled-label": "Benutzerdefiniertes Start-Präfix aktivieren", + "settings-custom-launch-prefix-label": "Benutzerdefiniertes Start-Präfix", + "settings-density-description": "Größe der Anwendungssymbole und Dichte des Launchers anpassen.", + "settings-density-label": "Dichte", + "settings-desc": "Verhalten und Erscheinungsbild des Starters anpassen.", + "settings-enable-session-search-description": "Sitzungsaktionen (Sperren, Herunterfahren, Neustarten usw.) beim Suchen im Launcher anzeigen.", + "settings-enable-session-search-label": "Sitzungssuche aktivieren", + "settings-enable-settings-search-description": "Einstellungen in den Suchergebnissen des Launchers anzeigen.", + "settings-enable-settings-search-label": "Einstellungen-Suche aktivieren", + "settings-enable-windows-search-description": "Aktive Fenster suchen und fokussieren.", + "settings-enable-windows-search-label": "Fenstersuche aktivieren", + "settings-grid-view-description": "Elemente in einem Raster statt in einer Liste anzeigen.", + "settings-icon-mode-description": "Verwende native System-Icons anstelle von Tabler-Icons.", + "settings-icon-mode-label": "Native Icons verwenden", + "settings-ignore-mouse-input-description": "Deaktivieren Sie die Mausinteraktion und das Mausrad im Launcher.", + "settings-ignore-mouse-input-label": "Maus-Eingabe ignorieren", + "settings-overlay-layer-description": "Zeigt das Startprogramm auf der Überlagerungsebene an, über Vollbildfenstern. Wenn aktiviert, wird das Startprogramm nicht an der Leiste befestigt.", + "settings-overlay-layer-label": "Über Vollbild anzeigen", + "settings-position-description": "Wählen Sie, wo das Starter-Panel erscheint.", + "settings-show-categories-description": "Kategorienregisterkarten zum Filtern von Anwendungen anzeigen.", + "settings-show-categories-label": "Kategorien anzeigen", + "settings-show-icon-background-description": "Zeige einen abgerundeten Rechteck-Hintergrund hinter Icons.", + "settings-show-icon-background-label": "Symbolhintergrund anzeigen", + "settings-sort-by-usage-description": "Wenn aktiviert, erscheinen häufig gestartete Apps zuerst in der Liste.", + "settings-sort-by-usage-label": "Nach Häufigkeit sortieren", + "settings-terminal-command-description": "Befehl zum Starten eines Terminals. Z.B. 'kitty -e' oder 'gnome-terminal --'.", + "settings-terminal-command-label": "Terminalbefehl", + "settings-use-app2unit-description": "Verwendet eine alternative Startmethode zur besseren Verwaltung von App-Prozessen und Problemvermeidung.", + "settings-use-app2unit-label": "App2Unit zum Starten von Anwendungen verwenden", + "settings-view-mode-description": "Wählen Sie das Layout für die Anwendungsstarter-Einträge.", + "settings-view-mode-label": "Ansichtsmodus", + "title": "Starter" + }, + "location": { + "auto-locate-description": "Ermittelt Ihren Standort automatisch und regelmäßig anhand Ihrer IP-Adresse.", + "auto-locate-label": "Automatische Ortung", + "calendar-cards-desc": "Karten im Kalender-Panel organisieren und aktivieren/deaktivieren.", + "calendar-cards-title": "Kalenderkarten", + "calendar-header-label": "Kalenderkopfzeile", + "calendar-month-label": "Kalendermonat", + "date-time-12hour-format-description": "Zeige die Uhrzeit im 12-Stunden-Format auf dem Sperrbildschirm und im Kalender an. Die Uhr in der Leiste hat eigene Einstellungen.", + "date-time-12hour-format-label": "12-Stunden-Zeitformat benutzen", + "date-time-desc": "Anpassen, wie Datum und Zeit erscheinen.", + "date-time-first-day-of-week-automatic": "Automatisch (Systemgebietsschema verwenden)", + "date-time-first-day-of-week-description": "Wählen Sie, welcher Tag die Woche im Kalender beginnen soll.", + "date-time-first-day-of-week-label": "Erster Tag der Woche", + "date-time-show-events-description": "Ereignisse im Kalender-Panel anzeigen.", + "date-time-show-events-label": "Kalenderereignisse anzeigen", + "date-time-title": "Datum & Zeit", + "date-time-use-analog-description": "Zeige eine Analoguhr im Kalenderfenster an.", + "date-time-use-analog-label": "Analoge Uhr verwenden", + "date-time-week-numbers-description": "Zeige die Woche des Jahres (z.B. Woche 38) im Kalender an.", + "date-time-week-numbers-label": "Wochennummern anzeigen", + "geolocate-now-button": "Jetzt orten", + "location-desc": "Genaues Wetter und Nachtlicht-Planung durch Festlegung Ihres Standorts erhalten.", + "location-search-description": "z.B. Dortmund, Deutschland", + "location-search-label": "Nach einem Standort suchen", + "location-search-placeholder": "Standortnamen eingeben", + "location-title": "Ihr Standort", + "weather-desc": "Bevorzugte Temperatureinheit wählen.", + "weather-enabled-description": "Wetterinformationen in der gesamten Benutzeroberfläche anzeigen und Wetterdaten abrufen. Wenn deaktiviert, werden alle Wetterelemente ausgeblendet und es werden keine Netzwerkanfragen gestellt.", + "weather-enabled-label": "Wetter aktivieren", + "weather-fahrenheit-description": "Temperatur in Fahrenheit statt Celsius anzeigen.", + "weather-fahrenheit-label": "Temperatur in Fahrenheit (°F) anzeigen", + "weather-hide-city-description": "Verberge den Stadtnamen in Wetteranzeigen in der gesamten Benutzeroberfläche.", + "weather-hide-city-label": "Stadtname ausblenden", + "weather-hide-timezone-description": "Die Zeitzonenabkürzung in Wetteranzeigen in der gesamten Benutzeroberfläche ausblenden.", + "weather-hide-timezone-label": "Zeitzone ausblenden", + "weather-show-effects-description": "Zeige zusätzliche visuelle Effekte (wie Regen, Schnee oder Blitze) auf der Wetterkarte an.", + "weather-show-effects-label": "Wettereffekte anzeigen", + "weather-show-in-calendar-description": "Zeige die tägliche Wettervorhersage direkt in deiner Kalenderansicht an.", + "weather-show-in-calendar-label": "Wetter im Kalender anzeigen", + "weather-talia-mascot-always-description": "Verwenden Sie immer Talia emojis anstelle von tabler icons.", + "weather-talia-mascot-always-label": "Immer Talia Wetter-Emojis anzeigen" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "Wenn fprintd (Fingerabdruck-Authentifizierung) aktiv ist, können Sie mit dieser Option weiterhin Ihr Passwort anstelle eines Fingerabdrucks verwenden.", + "allow-password-with-fprintd-label": "Passwort-Anmeldung mit fprintd erlauben", + "auto-start-auth-description": "Startet die Fingerabdruckauthentifizierung automatisch, ohne dass ein Tastendruck oder ein Klick erforderlich ist.", + "auto-start-auth-label": "Automatische Authentifizierung beim Start", + "clock-format-description": "Passe das Uhrformat mit Datums-/Uhrzeit-Syntax-Tokens an.", + "clock-format-label": "Uhrenformat", + "clock-style-analog": "Analog", + "clock-style-custom": "Benutzerdefiniert", + "clock-style-description": "Wählen Sie den visuellen Stil der Uhr auf dem Sperrbildschirm.", + "clock-style-digital": "Digital", + "clock-style-label": "Uhrstil", + "compact-lockscreen-description": "Zeige nur die Login-Eingabe und Systemsteuerung, blende Wetter- und Medien-Widgets aus.", + "compact-lockscreen-label": "Kompakter Sperrbildschirm", + "enable-lockscreen-media-controls-description": "Interaktive Medienwiedergabesteuerung auf dem Sperrbildschirm anzeigen.", + "enable-lockscreen-media-controls-label": "Sperrbildschirm-Mediensteuerung", + "lock-on-suspend-description": "Den Bildschirm beim Energiesparen automatisch sperren.", + "lock-on-suspend-label": "Sperren beim Energiesparen", + "lock-screen-animations-description": "Sperrbildschirm-Animationen aktivieren oder deaktivieren.", + "lock-screen-animations-label": "Sperrbildschirm-Animationen", + "lock-screen-blur-strength-description": "Wendet einen Weichzeichnungseffekt auf das Hintergrundbild des Sperrbildschirms an.", + "lock-screen-blur-strength-label": "Sperrbildschirm-Unschärfestärke", + "lock-screen-tint-strength-description": "Wendet eine Farbüberlagerung auf das Hintergrundbild des Sperrbildschirms an.", + "lock-screen-tint-strength-label": "Stärke der Sperrbildschirmtönung", + "monitors-desc": "Sperrbildschirm auf bestimmten Monitoren anzeigen. Standardmäßig auf alle, wenn keine ausgewählt sind.", + "password-chars-description": "Niedliche Symbole, die dein Passwort verstecken.", + "password-chars-label": "Zufällige Passwortsymbole", + "show-hibernate-description": "Die Option 'Ruhezustand' in den Energieoptionen anzeigen.", + "show-hibernate-label": "Ruhezustand anzeigen", + "show-session-buttons-description": "Zugriff auf die Energieeinstellungen vom Sperrbildschirm aus erlauben.", + "show-session-buttons-label": "Energieoptionen", + "title": "Sperrbildschirm" + }, + "network": { + "bluetooth-description": "Bluetooth-Verwaltung aktivieren.", + "bluetooth-rssi-polling-description": "Periodisch RSSI für verbundene Geräte über bluetoothctl abfragen. Ist möglicherweise nicht für alle Geräte verfügbar; verbraucht bei Aktivierung minimale Ressourcen.", + "bluetooth-rssi-polling-label": "Bluetooth-Signalabfrage", + "desc": "WLAN- und Bluetooth-Verbindungen verwalten.", + "wifi-description": "Drahtlosnetzwerke verwalten (erfordert NetworkManager)." + }, + "notifications": { + "duration-critical-urgency-description": "Wie lange kritische Benachrichtigungen sichtbar bleiben.", + "duration-critical-urgency-label": "Kritische Dringlichkeit", + "duration-desc": "Konfigurieren, wie lange Benachrichtigungen je nach Dringlichkeitsstufe sichtbar bleiben.", + "duration-low-urgency-description": "Wie lange Benachrichtigungen niedriger Priorität sichtbar bleiben.", + "duration-low-urgency-label": "Niedrige Dringlichkeit", + "duration-normal-urgency-description": "Wie lange Benachrichtigungen normaler Priorität sichtbar bleiben.", + "duration-normal-urgency-label": "Normale Dringlichkeit", + "duration-respect-expire-description": "Die in der Benachrichtigung gesetzte Ablaufzeit verwenden.", + "duration-respect-expire-label": "Ablaufzeit respektieren", + "duration-title": "Benachrichtigungsdauer", + "history-clear-dismiss-description": "Benachrichtigung aus dem Verlauf löschen, wenn sie verworfen wird.", + "history-clear-dismiss-label": "Löschen beim Schließen", + "history-critical-urgency-description": "Kritische Prioritätsbenachrichtigungen im Verlauf speichern.", + "history-critical-urgency-label": "Kritische Dringlichkeit im Verlauf speichern", + "history-desc": "Steuern, welche Benachrichtigungen basierend auf ihrer Dringlichkeitsstufe im Verlauf gespeichert werden.", + "history-low-urgency-description": "Niedrige Prioritätsbenachrichtigungen im Verlauf speichern.", + "history-low-urgency-label": "Niedrige Dringlichkeit im Verlauf speichern", + "history-normal-urgency-description": "Normale Prioritätsbenachrichtigungen im Verlauf speichern.", + "history-normal-urgency-label": "Normale Dringlichkeit im Verlauf speichern", + "monitors-desc": "Benachrichtigungen auf bestimmten Monitoren anzeigen. Standardmäßig werden sie auf allen Monitoren angezeigt.", + "rules-action-block": "Block", + "rules-action-block-desc": "Überspringt komplett.", + "rules-action-hide": "Ausblenden", + "rules-action-hide-desc": "Kein Popup, kein Ton, wird zur Historie hinzugefügt.", + "rules-action-label": "Aktion", + "rules-action-mute": "Stumm", + "rules-action-mute-desc": "Kein Ton, Pop-up wird trotzdem angezeigt und ist im Verlauf.", + "rules-add": "Regel hinzufügen", + "rules-delete": "Regel löschen", + "rules-description": "App-Namen oder Inhalt abgleichen. Regeln werden der Reihe nach geprüft, und die erste Übereinstimmung wird angewendet.", + "rules-edit": "Regel bearbeiten", + "rules-label": "Filterregeln", + "rules-pattern-label": "Muster", + "rules-pattern-placeholder": "firefox, discord, oder /regex/", + "rules-tab": "Regeln", + "settings-always-on-top-description": "Benachrichtigungen über Vollbildfenstern und anderen Ebenen anzeigen.", + "settings-background-opacity-description": "Transparenz der Benachrichtigungshintergründe anpassen.", + "settings-density-description": "Wählen Sie die Dichte der Benachrichtigungskarte.", + "settings-density-label": "Dichte", + "settings-desc": "Erscheinungsbild und Verhalten von Benachrichtigungen konfigurieren.", + "settings-do-not-disturb-description": "Alle Benachrichtigungs-Popups deaktivieren.", + "settings-enabled-description": "Benachrichtigungs-Daemon aktivieren oder deaktivieren, erfordert einen Neustart von Noctalia Shell.", + "settings-enabled-label": "Benachrichtigungen aktivieren", + "settings-location-description": "Wo Benachrichtigungen auf dem Bildschirm erscheinen.", + "settings-markdown-description": "Benachrichtigungsinhalt mit Markdown-Formatierung rendern.", + "settings-markdown-label": "Markdown aktivieren", + "sounds-desc": "Benachrichtigungs-Soundeffekte und Lautstärke konfigurieren.", + "sounds-enabled-description": "Soundeffekte für eingehende Benachrichtigungen aktivieren.", + "sounds-enabled-label": "Benachrichtigungstöne aktivieren", + "sounds-excluded-apps-description": "Überspringen Sie das Abspielen der konfigurierten Benachrichtigungstöne für bestimmte Anwendungen, die ihre eigenen integrierten Töne haben.", + "sounds-excluded-apps-label": "Ausgeschlossene Anwendungen", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Pfad zur Sound-Datei, die für Benachrichtigungen mit kritischer Priorität abgespielt wird.", + "sounds-files-critical-label": "Sound bei kritischer Dringlichkeit", + "sounds-files-critical-select-title": "Sound-Datei für kritische Dringlichkeit auswählen", + "sounds-files-desc": "Benutzerdefinierte Sound-Dateien für verschiedene Benachrichtigungsdringlichkeitsstufen konfigurieren.", + "sounds-files-low-description": "Pfad zur Sound-Datei, die für Benachrichtigungen mit niedriger Priorität abgespielt wird.", + "sounds-files-low-label": "Sound bei niedriger Dringlichkeit", + "sounds-files-low-select-title": "Sound-Datei für niedrige Dringlichkeit auswählen", + "sounds-files-normal-description": "Pfad zur Sound-Datei, die für Benachrichtigungen mit normaler Priorität abgespielt wird.", + "sounds-files-normal-label": "Sound bei normaler Dringlichkeit", + "sounds-files-normal-select-title": "Sound-Datei für normale Dringlichkeit auswählen", + "sounds-files-placeholder": "Pfad zur Sound-Datei eingeben", + "sounds-files-select-file": "Sound-Datei auswählen", + "sounds-files-title": "Sound-Dateien", + "sounds-files-unified-description": "Pfad zur Sound-Datei, die für Benachrichtigungen abgespielt wird.", + "sounds-files-unified-label": "Benachrichtigungssound", + "sounds-files-unified-select-title": "Benachrichtigungssound-Datei auswählen", + "sounds-separate-description": "Unterschiedliche Sound-Dateien für niedrige, normale und kritische Benachrichtigungsprioritäten verwenden.", + "sounds-separate-label": "Verschiedene Sounds pro Priorität verwenden", + "sounds-title": "Sound-Einstellungen", + "sounds-unavailable-description": "Installieren Sie Qt6 Multimedia, um Benachrichtigungstöne zu aktivieren.", + "sounds-unavailable-label": "Benachrichtigungstöne nicht verfügbar", + "sounds-volume-description": "Lautstärke für Benachrichtigungstöne anpassen.", + "sounds-volume-label": "Sound-Lautstärke", + "toast-battery-description": "Zeige eine Warnung an, wenn der Akkustand unter diesen Prozentsatz fällt.", + "toast-battery-label": "Akkuwarnung", + "toast-desc": "Konfigurieren Sie das Aussehen und Verhalten von Toast-Benachrichtigungen.", + "toast-keyboard-description": "Zeige eine Toast-Benachrichtigung an, wenn sich das Tastaturlayout ändert.", + "toast-keyboard-label": "Tastaturlayout", + "toast-media-description": "Eine Toast-Benachrichtigung anzeigen, wenn sich der Wiedergabestatus von Medien ändert.", + "toast-media-label": "Medien" + }, + "osd": { + "always-on-top-description": "On-Screen Display über Vollbildfenstern und anderen Ebenen anzeigen.", + "always-on-top-label": "Immer im Vordergrund", + "background-opacity-description": "Kontrolliert die Transparenz des OSD Hintergrunds.", + "background-opacity-label": "Hintergrundtransparenz", + "description": "Bildschirm-Overlays wie Lautstärke- und Helligkeitsanzeigen konfigurieren.", + "duration-auto-hide-description": "Zeitspanne bis zum automatischen Ausblenden anpassen.", + "duration-auto-hide-label": "Ausblenden nach", + "duration-desc": "Wie lange das On-Screen Display sichtbar bleibt, bevor es automatisch ausgeblendet wird.", + "duration-title": "Automatisches Ausblenden", + "enabled-description": "Lautstärke- und Helligkeitsänderungen in Echtzeit anzeigen.", + "enabled-label": "On-Screen Display aktivieren", + "events-desc": "Wählen Sie aus, welche Ereignisse die Bildschirmanzeige auslösen.", + "general-desc": "OSD-Sichtbarkeit und -Verhalten konfigurieren.", + "location-description": "Wo On-Screen Displays erscheinen.", + "monitors-desc": "On-Screen Display auf bestimmten Monitoren anzeigen. Standardmäßig auf allen angezeigt.", + "title": "On-Screen Display", + "types-brightness-description": "OSD anzeigen, wenn sich die Bildschirmhelligkeit ändert.", + "types-brightness-label": "Helligkeit", + "types-custom-text-description": "OSD für benutzerdefinierte Textnachrichten aus IPC anzeigen.", + "types-custom-text-label": "Benutzerdefinierter Text", + "types-desc": "Wählen Sie die Ereignisse aus, die das OSD auslösen.", + "types-input-volume-description": "OSD anzeigen, wenn sich die Mikrofonlautstärke ändert.", + "types-input-volume-label": "Eingangslautstärke", + "types-lockkey-description": "OSD anzeigen, wenn Caps Lock, Num Lock oder Scroll Lock umgeschaltet werden.", + "types-lockkey-label": "Feststelltasten", + "types-media-description": "OSD anzeigen, wenn sich der Medienwiedergabestatus ändert (Wiedergabe, Pause, Überspringen).", + "types-media-label": "Medienwiedergabe", + "types-title": "OSD-Auslöseereignisse", + "types-volume-description": "OSD anzeigen, wenn sich die Ausgabelautstärke ändert.", + "types-volume-label": "Ausgangslautstärke" + }, + "plugins": { + "auto-update": "Plugins automatisch aktualisieren", + "auto-update-description": "Automatisch alle Plugins aktualisieren, wenn die Shell startet.", + "available-description": "Plugins aus konfigurierten Quellen durchsuchen und installieren.", + "available-label": "Verfügbare Plugins", + "available-no-plugins-description": "Überprüfen Sie Ihre Plugin-Quellen oder aktualisieren Sie die Liste.", + "available-no-plugins-label": "Keine Plugins verfügbar", + "check-for-updates": "Nach Updates suchen", + "checking-for-updates": "Prüfe auf Updates...", + "collision-already-installed": "Dieses Plugin ist bereits installiert", + "collision-custom-version-exists": "Eine benutzerdefinierte Version von \"{source}\" ist bereits installiert", + "collision-official-version-exists": "Die offizielle Version dieses Plugins ist bereits installiert", + "development-disable": "Entwicklungsmodus für dieses Plugin deaktivieren", + "development-enable": "Entwicklungsmodus für dieses Plugin aktivieren.", + "filter-downloaded": "Heruntergeladen", + "filter-not-downloaded": "Nicht heruntergeladen", + "filter-tags-description": "Plugins nach Kategorie oder Download-Status filtern.", + "filter-tags-label": "Schlagwörter", + "hot-reload-description": "Plugins automatisch neu laden, wenn sich ihre Dateien ändern. Nützlich für die Plugin-Entwicklung.", + "hot-reload-label": "Hot Reload (Entwicklermodus)", + "hot-reloaded": "Plugin neu geladen: {name}", + "install-error": "Installation fehlgeschlagen: {error}", + "install-incompatible": "{plugin} benötigt Noctalia v{version} oder höher", + "install-success": "{plugin} erfolgreich installiert", + "installed-description": "Alle lokal installierten Plugins verwalten und konfigurieren.", + "installed-label": "Installierte Plugins", + "installed-no-plugins-description": "Installieren Sie Plugins aus dem Abschnitt \"Verfügbar\".", + "installed-no-plugins-label": "Keine Plugins installiert", + "installing": "{plugin} wird installiert...", + "notify-updates": "Plugin-Update-Benachrichtigungen", + "notify-updates-description": "Zeige ein Benachrichtigungs-Popup an, wenn neue Plugin-Updates verfügbar sind.", + "open-plugin-page": "Plugin-Seite öffnen", + "open-plugins-tab": "Plugin-Einstellungen öffnen", + "plugin-settings-title": "{plugin} Einstellungen", + "refresh-refreshing": "Aktualisiere Plugin-Liste...", + "refresh-tooltip": "Verfügbare Plugins aktualisieren", + "settings-error-not-loaded": "Plugin nicht geladen", + "settings-saved": "Plugin-Einstellungen gespeichert", + "settings-tooltip": "Plugin-Einstellungen", + "source-custom": "Benutzerdefinierte Quelle", + "sources-add-custom": "Benutzerdefiniertes Repository hinzufügen", + "sources-add-dialog-description": "Ein GitHub-Repository als Plugin-Quelle hinzufügen.", + "sources-add-dialog-error": "Fehler beim Hinzufügen der Plugin-Quelle", + "sources-add-dialog-name": "Repository-Name", + "sources-add-dialog-name-placeholder": "Meine benutzerdefinierten Plugins", + "sources-add-dialog-success": "Plugin-Quelle erfolgreich hinzugefügt", + "sources-add-dialog-title": "Plugin-Quelle hinzufügen", + "sources-add-dialog-url": "Repository-URL", + "sources-description": "Plugin-Repositories verwalten.", + "sources-placeholder": "Mein cooles Repository", + "sources-remove-tooltip": "Plugin-Quelle entfernen", + "title": "Plugins", + "translations-reloaded": "Übersetzungen neu geladen: {name}", + "uninstall-dialog-description": "Sind Sie sicher, dass Sie {plugin} deinstallieren möchten? Dadurch werden alle Plugin-Daten entfernt.", + "uninstall-dialog-title": "Plugin deinstallieren", + "uninstall-error": "Deinstallation fehlgeschlagen: {error}", + "uninstall-success": "{plugin} wurde erfolgreich deinstalliert", + "uninstalling": "{Plugin} wird deinstalliert...", + "update-all": "Alle aktualisieren ({count})", + "update-all-success": "Alle Plugins wurden erfolgreich aktualisiert", + "update-available": "Neues Plugin-Update verfügbar", + "update-available-plural": "Neue Plugin-Updates verfügbar ({count})", + "update-error": "Fehler beim Aktualisieren des Plugins: {plugin}: {error}", + "update-pending": "v{current} → v{new} (benötigt Noctalia v{required})", + "update-success": "{plugin} wurde auf v{version} aktualisiert", + "update-version": "v{current} → v{new}", + "updating": "Aktualisierung läuft..." + }, + "region": { + "title": "Region" + }, + "session-menu": { + "countdown-duration-description": "Legen Sie fest, wie lange der Countdown-Timer vor der Ausführung von Energieaktionen dauert.", + "countdown-duration-label": "Countdown-Dauer", + "enable-countdown-description": "Zeige einen Countdown-Timer vor der Ausführung von Energieaktionen.", + "enable-countdown-label": "Countdown-Timer aktivieren", + "entries-desc": "Anpassen, welche Energieaktionen im Sitzungsmenü angezeigt werden und in welcher Reihenfolge.", + "entries-title": "Energieaktionen", + "entry-settings-command-description": "Benutzerdefinierter Befehl, der für diese Aktion ausgeführt werden soll. Leer lassen, um den Standard-Systembefehl zu verwenden.", + "entry-settings-command-placeholder": "z.B. systemctl poweroff", + "entry-settings-default-command-lock": "Interner Sperrbildschirm (kein Befehl)", + "entry-settings-default-command-logout": "Interne Abmeldung (kein Befehl)", + "entry-settings-default-info-description": "Wenn kein benutzerdefinierter Befehl angegeben ist, wird der standardmäßige Systembefehl verwendet.", + "entry-settings-default-info-label": "Standardbefehl", + "entry-settings-keybind-description": "Weisen Sie eine Tastenkombination zu, um diese Aktion auszulösen, wenn das Sitzungsmenü geöffnet ist.", + "entry-settings-keybind-placeholder": "Drücke Aufnehmen, um eine Taste zuzuweisen...", + "entry-settings-keybind-recording": "Aufnahme läuft... Drücken Sie eine beliebige Tastenkombination", + "entry-settings-title": "Konfiguriere {entry}", + "entry-settings-tooltip": "Konfigurationsbefehl", + "general-desc": "Konfigurieren Sie das Verhalten und Erscheinungsbild des Sitzungsmenü-Panels.", + "large-buttons-layout-description": "Wählen Sie, wie die Sitzungsmenü-Schaltflächen angezeigt werden.", + "large-buttons-layout-label": "Layout mit großen Schaltflächen", + "large-buttons-style-description": "Das Sitzungsmenü mit großen Schaltflächen in einem Rasterlayout anzeigen.", + "large-buttons-style-label": "Stil: Große Schaltflächen", + "position-description": "Wählen Sie, wo das Sitzungsmenü-Panel beim Öffnen erscheint.", + "show-header-description": "Zeige den Titel und die Schließen-Schaltfläche oben im Sitzungsmenü.", + "show-header-label": "Kopfzeile anzeigen", + "show-keybinds-description": "Tastenkombinationshinweise bei den Sitzungsoptionen anzeigen.", + "show-keybinds-label": "Tastenkombinationen anzeigen" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Desktop-Widgets sichtbar lassen, während der Noctalia Leistungsmodus aktiviert ist.", + "noctalia-performance-disable-desktop-widgets-label": "Aktiviere Desktop-Widgets im Leistungsmodus", + "noctalia-performance-disable-wallpaper-description": "Halte Desktop-, Übersicht- und Sperrbildschirm-Hintergrundbilder sichtbar, während der Noctalia Leistungsmodus aktiviert ist.", + "noctalia-performance-disable-wallpaper-label": "Hintergrundbild-Rendering im Leistungsmodus aktivieren", + "title": "System" + }, + "system-monitor": { + "critical-color-label": "Kritische Farbe", + "custom-highlight-colors-title-label": "Benutzerdefinierte Hervorhebungsfarben", + "disk-available-label": "Verfügbarer Speicherplatz", + "disk-section-label": "Festplattennutzung", + "enable-dgpu-monitoring-description": "Warnung: Dies aktiviert Ihre dedizierte GPU (NVIDIA/AMD), was die Akkulaufzeit von Laptops mit Hybridgrafik erheblich beeinträchtigen kann.", + "enable-dgpu-monitoring-label": "Dedizierte GPU-Überwachung aktivieren", + "external-monitor-description": "Geben Sie den Befehl oder Anwendungspfad ein, um die externe Systemmonitor-Anwendung zu starten.", + "external-monitor-label": "Externer Systemmonitor-Befehl", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "Konfigurieren Sie das Verhalten und Aussehen des Systemmonitors.", + "gpu-section-label": "GPU-Temperatur", + "highlight-colors-section-label": "Hervorhebungsfarben", + "polling-interval-label": "Abfrageintervall", + "polling-section-description": "Konfigurieren Sie, wie oft jede Systemmetrik aktualisiert wird.", + "polling-section-label": "Abfrageintervalle", + "threshold-critical": "Kritisch", + "threshold-warning": "Warnung", + "thresholds-section-description": "Passe die Warn-/Kritisch-Schwellen und die Abfrageintervalle für jede Systemmetrik an.", + "use-custom-highlight-colors-description": "Wenn deaktiviert, werden die Standard-Hervorhebungsfarben des Themes verwendet.", + "use-custom-highlight-colors-label": "Benutzerdefinierte Hervorhebungsfarben verwenden", + "warning-color-label": "Warnfarbe" + }, + "user-interface": { + "allow-panels-without-bar-description": "Wenn aktiviert, können sich Panels auf jedem Bildschirm öffnen. Wenn deaktiviert, öffnen sich Panels nur auf Bildschirmen mit einer Leiste, was den Speicherverbrauch reduzieren kann.", + "allow-panels-without-bar-label": "Panels auf Bildschirmen ohne Leiste zulassen", + "animation-disable-description": "Deaktivieren Sie alle Animationen für eine schnellere und reaktionsfreudigere Erfahrung.", + "animation-disable-label": "UI-Animationen deaktivieren", + "animation-speed-description": "Globale Animationsgeschwindigkeit anpassen.", + "animation-speed-label": "Animationsgeschwindigkeit", + "animation-speed-reset": "Animationsgeschwindigkeit zurücksetzen", + "appearance-desc": "Visuelle Elemente wie Tooltips, Rahmen und Schatten anpassen.", + "blur-behind-description": "Macht den Bereich hinter Leisten und Panels unscharf mithilfe des Compositor-Blur-Protokolls.", + "blur-behind-label": "Hintergrundunschärfe", + "box-border-description": "Zeige eine Umrandung um Inhaltsbereiche an.", + "box-border-label": "Containerumriss", + "box-border-radius-description": "Passt die Eckenrundung wichtiger Layoutbereiche an, wie z.B. Seitenleisten, Karten und Inhaltsbereiche.", + "box-border-radius-label": "Eckenradius", + "control-border-radius-description": "Steuert die Krümmung interaktiver Elemente, einschließlich Schaltflächen, Umschalter und Textfelder.", + "control-border-radius-label": "Eingaberadius", + "desc": "Passen Sie das Aussehen, die Haptik und das Verhalten der Benutzeroberfläche an.", + "dim-desktop-description": "Den Desktop abdunkeln, wenn Fenster oder Menüs geöffnet sind.", + "dim-desktop-label": "Desktop abdunkeln", + "dimmer-opacity-description": "Deckkraft für das Abdunkeln des Desktops festlegen.", + "dimmer-opacity-label": "Deckkraft des gedimmten Desktops", + "panel-background-opacity-description": "Legen Sie die Hintergrundtransparenz für alle Panels fest (Leiste, Starter, Einstellungen, usw.).", + "panel-background-opacity-label": "Panel-Hintergrunddeckkraft", + "panels-attached-to-bar-description": "Die Panels rasten an der Stange und den Bildschirmrändern ein und sorgen so für ein nahtloses Erscheinungsbild mit stilvollen, umgekehrten Ecken.", + "panels-attached-to-bar-label": "Panels an Kanten ausrichten", + "panels-overlay-description": "Stellt sicher, dass Panels und die Leiste sichtbar bleiben, auch über Vollbildanwendungen.", + "panels-overlay-label": "Panels & Leiste oben behalten", + "scaling-description": "Ändert die Größe der allgemeinen Benutzeroberfläche, mit Ausnahme der Leiste.", + "scaling-label": "Oberflächenskalierung", + "scrollbar-always-visible-description": "Bildlaufleisten immer sichtbar lassen, wenn Inhalte scrollbar sind, anstatt sie nur beim Darüberfahren anzuzeigen.", + "scrollbar-always-visible-label": "Bildlaufleisten immer anzeigen", + "settings-panel-header": "Einstellungs-Panel", + "settings-panel-mode-description": "Wählen Sie das Layout der Einstellungen (möglicherweise ist ein Neustart erforderlich).", + "settings-panel-mode-label": "Einstellungs-Panel-Modus", + "settings-panel-sidebar-card-style-description": "Umhüllt die Einstellungs-Seitenleiste mit einem gefüllten Hintergrund und abgerundeten Ecken.", + "settings-panel-sidebar-card-style-label": "Seitenleisten-Kartenstil", + "shadows-description": "Aktiviert Schlagschatten unter Leisten und Panels.", + "shadows-direction-description": "Wählen Sie, wohin der Schatten geworfen wird.", + "shadows-direction-label": "Schattenrichtung", + "shadows-label": "Schlagschatten", + "title": "Benutzeroberfläche", + "tooltips-description": "Tooltips in der gesamten Benutzeroberfläche aktivieren oder deaktivieren.", + "tooltips-label": "Tooltips anzeigen", + "translucent-widgets-description": "Mache Schaltflächen, Tabs und andere Widgets in Panels halbtransparent.", + "translucent-widgets-label": "Transluzente Widgets" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Alphabetisch", + "automation-change-mode-description": "Wählen Sie, wie Hintergrundbilder bei automatischem Wechsel ausgewählt werden.", + "automation-change-mode-label": "Wechselmodus", + "automation-custom-interval-description": "Zeit als HH:MM eingeben (z.B. 01:30).", + "automation-custom-interval-label": "Benutzerdefiniertes Intervall", + "automation-interval-description": "Wie oft Hintergrundbilder automatisch gewechselt werden sollen.", + "automation-interval-label": "Hintergrundbild-Intervall", + "automation-random-wallpaper-description": "Zufällige Hintergrundbild-Wechsel in regelmäßigen Abständen planen.", + "automation-scheduled-change-description": "Hintergrundbilder automatisch in regelmäßigen Abständen wechseln.", + "automation-scheduled-change-label": "Geplanter Wechsel", + "look-feel-edge-smoothness-description": "Wendet einen weichen, gefiederten Effekt auf die Kante von Übergängen an.", + "look-feel-edge-smoothness-label": "Übergangseffekt weichzeichnen", + "look-feel-fill-color-description": "Füllfarbe wählen, die hinter dem Hintergrundbild erscheinen kann.", + "look-feel-fill-mode-description": "Wählen Sie, wie das Bild skaliert werden soll, um zur Auflösung Ihres Monitors zu passen.", + "look-feel-fill-mode-label": "Füllmodus", + "look-feel-skip-startup-transition-description": "Überspringe die Hintergrundbild-Animation, wenn die Shell startet.", + "look-feel-skip-startup-transition-label": "Startübergang überspringen", + "look-feel-title": "Aussehen & Verhalten", + "look-feel-transition-duration-description": "Dauer der Übergangsanimationen in Sekunden.", + "look-feel-transition-duration-label": "Übergangsdauer", + "look-feel-transition-type-description": "Animationstyp beim Wechsel zwischen Hintergrundbildern.", + "look-feel-transition-type-label": "Übergangstyp", + "settings-clear-cache-button": "Cache leeren", + "settings-clear-cache-description": "Zwischengespeicherte, angepasste Hintergrundbilder löschen, um Speicherplatz freizugeben.", + "settings-clear-cache-label": "Hintergrundbild-Cache", + "settings-clear-cache-toast": "Hintergrundbild-Cache geleert", + "settings-desc": "Steuern Sie, wie Hintergrundbilder verwaltet und angezeigt werden.", + "settings-enable-management-description": "Hintergrundbilder mit Noctalia verwalten. Deaktivieren, wenn Sie eine andere Anwendung bevorzugen.", + "settings-enable-management-label": "Hintergrundbild-Verwaltung aktivieren", + "settings-enable-overview-description": "Wendet ein verschwommenes und abgedunkeltes Hintergrundbild auf den Übersichts-Bildschirm an.", + "settings-enable-overview-label": "Übersichtshintergrund aktivieren", + "settings-folder-description": "Pfad zu Ihrem Haupt-Hintergrundbild-Ordner.", + "settings-folder-label": "Hintergrundbild-Ordner", + "settings-hide-wallpaper-filenames-tooltip-hide": "Dateinamen ausblenden", + "settings-hide-wallpaper-filenames-tooltip-show": "Dateinamen anzeigen", + "settings-monitor-specific-description": "Unterschiedlichen Hintergrundbild-Ordner für jeden Monitor festlegen.", + "settings-monitor-specific-label": "Monitor-spezifische Verzeichnisse", + "settings-monitor-specific-tooltip": "Monitor-Hintergrundbild-Ordner", + "settings-overview-blur-strength-description": "Wendet die Unschärfestärke auf die Übersicht an.", + "settings-overview-blur-strength-label": "Unschärfestärke der Übersicht", + "settings-overview-tint-description": "Wendet die Tönungsstärke auf die Übersicht an.", + "settings-overview-tint-label": "Stärke der Übersichts-Tönung", + "settings-recursive-search-description": "Auch nach Hintergrundbildern in Unterordnern des Hintergrundbild-Verzeichnisses suchen.", + "settings-recursive-search-label": "Unterordner durchsuchen", + "settings-select-monitor-folder": "Monitor-Hintergrundbild-Ordner auswählen", + "settings-selector-description": "Wählen Sie Ihr Hintergrundbild aus.", + "settings-selector-position-description": "Wählen Sie aus, wo das Hintergrundbild-Auswahlfeld angezeigt wird.", + "settings-show-hidden-files-tooltip-hide": "Versteckte Dateien ausblenden", + "settings-show-hidden-files-tooltip-show": "Versteckte Dateien anzeigen", + "settings-title": "Hintergrundbild-Einstellungen", + "settings-use-original-images-description": "Überspringe das Skalieren von Hintergrundbildern vor der Anzeige. Spart Speicherplatz und reduziert die CPU-Auslastung während des Hintergrundbildwechsels, kann aber bei sehr großen Bildern mehr Arbeitsspeicher verbrauchen.", + "settings-use-original-images-label": "Originalbilder verwenden", + "settings-view-mode-description": "Wählen Sie aus, wie Hintergrundbilder aus Ihrem Verzeichnis angezeigt werden.", + "settings-view-mode-label": "Ansichtsmodus", + "view-mode-browse": "Verzeichnisse durchsuchen", + "view-mode-cycle-tooltip": "Ansichtsmodus: {mode} (Klicken zum Ändern)", + "view-mode-recursive": "Vereinfachte Unterverzeichnisse", + "view-mode-single": "Stammverzeichnis" + } + }, + "placeholders": { + "add-new-keybind": "Neue Tastenkombination hinzufügen", + "command-example": "echo \"Hallo Welt\"", + "enter-command": "Befehl eingeben (App oder benutzerdefiniertes Skript)", + "enter-ipc-identifier": "Geben Sie eine eindeutige Kennung für IPC-Befehle ein", + "enter-path": "Pfad eingeben...", + "enter-text-to-collapse": "z.B. 'nothing is playing'. Verwenden Sie /regex/ für Muster.", + "enter-tooltip": "Tooltip eingeben", + "enter-width-pixels": "Breite in Pixeln eingeben", + "keybind-recording": "Tastenkombination aufzeichnen...", + "search": "Suchen...", + "search-icons": "z.B. noctalia, niri, battery, cloud", + "search-launcher": "Einträge suchen... oder > für Befehle verwenden", + "search-wallhaven": "Wallhaven durchsuchen...", + "search-wallpapers": "Zum Filtern von Hintergrundbildern eingeben..." + }, + "positions": { + "bottom": "Unten", + "bottom-center": "Unten mittig", + "bottom-left": "Unten links", + "bottom-right": "Unten rechts", + "center": "Zentriert", + "center-left": "Links mittig", + "center-right": "Rechts mittig", + "close-to-bar": "Nah zur Leisten-Schaltfläche", + "follow-bar": "Folge der Leiste", + "left": "Links", + "right": "Rechts", + "top": "Oben", + "top-center": "Oben mittig", + "top-left": "Oben links", + "top-right": "Oben rechts" + }, + "session-menu": { + "action-in-seconds": "{action} in {seconds} Sekunden...", + "cancel-timer": "Timer abbrechen", + "title": "Sitzungsmenü" + }, + "setup": { + "all-done": "Fertig!", + "appearance": { + "subheader": "Wählen Sie den Dunkelmodus und Farbquellen (Hintergrundbild oder vordefiniert)." + }, + "customize": { + "header": "Erlebnis anpassen", + "subheader": "Leistenposition, Dichte, Skalierung und mehr einstellen." + }, + "skip-setup": "Einrichtung überspringen", + "telemetry-wizard-done": "Verstanden!", + "telemetry-wizard-note": "Sie haben die Kontrolle – aktivieren oder deaktivieren Sie dies jederzeit in den Einstellungen.", + "telemetry-wizard-subtitle": "Wir haben anonyme Analysen hinzugefügt, um Noctalia zu verbessern", + "telemetry-wizard-title": "Datenschutz-Update", + "wallpaper": { + "choose-dir": "Wählen Sie ein Verzeichnis mit Ihren Hintergrundbildern", + "dir-browse": "Ordner auswählen", + "dir-description": "Wählen Sie den Ordner mit Ihren Hintergrundbildern.", + "dir-label": "Hintergrundbild-Verzeichnis", + "dir-select-title": "Hintergrundbild-Ordner wählen", + "header": "Wählen Sie Ihr Hintergrundbild", + "no-dir": "Kein Hintergrundbild-Verzeichnis ausgewählt", + "no-valid": "Keine gültigen Bilddateien gefunden in: {dir}", + "none-in-dir": "Keine Hintergrundbilder im Verzeichnis gefunden", + "preview-error": "Bild konnte nicht geladen werden", + "select-prompt": "Wählen Sie unten ein Hintergrundbild", + "subheader": "Bestimmen Sie die Stimmung mit einem schönen Hintergrund." + }, + "welcome": "Willkommen", + "welcome-note": "Nur ein paar Grundeinstellungen — alle Optionen befinden sich in den Einstellungen", + "welcome-subtitle": "Gestalten wir Ihren Desktop einzigartig", + "welcome-title": "Willkommen bei Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "Kein Medienplayer erkannt", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "Unbekannte App", + "uptime": "Laufzeit: {uptime}", + "user-requested": "Vom Benutzer angefordert", + "welcome-back": "Willkommen zurück,", + "widget-settings-title": "{widget} Einstellungen" + }, + "system-monitor": { + "core-usage": "Kern {id} Nutzung", + "cpu-temp": "CPU-Temperatur", + "cpu-usage": "CPU-Auslastung", + "disk": "Festplatte", + "download-speed": "Download-Speed", + "gpu-temp": "GPU-Temperatur", + "load-average": "Lastdurchschnitt", + "title": "Systemmonitor", + "upload-speed": "Upload-Speed" + }, + "toast": { + "airplane-mode": { + "description": "Alle drahtlosen Kommunikationen deaktivieren.", + "title": "Flugmodus" + }, + "battery": { + "critical": "Kritischer Akkustand", + "critical-desc": "Akku ist bei {percent}% — bitte sofort das Ladegerät anschließen", + "low": "Niedriger Akkustand", + "low-desc": "Akku ist bei {percent}% — bitte das Ladegerät anschließen" + }, + "bluetooth": { + "address-copied": "Adresse in die Zwischenablage kopiert", + "auto-connect-disabled": "Automatisches Verbinden deaktiviert", + "auto-connect-enabled": "Automatisches Verbinden aktiviert", + "auto-connecting": "Verbinde mit {count} Gerät(en)...", + "confirm-code": "Bestätigungscode {value} auf dem anderen Gerät bestätigen.", + "connect-failed": "Verbindung mit dem Gerät fehlgeschlagen", + "disconnect-failed": "Trennen vom Gerät fehlgeschlagen", + "discoverable-change-failed": "Ändern des Sichtbarkeitsstatus fehlgeschlagen", + "discoverable-disabled": "Sichtbar deaktiviert", + "discoverable-enabled": "Sichtbar aktiviert", + "display-code": "Geben Sie den Code {value} auf dem anderen Gerät ein.", + "forget-failed": "Entfernen des Geräts fehlgeschlagen", + "pair-failed": "Koppeln des Geräts fehlgeschlagen", + "passkey-required": "Passwortschlüssel vom Gerät erforderlich.", + "pincode-required": "PIN-Code vom Gerät erforderlich.", + "state-change-failed": "Ändern des Bluetooth-Status fehlgeschlagen" + }, + "clipboard": { + "long-text": "Langer Text", + "unavailable": "Zwischenablageverlauf nicht verfügbar", + "unavailable-desc": "Die 'cliphist' Anwendung ist nicht installiert. Bitte installieren Sie sie, um Zwischenablageverlauf-Features zu nutzen" + }, + "custom-command-failed": { + "description": "Befehl fehlgeschlagen: {command}\\nExit-Code: {code}", + "title": "Benutzerdefinierter Befehl fehlgeschlagen" + }, + "do-not-disturb": { + "disabled": "'Nicht stören' deaktiviert", + "disabled-desc": "Alle Benachrichtigungen werden angezeigt", + "enabled": "'Nicht stören' aktiviert", + "enabled-desc": "Sie finden diese Benachrichtigungen in Ihrem Verlauf" + }, + "donation-opened": "Spendenseite in Ihrem Browser geöffnet", + "internet-limited": "Verbunden ohne Internet", + "keyboard-layout": { + "changed": "Tastaturlayout geändert zu {layout}", + "title": "Tastatur" + }, + "missing-control-center": { + "description": "Das Kontrollzentrum-Widget wurde aus der Leiste entfernt. Um es erneut über die Leiste zu öffnen, fügen Sie das Widget wieder hinzu. Sie können es auch durch Rechtsklick auf die Leiste öffnen", + "label": "Letztes Kontrollzentrum-Widget entfernt" + }, + "night-light": { + "forced": "Aktivierung erzwungen", + "normal": "Normaler Modus", + "not-installed": "wlsunset nicht installiert" + }, + "noctalia-performance": { + "disabled": "Leistungsmodus deaktiviert", + "enabled": "Leistungsmodus aktiviert", + "label": "Noctalia-Performance" + }, + "power-profile": { + "changed": "Energieprofil geändert", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "Beim Verarbeiten von Vorlagen ist ein Fehler aufgetreten", + "title": "Vorlagenverarbeitung fehlgeschlagen" + }, + "vpn": { + "connected": "Verbunden mit '{name}'", + "disconnected": "Verbindung zu '{name}' getrennt" + }, + "wallpaper-colors": { + "disabled": "Hintergrundbild-Farben deaktiviert", + "enabled": "Hintergrundbild-Farben aktiviert", + "label": "Hintergrundfarbengenerierung" + }, + "wifi": { + "connected": "Verbunden mit '{ssid}'", + "connection-failed": "Verbindung fehlgeschlagen", + "connection-timeout": "Zeitüberschreitung bei der Verbindung", + "disconnected": "Getrennt von '{ssid}'", + "incorrect-password": "Falsches Passwort", + "network-not-found": "Netzwerk nicht gefunden" + } + }, + "tooltips": { + "add-widget": "Widget hinzufügen", + "auto-connect": "Automatisches Verbinden für dieses Gerät umschalten", + "bluetooth-auto-connect-off": "Automatisches Verbinden deaktiviert", + "bluetooth-auto-connect-on": "Automatisches Verbinden aktiviert", + "bluetooth-devices": "Bluetooth-Geräte", + "brightness-at": "Helligkeit: {brightness}%", + "click-to-start-recording": "Bildschirmrekorder (Aufnahme starten)", + "click-to-stop-recording": "Bildschirmrekorder (Aufnahme stoppen)", + "collapse": "Seitenleiste einklappen", + "copy-address": "Adresse kopieren", + "delete-notification": "Benachrichtigung löschen", + "dismiss-notification": "Benachrichtigung schließen", + "do-not-disturb-enabled": "Nicht stören", + "expand": "Seitenleiste ausklappen", + "forget-network": "Netzwerk vergessen", + "grid-view": "Rasteransicht", + "hidden-files-hide": "Versteckte Dateien", + "hide-unnamed-devices": "Unbenannte Geräte ausblenden", + "home": "Home-Verzeichnis", + "input-muted": "Eingangsstummschaltung umschalten", + "keep-awake": "Wach halten", + "keyboard-layout": "{layout} Tastaturlayout", + "list-view": "Listenansicht", + "manage-vpn": "VPN-Verbindungen", + "manage-wifi": "WLAN", + "max-widgets-reached": "Maximale Anzahl an Widgets erreicht", + "microphone-volume-at": "Mikrofonlautstärke: {volume}%", + "move-to-section": "Gehe zu {section}", + "mute": "Stummschalten", + "next-media": "Nächster Titel", + "next-month": "Nächster Monat", + "night-light-not-installed": "Nachtlicht (nicht verfügbar)", + "noctalia-performance-enabled": "Noctalia-Leistungsmodus", + "open-annotation-tool": "Mit dem Anmerkungswerkzeug öffnen", + "open-control-center": "Kontrollzentrum", + "open-notification-history-enable-dnd": "Benachrichtigungsverlauf", + "open-settings": "Einstellungen", + "open-tray-dropdown": "System-Tray", + "output-muted": "Ausgabe stummschalten", + "power-profile": "{profile} Energieprofil", + "previous-media": "Vorheriger Titel", + "previous-month": "Vorheriger Monat", + "refresh-devices": "Geräte aktualisieren", + "refresh-wallhaven": "Wallhaven-Ergebnisse aktualisieren", + "refresh-wallpaper-list": "Hintergrundbild-Liste aktualisieren", + "remove-widget": "Widget entfernen", + "search-close": "Suche schließen", + "session-menu": "Sitzungsmenü", + "show-all-devices": "Alle Geräte anzeigen", + "switch-to-dark-mode": "Dunkler Modus", + "switch-to-light-mode": "Heller Modus", + "unmute": "Stummschaltung aufheben", + "up": "Übergeordnetes Verzeichnis", + "volume-at": "Lautstärke: {volume}%", + "wallpaper-selector": "Hintergrundbild-Auswahl" + }, + "wallpaper": { + "browse": { + "empty-directory": "Dieses Verzeichnis ist leer.", + "go-root": "Zum Wallpaper-Stammverzeichnis gehen", + "go-up": "Zum übergeordneten Ordner gehen", + "go-up-hint": "Verwende die Zurück-Taste, um nach oben zu navigieren." + }, + "configure-directory": "Konfigurieren Sie Ihr Hintergrundbild-Verzeichnis mit Bildern.", + "fill-modes": { + "crop": "Zuschneiden (Füllen)", + "fit": "Einpassen (Beibehalten)", + "repeat": "Wiederholen (Kachel)", + "stretch": "Strecken" + }, + "no-match": "Keine Übereinstimmung gefunden.", + "no-wallpaper": "Kein Hintergrundbild gefunden.", + "panel": { + "apikey-help": "Ein API-Schlüssel ist erforderlich, um auf NSFW-Inhalte zuzugreifen.", + "apikey-label": "API-Schlüssel", + "apikey-managed-by-env": "Verwaltet über die Umgebungsvariable NOCTALIA_WALLHAVEN_API_KEY.", + "apikey-placeholder": "Geben Sie Ihren Wallhaven API-Schlüssel ein", + "appearance-dark-tab": "Dunkel", + "appearance-light-tab": "Hell", + "categories-anime": "Anime", + "categories-label": "Kategorien", + "categories-people": "Personen", + "color-extraction-disabled": "Hintergrundbild-Farbenextraktion verwenden", + "color-extraction-enabled": "Vordefinierte Farbschemata nutzen", + "header-devices-apply-all-tooltip": "Hintergrundbild pro Monitor wählen", + "header-devices-per-monitor-tooltip": "Auf alle Monitore anwenden", + "header-link-light-dark-tooltip": "Hintergrundbild beim Themenwechsel beibehalten", + "header-separate-light-dark-tooltip": "Hintergrundbild automatisch mit dem Thema wechseln", + "header-sun-linked-tooltip": "Helle und dunkle Hintergrundbilder trennen", + "header-sun-separate-tooltip": "Gleiches Hintergrundbild für hell und dunkel", + "order-asc": "Aufsteigend", + "order-desc": "Absteigend", + "order-label": "Reihenfolge", + "purity-label": "Inhaltsfilter", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Sketchy", + "ratios-all-portrait": "Alle Hochformate", + "ratios-all-wide": "Alle Querformate", + "ratios-any": "Alle", + "ratios-label": "Seitenverhältnis", + "resolution-atleast": "Mindestens", + "resolution-exact": "Exakt", + "resolution-label": "Auflösung", + "resolution-mode-label": "Modus", + "solid-color-tooltip": "Einfarbiger Hintergrund", + "sort-date-asc": "Nach Ältestem zuerst sortieren", + "sort-date-desc": "Nach Neuestem zuerst sortieren", + "sort-name-asc": "Nach Name sortieren (A-Z)", + "sort-name-desc": "Nach Name sortieren (Z-A)", + "sort-random": "Nach Zufall sortieren", + "sorting-date-added": "Hinzugefügt am", + "sorting-favorites": "Favoriten", + "sorting-label": "Sortiere nach", + "sorting-relevance": "Relevanz", + "sorting-toplist": "Topliste", + "sorting-views": "Aufrufe", + "source-label": "Quelle", + "source-wallhaven": "Wallhaven", + "title": "Hintergrundbild-Auswahl", + "wallhaven-settings-title": "Wallhaven-Einstellungen" + }, + "transitions": { + "disc": "Scheibe", + "fade": "Überblenden", + "honeycomb": "Honigwabe", + "pixelate": "Verpixeln", + "stripes": "Streifen", + "wipe": "Wischen" + }, + "try-different-search": "Versuchen Sie eine andere Suchanfrage.", + "wallhaven": { + "loading": "Hintergrundbilder werden geladen...", + "no-results": "Keine Hintergrundbilder gefunden. Versuchen Sie eine andere Suchanfrage.", + "page": "{current} von {total}", + "page-prefix": "Seite", + "page-suffix": "von {total}" + } + }, + "weather": { + "clear-sky": "Klarer Himmel", + "drizzle": "Nieselregen", + "fog": "Nebelig", + "mainly-clear": "Überwiegend klar", + "overcast": "Bedeckt", + "partly-cloudy": "Teilweise bewölkt", + "rain-showers": "Regenschauer", + "snow": "Schnee", + "thunderstorm": "Gewitter" + }, + "widgets": { + "color-picker": { + "palette-description": "Wählen Sie aus einer großen Auswahl vordefinierter Farben.", + "palette-label": "Palette", + "palette-theme-colors": "Schneller Zugriff auf die Farbpalette Ihres Themes.", + "title": "Farbauswahl" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm in Kleinbuchstaben", + "ampm-uppercase": "AM/PM in Großbuchstaben", + "common-12hour-time-minutes": "12-Stunden-Zeit mit Minuten", + "common-24hour-time-minutes": "24-Stunden-Zeit mit Minuten", + "common-24hour-time-seconds": "24-Stunden-Zeit mit Sekunden", + "common-european-date": "Europäisches Datumsformat", + "common-iso-date": "ISO-Datumsformat", + "common-us-date": "US-Datumsformat", + "common-weekday-date": "Wochentag mit Datum", + "common-weekday-month-day": "Wochentag, Monat und Tag", + "day-abbreviated": "Abgekürzter Tagesname", + "day-full": "Vollständiger Tagesname", + "day-leading-zero": "Tag mit führender Null (01-31)", + "day-no-leading-zero": "Tag ohne führende Null (1-31)", + "hour-leading-zero": "Stunde mit führender Null (00-23) — 24-Stunden-Format", + "hour-no-leading-zero": "Stunde ohne führende Null (0-23) — 24-Stunden-Format", + "minute-leading-zero": "Minute mit führender Null (00-59)", + "minute-no-leading-zero": "Minute ohne führende Null (0-59)", + "month-abbreviated": "Abgekürzter Monatsname", + "month-full": "Vollständiger Monatsname", + "month-number-leading-zero": "Monat als Zahl mit führender Null (01-12)", + "month-number-no-zero": "Monat als Zahl ohne führende Null (1-12)", + "second-leading-zero": "Sekunde mit führender Null (00-59)", + "second-no-leading-zero": "Sekunde ohne führende Null (0-59)", + "timezone-abbreviation": "Zeitzonenabkürzung", + "year-four-digit": "Jahr als vierstellige Zahl", + "year-two-digit": "Jahr als zweistellige Zahl (00-99)" + }, + "file-picker": { + "item": "Element", + "items": "Elemente", + "search-placeholder": "Dateien und Ordner suchen...", + "select-current": "Aktuelles Objekt auswählen", + "select-file": "Datei auswählen", + "select-folder": "Ordner auswählen", + "selected": "Ausgewählt:", + "title": "Dateiauswahl" + }, + "file-picker-title": "Datei auswählen", + "icon-picker": { + "title": "Symbol-Auswahl" + }, + "tooltip-placeholder": "Platzhalter" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "Anonyme Identität", + "ca-cert": "CA-Zertifikat", + "eap-method": "EAP-Methode", + "password": "Benutzerpasswort", + "phase2-auth": "Phase 2 Authentifizierung", + "username": "Identität / Benutzername" + }, + "panel": { + "action-required": "Aktion erforderlich", + "add-hidden-network": "Verstecktes Netzwerk hinzufügen", + "add-network": "Netzwerk hinzufügen", + "available-interfaces": "Verfügbare Schnittstellen", + "available-networks": "Verfügbare Netzwerke", + "disabled": "WLAN ist deaktiviert", + "disconnecting": "Verbindung wird getrennt...", + "dns": "DNS", + "enable-message": "Aktivieren Sie WLAN, um verfügbare Netzwerke zu sehen.", + "enter-password": "Passwort eingeben...", + "forget": "Vergessen", + "forget-network": "Dieses Netzwerk vergessen?", + "forgetting": "Wird vergessen...", + "hidden-network": "Verstecktes Netzwerk", + "interface": "Netzwerkschnittstelle", + "internet-connected": "Mit dem Internet verbunden", + "internet-limited": "Kein Internet", + "internet-status": "Internetstatus", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "Bekannte Netzwerke", + "link-speed": "Verbindungsgeschwindigkeit", + "network-name-ssid": "Netzwerkname (SSID)", + "no-ethernet-devices": "Keine Ethernet-Geräte erkannt", + "no-networks": "Keine Netzwerke gefunden", + "saved": "Gespeichert", + "scan-again": "Erneut scannen", + "searching": "Suche nach nahegelegenen Netzwerken...", + "security-open": "Öffnen", + "security-wep": "WEP", + "security-wpa": "WPA/WPA2/WPA3 Persönlich", + "security-wpa-ent": "WPA Enterprise", + "security-wpa2-ent": "WPA2 Enterprise", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 Enterprise", + "show-password": "Passwort anzeigen", + "title": "WLAN" + }, + "signal": { + "excellent": "Ausgezeichnet", + "fair": "Mäßig", + "good": "Gut", + "poor": "Schlecht", + "weak": "Schwach" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/en-GB.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/en-GB.json new file mode 100644 index 0000000..a311162 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/en-GB.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "Clear history", + "close-app": "Close {app}", + "connect-vpn": "Connect to {name}", + "cycle-visualizer": "Cycle visualizer", + "disable-bluetooth": "Disable Bluetooth", + "disable-dnd": "Disable Do Not Disturb", + "disable-wifi": "Disable Wi-Fi", + "disconnect-vpn": "Disconnect {name}", + "dock-settings": "Dock settings", + "enable-bluetooth": "Enable Bluetooth", + "enable-dnd": "Enable Do Not Disturb", + "enable-wifi": "Enable Wi-Fi", + "launcher-settings": "Launcher settings", + "lower-to-bottom": "Lower to bottom", + "open-calendar": "Open calendar", + "open-display-settings": "Display settings", + "open-launcher": "Open launcher", + "open-settings": "Open settings", + "raise-to-top": "Raise to top", + "random-wallpaper": "Random wallpaper", + "run-custom-command": "Run custom command", + "show-active-only": "Show active only", + "show-all": "Show all", + "toggle-mute": "Toggle mute", + "widget-settings": "Widget settings" + }, + "authentication": { + "error": "Authentication error", + "failed": "Authentication failed" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Apply theme colors to active window icon.", + "hide-mode-description": "Controls how the widget behaves when no window is active.", + "scrolling-mode-description": "Control when text scrolling is enabled for long window titles.", + "show-app-icon-description": "Display the application icon next to the window title.", + "show-app-icon-label": "Show app icon", + "show-app-text-description": "Display the title of the active window.", + "show-app-text-label": "Show window title" + }, + "audio-visualizer": { + "color-name-description": "Select the colour for the visualizer.", + "color-name-label": "Fill colour", + "height-description": "Custom component width.", + "hide-when-idle-description": "When enabled, the visualizer is hidden unless a player is actively playing.", + "hide-when-idle-label": "Hide when no media is playing", + "width-description": "Custom component width." + }, + "battery": { + "device-default": "Default (Display Device)", + "device-description": "Select which battery device to display.", + "device-label": "Battery device", + "display-mode-description": "Choose how the battery is displayed in the bar.", + "display-mode-graphic": "Graphical battery", + "display-mode-graphic-clean": "Graphical battery (no %)", + "display-mode-icon-always": "Icon - Always show %", + "display-mode-icon-hover": "Icon - Show on hover", + "display-mode-icon-only": "Icon only", + "hide-if-idle-description": "Hide the widget when the battery is not charging or discharging.", + "hide-if-idle-label": "Hide when idle", + "hide-if-not-detected-description": "Hide the widget when no battery is detected on the system.", + "hide-if-not-detected-label": "Hide when not detected", + "low-battery-threshold-label": "Low battery warning threshold", + "show-noctalia-performance-description": "Display the Noctalia performance mode toggle inside the battery panel.
Disables shadows and animations in Noctalia to reduce resource usage.", + "show-noctalia-performance-label": "Show Noctalia Performance toggle", + "show-power-profile-description": "Display the power profile selection inside the battery panel.", + "show-power-profile-label": "Show power profile controls" + }, + "brightness": { + "apply-all-description": "Change brightness for every monitor when using the scroll wheel.", + "apply-all-label": "Apply scroll changes to all monitors" + }, + "clock": { + "clock-display-description": "Customise your clock's display by adding tokens from the list below. To use the 12-hour format, you must include the 'AP' token.", + "clock-display-label": "Clock display", + "custom-font-description": "Select a custom font for the clock display.", + "custom-font-label": "Custom font", + "custom-font-placeholder": "Select custom font...", + "custom-font-search-placeholder": "Search fonts...", + "horizontal-bar-description": "Tip: Use \\n to create a line break.", + "horizontal-bar-label": "Horizontal bar", + "preview": "Preview", + "tooltip-format-description": "Format string for the tooltip shown when hovering over the clock. Leave empty to use the default tooltip.", + "tooltip-format-label": "Tooltip format", + "use-custom-font-description": "Override the default font selection with a custom font for the clock.", + "use-custom-font-label": "Use custom font", + "use-monospaced-font-description": "When enabled, the clock will use the monospaced font.", + "use-monospaced-font-label": "Use monospaced font", + "vertical-bar-description": "Use a space to separate each part onto a new line.", + "vertical-bar-label": "Vertical bar" + }, + "control-center": { + "browse-file": "Browse File", + "browse-library": "Browse Library", + "color-selection-description": "Apply theme colors to icons.", + "enable-colorization-description": "Enable colourisation for icon, applying theme colors.", + "icon-description": "Select an icon from the library or a custom file.", + "select-custom-icon": "Select a custom icon", + "use-distro-logo-description": "Use your distribution's logo instead of a custom icon.", + "use-distro-logo-label": "Use distro logo instead of icon" + }, + "custom-button": { + "collapse-condition-description": "If the output text matches this value, the button will collapse.", + "collapse-condition-label": "Collapse condition", + "color-selection-description": "Apply theme colors to icon and text.", + "default-tooltip": "Custom button, configure in settings", + "display-command-output-description": "Enter a command to run at a regular interval. The first line of its output will be displayed as text.", + "display-command-output-label": "Display command output", + "display-command-output-stream-description": "Enter a command to run continuously.", + "dynamic-text": "Dynamic text", + "enable-colorization-description": "Enable colourisation for the custom button icon and text, applying theme colors.", + "enable-colorization-label": "Enable colourisation", + "general-tooltip-text-description": "Custom text to display in the button's tooltip.", + "general-tooltip-text-label": "Custom tooltip text", + "hide-mode-always-expanded": "Always expanded", + "hide-mode-description": "Controls widget visibility when the command has no output.", + "hide-mode-expand-with-output": "Expand when has output", + "hide-mode-label": "Hide mode", + "hide-mode-max-transparent": "Max expanded but transparent", + "icon-description": "Select an icon from the library.", + "ipc-identifier-description": "Unique identifier for IPC commands. Use this identifier with 'qs -c noctalia-shell ipc call cb [action] [identifier]' to control this button via IPC.", + "ipc-identifier-label": "IPC Identifier", + "left-click-description": "Command to execute when the button is left-clicked.", + "left-click-label": "Left click", + "left-click-update-text": "Update displayed text on left-click", + "max-text-length-horizontal-description": "Maximum number of characters to show in horizontal bar (0 to hide text).", + "max-text-length-horizontal-label": "Max text length (horizontal)", + "max-text-length-vertical-description": "Maximum number of characters to show in vertical bar (0 to hide text).", + "max-text-length-vertical-label": "Max text length (vertical)", + "middle-click-description": "Command to execute when the button is middle-clicked.", + "middle-click-label": "Middle click", + "middle-click-update-text": "Update displayed text on middle-click", + "parse-json-description": "Parse the command output as a JSON object to dynamically set text and icon.", + "parse-json-label": "Parse output as JSON", + "refresh-interval-description": "Interval in milliseconds.", + "refresh-interval-label": "Refresh interval", + "right-click-description": "Command to execute when the button is right-clicked.", + "right-click-label": "Right click", + "right-click-update-text": "Update displayed text on right-click", + "show-exec-tooltip-description": "Show tooltips with command details (left/right/middle click, wheel).", + "show-exec-tooltip-label": "Show command tooltips", + "show-icon-description": "Toggles the visibility of the widget's icon.", + "show-icon-label": "Show icon", + "show-text-tooltip-description": "Show tooltips with the output from the text command.", + "show-text-tooltip-label": "Show dynamic text tooltips", + "text-stream-description": "Streamed lines from the command will be displayed as text on the button.", + "text-stream-label": "Stream", + "wheel-description": "Command to execute when the scroll wheel is used.
Use $delta for the scroll wheel delta in the command.", + "wheel-down": "Scroll down", + "wheel-down-description": "Command to execute when the scroll wheel is scrolled down.", + "wheel-down-label": "Wheel down command", + "wheel-label": "Scroll wheel", + "wheel-mode-separate-description": "Enable separate commands for wheel up and down.", + "wheel-mode-separate-label": "Separate wheel commands", + "wheel-up": "Scroll up", + "wheel-up-description": "Command to execute when the scroll wheel is scrolled up.", + "wheel-up-label": "Wheel up command", + "wheel-update-text": "Update displayed text on scroll" + }, + "keyboard-layout": { + "show-icon-description": "Display the keyboard layout icon." + }, + "lock-keys": { + "hide-when-off-description": "Hide the indicator when the key is not active.", + "hide-when-off-label": "Hide when off", + "show-caps-lock-description": "Display Caps Lock status.", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "Display Num Lock status.", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "Display Scroll Lock status.", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "Enable a space-saving layout for the media player panel.", + "compact-mode-label": "Compact mode", + "hide-mode-description": "Controls how the widget behaves when no media is playing.", + "max-width-description": "Sets the maximum horizontal size of the widget. The widget will shrink to fit shorter content.", + "no-active-player": "No active player", + "panel-section-description": "Configure the appearance and behaviour of the media player panel.", + "panel-section-label": "Media player panel", + "scrolling-mode-description": "Control when text scrolling is enabled for long track titles.", + "scrolling-mode-label": "Scrolling mode", + "show-album-art-description": "Display the album artwork for the currently playing track.", + "show-album-art-label": "Show album art", + "show-artist-first-description": "Display artist - title instead of title - artist.", + "show-artist-first-label": "Show artist first", + "show-progress-ring-description": "Display a circular progress indicator showing track progress.", + "show-progress-ring-label": "Show progress ring", + "show-visualizer-description": "Display an audio visualizer when music is playing.", + "show-visualizer-label": "Show visualizer", + "use-fixed-width-description": "When enabled, the widget will always use the maximum width instead of dynamically adjusting to content.", + "use-fixed-width-label": "Use fixed width", + "visualizer-type-description": "Choose the style of audio visualizer to display.", + "visualizer-type-label": "Visualizer type" + }, + "notification-history": { + "hide-widget-when-zero-description": "Hide the notification icon when there are no notifications.", + "hide-widget-when-zero-label": "Hide icon when no notifications", + "hide-widget-when-zero-unread-description": "Hide the notification icon when there are no unread notifications.", + "hide-widget-when-zero-unread-label": "Hide icon when no unread notifications", + "show-unread-badge-description": "Display a badge showing the number of unread notifications.", + "show-unread-badge-label": "Show unread badge", + "unread-badge-color-description": "Select the colour for the unread notification badge.", + "unread-badge-color-label": "Unread badge colour" + }, + "section-editor": { + "placeholder": "Select a widget to add...", + "search-placeholder": "Search widget..." + }, + "spacer": { + "width-description": "Spacing width in pixels." + }, + "system-monitor": { + "compact-mode-description": "Display stats as mini bar charts instead of text values. Prevents layout shifting.", + "compact-mode-label": "Compact mode", + "cpu-cores-description": "Display CPU cores usage individually.", + "cpu-cores-label": "CPU Cores", + "cpu-frequency-description": "Display the current CPU clock speed in GHz.", + "cpu-frequency-label": "Show CPU frequency", + "cpu-temperature-description": "Show CPU temperature readings if available.", + "cpu-temperature-label": "CPU temperature", + "cpu-usage-description": "Display current CPU usage percentage.", + "cpu-usage-label": "CPU usage", + "disk-path-description": "Select which disk mount point to monitor.", + "disk-path-label": "Disk path", + "gpu-temperature-description": "Show GPU temperature readings if available.", + "load-average-description": "Display system load average.", + "load-average-label": "Load average", + "memory-percentage-description": "Show memory usage as a percentage instead of absolute values.", + "memory-percentage-label": "Memory as percentage", + "memory-usage-description": "Display current RAM usage information.", + "memory-usage-label": "Memory usage", + "network-traffic-description": "Display network upload and download speeds.", + "network-traffic-label": "Network traffic", + "storage-as-percentage-description": "Show disk space as percentage instead of absolute values.", + "storage-as-percentage-label": "Disk as percentage", + "storage-available-description": "Shows how much disk space is available instead of how much is used.", + "storage-available-label": "Disk space available", + "storage-usage-description": "Show disk space usage information.", + "storage-usage-label": "Storage usage", + "swap-usage-description": "Show swap memory usage.", + "swap-usage-label": "Swap usage", + "use-monospace-font-description": "Use monospace font for consistent character width.", + "use-monospace-font-label": "Monospace font", + "use-padding-description": "Pads the text values with leading spaces to prevent layout shifting.", + "use-padding-description-disabled-monospace-font": "A monospace font is needed for this feature.", + "use-padding-description-disabled-vertical": "Vertical taskbar does not support padding text.", + "use-padding-label": "Pad text" + }, + "taskbar": { + "colorize-icons-description": "Apply theme colors to taskbar icons.", + "hide-mode-description": "Controls how the widget behaves when there are no matching windows.", + "hide-mode-label": "Hiding mode", + "icon-scale-description": "Sets the scaling factor for taskbar icons.", + "icon-scale-label": "Icon scaling", + "max-width-description": "Maximum width of the taskbar as a percentage of screen width.", + "max-width-label": "Maximum width", + "only-active-workspaces-description": "Show only apps from active workspaces.", + "only-active-workspaces-label": "Only from active workspaces", + "only-same-monitor-description": "Show only apps from the monitor where the bar is located.", + "only-same-monitor-label": "Only from same monitor", + "show-pinned-apps-description": "Show pinned apps from the dock in the taskbar.", + "show-pinned-apps-label": "Show pinned apps", + "show-title-description": "Display window titles in the taskbar.", + "show-title-description-disabled": "Vertical taskbar does not support showing titles.", + "show-title-label": "Show title", + "smart-width-description": "Automatically adjust entry width based on the number of entries.", + "smart-width-label": "Smart width", + "title-width-description": "Set the width of window titles in the taskbar (in pixels).", + "title-width-label": "Title width", + "title-width-reset-tooltip": "Reset title width" + }, + "tray": { + "chevron-color-description": "Apply theme colors to the drawer chevron icon.", + "chevron-color-label": "Chevron colour", + "colorize-icons-description": "Apply theme colors to tray icons.", + "colorize-icons-label": "Colourise icons", + "drawer-enabled-description": "When enabled, unpinned tray items are shown in a drawer panel.
When disabled, all tray items are shown inline.", + "drawer-enabled-label": "Enable drawer", + "hide-passive-description": "When enabled, tray items with 'Passive' status will be hidden.", + "hide-passive-label": "Hide passive items" + }, + "volume": { + "display-mode-description": "Choose how you'd like this value to appear." + }, + "workspace": { + "character-count-description": "Number of characters to display from workspace names (1-10).", + "character-count-label": "Character count", + "empty-color-description": "Set the background colour for empty workspaces.", + "empty-color-label": "Empty workspace colour", + "enable-scrollwheel-description": "Switch between workspaces using the mouse scroll wheel.", + "enable-scrollwheel-label": "Scroll to switch workspaces", + "focused-color-description": "Set the background colour for the focused workspace.", + "focused-color-label": "Focused workspace colour", + "follow-focused-screen-description": "Display workspaces from the currently focused screen, rather than the screen where the bar is located.", + "follow-focused-screen-label": "Follow focused screen", + "font-weight-description": "Set the visual weight for text within workspace.", + "font-weight-label": "Font weight", + "grouped-border-opacity-description": "Set the opacity level for workspace container borders.", + "grouped-border-opacity-label": "Border opacity", + "hide-unoccupied-description": "Don't display workspaces without windows.", + "hide-unoccupied-label": "Hide unoccupied", + "label-mode-description": "Choose how workspace labels are displayed.", + "label-mode-label": "Label mode", + "occupied-color-description": "Set the background colour for occupied workspaces.", + "occupied-color-label": "Occupied workspace colour", + "pill-size-description": "Adjust the size of workspace pills.", + "pill-size-label": "Pill size", + "reverse-scrolling-description": "Reverse the direction of workspace switching when scrolling.", + "reverse-scrolling-label": "Reverse scrolling", + "show-applications-description": "Display application icons inside each workspace.", + "show-applications-hover-description": "Display application icons inside each workspace when the workspace is hovered.", + "show-applications-hover-label": "Show applications when hovered", + "show-applications-label": "Show applications", + "show-badge-description": "Show the workspace number badge in grouped mode.", + "show-badge-label": "Show workspace badge", + "show-labels-only-when-occupied-description": "Only show workspace labels when they contain windows.", + "show-labels-only-when-occupied-label": "Show labels only when occupied", + "unfocused-icons-opacity-description": "Set the opacity level for unfocused app icons.", + "unfocused-icons-opacity-label": "Unfocused icons opacity" + } + }, + "battery": { + "all-batteries": "All batteries (combined)", + "battery-health": "Battery health", + "battery-level": "Battery level", + "capacity-level": "Capacity: {level}", + "charging-rate": "Charging rate: {rate} W", + "discharging-rate": "Discharging rate: {rate} W", + "health": "Health: {percent}%", + "inhibit-idle-description": "Keeps the system awake.", + "no-battery-detected": "No battery detected", + "plugged-in": "Plugged in", + "power-profile": "Power profile", + "time-left": "Time left: {time}", + "time-until-full": "Time until full: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Available devices", + "blocked": "Blocked", + "connected-devices": "Connected devices", + "device-address": "Device address", + "disabled": "Bluetooth is disabled", + "discoverable": "Discoverable", + "enable-message": "Enable Bluetooth to see available devices.", + "known-devices": "Known devices", + "no-devices": "No devices available", + "paired-devices": "Paired devices", + "pairing-mode": "Make sure your device is in pairing mode.", + "scanning": "Scanning for devices...", + "signal-text-excellent": "Signal: Excellent", + "signal-text-fair": "Signal: Fair", + "signal-text-good": "Signal: Good", + "signal-text-poor": "Signal: Poor", + "signal-text-unknown": "Signal: Unknown", + "signal-text-very-poor": "Signal: Very poor" + } + }, + "changelog": { + "error": { + "fetch-failed": "Unable to load changelog data. Please try again later.", + "rate-limit": "GitHub rate limit exceeded. Please try again in a few minutes." + }, + "panel": { + "buttons-discord": "Join our Discord", + "buttons-dismiss": "OK", + "buttons-feedback": "Give feedback", + "empty": "Release notes are not available yet.", + "highlight-title": "Highlights", + "released": "Released on {date}", + "subtitle-fresh": "Thanks for installing Noctalia! Here is what’s included in this build.", + "subtitle-updated": "Updated from {previousVersion}", + "title": "What's new in {version}", + "version": "Version {version}", + "version-new-user": "Fresh install" + } + }, + "common": { + "actions": "Actions", + "active": "Active", + "add": "Add", + "appearance": "Appearance", + "apply": "Apply", + "auto-connect": "Auto-connect", + "automation": "Automation", + "available": "Available", + "back": "Back", + "battery": "Battery", + "behavior": "Behaviour", + "bluetooth": "Bluetooth", + "brightness": "Brightness", + "browse": "Browse", + "calendar": "Calendar", + "calendar-panel": "Calendar Panel", + "cancel": "Cancel", + "cards": "Cards", + "charging": "Charging", + "clear": "Clear", + "clipboard": "Clipboard", + "close": "Close", + "color-muted": "Muted", + "colors": "Colors", + "command": "Command", + "confirm": "Confirm", + "connect": "Connect", + "connected": "Connected", + "connecting": "Connecting...", + "continue": "Continue", + "contributors": "Contributors", + "copied-to-clipboard": "Copied to clipboard", + "countdown": "Countdown", + "customize": "Customise", + "date": "Date", + "default": "Default", + "delete": "Delete", + "devices": "Devices", + "disabled": "Disabled", + "discharging": "Discharging", + "disconnect": "Disconnect", + "disconnected": "Disconnected", + "disconnecting": "Disconnecting...", + "display-mode": "Display mode", + "documentation": "Documentation", + "download": "Download", + "duration": "Duration", + "dysfunctional": "Dysfunctional", + "edit": "Edit", + "enabled": "Enabled", + "error": "Error", + "ethernet": "Ethernet", + "events": "Events", + "execute": "Execute", + "faithful": "Faithful", + "focus": "Focus", + "font-weight-bold": "Bold", + "font-weight-light": "Light", + "font-weight-medium": "Medium", + "font-weight-regular": "Regular", + "font-weight-semibold": "Semi-bold", + "frequency": "Frequency", + "gateway": "Gateway", + "general": "General", + "height": "Height", + "hibernate": "Hibernate", + "history": "History", + "icon": "Icon", + "idle": "Idle", + "info": "Info", + "input": "Input", + "install": "Install", + "installed": "Installed", + "interface": "Interface", + "internet": "Internet", + "keybind": "Keybind", + "language": "Language", + "loading": "Loading...", + "local": "Local", + "location": "Location", + "lock": "Lock", + "logout": "Logout", + "look": "Look", + "margins": "Margins", + "media": "Media", + "media-player": "Media Player", + "memory": "Memory", + "monitors": "Monitors", + "move-down": "Move down", + "move-up": "Move up", + "network": "Network", + "next": "Next", + "night-light": "Night Light", + "no": "No", + "no-results": "No results", + "no-summary": "No summary", + "none": "None", + "not-found": "Not found", + "notifications": "Notifications", + "official": "Official", + "on-surface": "On Surface", + "output": "Output", + "pair": "Pair", + "paired": "Paired", + "pairing": "Pairing...", + "panels": "Panels", + "password": "Password", + "pause": "Pause", + "performance": "Performance", + "pin": "Pin", + "play": "Play", + "polling": "Polling", + "position": "Position", + "previous": "Previous", + "primary": "Primary", + "random": "Random", + "reboot": "Reboot", + "reboot-to-uefi": "Reboot to UEFI", + "record": "Start recording", + "refresh": "Refresh", + "required": "(required)", + "reset": "Reset", + "reset-to-default": "Reset to Default", + "result-count": "{count} result", + "result-count-plural": "{count} results", + "resume": "Resume", + "save": "Save", + "scanning": "Scanning...", + "screen-corners": "Screen Corners", + "search": "Search", + "second": "{count} second", + "second-plural": "{count} seconds", + "secondary": "Secondary", + "security": "Security", + "select": "Select", + "select-color": "Select colour", + "select-color-description": "Apply theme colors for emphasis.", + "select-icon-color": "Select icon colour", + "settings": "Settings", + "shortcuts": "Shortcuts", + "shutdown": "Shutdown", + "signal": "Signal", + "sound": "Sound", + "sources": "Sources", + "start": "Start", + "stop": "Stop", + "supporters": "Supporters", + "suspend": "Suspend", + "templates": "Templates", + "tertiary": "Tertiary", + "test": "Test", + "thresholds": "Thresholds", + "title": "Title", + "toast": "Toast", + "trusted": "Trusted", + "uninstall": "Uninstall", + "unknown": "Unknown", + "unpair": "Unpair", + "unpin": "Unpin", + "update": "Update", + "upload": "Upload", + "username": "Username", + "userspace-reboot": "Userspace Reboot", + "version": "Version", + "vibrant": "Vibrant", + "visualizer": "Visualizer", + "volume": "Volume", + "volumes": "Volumes", + "wallpaper": "Wallpaper", + "weather": "Weather", + "weather-loading": "Loading weather...", + "weather-no-location": "Set a location in Settings to see weather", + "week": "Week", + "widgets": "Widgets", + "width": "Width", + "wifi": "Wi-Fi", + "windows": "Windows", + "yes": "Yes" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Power Profile", + "tooltip-disabled": "Install power-profiles-daemon to use power profiles" + }, + "wifi": { + "label-disconnected": "Wi-Fi disconnected", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Always hide", + "always-show": "Always show", + "force-open": "Force open", + "on-hover": "On hover" + }, + "hide-modes": { + "auto-hide": "Auto-hide", + "hidden": "Hide when empty", + "idle": "Hide when idle", + "non-exclusive": "Non-exclusive", + "transparent": "Transparent when empty", + "visible": "Always visible" + }, + "launcher": { + "categories": { + "all": "All", + "audiovideo": "Audio & Video", + "chat": "Chat", + "development": "Development", + "education": "Education", + "emoji-activity": "Activity", + "emoji-animals": "Animals", + "emoji-flags": "Flags", + "emoji-food": "Food & Drink", + "emoji-nature": "Nature", + "emoji-objects": "Objects", + "emoji-people": "People & Body", + "emoji-recent": "Recent", + "emoji-symbols": "Symbols", + "emoji-travel": "Travel & Places", + "game": "Games", + "graphics": "Graphics", + "misc": "Misc", + "office": "Office", + "pinned": "Pinned", + "system": "System", + "webbrowser": "Web Browser" + }, + "providers": { + "applications": "Applications", + "calculator": "Calculator", + "calculator-deprecated": "The >calc command is deprecated and will be removed soon. Please type math expressions directly in search instead.", + "calculator-description": "Calculator — evaluate mathematical expressions", + "calculator-enter-expression": "Enter a mathematical expression", + "calculator-press-enter-to-copy": "Press Enter to copy result", + "clipboard": "Clipboard history", + "clipboard-clear-description": "Clear all clipboard history", + "clipboard-clear-description-full": "Remove all items from clipboard history", + "clipboard-clear-history": "Clear clipboard history", + "clipboard-delete": "Delete clipboard entry", + "clipboard-history-disabled": "Clipboard history disabled", + "clipboard-history-disabled-description": "Enable clipboard history in settings or install cliphist", + "clipboard-loading": "Loading clipboard history...", + "clipboard-search-description": "Search clipboard history", + "command-description": "Run shell commands", + "emoji": "Emoji picker", + "emoji-loading": "Loading emojis...", + "emoji-loading-description": "Please wait", + "emoji-no-recent": "No recent emojis yet", + "emoji-search-description": "Search and copy emojis", + "settings-search-description": "Search and navigate to settings", + "windows-search-description": "Search and focus open windows" + } + }, + "lock-screen": { + "authenticating": "Authenticating...", + "password": "Enter your password...", + "restart": "Restart", + "shut-down": "Shut down" + }, + "notifications": { + "panel": { + "click-to-collapse": "Click to collapse", + "click-to-expand": "Click to expand", + "description": "Your notifications will show up here as they arrive.", + "no-notifications": "No notifications" + }, + "range": { + "earlier": "Earlier", + "today": "Today", + "yesterday": "Yesterday" + }, + "time": { + "diff-d": "1 day ago", + "diff-dd": "{diff} days ago", + "diff-h": "1 hour ago", + "diff-hh": "{diff} hours ago", + "diff-m": "1 minute ago", + "diff-mm": "{diff} minutes ago", + "now": "now" + } + }, + "options": { + "bar": { + "density-comfortable": "Comfortable", + "density-compact": "Compact", + "density-default": "Default", + "density-mini": "Mini", + "density-spacious": "Spacious", + "display-mode-always-visible": "Always Visible", + "type-floating": "Floating", + "type-framed": "Framed", + "type-simple": "Simple" + }, + "control-center": { + "quick-settings-style-classic": "Classic", + "quick-settings-style-modern": "Modern" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "Comfortable", + "compact": "Compact", + "default": "Default" + }, + "launcher-view-mode": { + "grid": "Grid", + "list": "List" + }, + "notification-density": { + "compact": "Compact", + "default": "Default" + }, + "scrolling-modes": { + "always": "Scroll always", + "hover": "Scroll on hover", + "never": "Never scroll" + }, + "session-menu-grid-layout": { + "grid": "Grid", + "single-row": "Single row" + }, + "settings-panel-mode": { + "attached": "Panel attached to bar", + "centered": "Centred panel", + "window": "Separate window" + }, + "visualizer-types": { + "linear": "Linear", + "mirrored": "Mirrored", + "wave": "Wave" + }, + "visualizer-visibility": { + "with-background": "With background only" + }, + "workspace-labels": { + "index": "Index", + "index-and-name": "Index and name", + "name": "Name" + } + }, + "panels": { + "about": { + "become-supporter": "Become a supporter", + "changelog": "View changelog", + "changelog-on-startup": "Show changelog on update", + "changelog-on-startup-desc": "Automatically show the changelog when Noctalia is updated", + "contributors-description": "Shout-out to our {count} awesome contributor!", + "contributors-description-plural": "Shout-out to our {count} awesome contributors!", + "copy-info": "Copy info", + "debug-disabled": "Debug mode disabled", + "debug-enabled": "Debug mode enabled", + "info-copied": "Info copied to clipboard", + "noctalia-available": "Available:", + "noctalia-desc": "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell.", + "noctalia-git-commit": "Git commit:", + "noctalia-installed-version": "Installed version:", + "noctalia-latest-version": "Latest version:", + "noctalia-qs-version": "Noctalia QS version:", + "noctalia-title": "Noctalia Shell", + "privacy-policy": "Privacy policy", + "support": "Support us", + "supporter-badge": "Supporter", + "supporters-desc": "A huge thank you to our amazing supporter!", + "supporters-desc-plural": "A huge thank you to our {count} amazing supporters!", + "supporters-loading": "Loading supporters...", + "system-board": "Board:", + "system-cpu": "CPU:", + "system-disk": "Disk:", + "system-gpu": "GPU:", + "system-host": "Host:", + "system-install-hint": "Install fastfetch to view system information", + "system-kernel": "Kernel:", + "system-loading": "Loading system info...", + "system-memory": "Memory:", + "system-monitor": "Monitor:", + "system-not-installed": "fastfetch is not installed", + "system-os": "OS:", + "system-packages": "Packages:", + "system-product": "Product:", + "system-title": "System Information", + "system-uptime": "Uptime:", + "system-wm": "WM:", + "telemetry-data-copied": "Telemetry data copied to clipboard", + "telemetry-desc": "Help improve Noctalia by sharing anonymous system info (screen resolution, compositor, distro). Sent once at startup, no tracking, data auto-deleted after 30 days.", + "telemetry-enabled": "Send anonymous system information", + "telemetry-show-data": "View data", + "telemetry-title": "Privacy", + "title": "About", + "up-to-date": "You're up to date!", + "update-available": "Update available", + "view-commit": "View commit on GitHub" + }, + "audio": { + "devices-desc": "Configure available audio input and output devices.", + "devices-input-device-description": "Select the desired audio input device.", + "devices-input-device-label": "Input device", + "devices-output-device-description": "Select the desired audio output device.", + "devices-output-device-label": "Output device", + "devices-title": "Audio devices", + "external-mixer-description": "Enter the command or application path to launch when activating the external audio mixer feature.", + "external-mixer-label": "External audio mixer command", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Set your preferred and ignored media applications.", + "media-excluded-player-description": "Add keywords for players you want the system to ignore. Each keyword should be on a new line.", + "media-excluded-player-label": "Excluded player", + "media-excluded-player-placeholder": "type substring and press +", + "media-frame-rate-description": "Higher rates are smoother but use more resources.", + "media-frame-rate-label": "Frame rate", + "media-primary-player-description": "Enter a keyword to identify your main player.", + "media-primary-player-label": "Primary player", + "media-primary-player-placeholder": "e.g. spotify, vlc, mpv", + "media-scrolling-speed-description": "Time in seconds for the title to scroll from start to end.", + "media-scrolling-speed-label": "Scrolling speed", + "media-scrolling-title-description": "Enable continuous scrolling for long media titles.", + "media-scrolling-title-label": "Scrolling title", + "media-title": "Media players", + "on-middle-clicked-description": "Command to execute when the button is middle-clicked.", + "panel-applications-empty": "No applications are currently playing audio", + "spectrum-mirrored-description": "Mirror the spectrum so low frequencies are in the centre. When off, low frequencies are on the left and high on the right.", + "spectrum-mirrored-label": "Mirror spectrum", + "title": "Audio", + "visualizer-type-description": "Choose a visualization type for media playback.", + "visualizer-type-label": "Visualization type", + "volumes-desc": "Adjust volume controls and audio levels.", + "volumes-feedback-sound-file-description": "Path to the sound file played when changing volume.", + "volumes-feedback-sound-file-label": "Volume feedback sound file", + "volumes-feedback-sound-file-select-title": "Select volume feedback sound file", + "volumes-input-volume-description": "Microphone input volume level.", + "volumes-mute-input-description": "Mute the default audio input (microphone).", + "volumes-mute-input-label": "Mute audio input", + "volumes-mute-output-description": "Mute the system's main audio output.", + "volumes-mute-output-label": "Mute audio output", + "volumes-output-volume-description": "System-wide volume level.", + "volumes-step-size-description": "Adjust the step size for volume changes (scroll wheel, keyboard shortcuts).", + "volumes-step-size-label": "Volume step size", + "volumes-volume-feedback-description": "Play a feedback sound when adjusting volume.", + "volumes-volume-feedback-label": "Play volume feedback sound", + "volumes-volume-overdrive-description": "Allow raising volume above 100%. May not be supported by all hardware.", + "volumes-volume-overdrive-label": "Allow volume overdrive" + }, + "bar": { + "appearance-auto-hide-delay-description": "Time before bar hides after mouse leaves", + "appearance-auto-hide-delay-label": "Hide Delay", + "appearance-auto-hide-exclusive-note": "Note: Exclusive zone is automatically disabled when auto-hide is active", + "appearance-auto-show-delay-description": "Time before bar shows when mouse enters edge", + "appearance-auto-show-delay-label": "Show Delay", + "appearance-background-opacity-description": "Set the background opacity specifically for the bar.", + "appearance-background-opacity-label": "Bar background opacity", + "appearance-capsule-color-description": "Choose a colour for bar capsules, or use none for the default surface colour.", + "appearance-capsule-color-label": "Capsule colour", + "appearance-capsule-opacity-description": "Set the opacity level for widget backgrounds when capsule is shown.", + "appearance-capsule-opacity-label": "Capsule opacity", + "appearance-content-padding-description": "Adjust the padding between bar edges and widgets.", + "appearance-content-padding-label": "Content padding", + "appearance-density-description": "Adjust the bar's padding for a compact or spacious look.", + "appearance-density-label": "Bar density", + "appearance-desc": "Customise the bar's appearance and position.", + "appearance-display-mode-description": "Choose when the bar is visible.", + "appearance-enable-exclusion-zone-inset-description": "Reduce the exclusion zone by 1 physical pixel so flush windows bleed perfectly under the bar edge.", + "appearance-enable-exclusion-zone-inset-label": "Inset exclusion zone", + "appearance-floating-description": "Display the bar as a floating 'pill'.", + "appearance-floating-label": "Floating bar", + "appearance-font-scale-description": "Adjust the font size scale for text displayed in the bar.", + "appearance-font-scale-label": "Font scale", + "appearance-frame-radius": "Inner Radius", + "appearance-frame-settings-description": "Adjust frame thickness and inner corner radius", + "appearance-frame-settings-label": "Frame Settings", + "appearance-frame-thickness": "Thickness", + "appearance-hide-on-overview-description": "Hide the bar and close panels when the compositor overview is active.", + "appearance-hide-on-overview-label": "Hide bar on overview", + "appearance-margins-description": "Adjust the margins around the floating bar.", + "appearance-margins-horizontal": "Horizontal Margin ", + "appearance-margins-vertical": "Vertical Margin", + "appearance-outer-corners-description": "Display outwardly curved corners on the bar.", + "appearance-outer-corners-label": "Outer corners", + "appearance-position-description": "Choose where to place the bar on the screen.", + "appearance-position-label": "Bar position", + "appearance-show-capsule-description": "Show widget backgrounds.", + "appearance-show-capsule-label": "Show capsule", + "appearance-show-on-workspace-switch-description": "Automatically show the bar briefly when the workspace changes.", + "appearance-show-on-workspace-switch-label": "Show bar on workspace switch", + "appearance-show-outline-description": "Displays a visible border around every widget.", + "appearance-show-outline-label": "Show widget outlines", + "appearance-type-description": "Choose the style of the bar: Simple, Floating or Framed.", + "appearance-type-label": "Bar type", + "appearance-use-separate-opacity-description": "Enable to use a separate opacity value for the bar background.", + "appearance-use-separate-opacity-label": "Use separate bar opacity", + "appearance-widget-spacing-description": "Adjust the spacing between each widget in the bar.", + "appearance-widget-spacing-label": "Widget spacing", + "behavior-middle-click-command-description": "Command to execute on middle click.", + "behavior-middle-click-command-label": "Middle click command", + "behavior-middle-click-command-placeholder": "niri msg action toggle-overview", + "behavior-middle-click-description": "Choose what middle click does on empty areas of the bar.", + "behavior-middle-click-follow-mouse-description": "Open the selected middle-click panel at the cursor position.", + "behavior-middle-click-follow-mouse-label": "Middle click follow mouse", + "behavior-middle-click-label": "Bar middle click action", + "behavior-right-click-command-description": "Command to execute on right click.", + "behavior-right-click-command-label": "Right click command", + "behavior-right-click-command-placeholder": "notify-send \"Right click\"", + "behavior-right-click-description": "Choose what right click does on empty areas of the bar.", + "behavior-right-click-follow-mouse-description": "Open the selected right-click panel at the cursor position.", + "behavior-right-click-follow-mouse-label": "Right click follow mouse", + "behavior-right-click-label": "Bar right click action", + "behavior-wheel-wrap-description": "When enabled, scrolling continues from the last item to the first.", + "behavior-wheel-wrap-label": "Wrap around", + "behavior-workspace-scroll-description": "Choose what the mouse wheel does on empty areas of the bar.", + "behavior-workspace-scroll-label": "Bar mouse wheel action", + "behavior-workspace-scroll-option-content": "Content", + "behavior-workspace-scroll-option-workspace": "Workspace", + "monitor-configure-widgets": "Configure widgets", + "monitor-override-settings": "Override global settings", + "monitor-override-settings-description": "Use custom settings for this monitor.", + "monitor-reset-all": "Reset all", + "monitor-widgets-title": "Widget Configuration - {monitor}", + "monitors-desc": "Show bar on specific monitors. Defaults to all if none are chosen.", + "monitors-desc-new": "Configure which monitors display the bar and customise settings per monitor.", + "monitors-title": "Monitors display", + "title": "Bar", + "tray-blacklist-description": "Add tray exclusion rules, supports wildcards (*).", + "tray-blacklist-label": "Blacklist", + "tray-blacklist-placeholder": "e.g. nm-applet, Fcitx*", + "tray-pin-application": "Pin Application", + "tray-unpin-application": "Unpin Application", + "use-global-widgets": "Use global widgets", + "widgets-desc": "Drag widgets to change their order. Use the right-click menu to transfer widgets between sections or remove them.", + "widgets-title": "Widgets positioning" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Generate colour schemes from your wallpaper. Automatically extracts colors to create a cohesive theme.", + "color-source-use-wallpaper-colors-label": "Use wallpaper colors", + "dark-mode-mode-description": "Enables automatic switching between Light and Dark Mode.", + "dark-mode-mode-label": "Dark Mode schedule", + "dark-mode-mode-manual": "Manual", + "dark-mode-mode-off": "Off", + "dark-mode-switch-description": "Switches to a darker theme for easier viewing at night.", + "delete-error-description": "Failed to delete {scheme}", + "delete-error-title": "Delete failed", + "delete-success-description": "Successfully deleted {scheme}", + "delete-success-title": "Colour scheme deleted", + "download-button": "Download more", + "download-downloading": "Downloading...", + "download-empty": "No colour schemes available", + "download-error-api-error": "API error: {status}", + "download-error-description": "Failed to download {scheme}", + "download-error-download-failed": "Download failed with exit code: {code}", + "download-error-invalid-response": "Invalid API response format", + "download-error-no-files": "No files found for scheme", + "download-error-parse-failed": "Failed to parse API response: {error}", + "download-error-rate-limit": "GitHub API rate limit exceeded", + "download-error-title": "Download failed", + "download-fetching": "Fetching available colour schemes...", + "download-success-description": "Successfully downloaded {scheme}", + "download-success-title": "Colour scheme downloaded", + "download-title": "Download Colour Schemes", + "method-description": { + "content": "Material Design scheme with high-fidelity colour extraction that closely matches the source content's actual colors.", + "dysfunctional": "Like Faithful but picks the second most dominant colour family as primary.", + "faithful": "Attempts to stay close to the source colour while still generating a harmonious palette.", + "fruit-salad": "Material Design scheme that produces a playful, vibrant palette with diverse and varied hues.", + "monochrome": "Material Design scheme using a single-hue grayscale with minimal chromatic content.", + "muted": "Generates softer, desaturated tones for a calm and understated appearance.", + "rainbow": "Material Design scheme that generates colors across the full spectrum for maximum colour diversity.", + "tonal-spot": "Material Design default algorithm. Creates balanced palettes using tonal variations of the source colour.", + "vibrant": "Maximizes colour saturation for bold, eye-catching palettes with vivid tones." + }, + "predefined-desc": "Choose from a collection of predefined colour schemes.", + "predefined-generate-templates-label": "Generate templates for predefined schemes", + "predefined-title": "Predefined colour schemes", + "sync-gsettings-description": "Match the system theme to the active light or dark variant.", + "sync-gsettings-label": "Sync system theme", + "templates-desc": "Apply colors to external applications.", + "templates-filter-description": "Show templates from a specific category.", + "templates-filter-label": "Filter by category", + "templates-misc-description": "Create your own templates.", + "templates-misc-label": "Advanced", + "templates-misc-user-templates-description": "Only enable if you know what you are doing — refer to our online documentation.", + "templates-misc-user-templates-label": "Enable user templates", + "templates-none-detected": "None detected", + "templates-write-path": "Writes: {filepath}", + "title": "Colour Scheme", + "wallpaper-method-description": "Choose your favorite palette generation method.", + "wallpaper-method-label": "Palette generation method", + "wallpaper-monitor-source-description": "Select which monitor to use for extracting wallpaper colors.", + "wallpaper-monitor-source-label": "Colour generation source" + }, + "connections": { + "authentication-required": "Authentication required", + "bluetooth-auto-connect-description": "Automatically connect to trusted paired devices when Bluetooth is enabled.", + "bluetooth-auto-connect-label": "Auto-connect paired devices", + "bluetooth-devices-unnamed": "Unnamed devices are not shown.", + "bluetooth-discoverable": "This device is discoverable as {hostName} while this settings tab is open.", + "bluetooth-rssi-polling-description": "Periodically sample RSSI for connected devices via bluetoothctl. May not be available for all devices; uses minimal resources when enabled.", + "bluetooth-rssi-polling-interval-description": "Configure how often to update signal strength for connected devices.", + "bluetooth-rssi-polling-interval-label": "Polling interval", + "bluetooth-rssi-polling-label": "Bluetooth signal polling", + "disable-discoverability-description": "Hide your device from nearby Bluetooth devices.", + "disable-discoverability-label": "Disable device visibility", + "hide-unnamed-devices-description": "Hide devices that appear only as Bluetooth addresses.", + "hide-unnamed-devices-label": "Hide unnamed devices", + "pin-instructions": "Please enter the PIN code displayed on your device.", + "title": "Connections", + "wifi-header-text": "Use Wi-Fi to see available networks and connect to the internet." + }, + "control-center": { + "cards-desc": "Customise which controls appear in the control centre and in what order.", + "desc": "Configure the control centre panel positioning and behaviour.", + "open-at-mouse-description": "When right-clicking an empty area of the bar, open the control centre near the cursor. When disabled, the position setting above is used instead.", + "open-at-mouse-label": "Open at cursor on right-click", + "position-description": "Choose where the control centre panel appears when opened.", + "shortcuts-custom-button-command-description": "The command to execute when the button is clicked.", + "shortcuts-custom-button-enable-on-state-logic-description": "Enable a second icon and 'hot' state based on a check command.", + "shortcuts-custom-button-enable-on-state-logic-label": "Enable on-state logic", + "shortcuts-custom-button-icon-description": "Select an icon from the library.", + "shortcuts-custom-button-on-clicked-label": "Left click command", + "shortcuts-custom-button-on-middle-clicked-description": "Command to execute when the button is middle-clicked.", + "shortcuts-custom-button-on-state-command-description": "Command to execute to check if the button should be in the 'on' state. Returns 0 for on, non-zero for off.", + "shortcuts-custom-button-on-state-command-label": "On-state check command", + "shortcuts-custom-button-on-state-icon-description": "The icon for the button when it's in the 'on' state.", + "shortcuts-custom-button-on-state-icon-label": "On-state icon", + "shortcuts-custom-button-state-checks-add": "Add state check", + "shortcuts-custom-button-state-checks-command": "Command to execute for this state check", + "shortcuts-custom-button-state-checks-label": "State checks", + "shortcuts-custom-button-state-checks-remove": "Remove", + "shortcuts-custom-button-tooltip-description": "The tooltip to show when hovering over the button.", + "shortcuts-custom-button-tooltip-label": "Tooltip", + "shortcuts-title": "Shortcuts widgets", + "system-monitor-disk-path-description": "Select which disk mount point the system monitor card in the control centre should monitor.", + "system-monitor-disk-path-label": "System monitor disk path", + "title": "Control Centre" + }, + "desktop-widgets": { + "clock-enabled-description": "Show a clock widget on the desktop.", + "clock-enabled-label": "Enable clock widget", + "clock-format-label": "Format", + "clock-height-description": "Height of the clock widget in pixels.", + "clock-height-label": "Height", + "clock-minimal-mode-description": "Use a minimal clock display with time and date.", + "clock-minimal-mode-label": "Minimal mode", + "clock-rounded-corners-description": "Use rounded corners for the widget background.", + "clock-rounded-corners-label": "Rounded corners", + "clock-show-background-description": "Show the background container for the clock widget.", + "clock-show-background-label": "Show background", + "clock-show-date-description": "Display the current date below the time.", + "clock-show-date-label": "Show date", + "clock-show-seconds-description": "Display seconds in the time.", + "clock-show-seconds-label": "Show seconds", + "clock-style-analog": "Analog", + "clock-style-binary": "Binary", + "clock-style-description": "Choose the clock display style.", + "clock-style-digital": "Digital", + "clock-style-label": "Clock style", + "clock-style-minimal": "Minimal", + "clock-width-description": "Width of the clock widget in pixels.", + "cpu-intensive-note": "Widgets marked with ! use more CPU and should be enabled mindfully.", + "edit-mode-button-label": "Enter edit mode", + "edit-mode-controls-explanation": "Left-click & drag: Move or resize the widget.\nRight-click: Open the context menu options.", + "edit-mode-description": "Enable edit mode to move and reposition desktop widgets. When enabled, widgets show a drag outline and can be repositioned.", + "edit-mode-exit-button": "Exit edit mode", + "edit-mode-grid-snap-label": "Grid snap", + "edit-mode-grid-snap-scale-label": "Grid snap scale", + "edit-mode-label": "Edit mode", + "enabled-description": "Enable or disable desktop widgets entirely.", + "enabled-label": "Enable desktop widgets", + "general-desc": "Configure widgets that appear on your desktop.", + "general-title": "Desktop widgets", + "media-player-enabled-description": "Show a media player widget on the desktop.", + "media-player-enabled-label": "Enable media player widget", + "media-player-rounded-corners-description": "Enable rounded corners on the widget edges.", + "media-player-show-album-art-description": "Show the album artwork and track information (title and artist).", + "media-player-show-album-art-label": "Show album art & title", + "media-player-show-background-description": "Show the background container.", + "media-player-show-buttons-description": "Show media control buttons (play/pause, previous, next).", + "media-player-show-buttons-label": "Show buttons", + "media-player-show-visualizer-description": "Show the audio visualizer overlay.", + "media-player-visualizer-type-description": "Choose a visualization type.", + "overview-enabled-description": "Show desktop widgets while the compositor overview is open.", + "overview-enabled-label": "Show in overview", + "system-stat-layout-bottom": "Bottom", + "system-stat-layout-description": "Choose how the legend is displayed relative to the graph.", + "system-stat-layout-label": "Layout", + "system-stat-layout-side": "Side", + "system-stat-rounded-corners-description": "Use rounded corners for the widget background.", + "system-stat-rounded-corners-label": "Rounded corners", + "system-stat-show-background-description": "Show the background container for the system stat widget.", + "system-stat-show-background-label": "Show background", + "system-stat-stat-type-description": "Choose which system statistic to display.", + "system-stat-stat-type-label": "Statistic Type", + "title": "Desktop Widgets", + "weather-enabled-description": "Show a weather widget on the desktop.", + "weather-enabled-label": "Enable weather widget", + "weather-show-background-description": "Show the background container for the weather widget." + }, + "display": { + "monitors-backlight-device-auto-option": "Default", + "monitors-backlight-device-description": "Select a backlight device for this output.", + "monitors-backlight-device-label": "Backlight device", + "monitors-brightness-step-description": "Adjust the step size for brightness changes (scroll wheel and keyboard shortcuts).", + "monitors-brightness-step-label": "Brightness step size", + "monitors-brightness-unavailable-ddc-disabled": "Brightness control unavailable. Enable \"External brightness support\" to control this display's brightness.", + "monitors-brightness-unavailable-generic": "Brightness control is not available for this display.", + "monitors-desc": "Adjust settings brightness for each display.", + "monitors-enforce-minimum-description": "Solves the problem of backlight completely turning off on some displays at 0% brightness.", + "monitors-enforce-minimum-label": "Enforce minimum brightness (1%)", + "monitors-external-brightness-description": "Enable DDCUtil support for controlling brightness on external displays via DDC/CI protocol.", + "monitors-external-brightness-label": "External brightness support", + "monitors-global-brightness-description": "Adjust brightness for all monitors at once.", + "monitors-global-brightness-label": "All monitors", + "monitors-title": "Per-monitor settings", + "night-light-auto-schedule-description": "Based on the sunset and sunrise time in {location} — recommended.", + "night-light-auto-schedule-label": "Automatic scheduling", + "night-light-desc": "Reduce blue light emission to help you sleep better and reduce eye strain.", + "night-light-enable-description": "Apply a warm colour filter to reduce blue light emission.", + "night-light-enable-label": "Enable Night Light", + "night-light-force-activation-description": "Ignores the schedule and applies the night filter immediately.", + "night-light-force-activation-label": "Force activation", + "night-light-manual-schedule-description": "Set custom times for sunrise and sunset.", + "night-light-manual-schedule-label": "Manual scheduling", + "night-light-manual-schedule-select-start": "Select start time", + "night-light-manual-schedule-select-stop": "Select stop time", + "night-light-manual-schedule-sunrise": "Sunrise time", + "night-light-manual-schedule-sunset": "Sunset time", + "night-light-temperature-day": "Day", + "night-light-temperature-day-description": "Controls the temperature during daytime.", + "night-light-temperature-description": "Set the colour warmth for nighttime and daytime.", + "night-light-temperature-label": "Colour temperature", + "night-light-temperature-night": "Night", + "night-light-temperature-night-description": "Controls the temperature during nighttime.", + "title": "Display" + }, + "dock": { + "appearance-background-opacity-description": "Adjust the dock's background opacity.", + "appearance-border-radius-description": "Adjust the dock's border radius.", + "appearance-border-radius-label": "Border radius", + "appearance-colorize-icons-description": "Apply theme colors to dock app icons (non-focused apps only).", + "appearance-colorize-icons-label": "Colourise icons", + "appearance-dead-opacity-description": "Adjust the opacity of app icons that are not running.", + "appearance-dead-opacity-label": "Dead opacity", + "appearance-desc": "Customise the dock's behaviour and appearance.", + "appearance-display-auto-hide": "Auto hide", + "appearance-display-description": "Choose how the dock behaves.", + "appearance-display-exclusive": "Exclusive", + "appearance-dock-indicator-description": "Show a small indicator when the dock is hidden.", + "appearance-dock-indicator-label": "Dock indicator", + "appearance-floating-distance-description": "Set the distance between the dock and the edge of the screen.", + "appearance-floating-distance-label": "Dock floating distance", + "appearance-group-apps-description": "Group multiple windows from the same app into one dock entry.", + "appearance-group-apps-label": "Group same apps", + "appearance-group-click-action-cycle": "Cycle windows", + "appearance-group-click-action-description": "Choose what left-click does for grouped apps.", + "appearance-group-click-action-label": "Grouped app click action", + "appearance-group-click-action-list": "Open window list", + "appearance-group-context-menu-mode-description": "Choose how the context menu behaves for grouped apps.", + "appearance-group-context-menu-mode-extended": "Extended", + "appearance-group-context-menu-mode-label": "Grouped app menu mode", + "appearance-group-context-menu-mode-list": "Window list", + "appearance-group-indicator-style-description": "Choose how grouped running indicators display focused window state.", + "appearance-group-indicator-style-dots": "Dots", + "appearance-group-indicator-style-label": "Grouped indicator style", + "appearance-group-indicator-style-number": "Number", + "appearance-hide-show-speed-description": "Adjust the speed of the dock hide/show animation.", + "appearance-hide-show-speed-label": "Hide/show speed", + "appearance-icon-size-description": "Adjust the overall size of the dock.", + "appearance-icon-size-label": "Dock size", + "appearance-inactive-indicators-description": "Display indicator pills for all apps, not just the currently active one.", + "appearance-inactive-indicators-label": "Running indicators", + "appearance-indicator-color-description": "Choose the colour of the hidden dock indicator.", + "appearance-indicator-color-label": "Indicator colour", + "appearance-indicator-opacity-description": "Adjust the opacity of the hidden dock indicator.", + "appearance-indicator-opacity-label": "Indicator opacity", + "appearance-indicator-thickness-description": "Toggle a thicker hidden dock indicator (6px instead of 3px).", + "appearance-indicator-thickness-label": "Thicker indicator", + "appearance-launcher-icon-description": "Choose the icon used for the dock launcher.", + "appearance-launcher-icon-label": "Launcher icon", + "appearance-launcher-position-description": "Choose where the launcher icon appears in the dock.", + "appearance-launcher-position-end": "End", + "appearance-launcher-position-label": "Launcher position", + "appearance-launcher-position-start": "Start", + "appearance-launcher-use-distro-logo-description": "Use your distribution logo as the dock launcher icon.", + "appearance-launcher-use-distro-logo-label": "Use distro logo instead of icon", + "appearance-pinned-static-description": "Always push pinned app icons to the left in static order.", + "appearance-pinned-static-label": "Static pinned apps", + "appearance-position-description": "Choose where the dock appears on screen.", + "appearance-position-label": "Position", + "appearance-show-launcher-icon-description": "Show the application launcher icon in the dock.", + "appearance-show-launcher-icon-label": "Show app launcher", + "appearance-sit-on-frame-description": "Align the dock inside the frame border instead of sitting on top.", + "appearance-sit-on-frame-label": "Dock sits on frame", + "appearance-type-attached": "Attached", + "appearance-type-description": "Choose between a floating pill or a static bar attached to the edge.", + "appearance-type-floating": "Floating", + "appearance-type-label": "Dock style", + "enabled-description": "Show or hide the dock entirely.", + "enabled-label": "Enable dock", + "monitors-desc": "Show dock on specific monitors. Defaults to all if none are chosen.", + "monitors-only-same-monitor-description": "Show only apps from the monitor where the dock is located.", + "monitors-only-same-monitor-label": "Only apps from same monitor", + "monitors-title": "Monitor display", + "title": "Dock" + }, + "general": { + "copy-settings": "Copy settings", + "fonts-default-description": "Main font used throughout the interface.", + "fonts-default-label": "Default font", + "fonts-default-placeholder": "Select default font...", + "fonts-default-scale-description": "Increase or decrease the size of the standard text.", + "fonts-default-scale-label": "Default font size", + "fonts-default-search-placeholder": "Search font...", + "fonts-desc": "Choose the fonts used throughout the interface.", + "fonts-monospace-description": "Monospaced font used for numbers and stats display.", + "fonts-monospace-label": "Monospaced font", + "fonts-monospace-placeholder": "Select monospace font...", + "fonts-monospace-scale-description": "Increase or decrease the size of the monospaced text.", + "fonts-monospace-scale-label": "Monospaced font size", + "fonts-monospace-search-placeholder": "Search monospace font...", + "fonts-title": "Fonts", + "keybinds-conflict-description": "The key combination is already assigned to {action}.", + "keybinds-conflict-title": "Keybind conflict", + "keybinds-description": "Configure global navigation keys for panels and launcher.", + "keybinds-down": "Move down", + "keybinds-enter": "Confirm / Action", + "keybinds-escape": "Close / Back", + "keybinds-left": "Move left", + "keybinds-modifier-description": "Shortcuts must include a modifier key (Ctrl or Alt).", + "keybinds-modifier-title": "Modifier required", + "keybinds-remove": "Remove / Delete", + "keybinds-right": "Move right", + "keybinds-title": "Navigation keybinds", + "keybinds-up": "Move up", + "language-desc": "Choose your preferred language for the application.", + "language-select-auto-detect": "Automatic", + "language-select-description": "Select the language used in the application's interface.", + "language-select-label": "Application language", + "launch-setup-wizard": "Setup wizard", + "profile-desc": "Edit your user details and avatar.", + "profile-picture-description": "Your profile picture that appears throughout the interface.", + "profile-picture-label": "Profile picture", + "profile-select-avatar": "Select avatar image", + "profile-title": "Profile", + "profile-tooltip": "Profile picture", + "reverse-scrolling-description": "Reverse the interpreted scroll direction", + "reverse-scrolling-label": "Reverse scrolling", + "screen-corners-desc": "Customise screen corner rounding and visual effects.", + "screen-corners-radius-description": "Adjust the rounded corners of the screen.", + "screen-corners-radius-label": "Screen corners radius", + "screen-corners-radius-reset": "Reset screen corners radius", + "screen-corners-show-corners-description": "Display rounded corners on the edge of the screen.", + "screen-corners-show-corners-label": "Show screen corners", + "screen-corners-solid-black-description": "Use solid black instead of the bar background colour.", + "screen-corners-solid-black-label": "Solid black corners", + "screen-corners-title": "Screen corners", + "settings-copied": "Settings copied to clipboard", + "smooth-scrolling-description": "Animate list scrolling for a smoother wheel experience.", + "smooth-scrolling-label": "Smooth scrolling", + "tab-basics": "Basics", + "tab-keybinds": "Keybinds" + }, + "hooks": { + "color-generation-description": "Command to be executed when colour generation completes (e.g. after wallpaper-based theming).", + "color-generation-label": "Colors generated", + "color-generation-placeholder": "e.g. notify-send \"Colors\" \"Generated\"", + "info-command-info-description": "• Commands are executed via shell (sh -lc)
• Commands run in background (detached)
• Test buttons execute with current values", + "info-parameters-description": "• Wallpaper hook: $1 = wallpaper path, $2 = screen name, $3 = theme (dark/light)
• Colour generation hook: $1 = theme (dark/light)
• Theme toggle hook: $1 = true/false (Dark Mode state)
• Screen lock/unlock hooks: $1 = lock/unlock (screen lock state)
• Performance mode hooks: No parameters
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "Available parameters", + "noctalia-started-description": "Command to execute when Noctalia has finished loading.", + "noctalia-started-label": "Noctalia started", + "noctalia-started-placeholder": "e.g. notify-send 'Noctalia Loaded'", + "performance-mode-disabled-description": "Command to be executed when Noctalia performance mode is disabled.", + "performance-mode-disabled-label": "Performance mode disabled", + "performance-mode-disabled-placeholder": "e.g. notify-send \"Performance\" \"Mode disabled\"", + "performance-mode-enabled-description": "Command to be executed when Noctalia performance mode is enabled.", + "performance-mode-enabled-label": "Performance mode enabled", + "performance-mode-enabled-placeholder": "e.g., notify-send \"Performance\" \"Mode enabled\"", + "screen-lock-description": "Command to be executed when the screen is locked.", + "screen-lock-label": "Screen locked", + "screen-lock-placeholder": "e.g. notify-send \"Screen\" \"Locked\"", + "screen-unlock-description": "Command to be executed when the lock screen is unlocked.", + "screen-unlock-label": "Screen unlocked", + "screen-unlock-placeholder": "e.g. notify-send \"Screen\" \"Unlocked\"", + "session-description": "Command to be executed before shutdown or reboot. Receives action type as $1 (shutdown/reboot).", + "session-label": "Session end", + "session-placeholder": "e.g. notify-send \"Session\" \"$1\"", + "system-hooks-desc": "Configure commands to be executed when system events occur.", + "system-hooks-enable-description": "Enable or disable all hook commands.", + "system-hooks-enable-label": "Enable hooks", + "system-hooks-title": "System hooks", + "theme-changed-description": "Command to be executed when theme toggles between Dark and Light Mode.", + "theme-changed-label": "Theme changed", + "theme-changed-placeholder": "e.g. notify-send \"Theme\" \"Toggled\"", + "title": "Hooks", + "wallpaper-changed-description": "Command to be executed when wallpaper changes.", + "wallpaper-changed-label": "Wallpaper changed", + "wallpaper-changed-placeholder": "e.g. notify-send \"Wallpaper\" \"Changed\"" + }, + "idle": { + "custom-add": "Add command", + "custom-description": "Run a shell command after a period of inactivity.", + "custom-entry-command": "Command", + "custom-entry-delete": "Delete", + "custom-entry-edit": "Edit Custom Command", + "custom-entry-name": "Name", + "custom-entry-name-placeholder": "e.g. Turn off lights", + "custom-entry-new": "New Custom Command", + "custom-entry-timeout": "Inactivity time", + "custom-entry-timeout-format": "{count} second", + "custom-entry-timeout-format-plural": "{count} seconds", + "custom-entry-unnamed": "Unnamed command", + "custom-label": "Custom idle commands", + "enable-description": "Automatically turn off the screen, lock, or suspend after a period of inactivity.", + "enable-label": "Enable idle management", + "fade-duration-description": "Seconds for the fade-to-black animation before each action fires. Any mouse movement cancels the fade.", + "fade-duration-label": "Fade duration", + "lock-description": "Seconds of inactivity before the lock screen activates.", + "lock-label": "Lock screen", + "resume-command-label": "Resume command", + "screen-off-description": "Seconds of inactivity before monitors are turned off.", + "screen-off-label": "Turn off screen", + "status-description": "Idle time as reported by the compositor.", + "status-label": "Idle time", + "suspend-description": "Seconds of inactivity before the system suspends.", + "tab-behavior": "Behaviour", + "tab-custom": "Custom", + "timeouts-description": "Set to 0 to disable a stage. Timeouts are paused while Keep Awake is active.", + "timeouts-label": "Timeouts", + "title": "Idle", + "unavailable": "Native idle monitoring is not available on this compositor." + }, + "indicator": { + "default-value": "Default: {value}", + "system-default": "System Default" + }, + "launcher": { + "clipboard-desc": "Access and manage your clipboard history from the launcher.", + "execute-desc": "Configure how applications are launched.", + "execute-title": "Execution", + "settings-annotation-tool-description": "Command to run when clicking the annotate button in clipboard history. The image will be piped to this command.", + "settings-annotation-tool-label": "Annotation tool", + "settings-annotation-tool-placeholder": "e.g. 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Automatically paste the selected clipboard item. Requires wtype.", + "settings-auto-paste-label": "Auto paste", + "settings-clip-chips-description": "Show a tab bar to filter clipboard history by type (Images, Links, Files, Code, etc).", + "settings-clip-chips-label": "Enable Category Chips", + "settings-clip-preview-description": "Show a preview of the clipboard content when using the >clip command.", + "settings-clip-preview-label": "Enable clip preview", + "settings-clip-smart-icons-description": "Show specific icons for links, files, colors, and code instead of a generic clipboard icon.", + "settings-clip-smart-icons-label": "Enable Smart Icons", + "settings-clip-wrap-text-description": "Wrap text in the clipboard list instead of truncating it.", + "settings-clip-wrap-text-label": "Wrap clipboard text", + "settings-clipboard-history-description": "Access previously copied items from the launcher.", + "settings-clipboard-history-label": "Enable clipboard history", + "settings-clipboard-watch-image-description": "Full command string passed to wl-paste for image changes. (requires restart)", + "settings-clipboard-watch-image-label": "Image watch command", + "settings-clipboard-watch-text-description": "Full command string passed to wl-paste for text changes. (requires restart)", + "settings-clipboard-watch-text-label": "Text watch command", + "settings-custom-launch-prefix-description": "Prefix commands with a custom launcher (e.g. 'runapp' for systemd integration).", + "settings-custom-launch-prefix-enabled-description": "Use a custom prefix for launching applications instead of the default method.", + "settings-custom-launch-prefix-enabled-label": "Enable custom launch prefix", + "settings-custom-launch-prefix-label": "Custom launch prefix", + "settings-density-description": "Adjust the size of application icons and the density of the launcher.", + "settings-density-label": "Density", + "settings-desc": "Customise the launcher's behaviour and appearance.", + "settings-enable-session-search-description": "Show session actions (lock, shutdown, reboot, etc.) when searching in the launcher.", + "settings-enable-session-search-label": "Enable session search", + "settings-enable-settings-search-description": "Show settings results when searching in the launcher.", + "settings-enable-settings-search-label": "Enable settings search", + "settings-enable-windows-search-description": "Search and focus active windows.", + "settings-enable-windows-search-label": "Enable windows search", + "settings-grid-view-description": "Display items in a grid layout instead of a list.", + "settings-icon-mode-description": "Use native system icons instead of Tabler icons.", + "settings-icon-mode-label": "Use native icons", + "settings-ignore-mouse-input-description": "Disable mouse interaction and scrollwheel in the launcher.", + "settings-ignore-mouse-input-label": "Ignore mouse input", + "settings-overlay-layer-description": "Display the launcher on the overlay layer, above fullscreen windows. When enabled, the launcher will not attach to the bar.", + "settings-overlay-layer-label": "Show above fullscreen", + "settings-position-description": "Choose where the launcher panel appears.", + "settings-show-categories-description": "Show category tabs for filtering applications.", + "settings-show-categories-label": "Show categories", + "settings-show-icon-background-description": "Show a rounded rectangle background behind icons.", + "settings-show-icon-background-label": "Show icon background", + "settings-sort-by-usage-description": "When enabled, frequently launched apps appear first in the list.", + "settings-sort-by-usage-label": "Sort by most used", + "settings-terminal-command-description": "Command to launch a terminal. E.g. 'kitty -e' or 'gnome-terminal --'.", + "settings-terminal-command-label": "Terminal command", + "settings-use-app2unit-description": "Uses an alternative launch method to better manage app processes and prevent issues.", + "settings-use-app2unit-label": "Use App2Unit to launch applications", + "settings-view-mode-description": "Choose the layout for the launcher entries.", + "settings-view-mode-label": "View mode", + "title": "Launcher" + }, + "location": { + "auto-locate-description": "Automatically detect your location periodically using your IP address.", + "auto-locate-label": "Auto-Locate", + "calendar-cards-desc": "Organize and enable/disable cards in the calendar panel.", + "calendar-cards-title": "Calendar cards", + "calendar-header-label": "Calendar Header", + "calendar-month-label": "Calendar Month", + "date-time-12hour-format-description": "Display time in 12-hour format on the lock screen and calendar. The bar clock has its own settings.", + "date-time-12hour-format-label": "Use 12-hour time format", + "date-time-desc": "Customise how date and time appear.", + "date-time-first-day-of-week-automatic": "Automatic (use system locale)", + "date-time-first-day-of-week-description": "Choose which day starts the week in the calendar.", + "date-time-first-day-of-week-label": "First day of week", + "date-time-show-events-description": "Display events in the calendar panel.", + "date-time-show-events-label": "Show calendar events", + "date-time-title": "Date & time", + "date-time-use-analog-description": "Show an analog style clock on the calendar window.", + "date-time-use-analog-label": "Use analog style clock", + "date-time-week-numbers-description": "Display the week of the year (e.g. week 38) in the calendar.", + "date-time-week-numbers-label": "Show week numbers", + "geolocate-now-button": "Locate now", + "location-desc": "Get accurate weather and Night Light scheduling by setting your location.", + "location-search-description": "e.g. Toronto, ON", + "location-search-label": "Search for a location", + "location-search-placeholder": "Enter the location name", + "location-title": "Your location", + "weather-desc": "Choose your preferred temperature unit.", + "weather-enabled-description": "Show weather information throughout the interface and fetch weather data. When disabled, all weather elements will be hidden and no network requests will be made.", + "weather-enabled-label": "Enable weather", + "weather-fahrenheit-description": "Display temperature in Fahrenheit instead of Celsius.", + "weather-fahrenheit-label": "Display temperature in Fahrenheit (°F)", + "weather-hide-city-description": "Hide the city name from weather displays throughout the interface.", + "weather-hide-city-label": "Hide city name", + "weather-hide-timezone-description": "Hide the timezone abbreviation from weather displays throughout the interface.", + "weather-hide-timezone-label": "Hide timezone", + "weather-show-effects-description": "Show additional visual effects (like rain, snow, or lightning) on the weather card.", + "weather-show-effects-label": "Display weather effects", + "weather-show-in-calendar-description": "Show the daily weather forecast directly in your calendar view.", + "weather-show-in-calendar-label": "Display weather in calendar", + "weather-talia-mascot-always-description": "Always use Talia emojis instead of tabler icons.", + "weather-talia-mascot-always-label": "Always show Talia weather emojis" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "When fprintd (fingerprint authentication) is active, this option lets you still login using your password instead of a fingerprint", + "allow-password-with-fprintd-label": "Allow password login with fprintd", + "auto-start-auth-description": "Automatically starts fingerprint authentication without requiring a key press or button click.", + "auto-start-auth-label": "Auto-start authentication", + "clock-format-description": "Customise the clock format using date/time syntax tokens.", + "clock-format-label": "Clock format", + "clock-style-analog": "Analog", + "clock-style-custom": "Custom", + "clock-style-description": "Choose the visual style of the clock on the lock screen.", + "clock-style-digital": "Digital", + "clock-style-label": "Clock style", + "compact-lockscreen-description": "Show only the login input and system controls, hiding weather and media widgets.", + "compact-lockscreen-label": "Compact lock screen", + "enable-lockscreen-media-controls-description": "Show interactive media playback controls on the lock screen.", + "enable-lockscreen-media-controls-label": "Lock screen media controls", + "lock-on-suspend-description": "Automatically lock the screen when suspending the system.", + "lock-on-suspend-label": "Lock on suspend", + "lock-screen-animations-description": "Enable or disable lockscreen animations.", + "lock-screen-animations-label": "Lockscreen animations", + "lock-screen-blur-strength-description": "Applies a blur effect to the lock screen wallpaper.", + "lock-screen-blur-strength-label": "Lock screen blur strength", + "lock-screen-tint-strength-description": "Applies a tint overlay to the lock screen wallpaper.", + "lock-screen-tint-strength-label": "Lock screen tint strength", + "monitors-desc": "Show lock screen on specific monitors. Defaults to all if none are chosen.", + "password-chars-description": "Cute icons used to hide your password.", + "password-chars-label": "Random password icons", + "show-hibernate-description": "Show the option 'hibernate' in the power controls.", + "show-hibernate-label": "Show hibernate", + "show-session-buttons-description": "Allow access to power settings from the lock screen.", + "show-session-buttons-label": "Power controls", + "title": "Lock Screen" + }, + "network": { + "bluetooth-description": "Activate Bluetooth management.", + "bluetooth-rssi-polling-description": "Periodically sample RSSI for connected devices via bluetoothctl. May not be available for all devices; uses minimal resources when enabled.", + "bluetooth-rssi-polling-label": "Bluetooth signal polling", + "desc": "Manage Wi-Fi and Bluetooth connections.", + "wifi-description": "Manage wireless networks (requires NetworkManager)." + }, + "notifications": { + "duration-critical-urgency-description": "How long critical priority notifications stay visible.", + "duration-critical-urgency-label": "Critical urgency", + "duration-desc": "Configure how long notifications stay visible based on their urgency level.", + "duration-low-urgency-description": "How long low priority notifications stay visible.", + "duration-low-urgency-label": "Low urgency", + "duration-normal-urgency-description": "How long normal priority notifications stay visible.", + "duration-normal-urgency-label": "Normal urgency", + "duration-respect-expire-description": "Use the expire timeout set in the notification.", + "duration-respect-expire-label": "Respect expire timeout", + "duration-title": "Notification duration", + "history-clear-dismiss-description": "Clear notification from history when dismissed.", + "history-clear-dismiss-label": "Clear on dismissed", + "history-critical-urgency-description": "Save critical priority notifications to history.", + "history-critical-urgency-label": "Save critical urgency to history", + "history-desc": "Control which notifications are saved to history based on their urgency level.", + "history-low-urgency-description": "Save low priority notifications to history.", + "history-low-urgency-label": "Save low urgency to history", + "history-normal-urgency-description": "Save normal priority notifications to history.", + "history-normal-urgency-label": "Save normal urgency to history", + "monitors-desc": "Show notification on specific monitors. Defaults to all if none are chosen.", + "rules-action-block": "Block", + "rules-action-block-desc": "Skips completely.", + "rules-action-hide": "Hide", + "rules-action-hide-desc": "No popup, no sound, adds to history.", + "rules-action-label": "Action", + "rules-action-mute": "Mute", + "rules-action-mute-desc": "No sound, still shows popup and in history.", + "rules-add": "Add rule", + "rules-delete": "Delete rule", + "rules-description": "Match app name or content. Rules are checked in order, and the first match is applied.", + "rules-edit": "Edit rule", + "rules-label": "Filter rules", + "rules-pattern-label": "Pattern", + "rules-pattern-placeholder": "firefox, discord, or /regex/", + "rules-tab": "Rules", + "settings-always-on-top-description": "Display notifications above fullscreen windows and other layers.", + "settings-background-opacity-description": "Adjust the opacity of notification backgrounds.", + "settings-density-description": "Choose the notification card density.", + "settings-density-label": "Density", + "settings-desc": "Configure notifications appearance and behaviour.", + "settings-do-not-disturb-description": "Disable all notification popups when enabled.", + "settings-enabled-description": "Enable or disable the notification daemon, requires a restart of Noctalia shell.", + "settings-enabled-label": "Enable notifications", + "settings-location-description": "Where notifications appear on screen.", + "settings-markdown-description": "Render notification content using Markdown formatting.", + "settings-markdown-label": "Enable Markdown", + "sounds-desc": "Configure notification sound effects and volume.", + "sounds-enabled-description": "Enable sound effects for incoming notifications.", + "sounds-enabled-label": "Enable notification sounds", + "sounds-excluded-apps-description": "Skip playing the configured notification sound for specific applications that have their own built-in sounds.", + "sounds-excluded-apps-label": "Excluded applications", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Path to the sound file played for critical priority notifications.", + "sounds-files-critical-label": "Critical urgency sound", + "sounds-files-critical-select-title": "Select critical urgency sound file", + "sounds-files-desc": "Configure custom sound files for different notification urgency levels.", + "sounds-files-low-description": "Path to the sound file played for low priority notifications.", + "sounds-files-low-label": "Low urgency sound", + "sounds-files-low-select-title": "Select low urgency sound file", + "sounds-files-normal-description": "Path to the sound file played for normal priority notifications.", + "sounds-files-normal-label": "Normal urgency sound", + "sounds-files-normal-select-title": "Select normal urgency sound file", + "sounds-files-placeholder": "Enter path to sound file", + "sounds-files-select-file": "Select sound file", + "sounds-files-title": "Sound files", + "sounds-files-unified-description": "Path to the sound file played for notifications.", + "sounds-files-unified-label": "Notification sound", + "sounds-files-unified-select-title": "Select notification sound file", + "sounds-separate-description": "Use different sound files for low, normal, and critical priority notifications.", + "sounds-separate-label": "Use different sounds per priority", + "sounds-title": "Sound settings", + "sounds-unavailable-description": "Install Qt6 Multimedia to enable notification sounds.", + "sounds-unavailable-label": "Notification sounds unavailable", + "sounds-volume-description": "Adjust the volume level for notification sounds.", + "sounds-volume-label": "Sound volume", + "toast-battery-description": "Show a warning when the battery level falls below this percentage.", + "toast-battery-label": "Battery warning", + "toast-desc": "Configure toast appearance and behaviour.", + "toast-keyboard-description": "Show a toast when the keyboard layout changes.", + "toast-keyboard-label": "Keyboard layout", + "toast-media-description": "Show a toast when media playback state changes.", + "toast-media-label": "Media" + }, + "osd": { + "always-on-top-description": "Display OSD above fullscreen windows and other layers.", + "always-on-top-label": "Always on top", + "background-opacity-description": "Controls the transparency of the OSD background.", + "background-opacity-label": "Background opacity", + "description": "Configure on-screen indicators such as volume and brightness overlays.", + "duration-auto-hide-description": "Adjust the time before OSD disappears.", + "duration-auto-hide-label": "Auto-hide after", + "duration-desc": "How long OSD remains visible before hiding automatically.", + "duration-title": "Auto-hide timeout", + "enabled-description": "Show volume and brightness changes in real-time.", + "enabled-label": "Enable on-screen display", + "events-desc": "Select which events trigger the on-screen display.", + "general-desc": "Configure visibility and behaviour of OSD.", + "location-description": "Where on-screen displays appear.", + "monitors-desc": "Show OSD on specific monitors. Defaults to all if none are chosen.", + "title": "On-Screen Display", + "types-brightness-description": "Show OSD when screen brightness changes.", + "types-brightness-label": "Brightness", + "types-custom-text-description": "Show OSD for custom text messages from IPC.", + "types-custom-text-label": "Custom Text", + "types-desc": "Select the events that trigger the OSD.", + "types-input-volume-description": "Show OSD when microphone volume changes.", + "types-input-volume-label": "Input volume", + "types-lockkey-description": "Show OSD when Caps Lock, Num Lock, or Scroll Lock are toggled.", + "types-lockkey-label": "Lock keys", + "types-media-description": "Show OSD when media playback state changes (play, pause, skip).", + "types-media-label": "Media playback", + "types-title": "OSD trigger events", + "types-volume-description": "Show OSD when audio output volume changes.", + "types-volume-label": "Output volume" + }, + "plugins": { + "auto-update": "Auto-update plugins", + "auto-update-description": "Automatically update all plugins when the shell starts.", + "available-description": "Browse and install plugins from configured sources.", + "available-label": "Available plugins", + "available-no-plugins-description": "Check your plugin sources or refresh the list.", + "available-no-plugins-label": "No plugins available", + "check-for-updates": "Check for updates", + "checking-for-updates": "Checking for updates...", + "collision-already-installed": "This plugin is already installed", + "collision-custom-version-exists": "A custom version from \"{source}\" is already installed", + "collision-official-version-exists": "The official version of this plugin is already installed", + "development-disable": "Disable development mode for this plugin", + "development-enable": "Enable development mode for this plugin", + "filter-downloaded": "Downloaded", + "filter-not-downloaded": "Not Downloaded", + "filter-tags-description": "Filter plugins by category or download status.", + "filter-tags-label": "Tags", + "hot-reload-description": "Automatically reload plugins when their files change. Useful for plugin development.", + "hot-reload-label": "Hot reload (dev mode)", + "hot-reloaded": "Reloaded plugin: {name}", + "install-error": "Failed to install: {error}", + "install-incompatible": "{plugin} requires Noctalia v{version} or higher", + "install-success": "Successfully installed {plugin}", + "installed-description": "Manage and configure all locally installed plugins.", + "installed-label": "Installed plugins", + "installed-no-plugins-description": "Install plugins from the \"Available\" section.", + "installed-no-plugins-label": "No plugins installed", + "installing": "Installing {plugin}...", + "notify-updates": "Plugin update notifications", + "notify-updates-description": "Show a notification popup when new plugin updates are available.", + "open-plugin-page": "Open plugin page", + "open-plugins-tab": "Open plugins settings", + "plugin-settings-title": "{plugin} Settings", + "refresh-refreshing": "Refreshing plugins list...", + "refresh-tooltip": "Refresh available plugins", + "settings-error-not-loaded": "Plugin not loaded", + "settings-saved": "Plugin settings saved", + "settings-tooltip": "Plugin settings", + "source-custom": "Custom source", + "sources-add-custom": "Add custom repository", + "sources-add-dialog-description": "Add a GitHub repository as a plugin source.", + "sources-add-dialog-error": "Failed to add plugin source", + "sources-add-dialog-name": "Repository name", + "sources-add-dialog-name-placeholder": "My Custom Plugins", + "sources-add-dialog-success": "Plugin source added successfully", + "sources-add-dialog-title": "Add plugin source", + "sources-add-dialog-url": "Repository URL", + "sources-description": "Manage plugin repositories.", + "sources-placeholder": "My cool repository", + "sources-remove-tooltip": "Remove plugin source", + "title": "Plugins", + "translations-reloaded": "Reloaded translations: {name}", + "uninstall-dialog-description": "Are you sure you want to uninstall {plugin}? This will remove all plugin data.", + "uninstall-dialog-title": "Uninstall plugin", + "uninstall-error": "Failed to uninstall: {error}", + "uninstall-success": "Successfully uninstalled {plugin}", + "uninstalling": "Uninstalling {plugin}...", + "update-all": "Update All ({count})", + "update-all-success": "All plugins updated successfully", + "update-available": "New plugin update available", + "update-available-plural": "Plugin updates available ({count})", + "update-error": "Failed to update plugin: {plugin}: {error}", + "update-pending": "v{current} → v{new} (requires Noctalia v{required})", + "update-success": "Updated {plugin} to v{version}", + "update-version": "v{current} → v{new}", + "updating": "Updating..." + }, + "region": { + "title": "Region" + }, + "session-menu": { + "countdown-duration-description": "Set how long the countdown timer lasts before executing power actions.", + "countdown-duration-label": "Countdown duration", + "enable-countdown-description": "Show a countdown timer before executing power actions.", + "enable-countdown-label": "Enable countdown timer", + "entries-desc": "Customise which power actions appear in the session menu and in what order.", + "entries-title": "Power actions", + "entry-settings-command-description": "Custom command to execute for this action. Leave empty to use the default system command.", + "entry-settings-command-placeholder": "e.g. systemctl poweroff", + "entry-settings-default-command-lock": "Internal lock screen (no command)", + "entry-settings-default-command-logout": "Internal logout (no command)", + "entry-settings-default-info-description": "If no custom command is specified, the default system command will be used.", + "entry-settings-default-info-label": "Default command", + "entry-settings-keybind-description": "Assign a key combination to trigger this action when the session menu is open.", + "entry-settings-keybind-placeholder": "Press Record to assign a key...", + "entry-settings-keybind-recording": "Recording... Press any key combination", + "entry-settings-title": "Configure {entry}", + "entry-settings-tooltip": "Configure command", + "general-desc": "Configure the session menu panel behaviour and appearance.", + "large-buttons-layout-description": "Choose how session menu buttons are displayed.", + "large-buttons-layout-label": "Large buttons layout", + "large-buttons-style-description": "Display the session menu with large buttons in a grid layout.", + "large-buttons-style-label": "Large buttons style", + "position-description": "Choose where the session menu panel appears when opened.", + "show-header-description": "Display the title and close button at the top of the session menu.", + "show-header-label": "Show header", + "show-keybinds-description": "Display keybind hints on session options.", + "show-keybinds-label": "Show keybinds" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Keep desktop widgets visible while Noctalia performance mode is enabled.", + "noctalia-performance-disable-desktop-widgets-label": "Enable desktop widgets in performance mode", + "noctalia-performance-disable-wallpaper-description": "Keep desktop, overview, and lock screen wallpapers visible while Noctalia performance mode is enabled.", + "noctalia-performance-disable-wallpaper-label": "Enable wallpaper rendering in performance mode", + "title": "System" + }, + "system-monitor": { + "critical-color-label": "Critical colour", + "custom-highlight-colors-title-label": "Custom highlight colors", + "disk-available-label": "Disk available", + "disk-section-label": "Disk usage", + "enable-dgpu-monitoring-description": "Warning: This will wake up your discrete GPU (NVIDIA/AMD), which may significantly impact battery life on laptops with hybrid graphics.", + "enable-dgpu-monitoring-label": "Enable discrete GPU monitoring", + "external-monitor-description": "Enter the command or application path to launch when activating the external system monitor application.", + "external-monitor-label": "External system monitor command", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-centre || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "Configure the system monitor behaviour and appearance.", + "gpu-section-label": "GPU temperature", + "highlight-colors-section-label": "Highlight colors", + "polling-interval-label": "Polling interval", + "polling-section-description": "Configure how often each system metric is updated.", + "polling-section-label": "Polling intervals", + "threshold-critical": "Critical threshold", + "threshold-warning": "Warning threshold", + "thresholds-section-description": "Adjust warning/critical thresholds for each system metric.", + "use-custom-highlight-colors-description": "When disabled, theme default highlight colors are used.", + "use-custom-highlight-colors-label": "Use custom highlight colors", + "warning-color-label": "Warning colour" + }, + "user-interface": { + "allow-panels-without-bar-description": "When enabled, panels can open on any screen. When disabled, panels will only open on screens that have a bar, which can reduce memory usage.", + "allow-panels-without-bar-label": "Allow panels on screens without a bar", + "animation-disable-description": "Disable all animations for a faster, more responsive experience.", + "animation-disable-label": "Disable UI animations", + "animation-speed-description": "Adjust global animation speed.", + "animation-speed-label": "Animation speed", + "animation-speed-reset": "Reset animation speed", + "appearance-desc": "Customise visual elements like tooltips, borders, and shadows.", + "blur-behind-description": "Blurs the area behind bars and panels using the compositor blur protocol.", + "blur-behind-label": "Blur behind", + "box-border-description": "Display an outline around content areas.", + "box-border-label": "Container outline", + "box-border-radius-description": "Adjusts the corner roundness of major layout sections, such as sidebars, cards, and content panels.", + "box-border-radius-label": "Container radius", + "control-border-radius-description": "Controls the curvature of interactive elements, including buttons, toggles, and text fields.", + "control-border-radius-label": "Input radius", + "desc": "Customise the look, feel, and behaviour of the interface.", + "dim-desktop-description": "Dim the desktop when panels or menus are open.", + "dim-desktop-label": "Dim desktop", + "dimmer-opacity-description": "Set the opacity level for desktop dimming.", + "dimmer-opacity-label": "Dimmed desktop opacity", + "panel-background-opacity-description": "Set the background opacity for all panels (bar, launcher, settings, etc...).", + "panel-background-opacity-label": "Panel background opacity", + "panels-attached-to-bar-description": "Panels lock to the bar and screen edges, creating a seamless look with stylish inverted corners.", + "panels-attached-to-bar-label": "Snap panels to edges", + "panels-overlay-description": "Ensures panels and the bar remain visible, even over fullscreen applications.", + "panels-overlay-label": "Keep panels & bar on top", + "scaling-description": "Changes the size of the general user interface, excluding the bar.", + "scaling-label": "Interface scaling", + "scrollbar-always-visible-description": "Keep scrollbars visible whenever content is scrollable, instead of only showing them on hover.", + "scrollbar-always-visible-label": "Always show scrollbars", + "settings-panel-header": "Settings panel", + "settings-panel-mode-description": "Choose settings layout (may require reopening).", + "settings-panel-mode-label": "Settings panel mode", + "settings-panel-sidebar-card-style-description": "Wraps the settings sidebar in a filled background with rounded corners.", + "settings-panel-sidebar-card-style-label": "Sidebar card style", + "shadows-description": "Enables drop shadows under bars and panels.", + "shadows-direction-description": "Choose where the shadow is cast to.", + "shadows-direction-label": "Shadow direction", + "shadows-label": "Drop shadows", + "title": "User Interface", + "tooltips-description": "Enable or disable tooltips throughout the interface.", + "tooltips-label": "Show tooltips", + "translucent-widgets-description": "Make buttons, tabs, and other widgets inside panels semi-transparent.", + "translucent-widgets-label": "Translucent widgets" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Alphabetical", + "automation-change-mode-description": "Choose how wallpapers are selected when changing automatically.", + "automation-change-mode-label": "Change mode", + "automation-custom-interval-description": "Enter time as HH:MM (e.g. 01:30).", + "automation-custom-interval-label": "Custom interval", + "automation-interval-description": "How often to change wallpapers automatically.", + "automation-interval-label": "Wallpaper interval", + "automation-random-wallpaper-description": "Schedule random wallpaper changes at regular intervals.", + "automation-scheduled-change-description": "Automatically change wallpapers at regular intervals.", + "automation-scheduled-change-label": "Scheduled change", + "look-feel-edge-smoothness-description": "Applies a soft, feathered effect to the edge of transitions.", + "look-feel-edge-smoothness-label": "Soften transition edge", + "look-feel-fill-color-description": "Choose a fill colour that may appear behind the wallpaper.", + "look-feel-fill-mode-description": "Select how the image should scale to match your monitor's resolution.", + "look-feel-fill-mode-label": "Fill mode", + "look-feel-skip-startup-transition-description": "Skip the wallpaper animation when the shell starts.", + "look-feel-skip-startup-transition-label": "Skip startup transition", + "look-feel-title": "Look & feel", + "look-feel-transition-duration-description": "Duration of transition animations in seconds.", + "look-feel-transition-duration-label": "Transition duration", + "look-feel-transition-type-description": "Animation type when switching between wallpapers.", + "look-feel-transition-type-label": "Transition type", + "settings-clear-cache-button": "Clear cache", + "settings-clear-cache-description": "Clear cached resized wallpapers to free disk space.", + "settings-clear-cache-label": "Wallpaper cache", + "settings-clear-cache-toast": "Wallpaper cache cleared", + "settings-desc": "Control how wallpapers are managed and displayed.", + "settings-enable-management-description": "Manage wallpapers with Noctalia. Uncheck if you prefer using another application.", + "settings-enable-management-label": "Enable wallpaper management", + "settings-enable-overview-description": "Applies a blurred and dimmed wallpaper to the overview screen.", + "settings-enable-overview-label": "Enable overview wallpaper", + "settings-folder-description": "Path to your main wallpaper folder.", + "settings-folder-label": "Wallpaper folder", + "settings-hide-wallpaper-filenames-tooltip-hide": "Hide filenames", + "settings-hide-wallpaper-filenames-tooltip-show": "Show filenames", + "settings-monitor-specific-description": "Set a different wallpaper folder for each monitor.", + "settings-monitor-specific-label": "Monitor-specific directories", + "settings-monitor-specific-tooltip": "Monitor wallpaper folder", + "settings-overview-blur-strength-description": "Applies the blur strength to the overview.", + "settings-overview-blur-strength-label": "Overview blur strength", + "settings-overview-tint-description": "Applies the tint strength to the overview.", + "settings-overview-tint-label": "Overview tint strength", + "settings-recursive-search-description": "Also search for wallpapers in subfolders of the wallpaper directory.", + "settings-recursive-search-label": "Search subfolders", + "settings-select-monitor-folder": "Select monitor wallpaper folder", + "settings-selector-description": "Choose your wallpaper.", + "settings-selector-position-description": "Choose where the wallpaper selector panel appears.", + "settings-show-hidden-files-tooltip-hide": "Hide hidden files", + "settings-show-hidden-files-tooltip-show": "Show hidden files", + "settings-title": "Wallpaper settings", + "settings-use-original-images-description": "Skip resizing wallpapers before display. Saves disk space and reduces CPU usage during wallpaper changes, but may use more memory for very large images.", + "settings-use-original-images-label": "Use original images", + "settings-view-mode-description": "Choose how wallpapers are displayed from your directory.", + "settings-view-mode-label": "Viewing mode", + "view-mode-browse": "Browse directories", + "view-mode-cycle-tooltip": "View mode: {mode} (click to change)", + "view-mode-recursive": "Flattened subdirectories", + "view-mode-single": "Root directory" + } + }, + "placeholders": { + "add-new-keybind": "Add a new keybind", + "command-example": "echo \"Hello World\"", + "enter-command": "Enter command to execute (app or custom script)", + "enter-ipc-identifier": "Enter unique identifier for IPC commands", + "enter-path": "Enter path...", + "enter-text-to-collapse": "e.g. 'nothing is playing'. Use /regex/ for patterns.", + "enter-tooltip": "Enter tooltip", + "enter-width-pixels": "Enter width in pixels", + "keybind-recording": "Recording keybind...", + "search": "Search...", + "search-icons": "e.g. noctalia, niri, battery, cloud", + "search-launcher": "Search entries... or use > for commands", + "search-wallhaven": "Search Wallhaven...", + "search-wallpapers": "Type to filter wallpapers..." + }, + "positions": { + "bottom": "Bottom", + "bottom-center": "Bottom centre", + "bottom-left": "Bottom left", + "bottom-right": "Bottom right", + "center": "Centre", + "center-left": "Left centre", + "center-right": "Right centre", + "close-to-bar": "Close to bar button", + "follow-bar": "Follow bar", + "left": "Left", + "right": "Right", + "top": "Top", + "top-center": "Top centre", + "top-left": "Top left", + "top-right": "Top right" + }, + "session-menu": { + "action-in-seconds": "{action} in {seconds} seconds...", + "cancel-timer": "Cancel timer", + "title": "Session Menu" + }, + "setup": { + "all-done": "All Done!", + "appearance": { + "subheader": "Choose Dark Mode and colour sources (wallpaper or predefined)." + }, + "customize": { + "header": "Customise your experience", + "subheader": "Adjust bar position, density, scaling and more." + }, + "skip-setup": "Skip Setup", + "telemetry-wizard-done": "Got it!", + "telemetry-wizard-note": "You're in control — enable or disable this anytime in settings", + "telemetry-wizard-subtitle": "We've added anonymous analytics to help improve Noctalia", + "telemetry-wizard-title": "Privacy Update", + "wallpaper": { + "choose-dir": "Choose a directory containing your wallpaper images", + "dir-browse": "Browse for wallpaper folder", + "dir-description": "Choose the folder containing your wallpapers.", + "dir-label": "Wallpaper directory", + "dir-select-title": "Select wallpaper folder", + "header": "Choose your wallpaper", + "no-dir": "No wallpaper directory selected", + "no-valid": "No valid image files found in: {dir}", + "none-in-dir": "No wallpapers found in directory", + "preview-error": "Failed to load image", + "select-prompt": "Select a wallpaper below", + "subheader": "Set the mood with a beautiful background." + }, + "welcome": "Welcome", + "welcome-note": "Just a few basics to get you started — full options are in the settings", + "welcome-subtitle": "Let's make your desktop uniquely yours", + "welcome-title": "Welcome to Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "No media player detected", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "Unknown app", + "uptime": "Uptime: {uptime}", + "user-requested": "User requested", + "welcome-back": "Welcome back,", + "widget-settings-title": "{widget} Settings" + }, + "system-monitor": { + "core-usage": "Core {id} usage", + "cpu-temp": "CPU temp", + "cpu-usage": "CPU usage", + "disk": "Disk", + "download-speed": "Download speed", + "gpu-temp": "GPU temp", + "load-average": "Load average", + "title": "System Monitor", + "upload-speed": "Upload speed" + }, + "toast": { + "airplane-mode": { + "description": "Disable all wireless communications.", + "title": "Aeroplane Mode" + }, + "battery": { + "critical": "Critical battery", + "critical-desc": "Battery is at {percent}% — please connect the charger immediately", + "low": "Low battery", + "low-desc": "Battery is at {percent}% — please connect the charger" + }, + "bluetooth": { + "address-copied": "Address copied to clipboard", + "auto-connect-disabled": "Auto-connect disabled", + "auto-connect-enabled": "Auto-connect enabled", + "auto-connecting": "Connecting to {count} device(s)...", + "confirm-code": "Confirm code {value} on the other device.", + "connect-failed": "Failed to connect to device", + "disconnect-failed": "Failed to disconnect from device", + "discoverable-change-failed": "Failed to change discoverable state", + "discoverable-disabled": "Discoverable disabled", + "discoverable-enabled": "Discoverable enabled", + "display-code": "Enter code {value} on the other device.", + "forget-failed": "Failed to forget device", + "pair-failed": "Failed to pair device", + "passkey-required": "Passkey required by the device.", + "pincode-required": "PIN code required by the device.", + "state-change-failed": "Failed to change Bluetooth state" + }, + "clipboard": { + "long-text": "Long text", + "unavailable": "Clipboard history unavailable", + "unavailable-desc": "The 'cliphist' application is not installed. Please install it to use clipboard history features" + }, + "custom-command-failed": { + "description": "Command failed: {command}\nExit code: {code}", + "title": "Custom command failed" + }, + "do-not-disturb": { + "disabled": "Do Not Disturb disabled", + "disabled-desc": "Showing all notifications", + "enabled": "Do Not Disturb enabled", + "enabled-desc": "You'll find these notifications in your history" + }, + "donation-opened": "Donation page opened in your browser", + "internet-limited": "Connected without internet", + "keyboard-layout": { + "changed": "Keyboard layout changed to {layout}", + "title": "Keyboard" + }, + "missing-control-center": { + "description": "The control centre widget has been removed from the bar. To access it from the bar again, you will need to re-add the widget. You can open it with right clicking on the bar too", + "label": "Last control centre widget removed" + }, + "night-light": { + "forced": "Forced activation", + "normal": "Normal mode", + "not-installed": "wlsunset not installed" + }, + "noctalia-performance": { + "disabled": "Performance mode disabled", + "enabled": "Performance mode enabled", + "label": "Noctalia Performance" + }, + "power-profile": { + "changed": "Power profile changed", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "Encountered an error while processing templates", + "title": "Templating processing failed" + }, + "vpn": { + "connected": "Connected to '{name}'", + "disconnected": "Disconnected from '{name}'" + }, + "wallpaper-colors": { + "disabled": "Wallpaper colors disabled", + "enabled": "Wallpaper colors enabled", + "label": "Wallpaper colors" + }, + "wifi": { + "connected": "Connected to '{ssid}'", + "connection-failed": "Connection failed", + "connection-timeout": "Connection timeout", + "disconnected": "Disconnected from '{ssid}'", + "incorrect-password": "Incorrect password", + "network-not-found": "Network not found" + } + }, + "tooltips": { + "add-widget": "Add widget", + "auto-connect": "Toggle auto-connection for this device", + "bluetooth-auto-connect-off": "Auto-connect is off", + "bluetooth-auto-connect-on": "Auto-connect is on", + "bluetooth-devices": "Bluetooth devices", + "brightness-at": "Brightness: {brightness}%", + "click-to-start-recording": "Screen recorder (start recording)", + "click-to-stop-recording": "Screen recorder (stop recording)", + "collapse": "Collapse sidebar", + "copy-address": "Copy address", + "delete-notification": "Delete notification", + "dismiss-notification": "Dismiss notification", + "do-not-disturb-enabled": "Do Not Disturb", + "expand": "Expand sidebar", + "forget-network": "Forget network", + "grid-view": "Grid view", + "hidden-files-hide": "Hidden files", + "hide-unnamed-devices": "Hide unnamed devices", + "home": "Home directory", + "input-muted": "Toggle input mute", + "keep-awake": "Keep Awake", + "keyboard-layout": "{layout} keyboard layout", + "list-view": "List view", + "manage-vpn": "VPN connections", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "Maximum widgets reached", + "microphone-volume-at": "Microphone volume: {volume}%", + "move-to-section": "Move to {section}", + "mute": "Mute", + "next-media": "Next track", + "next-month": "Next month", + "night-light-not-installed": "Night Light (not available)", + "noctalia-performance-enabled": "Noctalia performance mode", + "open-annotation-tool": "Open with annotation tool", + "open-control-center": "Control centre", + "open-notification-history-enable-dnd": "Notification history", + "open-settings": "Settings", + "open-tray-dropdown": "System tray", + "output-muted": "Toggle output mute", + "power-profile": "{profile} power profile", + "previous-media": "Previous track", + "previous-month": "Previous month", + "refresh-devices": "Refresh devices", + "refresh-wallhaven": "Refresh Wallhaven results", + "refresh-wallpaper-list": "Refresh wallpaper list", + "remove-widget": "Remove widget", + "search-close": "Close search", + "session-menu": "Session menu", + "show-all-devices": "Show all devices", + "switch-to-dark-mode": "Dark Mode", + "switch-to-light-mode": "Light Mode", + "unmute": "Unmute", + "up": "Parent directory", + "volume-at": "Output volume: {volume}%", + "wallpaper-selector": "Wallpaper selector" + }, + "wallpaper": { + "browse": { + "empty-directory": "This directory is empty.", + "go-root": "Go to wallpaper root", + "go-up": "Go to parent folder", + "go-up-hint": "Use the back button to navigate up." + }, + "configure-directory": "Configure your wallpaper directory with images.", + "fill-modes": { + "crop": "Crop (Fill)", + "fit": "Fit (Contain)", + "repeat": "Repeat (Tile)", + "stretch": "Stretch" + }, + "no-match": "No match found.", + "no-wallpaper": "No wallpaper found.", + "panel": { + "apikey-help": "An API key is required to access NSFW content.", + "apikey-label": "API Key", + "apikey-managed-by-env": "Managed via NOCTALIA_WALLHAVEN_API_KEY environment variable.", + "apikey-placeholder": "Enter your Wallhaven API Key", + "appearance-dark-tab": "Dark", + "appearance-light-tab": "Light", + "categories-anime": "Anime", + "categories-label": "Categories", + "categories-people": "People", + "color-extraction-disabled": "Use wallpaper colour extraction", + "color-extraction-enabled": "Use predefined colour schemes", + "header-devices-apply-all-tooltip": "Choose wallpaper per monitor", + "header-devices-per-monitor-tooltip": "Apply to all monitors", + "header-link-light-dark-tooltip": "Keep wallpaper when switching theme", + "header-separate-light-dark-tooltip": "Auto-switch wallpaper with theme", + "header-sun-linked-tooltip": "Separate light and dark wallpapers", + "header-sun-separate-tooltip": "Same wallpaper for light and dark", + "order-asc": "Ascending", + "order-desc": "Descending", + "order-label": "Order", + "purity-label": "Content filter", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Sketchy", + "ratios-all-portrait": "All portrait", + "ratios-all-wide": "All wide", + "ratios-any": "Any", + "ratios-label": "Aspect ratio", + "resolution-atleast": "At least", + "resolution-exact": "Exact", + "resolution-label": "Resolution", + "resolution-mode-label": "Mode", + "solid-color-tooltip": "Solid colour background", + "sort-date-asc": "Sort by oldest first", + "sort-date-desc": "Sort by newest first", + "sort-name-asc": "Sort by name (A-Z)", + "sort-name-desc": "Sort by name (Z-A)", + "sort-random": "Sort by random", + "sorting-date-added": "Date added", + "sorting-favorites": "Favorites", + "sorting-label": "Sort by", + "sorting-relevance": "Relevance", + "sorting-toplist": "Toplist", + "sorting-views": "Views", + "source-label": "Source", + "source-wallhaven": "Wallhaven", + "title": "Wallpaper Selector", + "wallhaven-settings-title": "Wallhaven Settings" + }, + "transitions": { + "disc": "Disc", + "fade": "Fade", + "honeycomb": "Honeycomb", + "pixelate": "Pixelate", + "stripes": "Stripes", + "wipe": "Wipe" + }, + "try-different-search": "Try a different search query.", + "wallhaven": { + "loading": "Loading wallpapers...", + "no-results": "No wallpapers found. Try a different search query.", + "page": "{current} of {total}", + "page-prefix": "Page", + "page-suffix": "of {total}" + } + }, + "weather": { + "clear-sky": "Clear sky", + "drizzle": "Drizzle", + "fog": "Fog", + "mainly-clear": "Mainly clear", + "overcast": "Overcast", + "partly-cloudy": "Partly cloudy", + "rain-showers": "Rain showers", + "snow": "Snow", + "thunderstorm": "Thunderstorm" + }, + "widgets": { + "color-picker": { + "palette-description": "Choose from a wide range of predefined colors.", + "palette-label": "Palette", + "palette-theme-colors": "Quick access to your theme's colour palette.", + "title": "Colour picker" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm in lowercase", + "ampm-uppercase": "AM/PM in uppercase", + "common-12hour-time-minutes": "12-hour time with minutes", + "common-24hour-time-minutes": "24-hour time with minutes", + "common-24hour-time-seconds": "24-hour time with seconds", + "common-european-date": "European date format", + "common-iso-date": "ISO date format", + "common-us-date": "US date format", + "common-weekday-date": "Weekday with date", + "common-weekday-month-day": "Weekday, month and day", + "day-abbreviated": "Abbreviated day name", + "day-full": "Full day name", + "day-leading-zero": "Day with leading zero (01-31)", + "day-no-leading-zero": "Day without leading zero (1-31)", + "hour-leading-zero": "Hour with leading zero (00-23) — 24-hour format", + "hour-no-leading-zero": "Hour without leading zero (0-23) — 24-hour format", + "minute-leading-zero": "Minute with leading zero (00-59)", + "minute-no-leading-zero": "Minute without leading zero (0-59)", + "month-abbreviated": "Abbreviated month name", + "month-full": "Full month name", + "month-number-leading-zero": "Month as number with leading zero (01-12)", + "month-number-no-zero": "Month as number without leading zero (1-12)", + "second-leading-zero": "Second with leading zero (00-59)", + "second-no-leading-zero": "Second without leading zero (0-59)", + "timezone-abbreviation": "Timezone abbreviation", + "year-four-digit": "Year as four-digit number", + "year-two-digit": "Year as two-digit number (00-99)" + }, + "file-picker": { + "item": "item", + "items": "items", + "search-placeholder": "Search files and folders...", + "select-current": "Select Current", + "select-file": "Select File", + "select-folder": "Select Folder", + "selected": "Selected:", + "title": "File Picker" + }, + "file-picker-title": "Select a file", + "icon-picker": { + "title": "Icon picker" + }, + "tooltip-placeholder": "Placeholder" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "Anonymous Identity", + "ca-cert": "CA Certificate", + "eap-method": "EAP Method", + "password": "User password", + "phase2-auth": "Phase 2 Authentication", + "username": "Identity / Username" + }, + "panel": { + "action-required": "Action required", + "add-hidden-network": "Add hidden network", + "add-network": "Add network", + "available-interfaces": "Available interfaces", + "available-networks": "Available networks", + "disabled": "Wi‑Fi is disabled", + "disconnecting": "Disconnecting...", + "dns": "DNS", + "enable-message": "Enable Wi‑Fi to see available networks.", + "enter-password": "Enter Wi‑Fi password...", + "forget": "Forget", + "forget-network": "Forget this network", + "forgetting": "Forgetting...", + "hidden-network": "Hidden network", + "interface": "Network interface", + "internet-connected": "Internet connected", + "internet-limited": "No internet", + "internet-status": "Internet status", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "Known networks", + "link-speed": "Link speed", + "network-name-ssid": "Network name (SSID)", + "no-ethernet-devices": "No Ethernet devices detected", + "no-networks": "No Wi‑Fi networks found", + "saved": "Saved", + "scan-again": "Scan again", + "searching": "Searching for networks...", + "security-open": "Open", + "security-wep": "WEP", + "security-wpa": "WPA", + "security-wpa-ent": "WPA Enterprise", + "security-wpa2-ent": "WPA2 Enterprise", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 Enterprise", + "show-password": "Show password", + "title": "Wi‑Fi" + }, + "signal": { + "excellent": "Excellent", + "fair": "Fair", + "good": "Good", + "poor": "Poor", + "weak": "Weak" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/en.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/en.json new file mode 100644 index 0000000..c28ee37 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/en.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "Clear history", + "close-app": "Close {app}", + "connect-vpn": "Connect to {name}", + "cycle-visualizer": "Cycle visualizer", + "disable-bluetooth": "Disable Bluetooth", + "disable-dnd": "Disable Do Not Disturb", + "disable-wifi": "Disable Wi-Fi", + "disconnect-vpn": "Disconnect {name}", + "dock-settings": "Dock settings", + "enable-bluetooth": "Enable Bluetooth", + "enable-dnd": "Enable Do Not Disturb", + "enable-wifi": "Enable Wi-Fi", + "launcher-settings": "Launcher settings", + "lower-to-bottom": "Lower to bottom", + "open-calendar": "Open calendar", + "open-display-settings": "Display settings", + "open-launcher": "Open launcher", + "open-settings": "Open settings", + "raise-to-top": "Raise to top", + "random-wallpaper": "Random wallpaper", + "run-custom-command": "Run custom command", + "show-active-only": "Show active only", + "show-all": "Show all", + "toggle-mute": "Toggle mute", + "widget-settings": "Widget settings" + }, + "authentication": { + "error": "Authentication error", + "failed": "Authentication failed" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Apply theme colors to active window icon.", + "hide-mode-description": "Controls how the widget behaves when no window is active.", + "scrolling-mode-description": "Control when text scrolling is enabled for long window titles.", + "show-app-icon-description": "Display the application icon next to the window title.", + "show-app-icon-label": "Show app icon", + "show-app-text-description": "Display the window title.", + "show-app-text-label": "Show window title" + }, + "audio-visualizer": { + "color-name-description": "Select the color for the visualizer.", + "color-name-label": "Fill color", + "height-description": "Custom component width.", + "hide-when-idle-description": "When enabled, the visualizer is hidden unless a player is actively playing.", + "hide-when-idle-label": "Hide when no media is playing", + "width-description": "Custom component width." + }, + "battery": { + "device-default": "Default (Display Device)", + "device-description": "Select which battery device to display.", + "device-label": "Battery device", + "display-mode-description": "Choose how the battery is displayed in the bar.", + "display-mode-graphic": "Graphical battery", + "display-mode-graphic-clean": "Graphical battery (no %)", + "display-mode-icon-always": "Icon - Always show %", + "display-mode-icon-hover": "Icon - Show on hover", + "display-mode-icon-only": "Icon only", + "hide-if-idle-description": "Hide the widget when the battery is not charging or discharging.", + "hide-if-idle-label": "Hide when idle", + "hide-if-not-detected-description": "Hide the widget when no battery is detected on the system.", + "hide-if-not-detected-label": "Hide when not detected", + "low-battery-threshold-label": "Low battery warning threshold", + "show-noctalia-performance-description": "Display the Noctalia performance mode toggle inside the battery panel.
Disables shadows and animations in Noctalia to reduce resource usage.", + "show-noctalia-performance-label": "Show Noctalia Performance toggle", + "show-power-profile-description": "Display the power profile selection inside the battery panel.", + "show-power-profile-label": "Show power profile controls" + }, + "brightness": { + "apply-all-description": "Change brightness for every monitor when using the scroll wheel.", + "apply-all-label": "Apply scroll changes to all monitors" + }, + "clock": { + "clock-display-description": "Customize your clock's display by adding tokens from the list below. To use the 12-hour format, you must include the 'AP' token.", + "clock-display-label": "Clock display", + "custom-font-description": "Select a custom font for the clock display.", + "custom-font-label": "Custom font", + "custom-font-placeholder": "Select custom font...", + "custom-font-search-placeholder": "Search fonts...", + "horizontal-bar-description": "Tip: Use \\n to create a line break.", + "horizontal-bar-label": "Horizontal bar", + "preview": "Preview", + "tooltip-format-description": "Format string for the tooltip shown when hovering over the clock. Leave empty to use the default tooltip.", + "tooltip-format-label": "Tooltip format", + "use-custom-font-description": "Override the default font selection with a custom font for the clock.", + "use-custom-font-label": "Use custom font", + "use-monospaced-font-description": "When enabled, the clock will use the monospaced font.", + "use-monospaced-font-label": "Use monospaced font", + "vertical-bar-description": "Use a space to separate each part onto a new line.", + "vertical-bar-label": "Vertical bar" + }, + "control-center": { + "browse-file": "Browse File", + "browse-library": "Browse Library", + "color-selection-description": "Apply theme colors to icons.", + "enable-colorization-description": "Enable colorization for icon, applying theme colors.", + "icon-description": "Select an icon from the library or a custom file.", + "select-custom-icon": "Select a custom icon", + "use-distro-logo-description": "Use your distribution's logo instead of a custom icon.", + "use-distro-logo-label": "Use distro logo instead of icon" + }, + "custom-button": { + "collapse-condition-description": "If the output text matches this value, the button will collapse.", + "collapse-condition-label": "Collapse condition", + "color-selection-description": "Apply theme colors to icon and text.", + "default-tooltip": "Custom button, configure in settings", + "display-command-output-description": "Enter a command to run at a regular interval. The first line of its output will be displayed as text.", + "display-command-output-label": "Display command output", + "display-command-output-stream-description": "Enter a command to run continuously.", + "dynamic-text": "Dynamic text", + "enable-colorization-description": "Enable colorization for the custom button icon and text, applying theme colors.", + "enable-colorization-label": "Enable colorization", + "general-tooltip-text-description": "Custom text to display in the button's tooltip.", + "general-tooltip-text-label": "Custom tooltip text", + "hide-mode-always-expanded": "Always expanded", + "hide-mode-description": "Controls widget visibility when the command has no output.", + "hide-mode-expand-with-output": "Expand when has output", + "hide-mode-label": "Hide mode", + "hide-mode-max-transparent": "Max expanded but transparent", + "icon-description": "Select an icon from the library.", + "ipc-identifier-description": "Unique identifier for IPC commands. Use this identifier with 'qs -c noctalia-shell ipc call cb [action] [identifier]' to control this button via IPC.", + "ipc-identifier-label": "IPC Identifier", + "left-click-description": "Command to execute when the button is left-clicked.", + "left-click-label": "Left click", + "left-click-update-text": "Update displayed text on left-click", + "max-text-length-horizontal-description": "Maximum number of characters to show in horizontal bar (0 to hide text).", + "max-text-length-horizontal-label": "Max text length (horizontal)", + "max-text-length-vertical-description": "Maximum number of characters to show in vertical bar (0 to hide text).", + "max-text-length-vertical-label": "Max text length (vertical)", + "middle-click-description": "Command to execute when the button is middle-clicked.", + "middle-click-label": "Middle click", + "middle-click-update-text": "Update displayed text on middle-click", + "parse-json-description": "Parse the command output as a JSON object to dynamically set text and icon.", + "parse-json-label": "Parse output as JSON", + "refresh-interval-description": "Interval in milliseconds.", + "refresh-interval-label": "Refresh interval", + "right-click-description": "Command to execute when the button is right-clicked.", + "right-click-label": "Right click", + "right-click-update-text": "Update displayed text on right-click", + "show-exec-tooltip-description": "Show tooltips with command details (left/right/middle click, wheel).", + "show-exec-tooltip-label": "Show command tooltips", + "show-icon-description": "Toggles the visibility of the widget's icon.", + "show-icon-label": "Show icon", + "show-text-tooltip-description": "Show tooltips with the output from the text command.", + "show-text-tooltip-label": "Show dynamic text tooltips", + "text-stream-description": "Streamed lines from the command will be displayed as text on the button.", + "text-stream-label": "Stream", + "wheel-description": "Command to execute when the scroll wheel is used.
Use $delta for the scroll wheel delta in the command.", + "wheel-down": "Scroll down", + "wheel-down-description": "Command to execute when the scroll wheel is scrolled down.", + "wheel-down-label": "Wheel down command", + "wheel-label": "Scroll wheel", + "wheel-mode-separate-description": "Enable separate commands for wheel up and down.", + "wheel-mode-separate-label": "Separate wheel commands", + "wheel-up": "Scroll up", + "wheel-up-description": "Command to execute when the scroll wheel is scrolled up.", + "wheel-up-label": "Wheel up command", + "wheel-update-text": "Update displayed text on scroll" + }, + "keyboard-layout": { + "show-icon-description": "Display the keyboard layout icon." + }, + "lock-keys": { + "hide-when-off-description": "Hide the indicator when the key is not active.", + "hide-when-off-label": "Hide when off", + "show-caps-lock-description": "Display Caps Lock status.", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "Display Num Lock status.", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "Display Scroll Lock status.", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "Enable a space-saving layout for the media player panel.", + "compact-mode-label": "Compact mode", + "hide-mode-description": "Controls how the widget behaves when no media is playing.", + "max-width-description": "Sets the maximum horizontal size of the widget. The widget will shrink to fit shorter content.", + "no-active-player": "No active player", + "panel-section-description": "Configure the appearance and behavior of the media player panel.", + "panel-section-label": "Media player panel", + "scrolling-mode-description": "Control when text scrolling is enabled for long track titles.", + "scrolling-mode-label": "Scrolling mode", + "show-album-art-description": "Display the album artwork for the currently playing track.", + "show-album-art-label": "Show album art", + "show-artist-first-description": "Display artist - title instead of title - artist.", + "show-artist-first-label": "Show artist first", + "show-progress-ring-description": "Display a circular progress indicator showing track progress.", + "show-progress-ring-label": "Show progress ring", + "show-visualizer-description": "Display an audio visualizer when music is playing.", + "show-visualizer-label": "Show visualizer", + "use-fixed-width-description": "When enabled, the widget will always use the maximum width instead of dynamically adjusting to content.", + "use-fixed-width-label": "Use fixed width", + "visualizer-type-description": "Choose the style of audio visualizer to display.", + "visualizer-type-label": "Visualizer type" + }, + "notification-history": { + "hide-widget-when-zero-description": "Hide the notification icon when there are no notifications.", + "hide-widget-when-zero-label": "Hide icon when no notifications", + "hide-widget-when-zero-unread-description": "Hide the notification icon when there are no unread notifications.", + "hide-widget-when-zero-unread-label": "Hide icon when no unread notifications", + "show-unread-badge-description": "Display a badge showing the number of unread notifications.", + "show-unread-badge-label": "Show unread badge", + "unread-badge-color-description": "Select the color for the unread notification badge.", + "unread-badge-color-label": "Unread badge color" + }, + "section-editor": { + "placeholder": "Select a widget to add...", + "search-placeholder": "Search widget..." + }, + "spacer": { + "width-description": "Spacing width in pixels." + }, + "system-monitor": { + "compact-mode-description": "Display stats as mini bar charts instead of text values. Prevents layout shifting.", + "compact-mode-label": "Compact mode", + "cpu-cores-description": "Display CPU cores usage individually.", + "cpu-cores-label": "CPU Cores", + "cpu-frequency-description": "Display the current CPU clock speed in GHz.", + "cpu-frequency-label": "Show CPU frequency", + "cpu-temperature-description": "Show CPU temperature readings if available.", + "cpu-temperature-label": "CPU temperature", + "cpu-usage-description": "Display current CPU usage percentage.", + "cpu-usage-label": "CPU usage", + "disk-path-description": "Select which disk mount point to monitor.", + "disk-path-label": "Disk path", + "gpu-temperature-description": "Show GPU temperature readings if available.", + "load-average-description": "Display system load average.", + "load-average-label": "Load average", + "memory-percentage-description": "Show memory usage as a percentage instead of absolute values.", + "memory-percentage-label": "Memory as percentage", + "memory-usage-description": "Display current RAM usage information.", + "memory-usage-label": "Memory usage", + "network-traffic-description": "Display network upload and download speeds.", + "network-traffic-label": "Network traffic", + "storage-as-percentage-description": "Show disk space as percentage instead of absolute values.", + "storage-as-percentage-label": "Disk as percentage", + "storage-available-description": "Shows how much disk space is available instead of how much is used.", + "storage-available-label": "Disk space available", + "storage-usage-description": "Show disk space usage information.", + "storage-usage-label": "Storage usage", + "swap-usage-description": "Show swap memory usage.", + "swap-usage-label": "Swap usage", + "use-monospace-font-description": "Use monospace font for consistent character width.", + "use-monospace-font-label": "Monospace font", + "use-padding-description": "Pads the text values with leading spaces to prevent layout shifting.", + "use-padding-description-disabled-monospace-font": "A monospace font is needed for this feature.", + "use-padding-description-disabled-vertical": "Vertical taskbar does not support padding text.", + "use-padding-label": "Pad text" + }, + "taskbar": { + "colorize-icons-description": "Apply theme colors to taskbar icons.", + "hide-mode-description": "Controls how the widget behaves when there are no matching windows.", + "hide-mode-label": "Hiding mode", + "icon-scale-description": "Sets the scaling factor for taskbar icons.", + "icon-scale-label": "Icon scaling", + "max-width-description": "Maximum width of the taskbar as a percentage of screen width.", + "max-width-label": "Maximum width", + "only-active-workspaces-description": "Show only apps from active workspaces.", + "only-active-workspaces-label": "Only from active workspaces", + "only-same-monitor-description": "Show only apps from the monitor where the bar is located.", + "only-same-monitor-label": "Only from same monitor", + "show-pinned-apps-description": "Show pinned apps from the dock in the taskbar.", + "show-pinned-apps-label": "Show pinned apps", + "show-title-description": "Display window titles in the taskbar.", + "show-title-description-disabled": "Vertical taskbar does not support showing titles.", + "show-title-label": "Show title", + "smart-width-description": "Automatically adjust entry width based on the number of entries.", + "smart-width-label": "Smart width", + "title-width-description": "Set the width of window titles in the taskbar (in pixels).", + "title-width-label": "Title width", + "title-width-reset-tooltip": "Reset title width" + }, + "tray": { + "chevron-color-description": "Apply theme colors to the drawer chevron icon.", + "chevron-color-label": "Chevron color", + "colorize-icons-description": "Apply theme colors to tray icons.", + "colorize-icons-label": "Colorize icons", + "drawer-enabled-description": "When enabled, unpinned tray items are shown in a drawer panel.
When disabled, all tray items are shown inline.", + "drawer-enabled-label": "Enable drawer", + "hide-passive-description": "When enabled, tray items with 'Passive' status will be hidden.", + "hide-passive-label": "Hide passive items" + }, + "volume": { + "display-mode-description": "Choose how you'd like this value to appear." + }, + "workspace": { + "character-count-description": "Number of characters to display from workspace names (1-10).", + "character-count-label": "Character count", + "empty-color-description": "Set the background color for empty workspaces.", + "empty-color-label": "Empty workspace color", + "enable-scrollwheel-description": "Switch between workspaces using the mouse scroll wheel.", + "enable-scrollwheel-label": "Scroll to switch workspaces", + "focused-color-description": "Set the background color for the focused workspace.", + "focused-color-label": "Focused workspace color", + "follow-focused-screen-description": "Display workspaces from the currently focused screen, rather than the screen where the bar is located.", + "follow-focused-screen-label": "Follow focused screen", + "font-weight-description": "Set the visual weight for text within workspace.", + "font-weight-label": "Font weight", + "grouped-border-opacity-description": "Set the opacity level for workspace container borders.", + "grouped-border-opacity-label": "Border opacity", + "hide-unoccupied-description": "Don't display workspaces without windows.", + "hide-unoccupied-label": "Hide unoccupied", + "label-mode-description": "Choose how workspace labels are displayed.", + "label-mode-label": "Label mode", + "occupied-color-description": "Set the background color for occupied workspaces.", + "occupied-color-label": "Occupied workspace color", + "pill-size-description": "Adjust the size of workspace pills.", + "pill-size-label": "Pill size", + "reverse-scrolling-description": "Reverse the direction of workspace switching when scrolling.", + "reverse-scrolling-label": "Reverse scrolling", + "show-applications-description": "Display application icons inside each workspace.", + "show-applications-hover-description": "Display application icons inside each workspace when the workspace is hovered.", + "show-applications-hover-label": "Show applications when hovered", + "show-applications-label": "Show applications", + "show-badge-description": "Show the workspace number badge in grouped mode.", + "show-badge-label": "Show workspace badge", + "show-labels-only-when-occupied-description": "Only show workspace labels when they contain windows.", + "show-labels-only-when-occupied-label": "Show labels only when occupied", + "unfocused-icons-opacity-description": "Set the opacity level for unfocused app icons.", + "unfocused-icons-opacity-label": "Unfocused icons opacity" + } + }, + "battery": { + "all-batteries": "All batteries (combined)", + "battery-health": "Battery health", + "battery-level": "Battery level", + "capacity-level": "Capacity: {level}", + "charging-rate": "Charging rate: {rate} W", + "discharging-rate": "Discharging rate: {rate} W", + "health": "Health: {percent}%", + "inhibit-idle-description": "Keeps the system awake.", + "no-battery-detected": "No battery detected", + "plugged-in": "Plugged in", + "power-profile": "Power profile", + "time-left": "Time left: {time}", + "time-until-full": "Time until full: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Available devices", + "blocked": "Blocked", + "connected-devices": "Connected devices", + "device-address": "Device address", + "disabled": "Bluetooth is disabled", + "discoverable": "Discoverable", + "enable-message": "Enable Bluetooth to see available devices.", + "known-devices": "Known devices", + "no-devices": "No devices available", + "paired-devices": "Paired devices", + "pairing-mode": "Make sure your device is in pairing mode.", + "scanning": "Scanning for devices...", + "signal-text-excellent": "Signal: Excellent", + "signal-text-fair": "Signal: Fair", + "signal-text-good": "Signal: Good", + "signal-text-poor": "Signal: Poor", + "signal-text-unknown": "Signal: Unknown", + "signal-text-very-poor": "Signal: Very poor" + } + }, + "changelog": { + "error": { + "fetch-failed": "Unable to load changelog data. Please try again later.", + "rate-limit": "GitHub rate limit exceeded. Please try again in a few minutes." + }, + "panel": { + "buttons-discord": "Join our Discord", + "buttons-dismiss": "OK", + "buttons-feedback": "Give feedback", + "empty": "Release notes are not available yet.", + "highlight-title": "Highlights", + "released": "Released on {date}", + "subtitle-fresh": "Thanks for installing Noctalia! Here is what’s included in this build.", + "subtitle-updated": "Updated from {previousVersion}", + "title": "What's new in {version}", + "version": "Version {version}", + "version-new-user": "Fresh install" + } + }, + "common": { + "actions": "Actions", + "active": "Active", + "add": "Add", + "appearance": "Appearance", + "apply": "Apply", + "auto-connect": "Auto-connect", + "automation": "Automation", + "available": "Available", + "back": "Back", + "battery": "Battery", + "behavior": "Behavior", + "bluetooth": "Bluetooth", + "brightness": "Brightness", + "browse": "Browse", + "calendar": "Calendar", + "calendar-panel": "Calendar Panel", + "cancel": "Cancel", + "cards": "Cards", + "charging": "Charging", + "clear": "Clear", + "clipboard": "Clipboard", + "close": "Close", + "color-muted": "Muted", + "colors": "Colors", + "command": "Command", + "confirm": "Confirm", + "connect": "Connect", + "connected": "Connected", + "connecting": "Connecting...", + "continue": "Continue", + "contributors": "Contributors", + "copied-to-clipboard": "Copied to clipboard", + "countdown": "Countdown", + "customize": "Customize", + "date": "Date", + "default": "Default", + "delete": "Delete", + "devices": "Devices", + "disabled": "Disabled", + "discharging": "Discharging", + "disconnect": "Disconnect", + "disconnected": "Disconnected", + "disconnecting": "Disconnecting...", + "display-mode": "Display mode", + "documentation": "Documentation", + "download": "Download", + "duration": "Duration", + "dysfunctional": "Dysfunctional", + "edit": "Edit", + "enabled": "Enabled", + "error": "Error", + "ethernet": "Ethernet", + "events": "Events", + "execute": "Execute", + "faithful": "Faithful", + "focus": "Focus", + "font-weight-bold": "Bold", + "font-weight-light": "Light", + "font-weight-medium": "Medium", + "font-weight-regular": "Regular", + "font-weight-semibold": "Semi-bold", + "frequency": "Frequency", + "gateway": "Gateway", + "general": "General", + "height": "Height", + "hibernate": "Hibernate", + "history": "History", + "icon": "Icon", + "idle": "Idle", + "info": "Info", + "input": "Input", + "install": "Install", + "installed": "Installed", + "interface": "Interface", + "internet": "Internet", + "keybind": "Keybind", + "language": "Language", + "loading": "Loading...", + "local": "Local", + "location": "Location", + "lock": "Lock", + "logout": "Logout", + "look": "Look", + "margins": "Margins", + "media": "Media", + "media-player": "Media Player", + "memory": "Memory", + "monitors": "Monitors", + "move-down": "Move down", + "move-up": "Move up", + "network": "Network", + "next": "Next", + "night-light": "Night Light", + "no": "No", + "no-results": "No results", + "no-summary": "No summary", + "none": "None", + "not-found": "Not found", + "notifications": "Notifications", + "official": "Official", + "on-surface": "On Surface", + "output": "Output", + "pair": "Pair", + "paired": "Paired", + "pairing": "Pairing...", + "panels": "Panels", + "password": "Password", + "pause": "Pause", + "performance": "Performance", + "pin": "Pin", + "play": "Play", + "polling": "Polling", + "position": "Position", + "previous": "Previous", + "primary": "Primary", + "random": "Random", + "reboot": "Reboot", + "reboot-to-uefi": "Reboot to UEFI", + "record": "Start recording", + "refresh": "Refresh", + "required": "(required)", + "reset": "Reset", + "reset-to-default": "Reset to Default", + "result-count": "{count} result", + "result-count-plural": "{count} results", + "resume": "Resume", + "save": "Save", + "scanning": "Scanning...", + "screen-corners": "Screen Corners", + "search": "Search", + "second": "{count} second", + "second-plural": "{count} seconds", + "secondary": "Secondary", + "security": "Security", + "select": "Select", + "select-color": "Select color", + "select-color-description": "Apply theme colors for emphasis.", + "select-icon-color": "Select icon color", + "settings": "Settings", + "shortcuts": "Shortcuts", + "shutdown": "Shutdown", + "signal": "Signal", + "sound": "Sound", + "sources": "Sources", + "start": "Start", + "stop": "Stop", + "supporters": "Supporters", + "suspend": "Suspend", + "templates": "Templates", + "tertiary": "Tertiary", + "test": "Test", + "thresholds": "Thresholds", + "title": "Title", + "toast": "Toast", + "trusted": "Trusted", + "uninstall": "Uninstall", + "unknown": "Unknown", + "unpair": "Unpair", + "unpin": "Unpin", + "update": "Update", + "upload": "Upload", + "username": "Username", + "userspace-reboot": "Userspace Reboot", + "version": "Version", + "vibrant": "Vibrant", + "visualizer": "Visualizer", + "volume": "Volume", + "volumes": "Volumes", + "wallpaper": "Wallpaper", + "weather": "Weather", + "weather-loading": "Loading weather...", + "weather-no-location": "Set a location in Settings to see weather", + "week": "Week", + "widgets": "Widgets", + "width": "Width", + "wifi": "Wi-Fi", + "windows": "Windows", + "yes": "Yes" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Power Profile", + "tooltip-disabled": "Install power-profiles-daemon to use power profiles" + }, + "wifi": { + "label-disconnected": "Wi-Fi disconnected", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Always hide", + "always-show": "Always show", + "force-open": "Force open", + "on-hover": "On hover" + }, + "hide-modes": { + "auto-hide": "Auto-hide", + "hidden": "Hide when empty", + "idle": "Hide when idle", + "non-exclusive": "Non-exclusive", + "transparent": "Transparent when empty", + "visible": "Always visible" + }, + "launcher": { + "categories": { + "all": "All", + "audiovideo": "Audio & Video", + "chat": "Chat", + "development": "Development", + "education": "Education", + "emoji-activity": "Activity", + "emoji-animals": "Animals", + "emoji-flags": "Flags", + "emoji-food": "Food & Drink", + "emoji-nature": "Nature", + "emoji-objects": "Objects", + "emoji-people": "People & Body", + "emoji-recent": "Recent", + "emoji-symbols": "Symbols", + "emoji-travel": "Travel & Places", + "game": "Games", + "graphics": "Graphics", + "misc": "Misc", + "office": "Office", + "pinned": "Pinned", + "system": "System", + "webbrowser": "Web Browser" + }, + "providers": { + "applications": "Applications", + "calculator": "Calculator", + "calculator-deprecated": "The >calc command is deprecated and will be removed soon. Please type math expressions directly in search instead.", + "calculator-description": "Calculator — evaluate mathematical expressions", + "calculator-enter-expression": "Enter a mathematical expression", + "calculator-press-enter-to-copy": "Press Enter to copy result", + "clipboard": "Clipboard history", + "clipboard-clear-description": "Clear all clipboard history", + "clipboard-clear-description-full": "Remove all items from clipboard history", + "clipboard-clear-history": "Clear clipboard history", + "clipboard-delete": "Delete clipboard entry", + "clipboard-history-disabled": "Clipboard history disabled", + "clipboard-history-disabled-description": "Enable clipboard history in settings or install cliphist", + "clipboard-loading": "Loading clipboard history...", + "clipboard-search-description": "Search clipboard history", + "command-description": "Run shell commands", + "emoji": "Emoji picker", + "emoji-loading": "Loading emojis...", + "emoji-loading-description": "Please wait", + "emoji-no-recent": "No recent emojis yet", + "emoji-search-description": "Search and copy emojis", + "settings-search-description": "Search and navigate to settings", + "windows-search-description": "Search and focus open windows" + } + }, + "lock-screen": { + "authenticating": "Authenticating...", + "password": "Enter your password...", + "restart": "Restart", + "shut-down": "Shut down" + }, + "notifications": { + "panel": { + "click-to-collapse": "Click to collapse", + "click-to-expand": "Click to expand", + "description": "Your notifications will show up here as they arrive.", + "no-notifications": "No notifications" + }, + "range": { + "earlier": "Earlier", + "today": "Today", + "yesterday": "Yesterday" + }, + "time": { + "diff-d": "1 day ago", + "diff-dd": "{diff} days ago", + "diff-h": "1 hour ago", + "diff-hh": "{diff} hours ago", + "diff-m": "1 minute ago", + "diff-mm": "{diff} minutes ago", + "now": "now" + } + }, + "options": { + "bar": { + "density-comfortable": "Comfortable", + "density-compact": "Compact", + "density-default": "Default", + "density-mini": "Mini", + "density-spacious": "Spacious", + "display-mode-always-visible": "Always Visible", + "type-floating": "Floating", + "type-framed": "Framed", + "type-simple": "Simple" + }, + "control-center": { + "quick-settings-style-classic": "Classic", + "quick-settings-style-modern": "Modern" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "Comfortable", + "compact": "Compact", + "default": "Default" + }, + "launcher-view-mode": { + "grid": "Grid", + "list": "List" + }, + "notification-density": { + "compact": "Compact", + "default": "Default" + }, + "scrolling-modes": { + "always": "Scroll always", + "hover": "Scroll on hover", + "never": "Never scroll" + }, + "session-menu-grid-layout": { + "grid": "Grid", + "single-row": "Single row" + }, + "settings-panel-mode": { + "attached": "Panel attached to bar", + "centered": "Centered panel", + "window": "Separate window" + }, + "visualizer-types": { + "linear": "Linear", + "mirrored": "Mirrored", + "wave": "Wave" + }, + "visualizer-visibility": { + "with-background": "With background only" + }, + "workspace-labels": { + "index": "Index", + "index-and-name": "Index and name", + "name": "Name" + } + }, + "panels": { + "about": { + "become-supporter": "Become a supporter", + "changelog": "View changelog", + "changelog-on-startup": "Show changelog on update", + "changelog-on-startup-desc": "Automatically show the changelog when Noctalia is updated", + "contributors-description": "Shout-out to our {count} awesome contributor!", + "contributors-description-plural": "Shout-out to our {count} awesome contributors!", + "copy-info": "Copy info", + "debug-disabled": "Debug mode disabled", + "debug-enabled": "Debug mode enabled", + "info-copied": "Info copied to clipboard", + "noctalia-available": "Available:", + "noctalia-desc": "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell.", + "noctalia-git-commit": "Git commit:", + "noctalia-installed-version": "Installed version:", + "noctalia-latest-version": "Latest version:", + "noctalia-qs-version": "Noctalia QS version:", + "noctalia-title": "Noctalia Shell", + "privacy-policy": "Privacy policy", + "support": "Support us", + "supporter-badge": "Supporter", + "supporters-desc": "A huge thank you to our amazing supporter!", + "supporters-desc-plural": "A huge thank you to our {count} amazing supporters!", + "supporters-loading": "Loading supporters...", + "system-board": "Board:", + "system-cpu": "CPU:", + "system-disk": "Disk:", + "system-gpu": "GPU:", + "system-host": "Host:", + "system-install-hint": "Install fastfetch to view system information", + "system-kernel": "Kernel:", + "system-loading": "Loading system info...", + "system-memory": "Memory:", + "system-monitor": "Monitor:", + "system-not-installed": "fastfetch is not installed", + "system-os": "OS:", + "system-packages": "Packages:", + "system-product": "Product:", + "system-title": "System Information", + "system-uptime": "Uptime:", + "system-wm": "WM:", + "telemetry-data-copied": "Telemetry data copied to clipboard", + "telemetry-desc": "Help improve Noctalia by sharing anonymous system info (screen resolution, compositor, distro). Sent once at startup, no tracking, data auto-deleted after 30 days.", + "telemetry-enabled": "Send anonymous system information", + "telemetry-show-data": "View data", + "telemetry-title": "Privacy", + "title": "About", + "up-to-date": "You're up to date!", + "update-available": "Update available", + "view-commit": "View commit on GitHub" + }, + "audio": { + "devices-desc": "Configure available audio input and output devices.", + "devices-input-device-description": "Select the desired audio input device.", + "devices-input-device-label": "Input device", + "devices-output-device-description": "Select the desired audio output device.", + "devices-output-device-label": "Output device", + "devices-title": "Audio devices", + "external-mixer-description": "Enter the command or application path to launch when activating the external audio mixer feature.", + "external-mixer-label": "External audio mixer command", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Set your preferred and ignored media applications.", + "media-excluded-player-description": "Add keywords for players you want the system to ignore. Each keyword should be on a new line.", + "media-excluded-player-label": "Excluded player", + "media-excluded-player-placeholder": "type substring and press +", + "media-frame-rate-description": "Higher rates are smoother but use more resources.", + "media-frame-rate-label": "Frame rate", + "media-primary-player-description": "Enter a keyword to identify your main player.", + "media-primary-player-label": "Primary player", + "media-primary-player-placeholder": "e.g. spotify, vlc, mpv", + "media-scrolling-speed-description": "Time in seconds for the title to scroll from start to end.", + "media-scrolling-speed-label": "Scrolling speed", + "media-scrolling-title-description": "Enable continuous scrolling for long media titles.", + "media-scrolling-title-label": "Scrolling title", + "media-title": "Media players", + "on-middle-clicked-description": "Command to execute when the button is middle-clicked.", + "panel-applications-empty": "No applications are currently playing audio", + "spectrum-mirrored-description": "Mirror the spectrum so low frequencies are in the center. When off, low frequencies are on the left and high on the right.", + "spectrum-mirrored-label": "Mirror spectrum", + "title": "Audio", + "visualizer-type-description": "Choose a visualization type for media playback.", + "visualizer-type-label": "Visualization type", + "volumes-desc": "Adjust volume controls and audio levels.", + "volumes-feedback-sound-file-description": "Path to the sound file played when changing volume.", + "volumes-feedback-sound-file-label": "Volume feedback sound file", + "volumes-feedback-sound-file-select-title": "Select volume feedback sound file", + "volumes-input-volume-description": "Microphone input volume level.", + "volumes-mute-input-description": "Mute the default audio input (microphone).", + "volumes-mute-input-label": "Mute audio input", + "volumes-mute-output-description": "Mute the system's main audio output.", + "volumes-mute-output-label": "Mute audio output", + "volumes-output-volume-description": "System-wide volume level.", + "volumes-step-size-description": "Adjust the step size for volume changes (scroll wheel, keyboard shortcuts).", + "volumes-step-size-label": "Volume step size", + "volumes-volume-feedback-description": "Play a feedback sound when adjusting volume.", + "volumes-volume-feedback-label": "Play volume feedback sound", + "volumes-volume-overdrive-description": "Allow raising volume above 100%. May not be supported by all hardware.", + "volumes-volume-overdrive-label": "Allow volume overdrive" + }, + "bar": { + "appearance-auto-hide-delay-description": "Time before bar hides after mouse leaves", + "appearance-auto-hide-delay-label": "Hide Delay", + "appearance-auto-hide-exclusive-note": "Note: Exclusive zone is automatically disabled when auto-hide is active", + "appearance-auto-show-delay-description": "Time before bar shows when mouse enters edge", + "appearance-auto-show-delay-label": "Show Delay", + "appearance-background-opacity-description": "Set the background opacity specifically for the bar.", + "appearance-background-opacity-label": "Bar background opacity", + "appearance-capsule-color-description": "Choose a color for bar capsules, or use none for the default surface color.", + "appearance-capsule-color-label": "Capsule color", + "appearance-capsule-opacity-description": "Set the opacity level for widget backgrounds when capsule is shown.", + "appearance-capsule-opacity-label": "Capsule opacity", + "appearance-content-padding-description": "Adjust the padding between bar edges and widgets.", + "appearance-content-padding-label": "Content padding", + "appearance-density-description": "Adjust the bar's padding for a compact or spacious look.", + "appearance-density-label": "Bar density", + "appearance-desc": "Customize the bar's appearance and position.", + "appearance-display-mode-description": "Choose when the bar is visible.", + "appearance-enable-exclusion-zone-inset-description": "Reduce the exclusion zone by 1 physical pixel so flush windows bleed perfectly under the bar edge.", + "appearance-enable-exclusion-zone-inset-label": "Inset exclusion zone", + "appearance-floating-description": "Display the bar as a floating 'pill'.", + "appearance-floating-label": "Floating bar", + "appearance-font-scale-description": "Adjust the font size scale for text displayed in the bar.", + "appearance-font-scale-label": "Font scale", + "appearance-frame-radius": "Inner Radius", + "appearance-frame-settings-description": "Adjust frame thickness and inner corner radius", + "appearance-frame-settings-label": "Frame Settings", + "appearance-frame-thickness": "Thickness", + "appearance-hide-on-overview-description": "Hide the bar and close panels when the compositor overview is active.", + "appearance-hide-on-overview-label": "Hide bar on overview", + "appearance-margins-description": "Adjust the margins around the floating bar.", + "appearance-margins-horizontal": "Horizontal Margin ", + "appearance-margins-vertical": "Vertical Margin", + "appearance-outer-corners-description": "Display outwardly curved corners on the bar.", + "appearance-outer-corners-label": "Outer corners", + "appearance-position-description": "Choose where to place the bar on the screen.", + "appearance-position-label": "Bar position", + "appearance-show-capsule-description": "Show widget backgrounds.", + "appearance-show-capsule-label": "Show capsule", + "appearance-show-on-workspace-switch-description": "Automatically show the bar briefly when the workspace changes.", + "appearance-show-on-workspace-switch-label": "Show bar on workspace switch", + "appearance-show-outline-description": "Displays a visible border around every widget.", + "appearance-show-outline-label": "Show widget outlines", + "appearance-type-description": "Choose the style of the bar: Simple, Floating or Framed.", + "appearance-type-label": "Bar type", + "appearance-use-separate-opacity-description": "Enable to use a separate opacity value for the bar background.", + "appearance-use-separate-opacity-label": "Use separate bar opacity", + "appearance-widget-spacing-description": "Adjust the spacing between each widget in the bar.", + "appearance-widget-spacing-label": "Widget spacing", + "behavior-middle-click-command-description": "Command to execute on middle click.", + "behavior-middle-click-command-label": "Middle click command", + "behavior-middle-click-command-placeholder": "niri msg action toggle-overview", + "behavior-middle-click-description": "Choose what middle click does on empty areas of the bar.", + "behavior-middle-click-follow-mouse-description": "Open the selected middle-click panel at the cursor position.", + "behavior-middle-click-follow-mouse-label": "Middle click follow mouse", + "behavior-middle-click-label": "Bar middle click action", + "behavior-right-click-command-description": "Command to execute on right click.", + "behavior-right-click-command-label": "Right click command", + "behavior-right-click-command-placeholder": "notify-send \"Right click\"", + "behavior-right-click-description": "Choose what right click does on empty areas of the bar.", + "behavior-right-click-follow-mouse-description": "Open the selected right-click panel at the cursor position.", + "behavior-right-click-follow-mouse-label": "Right click follow mouse", + "behavior-right-click-label": "Bar right click action", + "behavior-wheel-wrap-description": "When enabled, scrolling continues from the last item to the first.", + "behavior-wheel-wrap-label": "Wrap around", + "behavior-workspace-scroll-description": "Choose what the mouse wheel does on empty areas of the bar.", + "behavior-workspace-scroll-label": "Bar mouse wheel action", + "behavior-workspace-scroll-option-content": "Content", + "behavior-workspace-scroll-option-workspace": "Workspace", + "monitor-configure-widgets": "Configure widgets", + "monitor-override-settings": "Override global settings", + "monitor-override-settings-description": "Use custom settings for this monitor.", + "monitor-reset-all": "Reset all", + "monitor-widgets-title": "Widget Configuration - {monitor}", + "monitors-desc": "Show bar on specific monitors. Defaults to all if none are chosen.", + "monitors-desc-new": "Configure which monitors display the bar and customize settings per monitor.", + "monitors-title": "Monitors display", + "title": "Bar", + "tray-blacklist-description": "Add tray exclusion rules, supports wildcards (*).", + "tray-blacklist-label": "Blacklist", + "tray-blacklist-placeholder": "e.g. nm-applet, Fcitx*", + "tray-pin-application": "Pin Application", + "tray-unpin-application": "Unpin Application", + "use-global-widgets": "Use global widgets", + "widgets-desc": "Drag widgets to change their order. Use the right-click menu to transfer widgets between sections or remove them.", + "widgets-title": "Widgets positioning" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Generate color schemes from your wallpaper. Automatically extracts colors to create a cohesive theme.", + "color-source-use-wallpaper-colors-label": "Use wallpaper colors", + "dark-mode-mode-description": "Enables automatic switching between Light and Dark Mode.", + "dark-mode-mode-label": "Dark Mode schedule", + "dark-mode-mode-manual": "Manual", + "dark-mode-mode-off": "Off", + "dark-mode-switch-description": "Switches to a darker theme for easier viewing at night.", + "delete-error-description": "Failed to delete {scheme}", + "delete-error-title": "Delete failed", + "delete-success-description": "Successfully deleted {scheme}", + "delete-success-title": "Color scheme deleted", + "download-button": "Download more", + "download-downloading": "Downloading...", + "download-empty": "No color schemes available", + "download-error-api-error": "API error: {status}", + "download-error-description": "Failed to download {scheme}", + "download-error-download-failed": "Download failed with exit code: {code}", + "download-error-invalid-response": "Invalid API response format", + "download-error-no-files": "No files found for scheme", + "download-error-parse-failed": "Failed to parse API response: {error}", + "download-error-rate-limit": "GitHub API rate limit exceeded", + "download-error-title": "Download failed", + "download-fetching": "Fetching available color schemes...", + "download-success-description": "Successfully downloaded {scheme}", + "download-success-title": "Color scheme downloaded", + "download-title": "Download Color Schemes", + "method-description": { + "content": "Material Design scheme with high-fidelity color extraction that closely matches the source content's actual colors.", + "dysfunctional": "Like Faithful but picks the second most dominant color family as primary.", + "faithful": "Attempts to stay close to the source color while still generating a harmonious palette.", + "fruit-salad": "Material Design scheme that produces a playful, vibrant palette with diverse and varied hues.", + "monochrome": "Material Design scheme using a single-hue grayscale with minimal chromatic content.", + "muted": "Generates softer, desaturated tones for a calm and understated appearance.", + "rainbow": "Material Design scheme that generates colors across the full spectrum for maximum color diversity.", + "tonal-spot": "Material Design default algorithm. Creates balanced palettes using tonal variations of the source color.", + "vibrant": "Maximizes color saturation for bold, eye-catching palettes with vivid tones." + }, + "predefined-desc": "Choose from a collection of predefined color schemes.", + "predefined-generate-templates-label": "Generate templates for predefined schemes", + "predefined-title": "Predefined color schemes", + "sync-gsettings-description": "Match the system theme to the active light or dark variant.", + "sync-gsettings-label": "Sync system theme", + "templates-desc": "Apply colors to external applications.", + "templates-filter-description": "Show templates from a specific category.", + "templates-filter-label": "Filter by category", + "templates-misc-description": "Create your own templates.", + "templates-misc-label": "Advanced", + "templates-misc-user-templates-description": "Only enable if you know what you are doing — refer to our online documentation.", + "templates-misc-user-templates-label": "Enable user templates", + "templates-none-detected": "None detected", + "templates-write-path": "Writes: {filepath}", + "title": "Color Scheme", + "wallpaper-method-description": "Choose your favorite palette generation method.", + "wallpaper-method-label": "Palette generation method", + "wallpaper-monitor-source-description": "Select which monitor to use for extracting wallpaper colors.", + "wallpaper-monitor-source-label": "Color generation source" + }, + "connections": { + "authentication-required": "Authentication required", + "bluetooth-auto-connect-description": "Automatically connect to trusted paired devices when Bluetooth is enabled.", + "bluetooth-auto-connect-label": "Auto-connect paired devices", + "bluetooth-devices-unnamed": "Unnamed devices are not shown.", + "bluetooth-discoverable": "This device is discoverable as {hostName} while this settings tab is open.", + "bluetooth-rssi-polling-description": "Periodically sample RSSI for connected devices via bluetoothctl. May not be available for all devices; uses minimal resources when enabled.", + "bluetooth-rssi-polling-interval-description": "Configure how often to update signal strength for connected devices.", + "bluetooth-rssi-polling-interval-label": "Polling interval", + "bluetooth-rssi-polling-label": "Bluetooth signal polling", + "disable-discoverability-description": "Hide your device from nearby Bluetooth devices.", + "disable-discoverability-label": "Disable device visibility", + "hide-unnamed-devices-description": "Hide devices that appear only as Bluetooth addresses.", + "hide-unnamed-devices-label": "Hide unnamed devices", + "pin-instructions": "Please enter the PIN code displayed on your device.", + "title": "Connections", + "wifi-header-text": "Use Wi-Fi to see available networks and connect to the internet." + }, + "control-center": { + "cards-desc": "Customize which controls appear in the control center and in what order.", + "desc": "Configure the control center panel positioning and behavior.", + "open-at-mouse-description": "When right-clicking an empty area of the bar, open the control center near the cursor. When disabled, the position setting above is used instead.", + "open-at-mouse-label": "Open at cursor on right-click", + "position-description": "Choose where the control center panel appears when opened.", + "shortcuts-custom-button-command-description": "The command to execute when the button is clicked.", + "shortcuts-custom-button-enable-on-state-logic-description": "Enable a second icon and 'hot' state based on a check command.", + "shortcuts-custom-button-enable-on-state-logic-label": "Enable on-state logic", + "shortcuts-custom-button-icon-description": "Select an icon from the library.", + "shortcuts-custom-button-on-clicked-label": "Left click command", + "shortcuts-custom-button-on-middle-clicked-description": "Command to execute when the button is middle-clicked.", + "shortcuts-custom-button-on-state-command-description": "Command to execute to check if the button should be in the 'on' state. Returns 0 for on, non-zero for off.", + "shortcuts-custom-button-on-state-command-label": "On-state check command", + "shortcuts-custom-button-on-state-icon-description": "The icon for the button when it's in the 'on' state.", + "shortcuts-custom-button-on-state-icon-label": "On-state icon", + "shortcuts-custom-button-state-checks-add": "Add state check", + "shortcuts-custom-button-state-checks-command": "Command to execute for this state check", + "shortcuts-custom-button-state-checks-label": "State checks", + "shortcuts-custom-button-state-checks-remove": "Remove", + "shortcuts-custom-button-tooltip-description": "The tooltip to show when hovering over the button.", + "shortcuts-custom-button-tooltip-label": "Tooltip", + "shortcuts-title": "Shortcuts widgets", + "system-monitor-disk-path-description": "Select which disk mount point the system monitor card in the control center should monitor.", + "system-monitor-disk-path-label": "System monitor disk path", + "title": "Control Center" + }, + "desktop-widgets": { + "clock-enabled-description": "Show a clock widget on the desktop.", + "clock-enabled-label": "Enable clock widget", + "clock-format-label": "Format", + "clock-height-description": "Height of the clock widget in pixels.", + "clock-height-label": "Height", + "clock-minimal-mode-description": "Use a minimal clock display with time and date.", + "clock-minimal-mode-label": "Minimal mode", + "clock-rounded-corners-description": "Use rounded corners for the widget background.", + "clock-rounded-corners-label": "Rounded corners", + "clock-show-background-description": "Show the background container for the clock widget.", + "clock-show-background-label": "Show background", + "clock-show-date-description": "Display the current date below the time.", + "clock-show-date-label": "Show date", + "clock-show-seconds-description": "Display seconds in the time.", + "clock-show-seconds-label": "Show seconds", + "clock-style-analog": "Analog", + "clock-style-binary": "Binary", + "clock-style-description": "Choose the clock display style.", + "clock-style-digital": "Digital", + "clock-style-label": "Clock style", + "clock-style-minimal": "Minimal", + "clock-width-description": "Width of the clock widget in pixels.", + "cpu-intensive-note": "Widgets marked with ! use more CPU and should be enabled mindfully.", + "edit-mode-button-label": "Enter edit mode", + "edit-mode-controls-explanation": "Left-click & drag: Move or resize the widget.\nRight-click: Open the context menu options.", + "edit-mode-description": "Enable edit mode to move and reposition desktop widgets. When enabled, widgets show a drag outline and can be repositioned.", + "edit-mode-exit-button": "Exit edit mode", + "edit-mode-grid-snap-label": "Grid snap", + "edit-mode-grid-snap-scale-label": "Grid snap scale", + "edit-mode-label": "Edit mode", + "enabled-description": "Enable or disable desktop widgets entirely.", + "enabled-label": "Enable desktop widgets", + "general-desc": "Configure widgets that appear on your desktop.", + "general-title": "Desktop widgets", + "media-player-enabled-description": "Show a media player widget on the desktop.", + "media-player-enabled-label": "Enable media player widget", + "media-player-rounded-corners-description": "Enable rounded corners on the widget edges.", + "media-player-show-album-art-description": "Show the album artwork and track information (title and artist).", + "media-player-show-album-art-label": "Show album art & title", + "media-player-show-background-description": "Show the background container.", + "media-player-show-buttons-description": "Show media control buttons (play/pause, previous, next).", + "media-player-show-buttons-label": "Show buttons", + "media-player-show-visualizer-description": "Show the audio visualizer overlay.", + "media-player-visualizer-type-description": "Choose a visualization type.", + "overview-enabled-description": "Show desktop widgets while the compositor overview is open.", + "overview-enabled-label": "Show in overview", + "system-stat-layout-bottom": "Bottom", + "system-stat-layout-description": "Choose how the legend is displayed relative to the graph.", + "system-stat-layout-label": "Layout", + "system-stat-layout-side": "Side", + "system-stat-rounded-corners-description": "Use rounded corners for the widget background.", + "system-stat-rounded-corners-label": "Rounded corners", + "system-stat-show-background-description": "Show the background container for the system stat widget.", + "system-stat-show-background-label": "Show background", + "system-stat-stat-type-description": "Choose which system statistic to display.", + "system-stat-stat-type-label": "Statistic Type", + "title": "Desktop Widgets", + "weather-enabled-description": "Show a weather widget on the desktop.", + "weather-enabled-label": "Enable weather widget", + "weather-show-background-description": "Show the background container for the weather widget." + }, + "display": { + "monitors-backlight-device-auto-option": "Default", + "monitors-backlight-device-description": "Select a backlight device for this output.", + "monitors-backlight-device-label": "Backlight device", + "monitors-brightness-step-description": "Adjust the step size for brightness changes (scroll wheel and keyboard shortcuts).", + "monitors-brightness-step-label": "Brightness step size", + "monitors-brightness-unavailable-ddc-disabled": "Brightness control unavailable. Enable \"External brightness support\" to control this display's brightness.", + "monitors-brightness-unavailable-generic": "Brightness control is not available for this display.", + "monitors-desc": "Adjust settings brightness for each display.", + "monitors-enforce-minimum-description": "Solves the problem of backlight completely turning off on some displays at 0% brightness.", + "monitors-enforce-minimum-label": "Enforce minimum brightness (1%)", + "monitors-external-brightness-description": "Enable DDCUtil support for controlling brightness on external displays via DDC/CI protocol.", + "monitors-external-brightness-label": "External brightness support", + "monitors-global-brightness-description": "Adjust brightness for all monitors at once.", + "monitors-global-brightness-label": "All monitors", + "monitors-title": "Per-monitor settings", + "night-light-auto-schedule-description": "Based on the sunset and sunrise time in {location} — recommended.", + "night-light-auto-schedule-label": "Automatic scheduling", + "night-light-desc": "Reduce blue light emission to help you sleep better and reduce eye strain.", + "night-light-enable-description": "Apply a warm color filter to reduce blue light emission.", + "night-light-enable-label": "Enable Night Light", + "night-light-force-activation-description": "Ignores the schedule and applies the night filter immediately.", + "night-light-force-activation-label": "Force activation", + "night-light-manual-schedule-description": "Set custom times for sunrise and sunset.", + "night-light-manual-schedule-label": "Manual scheduling", + "night-light-manual-schedule-select-start": "Select start time", + "night-light-manual-schedule-select-stop": "Select stop time", + "night-light-manual-schedule-sunrise": "Sunrise time", + "night-light-manual-schedule-sunset": "Sunset time", + "night-light-temperature-day": "Day", + "night-light-temperature-day-description": "Controls the temperature during daytime.", + "night-light-temperature-description": "Set the color warmth for nighttime and daytime.", + "night-light-temperature-label": "Color temperature", + "night-light-temperature-night": "Night", + "night-light-temperature-night-description": "Controls the temperature during nighttime.", + "title": "Display" + }, + "dock": { + "appearance-background-opacity-description": "Adjust the dock's background opacity.", + "appearance-border-radius-description": "Adjust the dock's border radius.", + "appearance-border-radius-label": "Border radius", + "appearance-colorize-icons-description": "Apply theme colors to dock app icons (non-focused apps only).", + "appearance-colorize-icons-label": "Colorize icons", + "appearance-dead-opacity-description": "Adjust the opacity of app icons that are not running.", + "appearance-dead-opacity-label": "Dead opacity", + "appearance-desc": "Customize the dock's behavior and appearance.", + "appearance-display-auto-hide": "Auto hide", + "appearance-display-description": "Choose how the dock behaves.", + "appearance-display-exclusive": "Exclusive", + "appearance-dock-indicator-description": "Show a small indicator when the dock is hidden.", + "appearance-dock-indicator-label": "Dock indicator", + "appearance-floating-distance-description": "Set the distance between the dock and the edge of the screen.", + "appearance-floating-distance-label": "Dock floating distance", + "appearance-group-apps-description": "Group multiple windows from the same app into one dock entry.", + "appearance-group-apps-label": "Group same apps", + "appearance-group-click-action-cycle": "Cycle windows", + "appearance-group-click-action-description": "Choose what left-click does for grouped apps.", + "appearance-group-click-action-label": "Grouped app click action", + "appearance-group-click-action-list": "Open window list", + "appearance-group-context-menu-mode-description": "Choose how the context menu behaves for grouped apps.", + "appearance-group-context-menu-mode-extended": "Extended", + "appearance-group-context-menu-mode-label": "Grouped app menu mode", + "appearance-group-context-menu-mode-list": "Window list", + "appearance-group-indicator-style-description": "Choose how grouped running indicators display focused window state.", + "appearance-group-indicator-style-dots": "Dots", + "appearance-group-indicator-style-label": "Grouped indicator style", + "appearance-group-indicator-style-number": "Number", + "appearance-hide-show-speed-description": "Adjust the speed of the dock hide/show animation.", + "appearance-hide-show-speed-label": "Hide/show speed", + "appearance-icon-size-description": "Adjust the overall size of the dock.", + "appearance-icon-size-label": "Dock size", + "appearance-inactive-indicators-description": "Display indicator pills for all apps, not just the currently active one.", + "appearance-inactive-indicators-label": "Running indicators", + "appearance-indicator-color-description": "Choose the color of the hidden dock indicator.", + "appearance-indicator-color-label": "Indicator color", + "appearance-indicator-opacity-description": "Adjust the opacity of the hidden dock indicator.", + "appearance-indicator-opacity-label": "Indicator opacity", + "appearance-indicator-thickness-description": "Toggle a thicker hidden dock indicator (6px instead of 3px).", + "appearance-indicator-thickness-label": "Thicker indicator", + "appearance-launcher-icon-description": "Choose the icon used for the dock launcher.", + "appearance-launcher-icon-label": "Launcher icon", + "appearance-launcher-position-description": "Choose where the launcher icon appears in the dock.", + "appearance-launcher-position-end": "End", + "appearance-launcher-position-label": "Launcher position", + "appearance-launcher-position-start": "Start", + "appearance-launcher-use-distro-logo-description": "Use your distribution logo as the dock launcher icon.", + "appearance-launcher-use-distro-logo-label": "Use distro logo instead of icon", + "appearance-pinned-static-description": "Always push pinned app icons to the left in static order.", + "appearance-pinned-static-label": "Static pinned apps", + "appearance-position-description": "Choose where the dock appears on screen.", + "appearance-position-label": "Position", + "appearance-show-launcher-icon-description": "Show the application launcher icon in the dock.", + "appearance-show-launcher-icon-label": "Show app launcher", + "appearance-sit-on-frame-description": "Align the dock inside the frame border instead of sitting on top.", + "appearance-sit-on-frame-label": "Dock sits on frame", + "appearance-type-attached": "Attached", + "appearance-type-description": "Choose between a floating pill or a static bar attached to the edge.", + "appearance-type-floating": "Floating", + "appearance-type-label": "Dock style", + "enabled-description": "Show or hide the dock entirely.", + "enabled-label": "Enable dock", + "monitors-desc": "Show dock on specific monitors. Defaults to all if none are chosen.", + "monitors-only-same-monitor-description": "Show only apps from the monitor where the dock is located.", + "monitors-only-same-monitor-label": "Only apps from same monitor", + "monitors-title": "Monitor display", + "title": "Dock" + }, + "general": { + "copy-settings": "Copy settings", + "fonts-default-description": "Main font used throughout the interface.", + "fonts-default-label": "Default font", + "fonts-default-placeholder": "Select default font...", + "fonts-default-scale-description": "Increase or decrease the size of the standard text.", + "fonts-default-scale-label": "Default font size", + "fonts-default-search-placeholder": "Search font...", + "fonts-desc": "Choose the fonts used throughout the interface.", + "fonts-monospace-description": "Monospaced font used for numbers and stats display.", + "fonts-monospace-label": "Monospaced font", + "fonts-monospace-placeholder": "Select monospace font...", + "fonts-monospace-scale-description": "Increase or decrease the size of the monospaced text.", + "fonts-monospace-scale-label": "Monospaced font size", + "fonts-monospace-search-placeholder": "Search monospace font...", + "fonts-title": "Fonts", + "keybinds-conflict-description": "The key combination is already assigned to {action}.", + "keybinds-conflict-title": "Keybind conflict", + "keybinds-description": "Configure global navigation keys for panels and launcher.", + "keybinds-down": "Move down", + "keybinds-enter": "Confirm / Action", + "keybinds-escape": "Close / Back", + "keybinds-left": "Move left", + "keybinds-modifier-description": "Shortcuts must include a modifier key (Ctrl or Alt).", + "keybinds-modifier-title": "Modifier required", + "keybinds-remove": "Remove / Delete", + "keybinds-right": "Move right", + "keybinds-title": "Navigation keybinds", + "keybinds-up": "Move up", + "language-desc": "Choose your preferred language for the application.", + "language-select-auto-detect": "Automatic", + "language-select-description": "Select the language used in the application's interface.", + "language-select-label": "Application language", + "launch-setup-wizard": "Setup wizard", + "profile-desc": "Edit your user details and avatar.", + "profile-picture-description": "Your profile picture that appears throughout the interface.", + "profile-picture-label": "Profile picture", + "profile-select-avatar": "Select avatar image", + "profile-title": "Profile", + "profile-tooltip": "Profile picture", + "reverse-scrolling-description": "Reverse the interpreted scroll direction", + "reverse-scrolling-label": "Reverse scrolling", + "screen-corners-desc": "Customize screen corner rounding and visual effects.", + "screen-corners-radius-description": "Adjust the rounded corners of the screen.", + "screen-corners-radius-label": "Screen corners radius", + "screen-corners-radius-reset": "Reset screen corners radius", + "screen-corners-show-corners-description": "Display rounded corners on the edge of the screen.", + "screen-corners-show-corners-label": "Show screen corners", + "screen-corners-solid-black-description": "Use solid black instead of the bar background color.", + "screen-corners-solid-black-label": "Solid black corners", + "screen-corners-title": "Screen corners", + "settings-copied": "Settings copied to clipboard", + "smooth-scrolling-description": "Animate list scrolling for a smoother wheel experience.", + "smooth-scrolling-label": "Smooth scrolling", + "tab-basics": "Basics", + "tab-keybinds": "Keybinds" + }, + "hooks": { + "color-generation-description": "Command to be executed when color generation completes (e.g. after wallpaper-based theming).", + "color-generation-label": "Colors generated", + "color-generation-placeholder": "e.g. notify-send \"Colors\" \"Generated\"", + "info-command-info-description": "• Commands are executed via shell (sh -lc)
• Commands run in background (detached)
• Test buttons execute with current values", + "info-parameters-description": "• Wallpaper hook: $1 = wallpaper path, $2 = screen name, $3 = theme (dark/light)
• Color generation hook: $1 = theme (dark/light)
• Theme toggle hook: $1 = true/false (Dark Mode state)
• Screen lock/unlock hooks: $1 = lock/unlock (screen lock state)
• Performance mode hooks: No parameters
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "Available parameters", + "noctalia-started-description": "Command to execute when Noctalia has finished loading.", + "noctalia-started-label": "Noctalia started", + "noctalia-started-placeholder": "e.g. notify-send 'Noctalia Loaded'", + "performance-mode-disabled-description": "Command to be executed when Noctalia performance mode is disabled.", + "performance-mode-disabled-label": "Performance mode disabled", + "performance-mode-disabled-placeholder": "e.g. notify-send \"Performance\" \"Mode disabled\"", + "performance-mode-enabled-description": "Command to be executed when Noctalia performance mode is enabled.", + "performance-mode-enabled-label": "Performance mode enabled", + "performance-mode-enabled-placeholder": "e.g., notify-send \"Performance\" \"Mode enabled\"", + "screen-lock-description": "Command to be executed when the screen is locked.", + "screen-lock-label": "Screen locked", + "screen-lock-placeholder": "e.g. notify-send \"Screen\" \"Locked\"", + "screen-unlock-description": "Command to be executed when the lock screen is unlocked.", + "screen-unlock-label": "Screen unlocked", + "screen-unlock-placeholder": "e.g. notify-send \"Screen\" \"Unlocked\"", + "session-description": "Command to be executed before shutdown or reboot. Receives action type as $1 (shutdown/reboot).", + "session-label": "Session end", + "session-placeholder": "e.g. notify-send \"Session\" \"$1\"", + "system-hooks-desc": "Configure commands to be executed when system events occur.", + "system-hooks-enable-description": "Enable or disable all hook commands.", + "system-hooks-enable-label": "Enable hooks", + "system-hooks-title": "System hooks", + "theme-changed-description": "Command to be executed when theme toggles between Dark and Light Mode.", + "theme-changed-label": "Theme changed", + "theme-changed-placeholder": "e.g. notify-send \"Theme\" \"Toggled\"", + "title": "Hooks", + "wallpaper-changed-description": "Command to be executed when wallpaper changes.", + "wallpaper-changed-label": "Wallpaper changed", + "wallpaper-changed-placeholder": "e.g. notify-send \"Wallpaper\" \"Changed\"" + }, + "idle": { + "custom-add": "Add command", + "custom-description": "Run a shell command after a period of inactivity.", + "custom-entry-command": "Command", + "custom-entry-delete": "Delete", + "custom-entry-edit": "Edit Custom Command", + "custom-entry-name": "Name", + "custom-entry-name-placeholder": "e.g. Turn off lights", + "custom-entry-new": "New Custom Command", + "custom-entry-timeout": "Inactivity time", + "custom-entry-timeout-format": "{count} second", + "custom-entry-timeout-format-plural": "{count} seconds", + "custom-entry-unnamed": "Unnamed command", + "custom-label": "Custom idle commands", + "enable-description": "Automatically turn off the screen, lock, or suspend after a period of inactivity.", + "enable-label": "Enable idle management", + "fade-duration-description": "Seconds for the fade-to-black animation before each action fires. Any mouse movement cancels the fade.", + "fade-duration-label": "Fade duration", + "lock-description": "Seconds of inactivity before the lock screen activates.", + "lock-label": "Lock screen", + "resume-command-label": "Resume command", + "screen-off-description": "Seconds of inactivity before monitors are turned off.", + "screen-off-label": "Turn off screen", + "status-description": "Idle time as reported by the compositor.", + "status-label": "Idle time", + "suspend-description": "Seconds of inactivity before the system suspends.", + "tab-behavior": "Behavior", + "tab-custom": "Custom", + "timeouts-description": "Set to 0 to disable a stage. Timeouts are paused while Keep Awake is active.", + "timeouts-label": "Timeouts", + "title": "Idle", + "unavailable": "Native idle monitoring is not available on this compositor." + }, + "indicator": { + "default-value": "Default: {value}", + "system-default": "System Default" + }, + "launcher": { + "clipboard-desc": "Access and manage your clipboard history from the launcher.", + "execute-desc": "Configure how applications are launched.", + "execute-title": "Execution", + "settings-annotation-tool-description": "Command to run when clicking the annotate button in clipboard history. The image will be piped to this command.", + "settings-annotation-tool-label": "Annotation tool", + "settings-annotation-tool-placeholder": "e.g. 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Automatically paste the selected clipboard item. Requires wtype.", + "settings-auto-paste-label": "Auto paste", + "settings-clip-chips-description": "Show a tab bar to filter clipboard history by type (Images, Links, Files, Code, etc).", + "settings-clip-chips-label": "Enable Category Chips", + "settings-clip-preview-description": "Show a preview of the clipboard content when using the >clip command.", + "settings-clip-preview-label": "Enable clip preview", + "settings-clip-smart-icons-description": "Show specific icons for links, files, colors, and code instead of a generic clipboard icon.", + "settings-clip-smart-icons-label": "Enable Smart Icons", + "settings-clip-wrap-text-description": "Wrap text in the clipboard list instead of truncating it.", + "settings-clip-wrap-text-label": "Wrap clipboard text", + "settings-clipboard-history-description": "Access previously copied items from the launcher.", + "settings-clipboard-history-label": "Enable clipboard history", + "settings-clipboard-watch-image-description": "Full command string passed to wl-paste for image changes. (requires restart)", + "settings-clipboard-watch-image-label": "Image watch command", + "settings-clipboard-watch-text-description": "Full command string passed to wl-paste for text changes. (requires restart)", + "settings-clipboard-watch-text-label": "Text watch command", + "settings-custom-launch-prefix-description": "Prefix commands with a custom launcher (e.g. 'runapp' for systemd integration).", + "settings-custom-launch-prefix-enabled-description": "Use a custom prefix for launching applications instead of the default method.", + "settings-custom-launch-prefix-enabled-label": "Enable custom launch prefix", + "settings-custom-launch-prefix-label": "Custom launch prefix", + "settings-density-description": "Adjust the size of application icons and the density of the launcher.", + "settings-density-label": "Density", + "settings-desc": "Customize the launcher's behavior and appearance.", + "settings-enable-session-search-description": "Show session actions (lock, shutdown, reboot, etc.) when searching in the launcher.", + "settings-enable-session-search-label": "Enable session search", + "settings-enable-settings-search-description": "Show settings results when searching in the launcher.", + "settings-enable-settings-search-label": "Enable settings search", + "settings-enable-windows-search-description": "Search and focus active windows.", + "settings-enable-windows-search-label": "Enable windows search", + "settings-grid-view-description": "Display items in a grid layout instead of a list.", + "settings-icon-mode-description": "Use native system icons instead of Tabler icons.", + "settings-icon-mode-label": "Use native icons", + "settings-ignore-mouse-input-description": "Disable mouse interaction and scrollwheel in the launcher.", + "settings-ignore-mouse-input-label": "Ignore mouse input", + "settings-overlay-layer-description": "Display the launcher on the overlay layer, above fullscreen windows. When enabled, the launcher will not attach to the bar.", + "settings-overlay-layer-label": "Show above fullscreen", + "settings-position-description": "Choose where the launcher panel appears.", + "settings-show-categories-description": "Show category tabs for filtering applications.", + "settings-show-categories-label": "Show categories", + "settings-show-icon-background-description": "Show a rounded rectangle background behind icons.", + "settings-show-icon-background-label": "Show icon background", + "settings-sort-by-usage-description": "When enabled, frequently launched apps appear first in the list.", + "settings-sort-by-usage-label": "Sort by most used", + "settings-terminal-command-description": "Command to launch a terminal. E.g. 'kitty -e' or 'gnome-terminal --'.", + "settings-terminal-command-label": "Terminal command", + "settings-use-app2unit-description": "Uses an alternative launch method to better manage app processes and prevent issues.", + "settings-use-app2unit-label": "Use App2Unit to launch applications", + "settings-view-mode-description": "Choose the layout for the launcher entries.", + "settings-view-mode-label": "View mode", + "title": "Launcher" + }, + "location": { + "auto-locate-description": "Automatically detect your location periodically using your IP address.", + "auto-locate-label": "Auto-locate", + "calendar-cards-desc": "Organize and enable/disable cards in the calendar panel.", + "calendar-cards-title": "Calendar cards", + "calendar-header-label": "Calendar Header", + "calendar-month-label": "Calendar Month", + "date-time-12hour-format-description": "Display time in 12-hour format on the lock screen and calendar. The bar clock has its own settings.", + "date-time-12hour-format-label": "Use 12-hour time format", + "date-time-desc": "Customize how date and time appear.", + "date-time-first-day-of-week-automatic": "Automatic (use system locale)", + "date-time-first-day-of-week-description": "Choose which day starts the week in the calendar.", + "date-time-first-day-of-week-label": "First day of week", + "date-time-show-events-description": "Display events in the calendar panel.", + "date-time-show-events-label": "Show calendar events", + "date-time-title": "Date & time", + "date-time-use-analog-description": "Show an analog style clock on the calendar window.", + "date-time-use-analog-label": "Use analog style clock", + "date-time-week-numbers-description": "Display the week of the year (e.g. week 38) in the calendar.", + "date-time-week-numbers-label": "Show week numbers", + "geolocate-now-button": "Locate now", + "location-desc": "Get accurate weather and Night Light scheduling by setting your location.", + "location-search-description": "e.g. Toronto, ON", + "location-search-label": "Search for a location", + "location-search-placeholder": "Enter the location name", + "location-title": "Your location", + "weather-desc": "Choose your preferred temperature unit.", + "weather-enabled-description": "Show weather information throughout the interface and fetch weather data. When disabled, all weather elements will be hidden and no network requests will be made.", + "weather-enabled-label": "Enable weather", + "weather-fahrenheit-description": "Display temperature in Fahrenheit instead of Celsius.", + "weather-fahrenheit-label": "Display temperature in Fahrenheit (°F)", + "weather-hide-city-description": "Hide the city name from weather displays throughout the interface.", + "weather-hide-city-label": "Hide city name", + "weather-hide-timezone-description": "Hide the timezone abbreviation from weather displays throughout the interface.", + "weather-hide-timezone-label": "Hide timezone", + "weather-show-effects-description": "Show additional visual effects (like rain, snow, or lightning) on the weather card.", + "weather-show-effects-label": "Display weather effects", + "weather-show-in-calendar-description": "Show the daily weather forecast directly in your calendar view.", + "weather-show-in-calendar-label": "Display weather in calendar", + "weather-talia-mascot-always-description": "Always use Talia emojis instead of tabler icons.", + "weather-talia-mascot-always-label": "Always show Talia weather emojis" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "When fprintd (fingerprint authentication) is active, this option lets you still login using your password instead of a fingerprint", + "allow-password-with-fprintd-label": "Allow password login with fprintd", + "auto-start-auth-description": "Automatically starts fingerprint authentication without requiring a key press or button click.", + "auto-start-auth-label": "Auto-start authentication", + "clock-format-description": "Customize the clock format using date/time syntax tokens.", + "clock-format-label": "Clock format", + "clock-style-analog": "Analog", + "clock-style-custom": "Custom", + "clock-style-description": "Choose the visual style of the clock on the lock screen.", + "clock-style-digital": "Digital", + "clock-style-label": "Clock style", + "compact-lockscreen-description": "Show only the login input and system controls, hiding weather and media widgets.", + "compact-lockscreen-label": "Compact lock screen", + "enable-lockscreen-media-controls-description": "Show interactive media playback controls on the lock screen.", + "enable-lockscreen-media-controls-label": "Lock screen media controls", + "lock-on-suspend-description": "Automatically lock the screen when suspending the system.", + "lock-on-suspend-label": "Lock on suspend", + "lock-screen-animations-description": "Enable or disable lockscreen animations.", + "lock-screen-animations-label": "Lockscreen animations", + "lock-screen-blur-strength-description": "Applies a blur effect to the lock screen wallpaper.", + "lock-screen-blur-strength-label": "Lock screen blur strength", + "lock-screen-tint-strength-description": "Applies a tint overlay to the lock screen wallpaper.", + "lock-screen-tint-strength-label": "Lock screen tint strength", + "monitors-desc": "Show lock screen on specific monitors. Defaults to all if none are chosen.", + "password-chars-description": "Cute icons used to hide your password.", + "password-chars-label": "Random password icons", + "show-hibernate-description": "Show the option 'hibernate' in the power controls.", + "show-hibernate-label": "Show hibernate", + "show-session-buttons-description": "Allow access to power settings from the lock screen.", + "show-session-buttons-label": "Power controls", + "title": "Lock Screen" + }, + "network": { + "bluetooth-description": "Activate Bluetooth management.", + "bluetooth-rssi-polling-description": "Periodically sample RSSI for connected devices via bluetoothctl. May not be available for all devices; uses minimal resources when enabled.", + "bluetooth-rssi-polling-label": "Bluetooth signal polling", + "desc": "Manage Wi-Fi and Bluetooth connections.", + "wifi-description": "Manage wireless networks (requires NetworkManager)." + }, + "notifications": { + "duration-critical-urgency-description": "How long critical priority notifications stay visible.", + "duration-critical-urgency-label": "Critical urgency", + "duration-desc": "Configure how long notifications stay visible based on their urgency level.", + "duration-low-urgency-description": "How long low priority notifications stay visible.", + "duration-low-urgency-label": "Low urgency", + "duration-normal-urgency-description": "How long normal priority notifications stay visible.", + "duration-normal-urgency-label": "Normal urgency", + "duration-respect-expire-description": "Use the expire timeout set in the notification.", + "duration-respect-expire-label": "Respect expire timeout", + "duration-title": "Notification duration", + "history-clear-dismiss-description": "Clear notification from history when dismissed.", + "history-clear-dismiss-label": "Clear on dismissed", + "history-critical-urgency-description": "Save critical priority notifications to history.", + "history-critical-urgency-label": "Save critical urgency to history", + "history-desc": "Control which notifications are saved to history based on their urgency level.", + "history-low-urgency-description": "Save low priority notifications to history.", + "history-low-urgency-label": "Save low urgency to history", + "history-normal-urgency-description": "Save normal priority notifications to history.", + "history-normal-urgency-label": "Save normal urgency to history", + "monitors-desc": "Show notification on specific monitors. Defaults to all if none are chosen.", + "rules-action-block": "Block", + "rules-action-block-desc": "Skips completely.", + "rules-action-hide": "Hide", + "rules-action-hide-desc": "No popup, no sound, adds to history.", + "rules-action-label": "Action", + "rules-action-mute": "Mute", + "rules-action-mute-desc": "No sound, still shows popup and in history.", + "rules-add": "Add rule", + "rules-delete": "Delete rule", + "rules-description": "Match app name or content. Rules are checked in order, and the first match is applied.", + "rules-edit": "Edit rule", + "rules-label": "Filter rules", + "rules-pattern-label": "Pattern", + "rules-pattern-placeholder": "firefox, discord, or /regex/", + "rules-tab": "Rules", + "settings-always-on-top-description": "Display notifications above fullscreen windows and other layers.", + "settings-background-opacity-description": "Adjust the opacity of notification backgrounds.", + "settings-density-description": "Choose the notification card density.", + "settings-density-label": "Density", + "settings-desc": "Configure notifications appearance and behavior.", + "settings-do-not-disturb-description": "Disable all notification popups when enabled.", + "settings-enabled-description": "Enable or disable the notification daemon, requires a restart of Noctalia shell.", + "settings-enabled-label": "Enable notifications", + "settings-location-description": "Where notifications appear on screen.", + "settings-markdown-description": "Render notification content using Markdown formatting.", + "settings-markdown-label": "Enable Markdown", + "sounds-desc": "Configure notification sound effects and volume.", + "sounds-enabled-description": "Enable sound effects for incoming notifications.", + "sounds-enabled-label": "Enable notification sounds", + "sounds-excluded-apps-description": "Skip playing the configured notification sound for specific applications that have their own built-in sounds.", + "sounds-excluded-apps-label": "Excluded applications", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Path to the sound file played for critical priority notifications.", + "sounds-files-critical-label": "Critical urgency sound", + "sounds-files-critical-select-title": "Select critical urgency sound file", + "sounds-files-desc": "Configure custom sound files for different notification urgency levels.", + "sounds-files-low-description": "Path to the sound file played for low priority notifications.", + "sounds-files-low-label": "Low urgency sound", + "sounds-files-low-select-title": "Select low urgency sound file", + "sounds-files-normal-description": "Path to the sound file played for normal priority notifications.", + "sounds-files-normal-label": "Normal urgency sound", + "sounds-files-normal-select-title": "Select normal urgency sound file", + "sounds-files-placeholder": "Enter path to sound file", + "sounds-files-select-file": "Select sound file", + "sounds-files-title": "Sound files", + "sounds-files-unified-description": "Path to the sound file played for notifications.", + "sounds-files-unified-label": "Notification sound", + "sounds-files-unified-select-title": "Select notification sound file", + "sounds-separate-description": "Use different sound files for low, normal, and critical priority notifications.", + "sounds-separate-label": "Use different sounds per priority", + "sounds-title": "Sound settings", + "sounds-unavailable-description": "Install Qt6 Multimedia to enable notification sounds.", + "sounds-unavailable-label": "Notification sounds unavailable", + "sounds-volume-description": "Adjust the volume level for notification sounds.", + "sounds-volume-label": "Sound volume", + "toast-battery-description": "Show a warning when the battery level falls below this percentage.", + "toast-battery-label": "Battery warning", + "toast-desc": "Configure toast appearance and behavior.", + "toast-keyboard-description": "Show a toast when the keyboard layout changes.", + "toast-keyboard-label": "Keyboard layout", + "toast-media-description": "Show a toast when media playback state changes.", + "toast-media-label": "Media" + }, + "osd": { + "always-on-top-description": "Display OSD above fullscreen windows and other layers.", + "always-on-top-label": "Always on top", + "background-opacity-description": "Controls the transparency of the OSD background.", + "background-opacity-label": "Background opacity", + "description": "Configure on-screen indicators such as volume and brightness overlays.", + "duration-auto-hide-description": "Adjust the time before OSD disappears.", + "duration-auto-hide-label": "Auto-hide after", + "duration-desc": "How long OSD remains visible before hiding automatically.", + "duration-title": "Auto-hide timeout", + "enabled-description": "Show volume and brightness changes in real-time.", + "enabled-label": "Enable on-screen display", + "events-desc": "Select which events trigger the on-screen display.", + "general-desc": "Configure visibility and behavior of OSD.", + "location-description": "Where on-screen displays appear.", + "monitors-desc": "Show OSD on specific monitors. Defaults to all if none are chosen.", + "title": "On-Screen Display", + "types-brightness-description": "Show OSD when screen brightness changes.", + "types-brightness-label": "Brightness", + "types-custom-text-description": "Show OSD for custom text messages from IPC.", + "types-custom-text-label": "Custom Text", + "types-desc": "Select the events that trigger the OSD.", + "types-input-volume-description": "Show OSD when microphone volume changes.", + "types-input-volume-label": "Input volume", + "types-lockkey-description": "Show OSD when Caps Lock, Num Lock, or Scroll Lock are toggled.", + "types-lockkey-label": "Lock keys", + "types-media-description": "Show OSD when media playback state changes (play, pause, skip).", + "types-media-label": "Media playback", + "types-title": "OSD trigger events", + "types-volume-description": "Show OSD when audio output volume changes.", + "types-volume-label": "Output volume" + }, + "plugins": { + "auto-update": "Auto-update plugins", + "auto-update-description": "Automatically update all plugins when the shell starts.", + "available-description": "Browse and install plugins from configured sources.", + "available-label": "Available plugins", + "available-no-plugins-description": "Check your plugin sources or refresh the list.", + "available-no-plugins-label": "No plugins available", + "check-for-updates": "Check for updates", + "checking-for-updates": "Checking for updates...", + "collision-already-installed": "This plugin is already installed", + "collision-custom-version-exists": "A custom version from \"{source}\" is already installed", + "collision-official-version-exists": "The official version of this plugin is already installed", + "development-disable": "Disable development mode for this plugin", + "development-enable": "Enable development mode for this plugin", + "filter-downloaded": "Downloaded", + "filter-not-downloaded": "Not Downloaded", + "filter-tags-description": "Filter plugins by category or download status.", + "filter-tags-label": "Tags", + "hot-reload-description": "Automatically reload plugins when their files change. Useful for plugin development.", + "hot-reload-label": "Hot reload (dev mode)", + "hot-reloaded": "Reloaded plugin: {name}", + "install-error": "Failed to install: {error}", + "install-incompatible": "{plugin} requires Noctalia v{version} or higher", + "install-success": "Successfully installed {plugin}", + "installed-description": "Manage and configure all locally installed plugins.", + "installed-label": "Installed plugins", + "installed-no-plugins-description": "Install plugins from the \"Available\" section.", + "installed-no-plugins-label": "No plugins installed", + "installing": "Installing {plugin}...", + "notify-updates": "Plugin update notifications", + "notify-updates-description": "Show a notification popup when new plugin updates are available.", + "open-plugin-page": "Open plugin page", + "open-plugins-tab": "Open plugins settings", + "plugin-settings-title": "{plugin} Settings", + "refresh-refreshing": "Refreshing plugins list...", + "refresh-tooltip": "Refresh available plugins", + "settings-error-not-loaded": "Plugin not loaded", + "settings-saved": "Plugin settings saved", + "settings-tooltip": "Plugin settings", + "source-custom": "Custom source", + "sources-add-custom": "Add custom repository", + "sources-add-dialog-description": "Add a GitHub repository as a plugin source.", + "sources-add-dialog-error": "Failed to add plugin source", + "sources-add-dialog-name": "Repository name", + "sources-add-dialog-name-placeholder": "My Custom Plugins", + "sources-add-dialog-success": "Plugin source added successfully", + "sources-add-dialog-title": "Add plugin source", + "sources-add-dialog-url": "Repository URL", + "sources-description": "Manage plugin repositories.", + "sources-placeholder": "My cool repository", + "sources-remove-tooltip": "Remove plugin source", + "title": "Plugins", + "translations-reloaded": "Reloaded translations: {name}", + "uninstall-dialog-description": "Are you sure you want to uninstall {plugin}? This will remove all plugin data.", + "uninstall-dialog-title": "Uninstall plugin", + "uninstall-error": "Failed to uninstall: {error}", + "uninstall-success": "Successfully uninstalled {plugin}", + "uninstalling": "Uninstalling {plugin}...", + "update-all": "Update All ({count})", + "update-all-success": "All plugins updated successfully", + "update-available": "New plugin update available", + "update-available-plural": "Plugin updates available ({count})", + "update-error": "Failed to update plugin: {plugin}: {error}", + "update-pending": "v{current} → v{new} (requires Noctalia v{required})", + "update-success": "Updated {plugin} to v{version}", + "update-version": "v{current} → v{new}", + "updating": "Updating..." + }, + "region": { + "title": "Region" + }, + "session-menu": { + "countdown-duration-description": "Set how long the countdown timer lasts before executing power actions.", + "countdown-duration-label": "Countdown duration", + "enable-countdown-description": "Show a countdown timer before executing power actions.", + "enable-countdown-label": "Enable countdown timer", + "entries-desc": "Customize which power actions appear in the session menu and in what order.", + "entries-title": "Power actions", + "entry-settings-command-description": "Custom command to execute for this action. Leave empty to use the default system command.", + "entry-settings-command-placeholder": "e.g. systemctl poweroff", + "entry-settings-default-command-lock": "Internal lock screen (no command)", + "entry-settings-default-command-logout": "Internal logout (no command)", + "entry-settings-default-info-description": "If no custom command is specified, the default system command will be used.", + "entry-settings-default-info-label": "Default command", + "entry-settings-keybind-description": "Assign a key combination to trigger this action when the session menu is open.", + "entry-settings-keybind-placeholder": "Press Record to assign a key...", + "entry-settings-keybind-recording": "Recording... Press any key combination", + "entry-settings-title": "Configure {entry}", + "entry-settings-tooltip": "Configure command", + "general-desc": "Configure the session menu panel behavior and appearance.", + "large-buttons-layout-description": "Choose how session menu buttons are displayed.", + "large-buttons-layout-label": "Large buttons layout", + "large-buttons-style-description": "Display the session menu with large buttons in a grid layout.", + "large-buttons-style-label": "Large buttons style", + "position-description": "Choose where the session menu panel appears when opened.", + "show-header-description": "Display the title and close button at the top of the session menu.", + "show-header-label": "Show header", + "show-keybinds-description": "Display keybind hints on session options.", + "show-keybinds-label": "Show keybinds" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Keep desktop widgets visible while Noctalia performance mode is enabled.", + "noctalia-performance-disable-desktop-widgets-label": "Enable desktop widgets in performance mode", + "noctalia-performance-disable-wallpaper-description": "Keep desktop, overview, and lock screen wallpapers visible while Noctalia performance mode is enabled.", + "noctalia-performance-disable-wallpaper-label": "Enable wallpaper rendering in performance mode", + "title": "System" + }, + "system-monitor": { + "critical-color-label": "Critical color", + "custom-highlight-colors-title-label": "Custom highlight colors", + "disk-available-label": "Disk available", + "disk-section-label": "Disk usage", + "enable-dgpu-monitoring-description": "Warning: This will wake up your discrete GPU (NVIDIA/AMD), which may significantly impact battery life on laptops with hybrid graphics.", + "enable-dgpu-monitoring-label": "Enable discrete GPU monitoring", + "external-monitor-description": "Enter the command or application path to launch when activating the external system monitor application.", + "external-monitor-label": "External system monitor command", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "Configure the system monitor behavior and appearance.", + "gpu-section-label": "GPU temperature", + "highlight-colors-section-label": "Highlight colors", + "polling-interval-label": "Polling interval", + "polling-section-description": "Configure how often each system metric is updated.", + "polling-section-label": "Polling intervals", + "threshold-critical": "Critical threshold", + "threshold-warning": "Warning threshold", + "thresholds-section-description": "Adjust warning/critical thresholds for each system metric.", + "use-custom-highlight-colors-description": "When disabled, theme default highlight colors are used.", + "use-custom-highlight-colors-label": "Use custom highlight colors", + "warning-color-label": "Warning color" + }, + "user-interface": { + "allow-panels-without-bar-description": "When enabled, panels can open on any screen. When disabled, panels will only open on screens that have a bar, which can reduce memory usage.", + "allow-panels-without-bar-label": "Allow panels on screens without a bar", + "animation-disable-description": "Disable all animations for a faster, more responsive experience.", + "animation-disable-label": "Disable UI animations", + "animation-speed-description": "Adjust global animation speed.", + "animation-speed-label": "Animation speed", + "animation-speed-reset": "Reset animation speed", + "appearance-desc": "Customize visual elements like tooltips, borders, and shadows.", + "blur-behind-description": "Blurs the area behind bars and panels using the compositor blur protocol.", + "blur-behind-label": "Blur behind", + "box-border-description": "Display an outline around content areas.", + "box-border-label": "Container outline", + "box-border-radius-description": "Adjusts the corner roundness of major layout sections, such as sidebars, cards, and content panels.", + "box-border-radius-label": "Container radius", + "control-border-radius-description": "Controls the curvature of interactive elements, including buttons, toggles, and text fields.", + "control-border-radius-label": "Input radius", + "desc": "Customize the look, feel, and behavior of the interface.", + "dim-desktop-description": "Dim the desktop when panels or menus are open.", + "dim-desktop-label": "Dim desktop", + "dimmer-opacity-description": "Set the opacity level for desktop dimming.", + "dimmer-opacity-label": "Dimmed desktop opacity", + "panel-background-opacity-description": "Set the background opacity for all panels (bar, launcher, settings, etc...).", + "panel-background-opacity-label": "Panel background opacity", + "panels-attached-to-bar-description": "Panels lock to the bar and screen edges, creating a seamless look with stylish inverted corners.", + "panels-attached-to-bar-label": "Snap panels to edges", + "panels-overlay-description": "Ensures panels and the bar remain visible, even over fullscreen applications.", + "panels-overlay-label": "Keep panels & bar on top", + "scaling-description": "Changes the size of the general user interface, excluding the bar.", + "scaling-label": "Interface scaling", + "scrollbar-always-visible-description": "Keep scrollbars visible whenever content is scrollable, instead of only showing them on hover.", + "scrollbar-always-visible-label": "Always show scrollbars", + "settings-panel-header": "Settings panel", + "settings-panel-mode-description": "Choose settings layout (may require reopening).", + "settings-panel-mode-label": "Settings panel mode", + "settings-panel-sidebar-card-style-description": "Wraps the settings sidebar in a filled background with rounded corners.", + "settings-panel-sidebar-card-style-label": "Sidebar card style", + "shadows-description": "Enables drop shadows under bars and panels.", + "shadows-direction-description": "Choose where the shadow is cast to.", + "shadows-direction-label": "Shadow direction", + "shadows-label": "Drop shadows", + "title": "User Interface", + "tooltips-description": "Enable or disable tooltips throughout the interface.", + "tooltips-label": "Show tooltips", + "translucent-widgets-description": "Make buttons, tabs, and other widgets inside panels semi-transparent.", + "translucent-widgets-label": "Translucent widgets" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Alphabetical", + "automation-change-mode-description": "Choose how wallpapers are selected when changing automatically.", + "automation-change-mode-label": "Change mode", + "automation-custom-interval-description": "Enter time as HH:MM (e.g. 01:30).", + "automation-custom-interval-label": "Custom interval", + "automation-interval-description": "How often to change wallpapers automatically.", + "automation-interval-label": "Wallpaper interval", + "automation-random-wallpaper-description": "Schedule random wallpaper changes at regular intervals.", + "automation-scheduled-change-description": "Automatically change wallpapers at regular intervals.", + "automation-scheduled-change-label": "Scheduled change", + "look-feel-edge-smoothness-description": "Applies a soft, feathered effect to the edge of transitions.", + "look-feel-edge-smoothness-label": "Soften transition edge", + "look-feel-fill-color-description": "Choose a fill color that may appear behind the wallpaper.", + "look-feel-fill-mode-description": "Select how the image should scale to match your monitor's resolution.", + "look-feel-fill-mode-label": "Fill mode", + "look-feel-skip-startup-transition-description": "Skip the wallpaper animation when the shell starts.", + "look-feel-skip-startup-transition-label": "Skip startup transition", + "look-feel-title": "Look & feel", + "look-feel-transition-duration-description": "Duration of transition animations in seconds.", + "look-feel-transition-duration-label": "Transition duration", + "look-feel-transition-type-description": "Animation type when switching between wallpapers.", + "look-feel-transition-type-label": "Transition type", + "settings-clear-cache-button": "Clear cache", + "settings-clear-cache-description": "Clear cached resized wallpapers to free disk space.", + "settings-clear-cache-label": "Wallpaper cache", + "settings-clear-cache-toast": "Wallpaper cache cleared", + "settings-desc": "Control how wallpapers are managed and displayed.", + "settings-enable-management-description": "Manage wallpapers with Noctalia. Uncheck if you prefer using another application.", + "settings-enable-management-label": "Enable wallpaper management", + "settings-enable-overview-description": "Applies a blurred and dimmed wallpaper to the overview screen.", + "settings-enable-overview-label": "Enable overview wallpaper", + "settings-folder-description": "Path to your main wallpaper folder.", + "settings-folder-label": "Wallpaper folder", + "settings-hide-wallpaper-filenames-tooltip-hide": "Hide filenames", + "settings-hide-wallpaper-filenames-tooltip-show": "Show filenames", + "settings-monitor-specific-description": "Set a different wallpaper folder for each monitor.", + "settings-monitor-specific-label": "Monitor-specific directories", + "settings-monitor-specific-tooltip": "Monitor wallpaper folder", + "settings-overview-blur-strength-description": "Applies the blur strength to the overview.", + "settings-overview-blur-strength-label": "Overview blur strength", + "settings-overview-tint-description": "Applies the tint strength to the overview.", + "settings-overview-tint-label": "Overview tint strength", + "settings-recursive-search-description": "Also search for wallpapers in subfolders of the wallpaper directory.", + "settings-recursive-search-label": "Search subfolders", + "settings-select-monitor-folder": "Select monitor wallpaper folder", + "settings-selector-description": "Choose your wallpaper.", + "settings-selector-position-description": "Choose where the wallpaper selector panel appears.", + "settings-show-hidden-files-tooltip-hide": "Hide hidden files", + "settings-show-hidden-files-tooltip-show": "Show hidden files", + "settings-title": "Wallpaper settings", + "settings-use-original-images-description": "Skip resizing wallpapers before display. Saves disk space and reduces CPU usage during wallpaper changes, but may use more memory for very large images.", + "settings-use-original-images-label": "Use original images", + "settings-view-mode-description": "Choose how wallpapers are displayed from your directory.", + "settings-view-mode-label": "Viewing mode", + "view-mode-browse": "Browse directories", + "view-mode-cycle-tooltip": "View mode: {mode} (click to change)", + "view-mode-recursive": "Flattened subdirectories", + "view-mode-single": "Root directory" + } + }, + "placeholders": { + "add-new-keybind": "Add a new keybind", + "command-example": "echo \"Hello World\"", + "enter-command": "Enter command to execute (app or custom script)", + "enter-ipc-identifier": "Enter unique identifier for IPC commands", + "enter-path": "Enter path...", + "enter-text-to-collapse": "e.g. 'nothing is playing'. Use /regex/ for patterns.", + "enter-tooltip": "Enter tooltip", + "enter-width-pixels": "Enter width in pixels", + "keybind-recording": "Recording keybind...", + "search": "Search...", + "search-icons": "e.g. noctalia, niri, battery, cloud", + "search-launcher": "Search entries... or use > for commands", + "search-wallhaven": "Search Wallhaven...", + "search-wallpapers": "Type to filter wallpapers..." + }, + "positions": { + "bottom": "Bottom", + "bottom-center": "Bottom center", + "bottom-left": "Bottom left", + "bottom-right": "Bottom right", + "center": "Center", + "center-left": "Left center", + "center-right": "Right center", + "close-to-bar": "Close to bar button", + "follow-bar": "Follow bar", + "left": "Left", + "right": "Right", + "top": "Top", + "top-center": "Top center", + "top-left": "Top left", + "top-right": "Top right" + }, + "session-menu": { + "action-in-seconds": "{action} in {seconds} seconds...", + "cancel-timer": "Cancel timer", + "title": "Session Menu" + }, + "setup": { + "all-done": "All Done!", + "appearance": { + "subheader": "Choose Dark Mode and color sources (wallpaper or predefined)." + }, + "customize": { + "header": "Customize your experience", + "subheader": "Adjust bar position, density, scaling and more." + }, + "skip-setup": "Skip Setup", + "telemetry-wizard-done": "Got it!", + "telemetry-wizard-note": "You're in control — enable or disable this anytime in settings", + "telemetry-wizard-subtitle": "We've added anonymous analytics to help improve Noctalia", + "telemetry-wizard-title": "Privacy Update", + "wallpaper": { + "choose-dir": "Choose a directory containing your wallpaper images", + "dir-browse": "Browse for wallpaper folder", + "dir-description": "Choose the folder containing your wallpapers.", + "dir-label": "Wallpaper directory", + "dir-select-title": "Select wallpaper folder", + "header": "Choose your wallpaper", + "no-dir": "No wallpaper directory selected", + "no-valid": "No valid image files found in: {dir}", + "none-in-dir": "No wallpapers found in directory", + "preview-error": "Failed to load image", + "select-prompt": "Select a wallpaper below", + "subheader": "Set the mood with a beautiful background." + }, + "welcome": "Welcome", + "welcome-note": "Just a few basics to get you started — full options are in the settings", + "welcome-subtitle": "Let's make your desktop uniquely yours", + "welcome-title": "Welcome to Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "No media player detected", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "Unknown app", + "uptime": "Uptime: {uptime}", + "user-requested": "User requested", + "welcome-back": "Welcome back,", + "widget-settings-title": "{widget} Settings" + }, + "system-monitor": { + "core-usage": "Core {id} usage", + "cpu-temp": "CPU temp", + "cpu-usage": "CPU usage", + "disk": "Disk", + "download-speed": "Download speed", + "gpu-temp": "GPU temp", + "load-average": "Load average", + "title": "System Monitor", + "upload-speed": "Upload speed" + }, + "toast": { + "airplane-mode": { + "description": "Disable all wireless communications.", + "title": "Airplane Mode" + }, + "battery": { + "critical": "Critical battery", + "critical-desc": "Battery is at {percent}% — please connect the charger immediately", + "low": "Low battery", + "low-desc": "Battery is at {percent}% — please connect the charger" + }, + "bluetooth": { + "address-copied": "Address copied to clipboard", + "auto-connect-disabled": "Auto-connect disabled", + "auto-connect-enabled": "Auto-connect enabled", + "auto-connecting": "Connecting to {count} device(s)...", + "confirm-code": "Confirm code {value} on the other device.", + "connect-failed": "Failed to connect to device", + "disconnect-failed": "Failed to disconnect from device", + "discoverable-change-failed": "Failed to change discoverable state", + "discoverable-disabled": "Discoverable disabled", + "discoverable-enabled": "Discoverable enabled", + "display-code": "Enter code {value} on the other device.", + "forget-failed": "Failed to forget device", + "pair-failed": "Failed to pair device", + "passkey-required": "Passkey required by the device.", + "pincode-required": "PIN code required by the device.", + "state-change-failed": "Failed to change Bluetooth state" + }, + "clipboard": { + "long-text": "Long text", + "unavailable": "Clipboard history unavailable", + "unavailable-desc": "The 'cliphist' application is not installed. Please install it to use clipboard history features" + }, + "custom-command-failed": { + "description": "Command failed: {command}\nExit code: {code}", + "title": "Custom command failed" + }, + "do-not-disturb": { + "disabled": "Do Not Disturb disabled", + "disabled-desc": "Showing all notifications", + "enabled": "Do Not Disturb enabled", + "enabled-desc": "You'll find these notifications in your history" + }, + "donation-opened": "Donation page opened in your browser", + "internet-limited": "Connected without internet", + "keyboard-layout": { + "changed": "Keyboard layout changed to {layout}", + "title": "Keyboard" + }, + "missing-control-center": { + "description": "The control center widget has been removed from the bar. To access it from the bar again, you will need to re-add the widget. You can open it with right clicking on the bar too", + "label": "Last control center widget removed" + }, + "night-light": { + "forced": "Forced activation", + "normal": "Normal mode", + "not-installed": "wlsunset not installed" + }, + "noctalia-performance": { + "disabled": "Performance mode disabled", + "enabled": "Performance mode enabled", + "label": "Noctalia Performance" + }, + "power-profile": { + "changed": "Power profile changed", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "Encountered an error while processing templates", + "title": "Templating processing failed" + }, + "vpn": { + "connected": "Connected to '{name}'", + "disconnected": "Disconnected from '{name}'" + }, + "wallpaper-colors": { + "disabled": "Wallpaper colors disabled", + "enabled": "Wallpaper colors enabled", + "label": "Wallpaper colors" + }, + "wifi": { + "connected": "Connected to '{ssid}'", + "connection-failed": "Connection failed", + "connection-timeout": "Connection timeout", + "disconnected": "Disconnected from '{ssid}'", + "incorrect-password": "Incorrect password", + "network-not-found": "Network not found" + } + }, + "tooltips": { + "add-widget": "Add widget", + "auto-connect": "Toggle auto-connection for this device", + "bluetooth-auto-connect-off": "Auto-connect is off", + "bluetooth-auto-connect-on": "Auto-connect is on", + "bluetooth-devices": "Bluetooth devices", + "brightness-at": "Brightness: {brightness}%", + "click-to-start-recording": "Screen recorder (start recording)", + "click-to-stop-recording": "Screen recorder (stop recording)", + "collapse": "Collapse sidebar", + "copy-address": "Copy address", + "delete-notification": "Delete notification", + "dismiss-notification": "Dismiss notification", + "do-not-disturb-enabled": "Do Not Disturb", + "expand": "Expand sidebar", + "forget-network": "Forget network", + "grid-view": "Grid view", + "hidden-files-hide": "Hidden files", + "hide-unnamed-devices": "Hide unnamed devices", + "home": "Home directory", + "input-muted": "Toggle input mute", + "keep-awake": "Keep Awake", + "keyboard-layout": "{layout} keyboard layout", + "list-view": "List view", + "manage-vpn": "VPN connections", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "Maximum widgets reached", + "microphone-volume-at": "Microphone volume: {volume}%", + "move-to-section": "Move to {section}", + "mute": "Mute", + "next-media": "Next track", + "next-month": "Next month", + "night-light-not-installed": "Night Light (not available)", + "noctalia-performance-enabled": "Noctalia performance mode", + "open-annotation-tool": "Open with annotation tool", + "open-control-center": "Control center", + "open-notification-history-enable-dnd": "Notification history", + "open-settings": "Settings", + "open-tray-dropdown": "System tray", + "output-muted": "Toggle output mute", + "power-profile": "{profile} power profile", + "previous-media": "Previous track", + "previous-month": "Previous month", + "refresh-devices": "Refresh devices", + "refresh-wallhaven": "Refresh Wallhaven results", + "refresh-wallpaper-list": "Refresh wallpaper list", + "remove-widget": "Remove widget", + "search-close": "Close search", + "session-menu": "Session menu", + "show-all-devices": "Show all devices", + "switch-to-dark-mode": "Dark Mode", + "switch-to-light-mode": "Light Mode", + "unmute": "Unmute", + "up": "Parent directory", + "volume-at": "Output volume: {volume}%", + "wallpaper-selector": "Wallpaper selector" + }, + "wallpaper": { + "browse": { + "empty-directory": "This directory is empty.", + "go-root": "Go to wallpaper root", + "go-up": "Go to parent folder", + "go-up-hint": "Use the back button to navigate up." + }, + "configure-directory": "Configure your wallpaper directory with images.", + "fill-modes": { + "crop": "Crop (Fill)", + "fit": "Fit (Contain)", + "repeat": "Repeat (Tile)", + "stretch": "Stretch" + }, + "no-match": "No match found.", + "no-wallpaper": "No wallpaper found.", + "panel": { + "apikey-help": "An API key is required to access NSFW content.", + "apikey-label": "API Key", + "apikey-managed-by-env": "Managed via NOCTALIA_WALLHAVEN_API_KEY environment variable.", + "apikey-placeholder": "Enter your Wallhaven API Key", + "appearance-dark-tab": "Dark", + "appearance-light-tab": "Light", + "categories-anime": "Anime", + "categories-label": "Categories", + "categories-people": "People", + "color-extraction-disabled": "Use wallpaper color extraction", + "color-extraction-enabled": "Use predefined color schemes", + "header-devices-apply-all-tooltip": "Choose wallpaper per monitor", + "header-devices-per-monitor-tooltip": "Apply to all monitors", + "header-link-light-dark-tooltip": "Keep wallpaper when switching theme", + "header-separate-light-dark-tooltip": "Auto-switch wallpaper with theme", + "header-sun-linked-tooltip": "Separate light and dark wallpapers", + "header-sun-separate-tooltip": "Same wallpaper for light and dark", + "order-asc": "Ascending", + "order-desc": "Descending", + "order-label": "Order", + "purity-label": "Content filter", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Sketchy", + "ratios-all-portrait": "All portrait", + "ratios-all-wide": "All wide", + "ratios-any": "Any", + "ratios-label": "Aspect ratio", + "resolution-atleast": "At least", + "resolution-exact": "Exact", + "resolution-label": "Resolution", + "resolution-mode-label": "Mode", + "solid-color-tooltip": "Solid color background", + "sort-date-asc": "Sort by oldest first", + "sort-date-desc": "Sort by newest first", + "sort-name-asc": "Sort by name (A-Z)", + "sort-name-desc": "Sort by name (Z-A)", + "sort-random": "Sort by random", + "sorting-date-added": "Date added", + "sorting-favorites": "Favorites", + "sorting-label": "Sort by", + "sorting-relevance": "Relevance", + "sorting-toplist": "Toplist", + "sorting-views": "Views", + "source-label": "Source", + "source-wallhaven": "Wallhaven", + "title": "Wallpaper Selector", + "wallhaven-settings-title": "Wallhaven Settings" + }, + "transitions": { + "disc": "Disc", + "fade": "Fade", + "honeycomb": "Honeycomb", + "pixelate": "Pixelate", + "stripes": "Stripes", + "wipe": "Wipe" + }, + "try-different-search": "Try a different search query.", + "wallhaven": { + "loading": "Loading wallpapers...", + "no-results": "No wallpapers found. Try a different search query.", + "page": "{current} of {total}", + "page-prefix": "Page", + "page-suffix": "of {total}" + } + }, + "weather": { + "clear-sky": "Clear sky", + "drizzle": "Drizzle", + "fog": "Fog", + "mainly-clear": "Mainly clear", + "overcast": "Overcast", + "partly-cloudy": "Partly cloudy", + "rain-showers": "Rain showers", + "snow": "Snow", + "thunderstorm": "Thunderstorm" + }, + "widgets": { + "color-picker": { + "palette-description": "Choose from a wide range of predefined colors.", + "palette-label": "Palette", + "palette-theme-colors": "Quick access to your theme's color palette.", + "title": "Color picker" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm in lowercase", + "ampm-uppercase": "AM/PM in uppercase", + "common-12hour-time-minutes": "12-hour time with minutes", + "common-24hour-time-minutes": "24-hour time with minutes", + "common-24hour-time-seconds": "24-hour time with seconds", + "common-european-date": "European date format", + "common-iso-date": "ISO date format", + "common-us-date": "US date format", + "common-weekday-date": "Weekday with date", + "common-weekday-month-day": "Weekday, month and day", + "day-abbreviated": "Abbreviated day name", + "day-full": "Full day name", + "day-leading-zero": "Day with leading zero (01-31)", + "day-no-leading-zero": "Day without leading zero (1-31)", + "hour-leading-zero": "Hour with leading zero (00-23) — 24-hour format", + "hour-no-leading-zero": "Hour without leading zero (0-23) — 24-hour format", + "minute-leading-zero": "Minute with leading zero (00-59)", + "minute-no-leading-zero": "Minute without leading zero (0-59)", + "month-abbreviated": "Abbreviated month name", + "month-full": "Full month name", + "month-number-leading-zero": "Month as number with leading zero (01-12)", + "month-number-no-zero": "Month as number without leading zero (1-12)", + "second-leading-zero": "Second with leading zero (00-59)", + "second-no-leading-zero": "Second without leading zero (0-59)", + "timezone-abbreviation": "Timezone abbreviation", + "year-four-digit": "Year as four-digit number", + "year-two-digit": "Year as two-digit number (00-99)" + }, + "file-picker": { + "item": "item", + "items": "items", + "search-placeholder": "Search files and folders...", + "select-current": "Select Current", + "select-file": "Select File", + "select-folder": "Select Folder", + "selected": "Selected:", + "title": "File Picker" + }, + "file-picker-title": "Select a file", + "icon-picker": { + "title": "Icon picker" + }, + "tooltip-placeholder": "Placeholder" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "Anonymous Identity", + "ca-cert": "CA Certificate", + "eap-method": "EAP Method", + "password": "User password", + "phase2-auth": "Phase 2 Authentication", + "username": "Identity / Username" + }, + "panel": { + "action-required": "Action required", + "add-hidden-network": "Add hidden network", + "add-network": "Add network", + "available-interfaces": "Available interfaces", + "available-networks": "Available networks", + "disabled": "Wi‑Fi is disabled", + "disconnecting": "Disconnecting...", + "dns": "DNS", + "enable-message": "Enable Wi‑Fi to see available networks.", + "enter-password": "Enter Wi‑Fi password...", + "forget": "Forget", + "forget-network": "Forget this network", + "forgetting": "Forgetting...", + "hidden-network": "Hidden network", + "interface": "Network interface", + "internet-connected": "Internet connected", + "internet-limited": "No internet", + "internet-status": "Internet status", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "Known networks", + "link-speed": "Link speed", + "network-name-ssid": "Network name (SSID)", + "no-ethernet-devices": "No Ethernet devices detected", + "no-networks": "No Wi‑Fi networks found", + "saved": "Saved", + "scan-again": "Scan again", + "searching": "Searching for networks...", + "security-open": "Open", + "security-wep": "WEP", + "security-wpa": "WPA", + "security-wpa-ent": "WPA Enterprise", + "security-wpa2-ent": "WPA2 Enterprise", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 Enterprise", + "show-password": "Show password", + "title": "Wi‑Fi" + }, + "signal": { + "excellent": "Excellent", + "fair": "Fair", + "good": "Good", + "poor": "Poor", + "weak": "Weak" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/es.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/es.json new file mode 100644 index 0000000..4d7bfc5 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/es.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "Borrar historial", + "close-app": "Cerrar {app}", + "connect-vpn": "Conectarse a {name}", + "cycle-visualizer": "Visualizador de ciclos", + "disable-bluetooth": "Desactivar Bluetooth", + "disable-dnd": "Desactivar 'No molestar'", + "disable-wifi": "Desactivar Wi-Fi", + "disconnect-vpn": "Desconectar {name}", + "dock-settings": "Ajustes del dock", + "enable-bluetooth": "Activar Bluetooth", + "enable-dnd": "Activar 'No molestar'", + "enable-wifi": "Activar Wi-Fi", + "launcher-settings": "Ajustes del lanzador", + "lower-to-bottom": "Bajar hasta el fondo", + "open-calendar": "Abrir calendario", + "open-display-settings": "Configuración de pantalla", + "open-launcher": "Abrir lanzador", + "open-settings": "Abrir ajustes", + "raise-to-top": "Subir al principio", + "random-wallpaper": "Fondo de pantalla aleatorio", + "run-custom-command": "Ejecutar comando personalizado", + "show-active-only": "Mostrar solo activos", + "show-all": "Mostrar todo", + "toggle-mute": "Activar/desactivar silencio", + "widget-settings": "Configuración del widget" + }, + "authentication": { + "error": "Error de autentificación", + "failed": "Autentificación fallida" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Aplicar colores del tema al icono de la ventana activa.", + "hide-mode-description": "Controla el comportamiento del widget cuando no hay ventana activa.", + "scrolling-mode-description": "Controlar cuándo está habilitado el desplazamiento de texto para títulos de ventana largos.", + "show-app-icon-description": "Mostrar el icono de la aplicación junto al título de la ventana.", + "show-app-icon-label": "Mostrar icono de la aplicación", + "show-app-text-description": "Mostrar el título de la ventana activa.", + "show-app-text-label": "Mostrar título de ventana" + }, + "audio-visualizer": { + "color-name-description": "Selecciona el color para el visualizador.", + "color-name-label": "Color de relleno", + "height-description": "Ancho de componente personalizado.", + "hide-when-idle-description": "Si está activado, el visualizador se oculta salvo que haya reproducción activa.", + "hide-when-idle-label": "Ocultar cuando no se reproduce", + "width-description": "Ancho del componente personalizado." + }, + "battery": { + "device-default": "Predeterminado (Dispositivo de visualización)", + "device-description": "Seleccione qué dispositivo de batería mostrar.", + "device-label": "Dispositivo de batería", + "display-mode-description": "Elige cómo se muestra la batería en la barra.", + "display-mode-graphic": "Batería gráfica", + "display-mode-graphic-clean": "Batería gráfica (sin %)", + "display-mode-icon-always": "Icono - Siempre mostrar %", + "display-mode-icon-hover": "Icono - Mostrar al pasar", + "display-mode-icon-only": "Solo icono", + "hide-if-idle-description": "Ocultar el widget cuando la batería no se esté cargando ni descargando.", + "hide-if-idle-label": "Ocultar cuando inactivo", + "hide-if-not-detected-description": "Ocultar el widget cuando no se detecte batería en el sistema.", + "hide-if-not-detected-label": "Ocultar si no se detecta", + "low-battery-threshold-label": "Umbral de advertencia de batería baja", + "show-noctalia-performance-description": "Muestra el interruptor del modo de rendimiento Noctalia en el panel de batería.
Desactiva sombras y animaciones en Noctalia para reducir el uso de recursos.", + "show-noctalia-performance-label": "Mostrar interruptor Noctalia Rendimiento", + "show-power-profile-description": "Muestra la selección de perfil de energía en el panel de batería.", + "show-power-profile-label": "Mostrar controles de perfil de energía" + }, + "brightness": { + "apply-all-description": "Cambiar el brillo de todos los monitores al usar la rueda de desplazamiento.", + "apply-all-label": "Aplicar cambios de desplazamiento a todos los monitores" + }, + "clock": { + "clock-display-description": "Personaliza la visualización del reloj añadiendo tokens de la lista de abajo. Para usar el formato de 12 horas, debes incluir el token 'AP'.", + "clock-display-label": "Visualización del reloj", + "custom-font-description": "Seleccionar una fuente personalizada para la visualización del reloj.", + "custom-font-label": "Fuente personalizada", + "custom-font-placeholder": "Seleccionar fuente personalizada...", + "custom-font-search-placeholder": "Buscar fuentes...", + "horizontal-bar-description": "Consejo: Usa \\n para crear un salto de línea.", + "horizontal-bar-label": "Barra horizontal", + "preview": "Vista previa", + "tooltip-format-description": "Formato de la información mostrada al pasar el cursor sobre el reloj. Déjalo vacío para usar el formato por defecto.", + "tooltip-format-label": "Formato del texto emergente", + "use-custom-font-description": "Anular la fuente predeterminada del sistema con una fuente específica para el reloj.", + "use-custom-font-label": "Usar fuente personalizada", + "use-monospaced-font-description": "Cuando está activada, el reloj usará la fuente monoespaciada.", + "use-monospaced-font-label": "Usar fuente monoespaciada", + "vertical-bar-description": "Usa un espacio para separar cada parte en una nueva línea.", + "vertical-bar-label": "Barra vertical" + }, + "control-center": { + "browse-file": "Explorar archivo", + "browse-library": "Explorar biblioteca", + "color-selection-description": "Aplica colores del tema a los iconos.", + "enable-colorization-description": "Habilitar la colorización para el icono, aplicando los colores del tema.", + "icon-description": "Selecciona un icono de la biblioteca o un archivo personalizado.", + "select-custom-icon": "Seleccionar un icono personalizado", + "use-distro-logo-description": "Usar el logo de tu distribución en lugar de un icono personalizado.", + "use-distro-logo-label": "Usar logo de la distribución en lugar de icono" + }, + "custom-button": { + "collapse-condition-description": "Si el texto de salida coincide con este valor, el botón se colapsará.", + "collapse-condition-label": "Condición de colapso", + "color-selection-description": "Aplicar colores del tema al icono y al texto.", + "default-tooltip": "Botón personalizado, configurar en ajustes", + "display-command-output-description": "Introduce un comando para ejecutar a intervalos regulares. La primera línea de su salida se mostrará como texto.", + "display-command-output-label": "Mostrar salida del comando", + "display-command-output-stream-description": "Introduce un comando para ejecutar continuamente.", + "dynamic-text": "Texto dinámico", + "enable-colorization-description": "Habilitar el coloreado para el icono y el texto del botón personalizado, aplicando los colores del tema.", + "enable-colorization-label": "Habilitar coloreado", + "general-tooltip-text-description": "Texto personalizado para mostrar en la descripción emergente del botón.", + "general-tooltip-text-label": "Texto emergente personalizado", + "hide-mode-always-expanded": "Siempre expandido", + "hide-mode-description": "Controla la visibilidad del widget cuando el comando no tiene salida.", + "hide-mode-expand-with-output": "Expandir cuando tiene salida", + "hide-mode-label": "Modo de ocultación", + "hide-mode-max-transparent": "Máximo expandido pero transparente", + "icon-description": "Selecciona un icono de la biblioteca.", + "ipc-identifier-description": "Identificador único para comandos IPC. Utiliza este identificador con 'qs -c noctalia-shell ipc call cb [acción] [identificador]' para controlar este botón a través de IPC.", + "ipc-identifier-label": "Identificador IPC", + "left-click-description": "Comando a ejecutar cuando se hace clic izquierdo en el botón.", + "left-click-label": "Clic izquierdo", + "left-click-update-text": "Actualizar el texto mostrado al hacer clic izquierdo", + "max-text-length-horizontal-description": "Número máximo de caracteres a mostrar en la barra horizontal (0 para ocultar el texto).", + "max-text-length-horizontal-label": "Longitud máxima de texto (horizontal)", + "max-text-length-vertical-description": "Número máximo de caracteres a mostrar en la barra vertical (0 para ocultar el texto).", + "max-text-length-vertical-label": "Longitud máxima de texto (vertical)", + "middle-click-description": "Comando a ejecutar cuando se hace clic central en el botón.", + "middle-click-label": "Clic central", + "middle-click-update-text": "Actualizar el texto mostrado al hacer clic con el botón central", + "parse-json-description": "Analizar la salida del comando como un objeto JSON para establecer dinámicamente el texto y el ícono.", + "parse-json-label": "Interpretar salida como JSON", + "refresh-interval-description": "Intervalo en milisegundos.", + "refresh-interval-label": "Intervalo de actualización", + "right-click-description": "Comando a ejecutar cuando se hace clic derecho en el botón.", + "right-click-label": "Clic derecho", + "right-click-update-text": "Actualizar el texto mostrado al hacer clic derecho", + "show-exec-tooltip-description": "Mostrar texto emergente con los detalles del comandos (clic izquierdo/derecho/central, rueda).", + "show-exec-tooltip-label": "Mostrar texto emergente de comandos", + "show-icon-description": "Alterna la visibilidad del icono del widget.", + "show-icon-label": "Mostrar icono", + "show-text-tooltip-description": "Mostrar texto emergente con la salida del comando de texto.", + "show-text-tooltip-label": "Mostrar textos emergentes dinámicos", + "text-stream-description": "Las líneas transmitidas desde el comando se mostrarán como texto en el botón.", + "text-stream-label": "Salida del comando", + "wheel-description": "Comando a ejecutar cuando se usa la rueda de desplazamiento.
Usa $delta para el delta de la rueda de desplazamiento en el comando.", + "wheel-down": "Rueda abajo", + "wheel-down-description": "Comando a ejecutar cuando la rueda de desplazamiento se desplaza hacia abajo.", + "wheel-down-label": "Comando de rueda hacia abajo", + "wheel-label": "Rueda de desplazamiento", + "wheel-mode-separate-description": "Habilitar comandos separados para rueda arriba y abajo.", + "wheel-mode-separate-label": "Comandos de rueda separados", + "wheel-up": "Rueda arriba", + "wheel-up-description": "Comando a ejecutar cuando la rueda de desplazamiento se desplaza hacia arriba.", + "wheel-up-label": "Comando de rueda hacia arriba", + "wheel-update-text": "Actualizar texto mostrado al desplazarse" + }, + "keyboard-layout": { + "show-icon-description": "Mostrar el icono de la distribución del teclado." + }, + "lock-keys": { + "hide-when-off-description": "Ocultar el indicador cuando la tecla no está activa.", + "hide-when-off-label": "Ocultar cuando desactivado", + "show-caps-lock-description": "Mostrar el estado de Bloq Mayús.", + "show-caps-lock-label": "Bloq Mayús", + "show-num-lock-description": "Mostrar el estado de Bloq Num.", + "show-num-lock-label": "Bloq Num", + "show-scroll-lock-description": "Mostrar el estado de Bloq Despl.", + "show-scroll-lock-label": "Bloq Despl" + }, + "media-mini": { + "compact-mode-description": "Habilitar un diseño compacto para el panel del reproductor multimedia.", + "compact-mode-label": "Modo compacto", + "hide-mode-description": "Controla el comportamiento del widget cuando no se está reproduciendo ningún medio.", + "max-width-description": "Establece el tamaño horizontal máximo del widget. El widget se reducirá para adaptarse a contenido más corto.", + "no-active-player": "Sin reproductor activo", + "panel-section-description": "Configurar la apariencia y el comportamiento del panel del reproductor multimedia.", + "panel-section-label": "Panel del reproductor multimedia", + "scrolling-mode-description": "Controla cuándo está habilitado el desplazamiento de texto para títulos de pista largos.", + "scrolling-mode-label": "Modo de desplazamiento", + "show-album-art-description": "Mostrar la portada del álbum de la pista que se está reproduciendo.", + "show-album-art-label": "Mostrar arte del álbum", + "show-artist-first-description": "Mostrar 'artista - título' en lugar de 'título - artista'.", + "show-artist-first-label": "Mostrar primero el artista", + "show-progress-ring-description": "Mostrar un indicador de progreso circular que muestre el progreso de la pista.", + "show-progress-ring-label": "Mostrar anillo de progreso", + "show-visualizer-description": "Mostrar un visualizador de audio cuando se reproduce música.", + "show-visualizer-label": "Mostrar visualizador", + "use-fixed-width-description": "Cuando está activado, el widget siempre usará el ancho máximo en lugar de ajustarse dinámicamente al contenido.", + "use-fixed-width-label": "Usar ancho fijo", + "visualizer-type-description": "Elegir el estilo de visualizador de audio a mostrar.", + "visualizer-type-label": "Tipo de visualizador" + }, + "notification-history": { + "hide-widget-when-zero-description": "Ocultar el icono de notificaciones cuando no haya notificaciones.", + "hide-widget-when-zero-label": "Ocultar icono sin notificaciones", + "hide-widget-when-zero-unread-description": "Ocultar el icono de notificaciones cuando no haya notificaciones pendientes de leer.", + "hide-widget-when-zero-unread-label": "Ocultar icono sin notificaciones pendientes", + "show-unread-badge-description": "Mostrar una insignia con el número de notificaciones no leídas.", + "show-unread-badge-label": "Mostrar insignia de no leídos", + "unread-badge-color-description": "Selecciona el color para la insignia de notificación no leída.", + "unread-badge-color-label": "Color de la insignia de no leídos" + }, + "section-editor": { + "placeholder": "Selecciona un widget para añadir...", + "search-placeholder": "Buscar widgets..." + }, + "spacer": { + "width-description": "Ancho del espaciado en píxeles." + }, + "system-monitor": { + "compact-mode-description": "Mostrar las estadísticas como mini gráficos de barras en lugar de valores de texto. Evita el desplazamiento del diseño.", + "compact-mode-label": "Modo compacto", + "cpu-cores-description": "Mostrar el uso de los núcleos de la CPU individualmente.", + "cpu-cores-label": "Núcleos de CPU", + "cpu-frequency-description": "Mostrar la velocidad de reloj actual de la CPU en GHz.", + "cpu-frequency-label": "Mostrar frecuencia de la CPU", + "cpu-temperature-description": "Mostrar las lecturas de temperatura de CPU si están disponibles.", + "cpu-temperature-label": "Temperatura de la CPU", + "cpu-usage-description": "Mostrar el porcentaje actual de uso de CPU.", + "cpu-usage-label": "Uso de CPU", + "disk-path-description": "Seleccione qué punto de montaje de disco desea supervisar.", + "disk-path-label": "Ruta del disco", + "gpu-temperature-description": "Mostrar las lecturas de temperatura de la GPU si están disponibles.", + "load-average-description": "Mostrar la carga promedio del sistema.", + "load-average-label": "Carga promedio", + "memory-percentage-description": "Mostrar el uso de memoria como porcentaje en lugar de valores absolutos.", + "memory-percentage-label": "Memoria como porcentaje", + "memory-usage-description": "Mostrar la información actual del uso de RAM.", + "memory-usage-label": "Uso de memoria", + "network-traffic-description": "Mostrar las velocidades de carga y descarga de la red.", + "network-traffic-label": "Tráfico de red", + "storage-as-percentage-description": "Mostrar el espacio en disco como porcentaje en lugar de valores absolutos.", + "storage-as-percentage-label": "Disco como porcentaje", + "storage-available-description": "Muestra cuánto espacio en disco está disponible en lugar de cuánto se usa.", + "storage-available-label": "Espacio en disco disponible", + "storage-usage-description": "Mostrar la información del uso del espacio en disco.", + "storage-usage-label": "Uso de almacenamiento", + "swap-usage-description": "Mostrar el uso de la memoria swap.", + "swap-usage-label": "Uso de swap", + "use-monospace-font-description": "Utilice fuente monoespaciada para un ancho de carácter consistente.", + "use-monospace-font-label": "Fuente monoespaciada", + "use-padding-description": "Rellena los valores de texto con espacios iniciales para evitar el desplazamiento del diseño.", + "use-padding-description-disabled-monospace-font": "Se necesita una fuente monoespaciada para esta característica.", + "use-padding-description-disabled-vertical": "La barra de tareas vertical no admite relleno de texto.", + "use-padding-label": "Relleno de Texto" + }, + "taskbar": { + "colorize-icons-description": "Aplicar colores del tema a los iconos de la barra de tareas.", + "hide-mode-description": "Controla cómo se comporta el widget cuando no hay ventanas coincidentes.", + "hide-mode-label": "Modo de ocultación", + "icon-scale-description": "Establece el factor de escala para los iconos de la barra de tareas.", + "icon-scale-label": "Escalado de iconos", + "max-width-description": "Ancho máximo de la barra de tareas como porcentaje del ancho de la pantalla.", + "max-width-label": "Ancho máximo", + "only-active-workspaces-description": "Mostrar solo las aplicaciones de los espacios de trabajo activos.", + "only-active-workspaces-label": "Solo desde espacios de trabajo activos", + "only-same-monitor-description": "Mostrar solo las aplicaciones del monitor donde se encuentra la barra.", + "only-same-monitor-label": "Solo desde el mismo monitor", + "show-pinned-apps-description": "Mostrar las aplicaciones ancladas desde el dock en la barra de tareas.", + "show-pinned-apps-label": "Mostrar aplicaciones ancladas", + "show-title-description": "Mostrar títulos de ventanas en la barra de tareas.", + "show-title-description-disabled": "La barra de tareas vertical no admite mostrar títulos.", + "show-title-label": "Mostrar título", + "smart-width-description": "Ajustar automáticamente el ancho de las entradas según el número de entradas.", + "smart-width-label": "Ancho inteligente", + "title-width-description": "Establecer el ancho de los títulos de ventanas en la barra de tareas (en píxeles).", + "title-width-label": "Ancho del título", + "title-width-reset-tooltip": "Restablecer ancho del título" + }, + "tray": { + "chevron-color-description": "Aplicar los colores del tema al icono de flecha del cajón.", + "chevron-color-label": "Color de la flecha", + "colorize-icons-description": "Aplicar colores del tema a los iconos de la bandeja del sistema.", + "colorize-icons-label": "Colorear iconos", + "drawer-enabled-description": "Cuando está habilitado, los elementos de la bandeja no anclados se muestran en un panel cajón.
Cuando está deshabilitado, todos los elementos de la bandeja se muestran en línea.", + "drawer-enabled-label": "Habilitar cajón", + "hide-passive-description": "Cuando está activado, los elementos de la bandeja con estado 'Pasivo' se ocultarán.", + "hide-passive-label": "Ocultar objetos pasivos" + }, + "volume": { + "display-mode-description": "Elige cómo te gustaría que apareciera este valor." + }, + "workspace": { + "character-count-description": "Número de caracteres a mostrar de los nombres de espacios de trabajo (1-10).", + "character-count-label": "Número de caracteres", + "empty-color-description": "Establecer el color de fondo para los Workspaces vacíos.", + "empty-color-label": "Color de espacio de trabajo vacío", + "enable-scrollwheel-description": "Cambia entre espacios de trabajo usando la rueda de desplazamiento del ratón.", + "enable-scrollwheel-label": "Desplázate para cambiar de espacio de trabajo", + "focused-color-description": "Establece el color de fondo para el Workspace enfocado.", + "focused-color-label": "Color del espacio de trabajo enfocado", + "follow-focused-screen-description": "Mostrar espacios de trabajo de la pantalla enfocada, en lugar de la pantalla donde se encuentra la barra.", + "follow-focused-screen-label": "Seguir pantalla enfocada", + "font-weight-description": "Establecer el peso visual para el texto dentro del espacio de trabajo.", + "font-weight-label": "Peso de fuente", + "grouped-border-opacity-description": "Establecer el nivel de opacidad para los bordes del contenedor del espacio de trabajo.", + "grouped-border-opacity-label": "Opacidad del borde", + "hide-unoccupied-description": "No mostrar espacios de trabajo sin ventanas.", + "hide-unoccupied-label": "Ocultar desocupados", + "label-mode-description": "Elegir cómo se muestran las etiquetas de los espacios de trabajo.", + "label-mode-label": "Modo de etiqueta", + "occupied-color-description": "Establecer el color de fondo para los Workspaces ocupados.", + "occupied-color-label": "Color del espacio de trabajo ocupado", + "pill-size-description": "Ajusta el tamaño de las píldoras del espacio de trabajo (50%-100%).", + "pill-size-label": "Tamaño de la píldora", + "reverse-scrolling-description": "Invertir la dirección del cambio de espacios de trabajo al desplazarse.", + "reverse-scrolling-label": "Desplazamiento inverso", + "show-applications-description": "Mostrar los iconos de las aplicaciones dentro de cada espacio de trabajo.", + "show-applications-hover-description": "Mostrar iconos de aplicación dentro de cada espacio de trabajo cuando se pasa el ratón por encima.", + "show-applications-hover-label": "Mostrar aplicaciones al pasar", + "show-applications-label": "Mostrar aplicaciones", + "show-badge-description": "Mostrar la insignia del número de espacio de trabajo en modo agrupado.", + "show-badge-label": "Mostrar insignia del espacio de trabajo", + "show-labels-only-when-occupied-description": "Mostrar solo las etiquetas del espacio de trabajo cuando contengan ventanas.", + "show-labels-only-when-occupied-label": "Mostrar etiquetas solo cuando haya contenido", + "unfocused-icons-opacity-description": "Establecer el nivel de opacidad para los iconos de aplicaciones no enfocadas.", + "unfocused-icons-opacity-label": "Opacidad de iconos de aplicaciones no enfocadas" + } + }, + "battery": { + "all-batteries": "Todas las baterías (combinadas)", + "battery-health": "Estado de la batería", + "battery-level": "Nivel de batería", + "capacity-level": "Capacidad: {level}", + "charging-rate": "Tasa de carga: {rate} W", + "discharging-rate": "Tasa de descarga: {rate} W", + "health": "Salud: {percent}%", + "inhibit-idle-description": "Mantiene el sistema despierto.", + "no-battery-detected": "No se detectó ninguna batería", + "plugged-in": "Conectado", + "power-profile": "Perfil de energía", + "time-left": "Tiempo restante: {time}", + "time-until-full": "Tiempo hasta carga completa: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Dispositivos disponibles", + "blocked": "Bloqueado", + "connected-devices": "Dispositivos conectados", + "device-address": "Dirección del dispositivo", + "disabled": "Bluetooth está desactivado", + "discoverable": "Visible", + "enable-message": "Activa Bluetooth para ver los dispositivos disponibles.", + "known-devices": "Dispositivos conocidos", + "no-devices": "No hay dispositivos disponibles", + "paired-devices": "Dispositivos emparejados", + "pairing-mode": "Asegúrate de que tu dispositivo esté en modo de emparejamiento.", + "scanning": "Buscando dispositivos...", + "signal-text-excellent": "Señal: Excelente", + "signal-text-fair": "Señal: Razonable", + "signal-text-good": "Señal: Buena", + "signal-text-poor": "Señal: Débil", + "signal-text-unknown": "Señal: Desconocida", + "signal-text-very-poor": "Señal: Muy mala" + } + }, + "changelog": { + "error": { + "fetch-failed": "No se pudieron cargar los datos del registro de cambios. Inténtalo de nuevo más tarde.", + "rate-limit": "Se alcanzó el límite de GitHub. Inténtalo de nuevo en unos minutos." + }, + "panel": { + "buttons-discord": "Únete a nuestro Discord", + "buttons-dismiss": "OK", + "buttons-feedback": "Dejar comentario", + "empty": "Las notas de la versión aún no están disponibles.", + "highlight-title": "Cambios destacados", + "released": "Publicado el {date}", + "subtitle-fresh": "¡Gracias por instalar Noctalia! Esto es lo que incluye esta compilación.", + "subtitle-updated": "Actualizado desde {previousVersion}", + "title": "Novedades en {version}", + "version": "Versión {version}", + "version-new-user": "Instalación nueva" + } + }, + "common": { + "actions": "Acciones", + "active": "Activo", + "add": "Añadir", + "appearance": "Apariencia", + "apply": "Aplicar", + "auto-connect": "Conexión automática", + "automation": "Automatización", + "available": "Disponible", + "back": "Atrás", + "battery": "Batería", + "behavior": "Comportamiento", + "bluetooth": "Bluetooth", + "brightness": "Brillo", + "browse": "Navegar", + "calendar": "Calendario", + "calendar-panel": "Panel del calendario", + "cancel": "Cancelar", + "cards": "Tarjetas", + "charging": "Cargando", + "clear": "Borrar", + "clipboard": "Portapapeles", + "close": "Cerrar", + "color-muted": "Silenciado", + "colors": "Colores", + "command": "Comando", + "confirm": "Confirmar", + "connect": "Conectar", + "connected": "Conectado", + "connecting": "Conectando...", + "continue": "Continuar", + "contributors": "Colaboradores", + "copied-to-clipboard": "Copiado al portapapeles", + "countdown": "Cuenta regresiva", + "customize": "Personalizar", + "date": "Fecha", + "default": "Predeterminado", + "delete": "Borrar", + "devices": "Dispositivos", + "disabled": "Desactivado", + "discharging": "Descarga", + "disconnect": "Desconectar", + "disconnected": "Desconectado", + "disconnecting": "Desconectando...", + "display-mode": "Modo de pantalla", + "documentation": "Documentación", + "download": "Descargar", + "duration": "Duración", + "dysfunctional": "Disfuncional", + "edit": "Editar", + "enabled": "Activado", + "error": "Error", + "ethernet": "Ethernet", + "events": "Eventos", + "execute": "Ejecutar", + "faithful": "Fiel", + "focus": "Enfoque", + "font-weight-bold": "Negrita", + "font-weight-light": "Fina", + "font-weight-medium": "Medio", + "font-weight-regular": "Regular", + "font-weight-semibold": "Seminegrita", + "frequency": "Frecuencia", + "gateway": "Puerta de enlace", + "general": "General", + "height": "Altura", + "hibernate": "Hibernar", + "history": "Historia", + "icon": "Icono", + "idle": "Inactivo", + "info": "Información", + "input": "Entrada", + "install": "Instalar", + "installed": "Instalado", + "interface": "Interfaz", + "internet": "Internet", + "keybind": "Atajo de teclado", + "language": "Idioma", + "loading": "Cargando...", + "local": "Local", + "location": "Ubicación", + "lock": "Bloquear", + "logout": "Cerrar sesión", + "look": "Aspecto", + "margins": "Márgenes", + "media": "Medios", + "media-player": "Reproductor multimedia", + "memory": "Memoria", + "monitors": "Monitores", + "move-down": "Mover hacia abajo", + "move-up": "Mover arriba", + "network": "Red", + "next": "Siguiente", + "night-light": "Luz nocturna", + "no": "No", + "no-results": "Sin resultados", + "no-summary": "Sin resumen", + "none": "Ninguno", + "not-found": "No encontrado", + "notifications": "Notificaciones", + "official": "Oficial", + "on-surface": "En superficie", + "output": "Salida", + "pair": "Emparejar", + "paired": "Emparejado", + "pairing": "Emparejando...", + "panels": "Paneles", + "password": "Contraseña", + "pause": "Pausa", + "performance": "Rendimiento", + "pin": "Anclar", + "play": "Reproducir", + "polling": "Sondeo", + "position": "Posición", + "previous": "Anterior", + "primary": "Primario", + "random": "Aleatorio", + "reboot": "Reiniciar", + "reboot-to-uefi": "Reiniciar a UEFI", + "record": "Iniciar grabación", + "refresh": "Refrescar", + "required": "(obligatorio)", + "reset": "Restablecer", + "reset-to-default": "Restablecer a Valores Predeterminados", + "result-count": "{count} resultado", + "result-count-plural": "{count} resultados", + "resume": "Reanudar", + "save": "Guardar", + "scanning": "Escaneando...", + "screen-corners": "Esquinas de la pantalla", + "search": "Buscar", + "second": "{count} segundo", + "second-plural": "{count} segundos", + "secondary": "Secundario", + "security": "Seguridad", + "select": "Seleccionar", + "select-color": "Seleccionar color", + "select-color-description": "Aplicar colores del tema para enfatizar.", + "select-icon-color": "Seleccionar color de icono", + "settings": "Ajustes", + "shortcuts": "Atajos", + "shutdown": "Apagar", + "signal": "Señal", + "sound": "Sonido", + "sources": "Fuentes", + "start": "Comienzo", + "stop": "Parar", + "supporters": "Patrocinadores", + "suspend": "Suspender", + "templates": "Plantillas", + "tertiary": "Terciario", + "test": "Prueba", + "thresholds": "Umbrales", + "title": "Título", + "toast": "Emergente", + "trusted": "De confianza", + "uninstall": "Desinstalar", + "unknown": "Desconocido", + "unpair": "Desvincular", + "unpin": "Desanclar", + "update": "Actualizar", + "upload": "Subir", + "username": "Nombre de usuario", + "userspace-reboot": "Reiniciar espacio de usuario", + "version": "Versión", + "vibrant": "Vibrante", + "visualizer": "Visualizador", + "volume": "Volumen", + "volumes": "Volúmenes", + "wallpaper": "Fondo de pantalla", + "weather": "Clima", + "weather-loading": "Cargando el clima...", + "weather-no-location": "Establece una ubicación en Ajustes para ver el tiempo", + "week": "Semana", + "widgets": "Widgets", + "width": "Ancho", + "wifi": "Wi-Fi", + "windows": "Ventanas", + "yes": "Sí" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Perfil de energía", + "tooltip-disabled": "Instala power-profiles-daemon para usar perfiles de energía" + }, + "wifi": { + "label-disconnected": "Wi-Fi desconectado", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Ocultar siempre", + "always-show": "Mostrar siempre", + "force-open": "Forzar abierto", + "on-hover": "Al pasar" + }, + "hide-modes": { + "auto-hide": "Ocultación Automática", + "hidden": "Ocultar cuando vacío", + "idle": "Ocultar cuando inactivo", + "non-exclusive": "No exclusivo", + "transparent": "Transparente cuando esté vacío", + "visible": "Mostrar siempre" + }, + "launcher": { + "categories": { + "all": "Todo", + "audiovideo": "Audio y video", + "chat": "Chat", + "development": "Desarrollo", + "education": "Educación", + "emoji-activity": "Actividad", + "emoji-animals": "Animales", + "emoji-flags": "Banderas", + "emoji-food": "Comida y bebida", + "emoji-nature": "Naturaleza", + "emoji-objects": "Objetos", + "emoji-people": "Personas y cuerpo", + "emoji-recent": "Recientes", + "emoji-symbols": "Símbolos", + "emoji-travel": "Viajes y lugares", + "game": "Juegos", + "graphics": "Gráficos", + "misc": "Varios", + "office": "Oficina", + "pinned": "Anclado", + "system": "Sistema", + "webbrowser": "Navegador web" + }, + "providers": { + "applications": "Aplicaciones", + "calculator": "Calculadora", + "calculator-deprecated": "El comando >calc está obsoleto y será eliminado pronto. Por favor, en su lugar escribe expresiones matemáticas directamente en la búsqueda.", + "calculator-description": "Calculadora — evalúa expresiones matemáticas", + "calculator-enter-expression": "Introduce una expresión matemática", + "calculator-press-enter-to-copy": "Pulsa Enter para copiar el resultado", + "clipboard": "Historial del portapapeles", + "clipboard-clear-description": "Limpiar todo el historial del portapapeles", + "clipboard-clear-description-full": "Eliminar todos los elementos del historial del portapapeles", + "clipboard-clear-history": "Limpiar historial del portapapeles", + "clipboard-delete": "Borrar entrada del portapapeles", + "clipboard-history-disabled": "Historial del portapapeles desactivado", + "clipboard-history-disabled-description": "Activa el historial del portapapeles en la configuración o instala cliphist", + "clipboard-loading": "Cargando historial del portapapeles...", + "clipboard-search-description": "Buscar en el historial del portapapeles", + "command-description": "Ejecutar comandos de shell", + "emoji": "Selector de emojis", + "emoji-loading": "Cargando emojis...", + "emoji-loading-description": "Por favor espera", + "emoji-no-recent": "No hay emojis recientes", + "emoji-search-description": "Buscar y copiar emojis", + "settings-search-description": "Buscar en los ajustes", + "windows-search-description": "Buscar y enfocar ventanas abiertas" + } + }, + "lock-screen": { + "authenticating": "Autentificando...", + "password": "Ingresa tu contraseña...", + "restart": "Reiniciar", + "shut-down": "Apagar" + }, + "notifications": { + "panel": { + "click-to-collapse": "Clic para contraer", + "click-to-expand": "Haz clic para expandir", + "description": "Tus notificaciones aparecerán aquí a medida que lleguen.", + "no-notifications": "No hay notificaciones" + }, + "range": { + "earlier": "Anteriores", + "today": "Hoy", + "yesterday": "Ayer" + }, + "time": { + "diff-d": "hace 1 día", + "diff-dd": "hace {diff} días", + "diff-h": "hace 1 hora", + "diff-hh": "hace {diff} horas", + "diff-m": "hace 1 minuto", + "diff-mm": "hace {diff} minutos", + "now": "ahora" + } + }, + "options": { + "bar": { + "density-comfortable": "Cómoda", + "density-compact": "Compacta", + "density-default": "Predeterminada", + "density-mini": "Mini", + "density-spacious": "Espaciosa", + "display-mode-always-visible": "Siempre visible", + "type-floating": "Flotante", + "type-framed": "Con marco", + "type-simple": "Simple" + }, + "control-center": { + "quick-settings-style-classic": "Clásico", + "quick-settings-style-modern": "Moderno" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "Cómodo", + "compact": "Compacto", + "default": "Predeterminado" + }, + "launcher-view-mode": { + "grid": "Cuadrícula", + "list": "Lista" + }, + "notification-density": { + "compact": "Compacto", + "default": "Predeterminado" + }, + "scrolling-modes": { + "always": "Desplazar siempre", + "hover": "Desplazar encima", + "never": "No desplazar nunca" + }, + "session-menu-grid-layout": { + "grid": "Cuadrícula", + "single-row": "Fila única" + }, + "settings-panel-mode": { + "attached": "Panel adjunto a la barra", + "centered": "Panel centrado", + "window": "Ventana separada" + }, + "visualizer-types": { + "linear": "Lineal", + "mirrored": "Espejado", + "wave": "Onda" + }, + "visualizer-visibility": { + "with-background": "Solo con fondo" + }, + "workspace-labels": { + "index": "Índice", + "index-and-name": "Índice y Nombre", + "name": "Nombre" + } + }, + "panels": { + "about": { + "become-supporter": "Hazte patrocinador", + "changelog": "Ver registro de cambios", + "changelog-on-startup": "Mostrar registro de cambios al actualizar", + "changelog-on-startup-desc": "Mostrar automáticamente el registro de cambios cuando Noctalia se actualice.", + "contributors-description": "¡Un saludo a nuestro increíble colaborador número {count}!", + "contributors-description-plural": "¡Un saludo a nuestros {count} increíbles colaboradores!", + "copy-info": "Copiar información", + "debug-disabled": "Modo de depuración deshabilitado", + "debug-enabled": "Modo de depuración activado", + "info-copied": "Información copiada al portapapeles", + "noctalia-available": "Disponible:", + "noctalia-desc": "Un shell de escritorio elegante y minimalista cuidadosamente diseñado para Wayland, construido con Quickshell.", + "noctalia-git-commit": "Commit de Git:", + "noctalia-installed-version": "Versión instalada:", + "noctalia-latest-version": "Última versión:", + "noctalia-qs-version": "Noctalia QS versión:", + "noctalia-title": "Shell Noctalia", + "privacy-policy": "Política de privacidad", + "support": "Apóyanos", + "supporter-badge": "Colaborador", + "supporters-desc": "¡Un enorme agradecimiento a nuestro increíble colaborador!", + "supporters-desc-plural": "¡Muchísimas gracias a nuestros {count} increíbles seguidores!", + "supporters-loading": "Cargando patrocinadores...", + "system-board": "Placa:", + "system-cpu": "CPU:", + "system-disk": "Disco:", + "system-gpu": "GPU:", + "system-host": "Host:", + "system-install-hint": "Instala fastfetch para ver la información del sistema", + "system-kernel": "Kernel:", + "system-loading": "Cargando información del sistema...", + "system-memory": "Memoria:", + "system-monitor": "Monitor:", + "system-not-installed": "fastfetch no está instalado", + "system-os": "SO:", + "system-packages": "Paquetes:", + "system-product": "Producto:", + "system-title": "Información del sistema", + "system-uptime": "Tiempo de actividad:", + "system-wm": "WM:", + "telemetry-data-copied": "Datos de telemetría copiados al portapapeles", + "telemetry-desc": "Ayuda a mejorar Noctalia compartiendo información anónima del sistema (resolución de pantalla, compositor, distribución). Se envía una vez al inicio, sin seguimiento, los datos se eliminan automáticamente después de 30 días.", + "telemetry-enabled": "Enviar información anónima del sistema", + "telemetry-show-data": "Ver datos", + "telemetry-title": "Privacidad", + "title": "Acerca de", + "up-to-date": "¡Estás al día!", + "update-available": "Actualización disponible", + "view-commit": "Ver commit en GitHub" + }, + "audio": { + "devices-desc": "Configura los dispositivos de entrada y salida de audio disponibles.", + "devices-input-device-description": "Selecciona el dispositivo preferido de entrada de audio.", + "devices-input-device-label": "Dispositivo de entrada", + "devices-output-device-description": "Selecciona el dispositivo preferido de salida de audio.", + "devices-output-device-label": "Dispositivo de salida", + "devices-title": "Dispositivos de audio", + "external-mixer-description": "Ingrese el comando o la ruta de la aplicación para ejecutar al activar la función de mezclador de audio externo.", + "external-mixer-label": "Comando de mezclador de audio externo", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Establece tus aplicaciones multimedia preferidas e ignoradas.", + "media-excluded-player-description": "Agrega palabras clave para los reproductores que deseas que el sistema ignore. Cada palabra clave debe estar en una línea nueva.", + "media-excluded-player-label": "Reproductor excluido", + "media-excluded-player-placeholder": "escribe una subcadena y presiona +", + "media-frame-rate-description": "Tasas más altas son más fluidas pero usan más recursos.", + "media-frame-rate-label": "Tasa de fotogramas", + "media-primary-player-description": "Introduce una palabra clave para identificar tu reproductor principal.", + "media-primary-player-label": "Reproductor principal", + "media-primary-player-placeholder": "p. ej. spotify, vlc, mpv", + "media-scrolling-speed-description": "Tiempo en segundos para que el título se desplace del inicio al final.", + "media-scrolling-speed-label": "Velocidad de desplazamiento", + "media-scrolling-title-description": "Activar desplazamiento continuo para títulos de medios largos.", + "media-scrolling-title-label": "Título con desplazamiento", + "media-title": "Reproductores multimedia", + "on-middle-clicked-description": "Comando para ejecutar cuando se hace clic con el botón central.", + "panel-applications-empty": "Actualmente no hay aplicaciones reproduciendo audio", + "spectrum-mirrored-description": "Refleja el espectro para que las frecuencias bajas estén en el centro. Cuando está desactivado, las frecuencias bajas están a la izquierda y las altas a la derecha.", + "spectrum-mirrored-label": "Espectro espejo", + "title": "Audio", + "visualizer-type-description": "Elige un tipo de visualización para la reproducción de medios.", + "visualizer-type-label": "Tipo de visualización", + "volumes-desc": "Ajusta los controles de volumen y los niveles de audio.", + "volumes-feedback-sound-file-description": "Ruta al archivo de sonido que se reproduce al cambiar el volumen.", + "volumes-feedback-sound-file-label": "Archivo de sonido de feedback de volumen", + "volumes-feedback-sound-file-select-title": "Seleccionar archivo de sonido de retroalimentación de volumen", + "volumes-input-volume-description": "Nivel de volumen de entrada del micrófono.", + "volumes-mute-input-description": "Silencia la entrada de audio predeterminada (micrófono).", + "volumes-mute-input-label": "Silenciar entrada de audio", + "volumes-mute-output-description": "Silencia la salida de audio principal del sistema.", + "volumes-mute-output-label": "Silenciar salida de audio", + "volumes-output-volume-description": "Nivel de volumen de todo el sistema.", + "volumes-step-size-description": "Ajusta el tamaño del paso para los cambios de volumen (rueda del ratón, atajos de teclado).", + "volumes-step-size-label": "Tamaño del paso de volumen", + "volumes-volume-feedback-description": "Reproducir un sonido cuando se ajusta el volumen.", + "volumes-volume-feedback-label": "Reproducir sonido de cambio de volumen", + "volumes-volume-overdrive-description": "Permite subir el volumen por encima del 100 %. Es posible que no sea compatible con todo el hardware.", + "volumes-volume-overdrive-label": "Permitir sobreamplificación de volumen" + }, + "bar": { + "appearance-auto-hide-delay-description": "Tiempo que tarda la barra en ocultarse cuando se aparta el ratón", + "appearance-auto-hide-delay-label": "Demora de Ocultación", + "appearance-auto-hide-exclusive-note": "Nota: La zona exclusiva se desactiva automáticamente cuando la ocultación automática está activa", + "appearance-auto-show-delay-description": "Tiempo que tarda la barra en mostrarse cuando el ratón llega al borde", + "appearance-auto-show-delay-label": "Demora de Despliegue", + "appearance-background-opacity-description": "Establece la opacidad del fondo específicamente para la barra.", + "appearance-background-opacity-label": "Opacidad del fondo de la barra", + "appearance-capsule-color-description": "Elige un color para las cápsulas de la barra, o no uses ninguno para el color de superficie predeterminado.", + "appearance-capsule-color-label": "Color de la cápsula", + "appearance-capsule-opacity-description": "Establecer el nivel de opacidad para los fondos de los widgets cuando se muestra la cápsula.", + "appearance-capsule-opacity-label": "Opacidad de la cápsula", + "appearance-content-padding-description": "Ajusta el espacio entre los bordes de la barra y los widgets.", + "appearance-content-padding-label": "Espaciado del contenido", + "appearance-density-description": "Ajusta el relleno de la barra para un aspecto compacto o espacioso.", + "appearance-density-label": "Densidad de la barra", + "appearance-desc": "Personaliza la apariencia y posición de la barra.", + "appearance-display-mode-description": "Elige cuando será visible la barra.", + "appearance-enable-exclusion-zone-inset-description": "Reduce la zona de exclusión en 1 píxel físico para que las ventanas a ras se extiendan perfectamente por debajo del borde de la barra.", + "appearance-enable-exclusion-zone-inset-label": "Zona de exclusión interior", + "appearance-floating-description": "Muestra la barra como una 'píldora' flotante.", + "appearance-floating-label": "Barra flotante", + "appearance-font-scale-description": "Ajustar la escala del tamaño de fuente para el texto mostrado en la barra.", + "appearance-font-scale-label": "Escala de fuente", + "appearance-frame-radius": "Radio interno", + "appearance-frame-settings-description": "Ajustar el grosor del marco y el radio de la esquina interior", + "appearance-frame-settings-label": "Ajustes del marco", + "appearance-frame-thickness": "Grosor", + "appearance-hide-on-overview-description": "Ocultar la barra y cerrar los paneles cuando la vista general del compositor esté activa.", + "appearance-hide-on-overview-label": "Ocultar barra en la vista general", + "appearance-margins-description": "Ajusta los márgenes alrededor de la barra flotante.", + "appearance-margins-horizontal": "Margen horizontal", + "appearance-margins-vertical": "Margen vertical", + "appearance-outer-corners-description": "Muestra esquinas curvadas hacia afuera en la barra.", + "appearance-outer-corners-label": "Esquinas exteriores", + "appearance-position-description": "Elige dónde colocar la barra en la pantalla.", + "appearance-position-label": "Posición de la barra", + "appearance-show-capsule-description": "Mostrar fondos de los widgets.", + "appearance-show-capsule-label": "Mostrar cápsula", + "appearance-show-on-workspace-switch-description": "Mostrar la barra brevemente de forma automática cuando el espacio de trabajo cambie.", + "appearance-show-on-workspace-switch-label": "Mostrar barra al cambiar de espacio de trabajo", + "appearance-show-outline-description": "Muestra un borde visible alrededor de cada widget.", + "appearance-show-outline-label": "Mostrar contornos de widgets", + "appearance-type-description": "Elige el estilo de la barra: Simple, Flotante o Con marco.", + "appearance-type-label": "Tipo de barra", + "appearance-use-separate-opacity-description": "Permitir usar un valor de opacidad distinto para el fondo de la barra.", + "appearance-use-separate-opacity-label": "La barra usa opacidad independiente", + "appearance-widget-spacing-description": "Ajusta el espaciado entre cada widget en la barra.", + "appearance-widget-spacing-label": "Espaciado de widgets", + "behavior-middle-click-command-description": "Comando a ejecutar al hacer clic central.", + "behavior-middle-click-command-label": "Comando de Clic Central", + "behavior-middle-click-command-placeholder": "niri msg action alternar vista general", + "behavior-middle-click-description": "Elige qué hace el clic central en las áreas vacías de la Bar.", + "behavior-middle-click-follow-mouse-description": "Abrir el panel de clic central seleccionado en la posición del cursor.", + "behavior-middle-click-follow-mouse-label": "Clic central sigue al ratón", + "behavior-middle-click-label": "Acción de clic central en la Barra", + "behavior-right-click-command-description": "Comando a ejecutar al hacer clic derecho.", + "behavior-right-click-command-label": "Comando de Clic Derecho", + "behavior-right-click-command-placeholder": "notify-send \"Clic derecho\"", + "behavior-right-click-description": "Elige qué hace el clic derecho en las áreas vacías de la barra.", + "behavior-right-click-follow-mouse-description": "Abre el panel de clic derecho seleccionado en la posición del cursor.", + "behavior-right-click-follow-mouse-label": "Clic derecho sigue al ratón", + "behavior-right-click-label": "Acción de clic derecho en la Barra", + "behavior-wheel-wrap-description": "Cuando está activado, el desplazamiento continúa del último elemento al primero.", + "behavior-wheel-wrap-label": "Circular", + "behavior-workspace-scroll-description": "Elige qué hace la rueda del ratón en las zonas vacías de la barra.", + "behavior-workspace-scroll-label": "Acción de la rueda del ratón en la barra", + "behavior-workspace-scroll-option-content": "Contenido", + "behavior-workspace-scroll-option-workspace": "Espacio de trabajo", + "monitor-configure-widgets": "Configurar widgets", + "monitor-override-settings": "Anular la configuración global", + "monitor-override-settings-description": "Usar ajustes personalizados para este monitor.", + "monitor-reset-all": "Restablecer todo", + "monitor-widgets-title": "Configuración del Widget - {monitor}", + "monitors-desc": "Muestra la barra en monitores específicos. Por defecto, se muestra en todos si no se elige ninguno.", + "monitors-desc-new": "Configura qué monitores muestran la barra y personaliza los ajustes por monitor.", + "monitors-title": "Visualización en monitores", + "title": "Barra", + "tray-blacklist-description": "Agregar reglas de exclusión de la bandeja, admite comodines (*).", + "tray-blacklist-label": "Lista negra", + "tray-blacklist-placeholder": "p. ej., nm-applet, Fcitx*", + "tray-pin-application": "Fijar aplicación", + "tray-unpin-application": "Desfijar aplicación", + "use-global-widgets": "Usar widgets globales", + "widgets-desc": "Arrastra los widgets para cambiar su orden. Usa el menú contextual (clic derecho) para transferir widgets entre secciones o eliminarlos.", + "widgets-title": "Posicionamiento de widgets" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Genera esquemas de color a partir de tu fondo de pantalla. Extrae automáticamente los colores para crear un tema cohesivo.", + "color-source-use-wallpaper-colors-label": "Usar colores del fondo de pantalla", + "dark-mode-mode-description": "Permite el cambio automático entre el modo claro y el modo oscuro.", + "dark-mode-mode-label": "Programación del modo oscuro", + "dark-mode-mode-manual": "Manual", + "dark-mode-mode-off": "Apagado", + "dark-mode-switch-description": "Cambia a un tema más oscuro para una visualización más cómoda por la noche.", + "delete-error-description": "Error al eliminar {scheme}", + "delete-error-title": "Error al eliminar", + "delete-success-description": "{scheme} eliminado correctamente", + "delete-success-title": "Esquema de colores eliminado", + "download-button": "Descargar más", + "download-downloading": "Descargando...", + "download-empty": "No hay esquemas de colores disponibles", + "download-error-api-error": "Error de API: {status}", + "download-error-description": "Error al descargar {scheme}", + "download-error-download-failed": "Error al descargar con código de salida: {code}", + "download-error-invalid-response": "Formato de respuesta de API inválido", + "download-error-no-files": "No se encontraron archivos para el esquema", + "download-error-parse-failed": "Error al analizar la respuesta de la API: {error}", + "download-error-rate-limit": "Límite de velocidad de la API de GitHub excedido", + "download-error-title": "Error al descargar", + "download-fetching": "Obteniendo esquemas de colores disponibles...", + "download-success-description": "{scheme} descargado correctamente", + "download-success-title": "Esquema de colores descargado", + "download-title": "Descargar Esquemas de Color", + "method-description": { + "content": "Esquema de Material Design con extracción de color de alta fidelidad que coincide estrechamente con los colores reales del contenido de origen.", + "dysfunctional": "Como Fiel, pero elige la segunda familia de colores más dominante como primaria.", + "faithful": "Intenta mantenerse cerca del color original mientras genera una paleta armoniosa.", + "fruit-salad": "Esquema de Material Design que produce una paleta alegre y vibrante con matices diversos y variados.", + "monochrome": "Esquema de Material Design que utiliza una escala de grises de un solo tono con un contenido cromático mínimo.", + "muted": "Genera tonos más suaves y desaturados para una apariencia tranquila y discreta.", + "rainbow": "Esquema de Material Design que genera colores en todo el espectro para una máxima diversidad de color.", + "tonal-spot": "Algoritmo predeterminado de Material Design. Crea paletas equilibradas utilizando variaciones tonales del color de origen.", + "vibrant": "Maximiza la saturación del color para paletas llamativas y audaces con tonos vívidos." + }, + "predefined-desc": "Elige entre una colección de esquemas de colores predefinidos.", + "predefined-generate-templates-label": "Generar plantillas para esquemas predefinidos", + "predefined-title": "Esquemas de colores predefinidos", + "sync-gsettings-description": "Adaptar el tema del sistema a la variante clara u oscura activa.", + "sync-gsettings-label": "Sincronizar tema del sistema", + "templates-desc": "Aplicar colores a aplicaciones externas.", + "templates-filter-description": "Mostrar plantillas de una categoría específica.", + "templates-filter-label": "Filtrar por categoría", + "templates-misc-description": "Crea tus propias plantillas.", + "templates-misc-label": "Avanzado", + "templates-misc-user-templates-description": "Habilita sólamente si sabes lo que estás haciendo — consulta nuestra documentación en línea.", + "templates-misc-user-templates-label": "Habilitar plantillas de usuario", + "templates-none-detected": "Ninguno detectado", + "templates-write-path": "Escribe: {filepath}", + "title": "Esquema de colores", + "wallpaper-method-description": "Elige tu método de generación de paleta favorito.", + "wallpaper-method-label": "Método de generación de paleta", + "wallpaper-monitor-source-description": "Selecciona qué monitor usar para extraer los colores del fondo de pantalla.", + "wallpaper-monitor-source-label": "Fuente de generación de color" + }, + "connections": { + "authentication-required": "Autenticación requerida", + "bluetooth-auto-connect-description": "Conectarse automáticamente a dispositivos emparejados de confianza cuando Bluetooth esté activado.", + "bluetooth-auto-connect-label": "Conectar automáticamente dispositivos emparejados", + "bluetooth-devices-unnamed": "Los dispositivos sin nombre no se muestran.", + "bluetooth-discoverable": "Este dispositivo es detectable como {hostName} mientras esta pestaña de ajustes esté abierta.", + "bluetooth-rssi-polling-description": "Muestrea periódicamente el RSSI de los dispositivos conectados a través de bluetoothctl. Puede que no esté disponible para todos los dispositivos; usa recursos mínimos cuando está activado.", + "bluetooth-rssi-polling-interval-description": "Configura con qué frecuencia actualizar la intensidad de la señal para los dispositivos conectados.", + "bluetooth-rssi-polling-interval-label": "Intervalo de sondeo", + "bluetooth-rssi-polling-label": "Sondeo de señal Bluetooth", + "disable-discoverability-description": "Ocultar tu dispositivo de dispositivos Bluetooth cercanos.", + "disable-discoverability-label": "Desactivar visibilidad del dispositivo", + "hide-unnamed-devices-description": "Ocultar dispositivos que aparecen solo como direcciones Bluetooth.", + "hide-unnamed-devices-label": "Ocultar dispositivos sin nombre", + "pin-instructions": "Por favor, introduce el código PIN que aparece en tu dispositivo.", + "title": "Conexiones", + "wifi-header-text": "Usa Wi-Fi para ver las redes disponibles y conectarte a internet." + }, + "control-center": { + "cards-desc": "Personaliza qué controles aparecen en el centro de control y en qué orden.", + "desc": "Configurar el posicionamiento y el comportamiento del panel del centro de control.", + "open-at-mouse-description": "Al hacer clic derecho en un área vacía de la Bar, abre el Centro de Control cerca del cursor. Cuando está deshabilitado, se usa la configuración de posición anterior en su lugar.", + "open-at-mouse-label": "Abrir en el cursor al hacer clic derecho", + "position-description": "Elige dónde aparece el panel del centro de control cuando se abre.", + "shortcuts-custom-button-command-description": "El comando a ejecutar cuando se hace clic en el botón.", + "shortcuts-custom-button-enable-on-state-logic-description": "Habilita un segundo icono y un estado 'caliente' basado en un comando de comprobación.", + "shortcuts-custom-button-enable-on-state-logic-label": "Habilitar lógica de estado activado", + "shortcuts-custom-button-icon-description": "Selecciona un icono de la biblioteca.", + "shortcuts-custom-button-on-clicked-label": "Comando de clic izquierdo", + "shortcuts-custom-button-on-middle-clicked-description": "Comando para ejecutar al hacer clic con el botón central.", + "shortcuts-custom-button-on-state-command-description": "Comando a ejecutar para comprobar si el botón debe estar en el estado 'encendido'. Devuelve 0 para encendido, no cero para apagado.", + "shortcuts-custom-button-on-state-command-label": "Comando de comprobación de estado activado", + "shortcuts-custom-button-on-state-icon-description": "El icono para el botón cuando está en el estado 'encendido'.", + "shortcuts-custom-button-on-state-icon-label": "Icono de estado activado", + "shortcuts-custom-button-state-checks-add": "Añadir comprobación de estado", + "shortcuts-custom-button-state-checks-command": "Comando a ejecutar para esta comprobación de estado", + "shortcuts-custom-button-state-checks-label": "Comprobaciones de estado", + "shortcuts-custom-button-state-checks-remove": "Eliminar", + "shortcuts-custom-button-tooltip-description": "El texto emergente a mostrar al pasar el cursor sobre el botón.", + "shortcuts-custom-button-tooltip-label": "Texto emergente", + "shortcuts-title": "Widgets de accesos directos", + "system-monitor-disk-path-description": "Selecciona qué punto de montaje de disco debe monitorear la tarjeta del monitor del sistema en el centro de control.", + "system-monitor-disk-path-label": "Ruta del disco del monitor del sistema", + "title": "Centro de control" + }, + "desktop-widgets": { + "clock-enabled-description": "Mostrar un widget de reloj en el escritorio.", + "clock-enabled-label": "Activar widget de reloj", + "clock-format-label": "Formato", + "clock-height-description": "Altura del widget de reloj en píxeles.", + "clock-height-label": "Altura", + "clock-minimal-mode-description": "Usar una visualización de reloj minimalista con hora y fecha.", + "clock-minimal-mode-label": "Modo minimalista", + "clock-rounded-corners-description": "Usa esquinas redondeadas para el fondo del widget.", + "clock-rounded-corners-label": "Esquinas redondeadas", + "clock-show-background-description": "Mostrar el contenedor de fondo para el widget de reloj.", + "clock-show-background-label": "Mostrar fondo", + "clock-show-date-description": "Muestra la fecha actual debajo de la hora.", + "clock-show-date-label": "Mostrar fecha", + "clock-show-seconds-description": "Mostrar los segundos en la hora.", + "clock-show-seconds-label": "Mostrar segundos", + "clock-style-analog": "Analógico", + "clock-style-binary": "Binario", + "clock-style-description": "Elige el estilo de visualización del reloj.", + "clock-style-digital": "Digital", + "clock-style-label": "Estilo de reloj", + "clock-style-minimal": "Minimal", + "clock-width-description": "Ancho del widget de reloj en píxeles.", + "cpu-intensive-note": "Los widgets marcados con ! usan más CPU y deberían activarse con precaución.", + "edit-mode-button-label": "Entrar en modo de edición", + "edit-mode-controls-explanation": "Clic izquierdo y arrastrar: Mover o redimensionar el widget.\nClic derecho: Abrir las opciones del menú contextual.", + "edit-mode-description": "Habilita el modo de edición para mover y cambiar la posición de los widgets del escritorio. Cuando está habilitado, los widgets muestran un contorno de arrastre y se pueden reposicionar.", + "edit-mode-exit-button": "Salir del modo de edición", + "edit-mode-grid-snap-label": "Ajustar a cuadrícula", + "edit-mode-grid-snap-scale-label": "Escala de ajuste a la cuadrícula", + "edit-mode-label": "Modo de edición", + "enabled-description": "Activar o desactivar los widgets de escritorio por completo.", + "enabled-label": "Habilitar widgets de escritorio", + "general-desc": "Configura los widgets que aparecen en tu escritorio.", + "general-title": "Widgets de escritorio", + "media-player-enabled-description": "Mostrar un widget de reproductor multimedia en el escritorio.", + "media-player-enabled-label": "Habilitar widget de reproductor multimedia", + "media-player-rounded-corners-description": "Habilitar esquinas redondeadas en los bordes del widget.", + "media-player-show-album-art-description": "Mostrar la carátula del álbum y la información de las pistas (título y artista).", + "media-player-show-album-art-label": "Mostrar la carátula y el título del álbum", + "media-player-show-background-description": "Mostrar el contenedor de fondo para el widget del reproductor multimedia.", + "media-player-show-buttons-description": "Mostrar botones de control de medios (reproducir/pausar, anterior, siguiente) en el widget del reproductor multimedia.", + "media-player-show-buttons-label": "Mostrar botones", + "media-player-show-visualizer-description": "Mostrar la superposición del visualizador de audio.", + "media-player-visualizer-type-description": "Elige un tipo de visualización para el fondo del reproductor multimedia de escritorio.", + "overview-enabled-description": "Mostrar widgets de escritorio mientras la vista general del compositor esté abierta.", + "overview-enabled-label": "Mostrar en la vista general", + "system-stat-layout-bottom": "Abajo", + "system-stat-layout-description": "Elige cómo se muestra la leyenda en relación con el gráfico.", + "system-stat-layout-label": "Diseño", + "system-stat-layout-side": "Lado", + "system-stat-rounded-corners-description": "Usar esquinas redondeadas para el fondo del widget.", + "system-stat-rounded-corners-label": "Esquinas Redondeadas", + "system-stat-show-background-description": "Mostrar el contenedor de fondo para el widget de estadísticas del sistema.", + "system-stat-show-background-label": "Mostrar fondo", + "system-stat-stat-type-description": "Elige qué estadística del sistema mostrar.", + "system-stat-stat-type-label": "Tipo de Estadística", + "title": "Widgets de escritorio", + "weather-enabled-description": "Mostrar un widget del clima en el escritorio.", + "weather-enabled-label": "Activar widget del clima", + "weather-show-background-description": "Mostrar el contenedor de fondo para el widget del clima." + }, + "display": { + "monitors-backlight-device-auto-option": "Predeterminado", + "monitors-backlight-device-description": "Selecciona un dispositivo de retroiluminación para esta salida.", + "monitors-backlight-device-label": "Dispositivo de Retroiluminación", + "monitors-brightness-step-description": "Ajusta el tamaño del paso para los cambios de brillo (rueda de desplazamiento y atajos de teclado).", + "monitors-brightness-step-label": "Tamaño del paso de brillo", + "monitors-brightness-unavailable-ddc-disabled": "Control de brillo no disponible. Habilita \"Soporte de brillo externo\" para controlar el brillo de esta pantalla.", + "monitors-brightness-unavailable-generic": "El control de brillo no está disponible para esta pantalla.", + "monitors-desc": "Ajustar el brillo de la configuración para cada pantalla.", + "monitors-enforce-minimum-description": "Resuelve el problema de la retroiluminación que se apaga completamente en algunas pantallas al 0% de brillo.", + "monitors-enforce-minimum-label": "Forzar brillo mínimo (1%)", + "monitors-external-brightness-description": "Habilitar soporte DDCUtil para controlar el brillo en pantallas externas mediante el protocolo DDC/CI.", + "monitors-external-brightness-label": "Soporte de brillo externo", + "monitors-global-brightness-description": "Ajustar el brillo de todos los monitores a la vez.", + "monitors-global-brightness-label": "Todos los monitores", + "monitors-title": "Configuración por monitor", + "night-light-auto-schedule-description": "Basado en la hora del atardecer y amanecer en {location} — recomendado.", + "night-light-auto-schedule-label": "Programación automática", + "night-light-desc": "Reduce la emisión de luz azul para ayudarte a dormir mejor y reducir la fatiga visual.", + "night-light-enable-description": "Aplica un filtro de color cálido para reducir la emisión de luz azul.", + "night-light-enable-label": "Activar luz nocturna", + "night-light-force-activation-description": "Ignora la programación y aplica el filtro nocturno inmediatamente.", + "night-light-force-activation-label": "Forzar activación", + "night-light-manual-schedule-description": "Establece horas personalizadas para el amanecer y el atardecer.", + "night-light-manual-schedule-label": "Programación manual", + "night-light-manual-schedule-select-start": "Seleccionar hora de inicio", + "night-light-manual-schedule-select-stop": "Seleccionar hora de finalización", + "night-light-manual-schedule-sunrise": "Hora del amanecer", + "night-light-manual-schedule-sunset": "Hora del anochecer", + "night-light-temperature-day": "Día", + "night-light-temperature-day-description": "Controla la temperatura durante el día.", + "night-light-temperature-description": "Establece la calidez del color para la noche y el día.", + "night-light-temperature-label": "Temperatura de color", + "night-light-temperature-night": "Noche", + "night-light-temperature-night-description": "Controla la temperatura durante la noche.", + "title": "Pantalla" + }, + "dock": { + "appearance-background-opacity-description": "Ajusta la opacidad del fondo del dock.", + "appearance-border-radius-description": "Ajustar el radio del borde del dock.", + "appearance-border-radius-label": "Radio de borde", + "appearance-colorize-icons-description": "Aplicar colores del tema a los iconos de aplicaciones del dock (solo aplicaciones no enfocadas).", + "appearance-colorize-icons-label": "Colorear iconos", + "appearance-dead-opacity-description": "Ajustar la opacidad de los iconos de las aplicaciones que no están en ejecución.", + "appearance-dead-opacity-label": "Opacidad inactivas", + "appearance-desc": "Personaliza el comportamiento y la apariencia del dock.", + "appearance-display-auto-hide": "Ocultar autom.", + "appearance-display-description": "Elige cómo se comporta el dock.", + "appearance-display-exclusive": "Exclusivo", + "appearance-dock-indicator-description": "Mostrar un pequeño indicador cuando el dock está oculto.", + "appearance-dock-indicator-label": "Indicador del dock", + "appearance-floating-distance-description": "Ajusta la distancia de flotación desde el borde de la pantalla.", + "appearance-floating-distance-label": "Distancia de flotación del dock", + "appearance-group-apps-description": "Agrupar varias ventanas de la misma app en una entrada del Dock.", + "appearance-group-apps-label": "Agrupar aplicaciones similares", + "appearance-group-click-action-cycle": "Alternar ventanas", + "appearance-group-click-action-description": "Elige qué hace el clic izquierdo para las aplicaciones agrupadas.", + "appearance-group-click-action-label": "Acción al hacer clic en aplicación agrupada", + "appearance-group-click-action-list": "Abrir lista de ventanas", + "appearance-group-context-menu-mode-description": "Elige cómo se comporta el menú contextual para las aplicaciones agrupadas.", + "appearance-group-context-menu-mode-extended": "Extendido", + "appearance-group-context-menu-mode-label": "Modo de menú de aplicaciones agrupado", + "appearance-group-context-menu-mode-list": "Lista de ventanas", + "appearance-group-indicator-style-description": "Elige cómo los indicadores de aplicaciones agrupadas muestran el estado de la ventana enfocada.", + "appearance-group-indicator-style-dots": "Puntos", + "appearance-group-indicator-style-label": "Estilo de indicador agrupado", + "appearance-group-indicator-style-number": "Número", + "appearance-hide-show-speed-description": "Ajusta la velocidad de la animación de ocultar/mostrar del dock.", + "appearance-hide-show-speed-label": "Velocidad de ocultar/mostrar", + "appearance-icon-size-description": "Ajusta el tamaño general del dock.", + "appearance-icon-size-label": "Tamaño del dock", + "appearance-inactive-indicators-description": "Mostrar indicadores en forma de pastillas para todas las aplicaciones, no solo para la que está activa actualmente.", + "appearance-inactive-indicators-label": "Indicadores de ejecución", + "appearance-indicator-color-description": "Elige el color del indicador del dock oculto.", + "appearance-indicator-color-label": "Color del indicador", + "appearance-indicator-opacity-description": "Ajusta la opacidad del indicador del dock oculto.", + "appearance-indicator-opacity-label": "Opacidad del indicador", + "appearance-indicator-thickness-description": "Usa un indicador del dock oculto más grueso (6 px en lugar de 3 px).", + "appearance-indicator-thickness-label": "Indicador más grueso", + "appearance-launcher-icon-description": "Elige el icono usado para el Dock Launcher.", + "appearance-launcher-icon-label": "Icono del Lanzador", + "appearance-launcher-position-description": "Elige dónde aparece el icono del lanzador en el dock.", + "appearance-launcher-position-end": "Fin", + "appearance-launcher-position-label": "Posición del lanzador", + "appearance-launcher-position-start": "Iniciar", + "appearance-launcher-use-distro-logo-description": "Usa el logo de tu distribución como icono del Dock Launcher.", + "appearance-launcher-use-distro-logo-label": "Usar el logo de la distro en lugar del icono", + "appearance-pinned-static-description": "Siempre empuja los iconos de las aplicaciones ancladas a la izquierda en orden estático.", + "appearance-pinned-static-label": "Aplicaciones ancladas estáticas", + "appearance-position-description": "Elige dónde aparece el dock en la pantalla.", + "appearance-position-label": "Posición", + "appearance-show-launcher-icon-description": "Mostrar el icono del lanzador de aplicaciones en el dock.", + "appearance-show-launcher-icon-label": "Mostrar lanzador de aplicaciones", + "appearance-sit-on-frame-description": "Alinear el dock dentro del borde del marco en lugar de que se superponga.", + "appearance-sit-on-frame-label": "El Dock se asienta en el marco", + "appearance-type-attached": "Anclado", + "appearance-type-description": "Elige entre una barra flotante con forma de píldora o una barra estática anclada al borde.", + "appearance-type-floating": "Flotante", + "appearance-type-label": "Estilo del dock", + "enabled-description": "Mostrar u ocultar el dock por completo.", + "enabled-label": "Habilitar dock", + "monitors-desc": "Mostrar el dock en monitores específicos. Por defecto, se muestra en todos si no se elige ninguno.", + "monitors-only-same-monitor-description": "Mostrar solo las aplicaciones del monitor donde se encuentra el dock.", + "monitors-only-same-monitor-label": "Solo aplicaciones del mismo monitor", + "monitors-title": "Visualización en monitor", + "title": "Dock" + }, + "general": { + "copy-settings": "Copiar ajustes", + "fonts-default-description": "Fuente principal utilizada en toda la interfaz.", + "fonts-default-label": "Fuente predeterminada", + "fonts-default-placeholder": "Seleccionar fuente predeterminada...", + "fonts-default-scale-description": "Aumentar o disminuir el tamaño del texto estándar.", + "fonts-default-scale-label": "Tamaño de fuente predeterminado", + "fonts-default-search-placeholder": "Buscar fuentes...", + "fonts-desc": "Elige las fuentes utilizadas en toda la interfaz.", + "fonts-monospace-description": "Fuente monoespaciada utilizada para la visualización de números y estadísticas.", + "fonts-monospace-label": "Fuente monoespaciada", + "fonts-monospace-placeholder": "Seleccionar fuente monoespaciada...", + "fonts-monospace-scale-description": "Aumentar o disminuir el tamaño del texto monoespaciado.", + "fonts-monospace-scale-label": "Tamaño de fuente monoespaciada", + "fonts-monospace-search-placeholder": "Buscar fuentes monoespaciadas...", + "fonts-title": "Fuentes", + "keybinds-conflict-description": "La combinación de teclas ya está asignada a {action}.", + "keybinds-conflict-title": "Conflicto de atajos de teclado", + "keybinds-description": "Configura las teclas de navegación globales para paneles y el lanzador.", + "keybinds-down": "Mover hacia abajo", + "keybinds-enter": "Confirmar / Acción", + "keybinds-escape": "Cerrar / Volver", + "keybinds-left": "Mover a la izquierda", + "keybinds-modifier-description": "Los atajos deben incluir una tecla modificadora (Ctrl o Alt).", + "keybinds-modifier-title": "Modificador requerido", + "keybinds-remove": "Eliminar / Quitar", + "keybinds-right": "Mover a la derecha", + "keybinds-title": "Atajos de teclado de navegación", + "keybinds-up": "Mover arriba", + "language-desc": "Elige tu idioma preferido para la aplicación.", + "language-select-auto-detect": "Automático", + "language-select-description": "Selecciona el idioma utilizado en la interfaz de la aplicación.", + "language-select-label": "Idioma de la aplicación", + "launch-setup-wizard": "Asistente de configuración", + "profile-desc": "Edita los detalles de tu usuario y tu avatar.", + "profile-picture-description": "Tu foto de perfil que aparece en toda la interfaz.", + "profile-picture-label": "Foto de perfil", + "profile-select-avatar": "Seleccionar imagen de avatar", + "profile-title": "Perfil", + "profile-tooltip": "Foto de perfil", + "reverse-scrolling-description": "Invertir dirección de desplazamiento interpretada.", + "reverse-scrolling-label": "Desplazamiento inverso", + "screen-corners-desc": "Personaliza el redondeo de las esquinas de la pantalla y los efectos visuales.", + "screen-corners-radius-description": "Ajusta las esquinas redondeadas de la pantalla.", + "screen-corners-radius-label": "Radio de las esquinas de la pantalla", + "screen-corners-radius-reset": "Restablecer el radio de las esquinas de la pantalla", + "screen-corners-show-corners-description": "Muestra esquinas redondeadas en el borde de la pantalla.", + "screen-corners-show-corners-label": "Mostrar esquinas de la pantalla", + "screen-corners-solid-black-description": "Usa negro sólido en lugar del color de fondo de la barra.", + "screen-corners-solid-black-label": "Esquinas negras sólidas", + "screen-corners-title": "Esquinas de la pantalla", + "settings-copied": "Ajustes copiados al portapapeles", + "smooth-scrolling-description": "Animar el desplazamiento de listas para una experiencia de rueda más fluida.", + "smooth-scrolling-label": "Desplazamiento suave", + "tab-basics": "Básico", + "tab-keybinds": "Atajos de teclado" + }, + "hooks": { + "color-generation-description": "Comando a ejecutar cuando la generación de color se complete (p. ej., después de la tematización basada en el fondo de pantalla).", + "color-generation-label": "Colores generados", + "color-generation-placeholder": "p. ej. notify-send \"Colores\" \"Generados\"", + "info-command-info-description": "• Los comandos se ejecutan a través de shell (sh -lc)
• Los comandos se ejecutan en segundo plano (desvinculados)
• Los botones de prueba se ejecutan con los valores actuales", + "info-parameters-description": "• Wallpaper hook: $1 = ruta del fondo de pantalla, $2 = nombre de la pantalla, $3 = tema (oscuro/claro)
• Theme toggle hook: $1 = true/false (estado del modo oscuro)
• Screen lock/unlock hooks: $1 = lock/unlock (estado de bloqueo de pantalla)
• Performance mode hooks: Sin parámetros
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "Parámetros disponibles", + "noctalia-started-description": "Comando para ejecutar cuando Noctalia ha terminado de cargarse.", + "noctalia-started-label": "Noctalia iniciado", + "noctalia-started-placeholder": "p. ej. notify-send 'Noctalia cargado'", + "performance-mode-disabled-description": "Comando a ejecutar cuando se deshabilita el modo de rendimiento Noctalia.", + "performance-mode-disabled-label": "Modo de rendimiento deshabilitado", + "performance-mode-disabled-placeholder": "p. ej. notify-send \"Rendimiento\" \"Modo deshabilitado\"", + "performance-mode-enabled-description": "Comando a ejecutar cuando se habilita el modo de rendimiento Noctalia.", + "performance-mode-enabled-label": "Modo de rendimiento habilitado", + "performance-mode-enabled-placeholder": "p. ej. notify-send \"Rendimiento\" \"Modo habilitado\"", + "screen-lock-description": "Comando a ejecutar cuando se bloquea la pantalla.", + "screen-lock-label": "Pantalla bloqueada", + "screen-lock-placeholder": "p. ej. notify-send \"Pantalla\" \"Bloqueada\"", + "screen-unlock-description": "Comando a ejecutar cuando se desbloquea la pantalla.", + "screen-unlock-label": "Pantalla desbloqueada", + "screen-unlock-placeholder": "p. ej. notify-send \"Pantalla\" \"Desbloqueada\"", + "session-description": "Comando que se ejecutará antes del apagado o reinicio. Recibe el tipo de acción como $1 (apagado/reinicio).", + "session-label": "Fin de la sesión", + "session-placeholder": "p. ej. notify-send \"Session\" \"$1\"", + "system-hooks-desc": "Configura comandos para que se ejecuten cuando ocurran eventos del sistema.", + "system-hooks-enable-description": "Activa o desactiva todos los comandos hook.", + "system-hooks-enable-label": "Activar hooks", + "system-hooks-title": "Hooks del sistema", + "theme-changed-description": "Comando a ejecutar cuando el tema cambia entre modo oscuro y claro.", + "theme-changed-label": "Tema cambiado", + "theme-changed-placeholder": "p. ej. notify-send \"Tema\" \"Cambiado\"", + "title": "Hooks", + "wallpaper-changed-description": "Comando a ejecutar cuando cambia el fondo de pantalla.", + "wallpaper-changed-label": "Fondo de pantalla cambiado", + "wallpaper-changed-placeholder": "p. ej. notify-send \"Fondo de pantalla\" \"Cambiado\"" + }, + "idle": { + "custom-add": "Añadir comando", + "custom-description": "Ejecuta un comando de shell después de un período de inactividad.", + "custom-entry-command": "Comando", + "custom-entry-delete": "Eliminar", + "custom-entry-edit": "Editar Comando Personalizado", + "custom-entry-name": "Nombre", + "custom-entry-name-placeholder": "p. ej. Apagar luces", + "custom-entry-new": "Nuevo Comando Personalizado", + "custom-entry-timeout": "Tiempo de inactividad", + "custom-entry-timeout-format": "{count} segundo", + "custom-entry-timeout-format-plural": "{count} segundos", + "custom-entry-unnamed": "Comando sin nombre", + "custom-label": "Comandos de inactividad personalizados", + "enable-description": "Apagar la pantalla, bloquear o suspender automáticamente después de un período de inactividad.", + "enable-label": "Activar gestión de inactividad", + "fade-duration-description": "Segundos para la animación de fundido a negro antes de que se active cada acción. Cualquier movimiento del ratón cancela el fundido.", + "fade-duration-label": "Duración del fundido", + "lock-description": "Segundos de inactividad antes de que se active la pantalla de bloqueo.", + "lock-label": "Bloquear pantalla", + "resume-command-label": "Reanudar comando", + "screen-off-description": "Segundos de inactividad antes de que los monitores se apaguen.", + "screen-off-label": "Apagar pantalla", + "status-description": "Tiempo de inactividad según lo informado por el compositor.", + "status-label": "Tiempo de inactividad", + "suspend-description": "Segundos de inactividad antes de que el sistema se suspenda.", + "tab-behavior": "Comportamiento", + "tab-custom": "Personalizado", + "timeouts-description": "Establece en 0 para deshabilitar una etapa. Los tiempos de espera se pausan mientras 'Mantener despierto' está activo.", + "timeouts-label": "Tiempos de espera", + "title": "Inactividad", + "unavailable": "La monitorización nativa de inactividad no está disponible en este Compositor." + }, + "indicator": { + "default-value": "Predeterminado: {value}", + "system-default": "Predeterminado del sistema" + }, + "launcher": { + "clipboard-desc": "Accede y gestiona tu historial del portapapeles desde el lanzador.", + "execute-desc": "Configurar cómo se inician las aplicaciones.", + "execute-title": "Ejecución", + "settings-annotation-tool-description": "Comando para ejecutar al hacer clic en el botón de anotar en el historial del portapapeles. La imagen se enviará a este comando.", + "settings-annotation-tool-label": "Herramienta de anotación", + "settings-annotation-tool-placeholder": "p. ej. 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Pegar automáticamente el elemento seleccionado del portapapeles. Requiere wtype.", + "settings-auto-paste-label": "Pegado automático", + "settings-clip-chips-description": "Mostrar una barra de pestañas para filtrar el historial del portapapeles por tipo (Imágenes, Enlaces, Archivos, Código, etc.).", + "settings-clip-chips-label": "Habilitar chips de categoría", + "settings-clip-preview-description": "Muestra una vista previa del contenido del portapapeles al usar el comando >clip.", + "settings-clip-preview-label": "Activar vista previa del portapapeles", + "settings-clip-smart-icons-description": "Mostrar iconos específicos para enlaces, archivos, colores y código en lugar de un icono de portapapeles genérico.", + "settings-clip-smart-icons-label": "Habilitar iconos inteligentes", + "settings-clip-wrap-text-description": "Ajustar el texto en la lista del portapapeles en lugar de truncarlo.", + "settings-clip-wrap-text-label": "Ajustar texto del portapapeles", + "settings-clipboard-history-description": "Accede a los elementos copiados anteriormente desde el lanzador.", + "settings-clipboard-history-label": "Activar historial del portapapeles", + "settings-clipboard-watch-image-description": "Cadena de comando completa pasada a wl-paste para cambios de imagen (requiere reinicio).", + "settings-clipboard-watch-image-label": "Comando de vigilancia de imágenes", + "settings-clipboard-watch-text-description": "Cadena de comando completa pasada a wl-paste para cambios de texto (requiere reinicio).", + "settings-clipboard-watch-text-label": "Comando de vigilancia de texto", + "settings-custom-launch-prefix-description": "Prefijar comandos con un lanzador personalizado (p. ej. 'runapp' para integración con systemd).", + "settings-custom-launch-prefix-enabled-description": "Usar un prefijo personalizado para lanzar aplicaciones en lugar del método predeterminado.", + "settings-custom-launch-prefix-enabled-label": "Habilitar prefijo de lanzamiento personalizado", + "settings-custom-launch-prefix-label": "Prefijo de lanzamiento personalizado", + "settings-density-description": "Ajusta el tamaño de los iconos de las aplicaciones y la densidad del Launcher.", + "settings-density-label": "Densidad", + "settings-desc": "Personaliza el comportamiento y la apariencia del lanzador.", + "settings-enable-session-search-description": "Mostrar acciones de sesión (bloquear, apagar, reiniciar, etc.) al buscar en el Launcher.", + "settings-enable-session-search-label": "Habilitar búsqueda de sesión", + "settings-enable-settings-search-description": "Mostrar los resultados de configuración al buscar en el lanzador.", + "settings-enable-settings-search-label": "Activar la búsqueda en la configuración", + "settings-enable-windows-search-description": "Buscar y enfocar ventanas activas.", + "settings-enable-windows-search-label": "Habilitar búsqueda de ventanas", + "settings-grid-view-description": "Mostrar elementos en una cuadrícula en lugar de una lista.", + "settings-icon-mode-description": "Usa iconos nativos del sistema en lugar de iconos Tabler.", + "settings-icon-mode-label": "Usar iconos nativos", + "settings-ignore-mouse-input-description": "Deshabilitar la interacción del ratón y la rueda de desplazamiento en el lanzador.", + "settings-ignore-mouse-input-label": "Ignorar la entrada del ratón", + "settings-overlay-layer-description": "Muestra el lanzador en la capa de superposición, encima de las ventanas a pantalla completa. Cuando está activado, el lanzador no se adjuntará a la barra.", + "settings-overlay-layer-label": "Mostrar sobre pantalla completa", + "settings-position-description": "Elige dónde aparece el panel del lanzador.", + "settings-show-categories-description": "Mostrar pestañas de categorías para filtrar aplicaciones.", + "settings-show-categories-label": "Mostrar categorías", + "settings-show-icon-background-description": "Mostrar un fondo rectangular redondeado detrás de los iconos.", + "settings-show-icon-background-label": "Mostrar fondo del icono", + "settings-sort-by-usage-description": "Cuando está activado, las aplicaciones más utilizadas aparecen primero en la lista.", + "settings-sort-by-usage-label": "Ordenar por más usados", + "settings-terminal-command-description": "Comando para iniciar el terminal. P. ej. 'kitty -e' o 'gnome-terminal --'.", + "settings-terminal-command-label": "Comando de terminal", + "settings-use-app2unit-description": "Usa un método de lanzamiento alternativo para gestionar mejor los procesos de las aplicaciones y prevenir problemas.", + "settings-use-app2unit-label": "Usar App2Unit para lanzar aplicaciones", + "settings-view-mode-description": "Elige la disposición para las entradas del Lanzador.", + "settings-view-mode-label": "Modo de vista", + "title": "Lanzador" + }, + "location": { + "auto-locate-description": "Detecta automáticamente tu ubicación periódicamente usando tu dirección IP.", + "auto-locate-label": "Auto-Localizar", + "calendar-cards-desc": "Organizar y activar/desactivar tarjetas en el panel del calendario.", + "calendar-cards-title": "Tarjetas de calendario", + "calendar-header-label": "Encabezado del calendario", + "calendar-month-label": "Mes del calendario", + "date-time-12hour-format-description": "Muestra la hora en formato de 12 horas en la pantalla de bloqueo y el calendario. El reloj de la barra tiene su propia configuración.", + "date-time-12hour-format-label": "Utiliza el formato de 12 horas", + "date-time-desc": "Personaliza cómo aparecen la fecha y la hora.", + "date-time-first-day-of-week-automatic": "Automático (usar la configuración regional del sistema)", + "date-time-first-day-of-week-description": "Elige qué día empieza la semana en el calendario.", + "date-time-first-day-of-week-label": "Primer día de la semana", + "date-time-show-events-description": "Mostrar eventos en el panel del calendario.", + "date-time-show-events-label": "Mostrar eventos del calendario", + "date-time-title": "Fecha y hora", + "date-time-use-analog-description": "Mostrar un reloj de estilo analógico en la ventana del calendario.", + "date-time-use-analog-label": "Usar reloj de estilo analógico", + "date-time-week-numbers-description": "Muestra la semana del año (p. ej. Semana 38) en el calendario.", + "date-time-week-numbers-label": "Mostrar números de semana", + "geolocate-now-button": "Localizar ahora", + "location-desc": "Obtén el clima y la programación de la luz nocturna precisos estableciendo tu ubicación.", + "location-search-description": "p. ej. Toronto, ON", + "location-search-label": "Buscar una ubicación", + "location-search-placeholder": "Ingresa el nombre de la ubicación", + "location-title": "Tu ubicación", + "weather-desc": "Elige tu unidad de temperatura preferida.", + "weather-enabled-description": "Mostrar información del clima en toda la interfaz y obtener datos meteorológicos. Cuando está desactivado, todos los elementos del clima estarán ocultos y no se realizarán solicitudes de red.", + "weather-enabled-label": "Activar clima", + "weather-fahrenheit-description": "Muestra la temperatura en Fahrenheit en lugar de Celsius.", + "weather-fahrenheit-label": "Mostrar temperatura en Fahrenheit (°F)", + "weather-hide-city-description": "Ocultar el nombre de la ciudad en las pantallas del tiempo en toda la interfaz.", + "weather-hide-city-label": "Ocultar nombre de la ciudad", + "weather-hide-timezone-description": "Ocultar la abreviatura de la zona horaria en las pantallas del tiempo en toda la interfaz.", + "weather-hide-timezone-label": "Ocultar zona horaria", + "weather-show-effects-description": "Muestra efectos visuales adicionales (como lluvia, nieve o relámpagos) en la tarjeta del clima.", + "weather-show-effects-label": "Mostrar efectos climáticos", + "weather-show-in-calendar-description": "Muestra el pronóstico del tiempo diario directamente en la vista de tu calendario.", + "weather-show-in-calendar-label": "Mostrar el clima en el calendario", + "weather-talia-mascot-always-description": "Siempre usa Talia emojis en lugar de tabler icons.", + "weather-talia-mascot-always-label": "Mostrar siempre los emojis del tiempo de Talia" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "Cuando fprintd (autenticación por huella dactilar) está activo, esta opción te permite iniciar sesión con tu contraseña en lugar de una huella dactilar.", + "allow-password-with-fprintd-label": "Permitir inicio de sesión con contraseña usando fprintd", + "auto-start-auth-description": "Inicia automáticamente la autenticación de huellas dactilares sin requerir una pulsación de tecla o un clic de botón.", + "auto-start-auth-label": "Autentificación de inicio automática", + "clock-format-description": "Personaliza el formato del reloj usando tokens de sintaxis de fecha/hora.", + "clock-format-label": "Formato del reloj", + "clock-style-analog": "Analógico", + "clock-style-custom": "Personalizado", + "clock-style-description": "Elige el estilo visual del reloj en la Pantalla de Bloqueo.", + "clock-style-digital": "Digital", + "clock-style-label": "Estilo de Reloj", + "compact-lockscreen-description": "Mostrar solo el campo de inicio de sesión y los controles del sistema, ocultando los widgets del clima y multimedia.", + "compact-lockscreen-label": "Pantalla de bloqueo compacta", + "enable-lockscreen-media-controls-description": "Mostrar controles interactivos de reproducción multimedia en la pantalla de bloqueo.", + "enable-lockscreen-media-controls-label": "Controles Multimedia de la Pantalla de Bloqueo", + "lock-on-suspend-description": "Bloquear la pantalla automáticamente al suspender el sistema.", + "lock-on-suspend-label": "Bloquear al suspender", + "lock-screen-animations-description": "Activar o desactivar las animaciones de la pantalla de bloqueo.", + "lock-screen-animations-label": "Animaciones de la Pantalla de Bloqueo", + "lock-screen-blur-strength-description": "Aplica un efecto de desenfoque al fondo de pantalla de la pantalla de bloqueo.", + "lock-screen-blur-strength-label": "Intensidad del desenfoque de la pantalla de bloqueo", + "lock-screen-tint-strength-description": "Aplica una superposición de tinte al fondo de pantalla de la pantalla de bloqueo.", + "lock-screen-tint-strength-label": "Intensidad del tinte de la pantalla de bloqueo", + "monitors-desc": "Mostrar pantalla de bloqueo en monitores específicos. Por defecto, se mostrará en todos si no se elige ninguno.", + "password-chars-description": "Iconos bonitos usados para ocultar tu contraseña.", + "password-chars-label": "Iconos de contraseña aleatorios", + "show-hibernate-description": "Mostrar la opción 'hibernar' en los controles de energía.", + "show-hibernate-label": "Mostrar hibernar", + "show-session-buttons-description": "Permitir el acceso a las opciones de sesión desde la pantalla de bloqueo.", + "show-session-buttons-label": "Opciones de sesión", + "title": "Pantalla de bloqueo" + }, + "network": { + "bluetooth-description": "Activar la gestión de Bluetooth.", + "bluetooth-rssi-polling-description": "Muestrear periódicamente el RSSI de los dispositivos conectados a través de bluetoothctl. Puede no estar disponible para todos los dispositivos; utiliza recursos mínimos cuando está habilitado.", + "bluetooth-rssi-polling-label": "Sondeo de señal Bluetooth", + "desc": "Gestiona las conexiones Wi-Fi y Bluetooth.", + "wifi-description": "Administrar redes inalámbricas (requiere NetworkManager)." + }, + "notifications": { + "duration-critical-urgency-description": "Cuánto tiempo permanecen visibles las notificaciones de prioridad crítica.", + "duration-critical-urgency-label": "Urgencia crítica", + "duration-desc": "Configura cuánto tiempo permanecen visibles las notificaciones según su nivel de urgencia.", + "duration-low-urgency-description": "Cuánto tiempo permanecen visibles las notificaciones de baja prioridad.", + "duration-low-urgency-label": "Urgencia baja", + "duration-normal-urgency-description": "Cuánto tiempo permanecen visibles las notificaciones de prioridad normal.", + "duration-normal-urgency-label": "Urgencia normal", + "duration-respect-expire-description": "Usa el tiempo de expiración establecido en la notificación.", + "duration-respect-expire-label": "Respetar tiempo de expiración", + "duration-title": "Duración de la notificación", + "history-clear-dismiss-description": "Borrar notificación del historial al descartarla.", + "history-clear-dismiss-label": "Borrar al descartar", + "history-critical-urgency-description": "Guardar notificaciones de prioridad crítica en el historial.", + "history-critical-urgency-label": "Guardar urgencia crítica en el historial", + "history-desc": "Controlar qué notificaciones se guardan en el historial según su nivel de urgencia.", + "history-low-urgency-description": "Guardar notificaciones de prioridad baja en el historial.", + "history-low-urgency-label": "Guardar urgencia baja en el historial", + "history-normal-urgency-description": "Guardar notificaciones de prioridad normal en el historial.", + "history-normal-urgency-label": "Guardar urgencia normal en el historial", + "monitors-desc": "Muestra notificaciones en monitores específicos. Por defecto, se muestra en todos si no se elige ninguno.", + "rules-action-block": "Bloque", + "rules-action-block-desc": "Salta completamente.", + "rules-action-hide": "Ocultar", + "rules-action-hide-desc": "Sin ventana emergente, sin sonido, se añade al historial.", + "rules-action-label": "Acción", + "rules-action-mute": "Silenciar", + "rules-action-mute-desc": "Sin sonido, aún muestra la ventana emergente y en el historial.", + "rules-add": "Añadir regla", + "rules-delete": "Eliminar regla", + "rules-description": "Coincidir nombre de la aplicación o contenido. Las reglas se comprueban en orden, y la primera coincidencia se aplica.", + "rules-edit": "Editar regla", + "rules-label": "Reglas de Filtrado", + "rules-pattern-label": "Patrón", + "rules-pattern-placeholder": "firefox, discord, o /regex/", + "rules-tab": "Reglas", + "settings-always-on-top-description": "Muestra las notificaciones sobre ventanas a pantalla completa y otras capas.", + "settings-background-opacity-description": "Ajustar la opacidad de los fondos de las notificaciones.", + "settings-density-description": "Elige la densidad de la tarjeta de notificación.", + "settings-density-label": "Densidad", + "settings-desc": "Configura la apariencia y el comportamiento de las notificaciones.", + "settings-do-not-disturb-description": "Desactiva todas las ventanas emergentes de notificación cuando está activado.", + "settings-enabled-description": "Activar o desactivar el demonio de notificaciones, requiere reiniciar Noctalia shell.", + "settings-enabled-label": "Activar notificaciones", + "settings-location-description": "Dónde aparecen las notificaciones en la pantalla.", + "settings-markdown-description": "Renderizar el contenido de las notificaciones usando formato Markdown.", + "settings-markdown-label": "Habilitar Markdown", + "sounds-desc": "Configurar efectos de sonido y volumen de notificaciones.", + "sounds-enabled-description": "Activar efectos de sonido para notificaciones entrantes.", + "sounds-enabled-label": "Activar sonidos de notificación", + "sounds-excluded-apps-description": "Omitir la reproducción del sonido de notificación configurado para aplicaciones específicas que tienen sus propios sonidos integrados.", + "sounds-excluded-apps-label": "Aplicaciones excluidas", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Ruta al archivo de sonido reproducido para notificaciones de prioridad crítica.", + "sounds-files-critical-label": "Sonido de urgencia crítica", + "sounds-files-critical-select-title": "Seleccionar archivo de sonido de urgencia crítica", + "sounds-files-desc": "Configurar archivos de sonido personalizados para diferentes niveles de urgencia de notificación.", + "sounds-files-low-description": "Ruta al archivo de sonido reproducido para notificaciones de prioridad baja.", + "sounds-files-low-label": "Sonido de urgencia baja", + "sounds-files-low-select-title": "Seleccionar archivo de sonido de urgencia baja", + "sounds-files-normal-description": "Ruta al archivo de sonido reproducido para notificaciones de prioridad normal.", + "sounds-files-normal-label": "Sonido de urgencia normal", + "sounds-files-normal-select-title": "Seleccionar archivo de sonido de urgencia normal", + "sounds-files-placeholder": "Introducir ruta al archivo de sonido", + "sounds-files-select-file": "Seleccionar archivo de sonido", + "sounds-files-title": "Archivos de sonido", + "sounds-files-unified-description": "Ruta al archivo de sonido reproducido para notificaciones.", + "sounds-files-unified-label": "Sonido de notificación", + "sounds-files-unified-select-title": "Seleccionar archivo de sonido de notificación", + "sounds-separate-description": "Usar diferentes archivos de sonido para notificaciones de prioridad baja, normal y crítica.", + "sounds-separate-label": "Usar sonidos diferentes por prioridad", + "sounds-title": "Configuración de sonido", + "sounds-unavailable-description": "Instala Qt6 Multimedia para habilitar los sonidos de notificación.", + "sounds-unavailable-label": "Sonidos de notificación no disponibles", + "sounds-volume-description": "Ajustar el nivel de volumen para los sonidos de notificación.", + "sounds-volume-label": "Volumen del sonido", + "toast-battery-description": "Mostrar una advertencia cuando el nivel de batería caiga por debajo de este porcentaje.", + "toast-battery-label": "Advertencia de batería", + "toast-desc": "Configurar la apariencia y el comportamiento de las notificaciones toast.", + "toast-keyboard-description": "Mostrar una notificación cuando cambie la distribución del teclado.", + "toast-keyboard-label": "Distribución del teclado", + "toast-media-description": "Mostrar un toast cuando cambie el estado de reproducción multimedia.", + "toast-media-label": "Multimedia" + }, + "osd": { + "always-on-top-description": "Mostrar OSD por encima de ventanas de pantalla completa y otras capas.", + "always-on-top-label": "Siempre encima", + "background-opacity-description": "Controls the transparency of the OSD background.", + "background-opacity-label": "Opacidad del fondo", + "description": "Configura indicadores superpuestos como volumen y brillo.", + "duration-auto-hide-description": "Ajusta el tiempo antes de que el OSD desaparezca.", + "duration-auto-hide-label": "Ocultar después de", + "duration-desc": "Cuánto tiempo permanece visible el OSD antes de ocultarse automáticamente.", + "duration-title": "Tiempo de ocultación automática", + "enabled-description": "Mostrar cambios de volumen y brillo en tiempo real.", + "enabled-label": "Activar visualización en pantalla (OSD)", + "events-desc": "Seleccione qué eventos activan la visualización en pantalla.", + "general-desc": "Configurar la visibilidad y el comportamiento del OSD.", + "location-description": "Dónde aparece la visualización en pantalla.", + "monitors-desc": "Mostrar el OSD en monitores específicos. Por defecto, en todos si no se elige ninguno.", + "title": "Visualización en pantalla", + "types-brightness-description": "Mostrar el OSD cuando cambie el brillo de la pantalla.", + "types-brightness-label": "Brillo", + "types-custom-text-description": "Mostrar el OSD para mensajes de texto personalizados desde IPC.", + "types-custom-text-label": "Texto personalizado", + "types-desc": "Seleccione los eventos que activan el OSD.", + "types-input-volume-description": "Mostrar el OSD cuando cambie el volumen del micrófono.", + "types-input-volume-label": "Volumen de entrada", + "types-lockkey-description": "Mostrar el OSD cuando se activen o desactiven Bloq Mayús, Bloq Num o Bloq Despl.", + "types-lockkey-label": "Teclas de bloqueo", + "types-media-description": "Mostrar OSD cuando cambie el estado de reproducción multimedia (reproducir, pausar, saltar).", + "types-media-label": "Reproducción de medios", + "types-title": "Eventos de activación OSD", + "types-volume-description": "Mostrar el OSD cuando cambie el volumen de salida de audio.", + "types-volume-label": "Volumen de salida" + }, + "plugins": { + "auto-update": "Actualizar plugins automáticamente", + "auto-update-description": "Actualizar automáticamente todos los plugins cuando se inicie la shell.", + "available-description": "Navegar e instalar plugins desde fuentes configuradas.", + "available-label": "Plugins disponibles", + "available-no-plugins-description": "Comprueba las fuentes de tu plugin o actualiza la lista.", + "available-no-plugins-label": "No hay plugins disponibles", + "check-for-updates": "Buscar actualizaciones", + "checking-for-updates": "Buscando actualizaciones...", + "collision-already-installed": "Este plugin ya está instalado", + "collision-custom-version-exists": "Ya hay instalada una versión personalizada de \"{source}\"", + "collision-official-version-exists": "La versión oficial de este plugin ya está instalada", + "development-disable": "Desactivar el modo de desarrollo para este complemento", + "development-enable": "Habilitar el modo de desarrollo para este plugin.", + "filter-downloaded": "Descargado", + "filter-not-downloaded": "No descargado", + "filter-tags-description": "Filtrar plugins por categoría o estado de descarga.", + "filter-tags-label": "Etiquetas", + "hot-reload-description": "Recarga automáticamente los plugins cuando sus archivos cambian. Útil para el desarrollo de plugins.", + "hot-reload-label": "Recarga en caliente (modo desarrollo)", + "hot-reloaded": "Plugin recargado: {name}", + "install-error": "Error al instalar: {error}", + "install-incompatible": "{plugin} requiere Noctalia v{version} o superior", + "install-success": "Se instaló {plugin} correctamente", + "installed-description": "Gestiona y configura todos los plugins instalados localmente.", + "installed-label": "Plugins instalados", + "installed-no-plugins-description": "Instale los plugins de la sección \"Disponible\".", + "installed-no-plugins-label": "No hay plugins instalados", + "installing": "Instalando {plugin}...", + "notify-updates": "Notificaciones de actualización de plugins", + "notify-updates-description": "Mostrar una notificación emergente cuando haya nuevas actualizaciones de plugins disponibles.", + "open-plugin-page": "Abrir página del plugin", + "open-plugins-tab": "Abrir la configuración de los plugins", + "plugin-settings-title": "Ajustes de {plugin}", + "refresh-refreshing": "Actualizando la lista de plugins...", + "refresh-tooltip": "Actualizar plugins disponibles", + "settings-error-not-loaded": "Plugin no cargado", + "settings-saved": "Ajustes del plugin guardados", + "settings-tooltip": "Ajustes del plugin", + "source-custom": "Fuente personalizada", + "sources-add-custom": "Añadir repositorio personalizado", + "sources-add-dialog-description": "Añadir un repositorio de GitHub como fuente de plugin.", + "sources-add-dialog-error": "Error al agregar la fuente del plugin", + "sources-add-dialog-name": "Nombre del repositorio", + "sources-add-dialog-name-placeholder": "Mis plugins personalizados", + "sources-add-dialog-success": "Fuente del plugin añadida con éxito", + "sources-add-dialog-title": "Añadir fuente del plugin", + "sources-add-dialog-url": "URL del repositorio", + "sources-description": "Gestionar repositorios de plugins.", + "sources-placeholder": "Mi repositorio genial", + "sources-remove-tooltip": "Eliminar fuente del plugin", + "title": "Plugins", + "translations-reloaded": "Traducciones recargadas: {name}", + "uninstall-dialog-description": "¿Está seguro de que quiere desinstalar {plugin}? Esto eliminará todos los datos del plugin.", + "uninstall-dialog-title": "Desinstalar plugin", + "uninstall-error": "Error al desinstalar: {error}", + "uninstall-success": "{plugin} desinstalado correctamente", + "uninstalling": "Desinstalando {plugin}...", + "update-all": "Actualizar todo ({count})", + "update-all-success": "Todos los plugins se actualizaron correctamente", + "update-available": "Nueva actualización de plugin disponible", + "update-available-plural": "Nuevas actualizaciones de plugins disponibles ({count})", + "update-error": "Error al actualizar el plugin: {plugin}: {error}", + "update-pending": "v{current} → v{new} (requiere Noctalia v{required})", + "update-success": "{plugin} actualizado a la versión {version}", + "update-version": "v{current} → v{new}", + "updating": "Actualizando..." + }, + "region": { + "title": "Región" + }, + "session-menu": { + "countdown-duration-description": "Establecer cuánto tiempo dura el temporizador de cuenta regresiva antes de ejecutar acciones de energía.", + "countdown-duration-label": "Duración de la cuenta regresiva", + "enable-countdown-description": "Mostrar un temporizador de cuenta regresiva antes de ejecutar acciones de energía.", + "enable-countdown-label": "Activar temporizador de cuenta regresiva", + "entries-desc": "Personalizar qué acciones de energía aparecen en el menú de sesión y en qué orden.", + "entries-title": "Acciones de energía", + "entry-settings-command-description": "Comando personalizado para ejecutar para esta acción. Dejar vacío para usar el comando del sistema predeterminado.", + "entry-settings-command-placeholder": "p. ej. systemctl poweroff", + "entry-settings-default-command-lock": "Pantalla de bloqueo interna (sin comando)", + "entry-settings-default-command-logout": "Cierre de sesión interno (sin comando)", + "entry-settings-default-info-description": "Si no se especifica un comando personalizado, se utilizará el comando predeterminado del sistema.", + "entry-settings-default-info-label": "Comando predeterminado", + "entry-settings-keybind-description": "Asigna una combinación de teclas para activar esta acción cuando el menú de sesión esté abierto.", + "entry-settings-keybind-placeholder": "Pulsa Grabar para asignar una tecla...", + "entry-settings-keybind-recording": "Grabando... Pulsa cualquier combinación de teclas", + "entry-settings-title": "Configurar {entry}", + "entry-settings-tooltip": "Configurar comando", + "general-desc": "Configurar el comportamiento y la apariencia del panel del menú de sesión.", + "large-buttons-layout-description": "Elige cómo se muestran los botones del menú de sesión.", + "large-buttons-layout-label": "Diseño de botones grandes", + "large-buttons-style-description": "Mostrar el menú de sesión con botones grandes en un diseño de cuadrícula.", + "large-buttons-style-label": "Estilo de botones grandes", + "position-description": "Elige dónde aparece el panel del menú de sesión cuando se abre.", + "show-header-description": "Mostrar el título y el botón de cerrar en la parte superior del menú de sesión.", + "show-header-label": "Mostrar encabezado", + "show-keybinds-description": "Mostrar sugerencias de atajos de teclado en las opciones de sesión.", + "show-keybinds-label": "Mostrar atajos de teclado" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Mantener los widgets de escritorio visibles mientras el modo de rendimiento Noctalia esté habilitado.", + "noctalia-performance-disable-desktop-widgets-label": "Habilitar widgets de escritorio en modo de rendimiento", + "noctalia-performance-disable-wallpaper-description": "Mantén visibles los fondos de pantalla del escritorio, la vista general y la pantalla de bloqueo mientras el modo de rendimiento Noctalia esté activado.", + "noctalia-performance-disable-wallpaper-label": "Habilitar el renderizado de fondo de pantalla en modo de rendimiento", + "title": "Sistema" + }, + "system-monitor": { + "critical-color-label": "Color crítico", + "custom-highlight-colors-title-label": "Colores de resaltado personalizados", + "disk-available-label": "Espacio en disco disponible", + "disk-section-label": "Uso de disco", + "enable-dgpu-monitoring-description": "Advertencia: Esto activará tu GPU dedicada (NVIDIA/AMD), lo que podría afectar significativamente la duración de la batería en portátiles con gráficos híbridos.", + "enable-dgpu-monitoring-label": "Habilitar la monitorización de la GPU dedicada", + "external-monitor-description": "Ingrese el comando o ruta de la aplicación para ejecutar al activar la aplicación externa del monitor del sistema.", + "external-monitor-label": "Comando del monitor del sistema externo", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "Configurar el comportamiento y la apariencia del monitor del sistema.", + "gpu-section-label": "Temperatura de la GPU", + "highlight-colors-section-label": "Colores de resaltado", + "polling-interval-label": "Intervalo de sondeo", + "polling-section-description": "Configure la frecuencia con la que se actualiza cada métrica del sistema.", + "polling-section-label": "Intervalos de sondeo", + "threshold-critical": "Crítico", + "threshold-warning": "Advertencia", + "thresholds-section-description": "Ajusta los umbrales de advertencia/crítico y los intervalos de sondeo para cada métrica del sistema.", + "use-custom-highlight-colors-description": "Cuando está desactivado, se usan los colores de resaltado predeterminados del tema.", + "use-custom-highlight-colors-label": "Usar colores de resaltado personalizados", + "warning-color-label": "Color de advertencia" + }, + "user-interface": { + "allow-panels-without-bar-description": "Cuando está activado, los paneles se pueden abrir en cualquier pantalla. Cuando está desactivado, los paneles solo se abrirán en las pantallas que tengan una barra, lo que puede reducir el uso de memoria.", + "allow-panels-without-bar-label": "Permitir paneles en pantallas sin barra", + "animation-disable-description": "Desactiva todas las animaciones para una experiencia más rápida y con mayor capacidad de respuesta.", + "animation-disable-label": "Desactivar animaciones de la interfaz de usuario", + "animation-speed-description": "Ajustar la velocidad global de la animación.", + "animation-speed-label": "Velocidad de animación", + "animation-speed-reset": "Restablecer la velocidad de la animación", + "appearance-desc": "Personaliza elementos visuales como texto emergente, bordes y sombras.", + "blur-behind-description": "Difumina el área detrás de las barras y los paneles usando el protocolo de desenfoque del compositor.", + "blur-behind-label": "Desenfoque de fondo", + "box-border-description": "Muestra un contorno alrededor de las áreas de contenido.", + "box-border-label": "Contorno del contenedor", + "box-border-radius-description": "Ajusta la redondez de las esquinas de las secciones principales del diseño, como barras laterales, tarjetas y paneles de contenido.", + "box-border-radius-label": "Radio del contenedor", + "control-border-radius-description": "Controla la curvatura de los elementos interactivos, incluyendo botones, interruptores y campos de texto.", + "control-border-radius-label": "Radio de entrada", + "desc": "Personaliza la apariencia, el ambiente y el comportamiento de la interfaz.", + "dim-desktop-description": "Atenuar el escritorio cuando los paneles o menús estén abiertos.", + "dim-desktop-label": "Atenuar escritorio", + "dimmer-opacity-description": "Define el nivel de opacidad para el atenuado del escritorio.", + "dimmer-opacity-label": "Opacidad del escritorio atenuado", + "panel-background-opacity-description": "Establecer la opacidad del fondo para todos los paneles (barra, lanzador, ajustes, etc...).", + "panel-background-opacity-label": "Opacidad del fondo del panel", + "panels-attached-to-bar-description": "Los paneles se fijan a la barra y a los bordes de la pantalla, creando una apariencia integrada con elegantes esquinas invertidas.", + "panels-attached-to-bar-label": "Ajustar paneles a los bordes", + "panels-overlay-description": "Garantiza que los paneles y la barra permanezcan visibles, incluso sobre aplicaciones en pantalla completa.", + "panels-overlay-label": "Mantener paneles y barra en la parte superior", + "scaling-description": "Cambia el tamaño de la interfaz de usuario general, excluyendo la barra.", + "scaling-label": "Escalado de la interfaz", + "scrollbar-always-visible-description": "Mantener las barras de desplazamiento visibles siempre que el contenido sea desplazable, en lugar de mostrarlas solo al pasar el cursor.", + "scrollbar-always-visible-label": "Mostrar siempre barras de desplazamiento", + "settings-panel-header": "Panel de Configuración", + "settings-panel-mode-description": "Elegir diseño de configuración (puede requerir reapertura).", + "settings-panel-mode-label": "Modo del panel de configuración", + "settings-panel-sidebar-card-style-description": "Envuelve la barra lateral de ajustes en un fondo relleno con esquinas redondeadas.", + "settings-panel-sidebar-card-style-label": "Estilo de Tarjeta de la Barra Lateral", + "shadows-description": "Habilita sombras paralelas debajo de las barras y los paneles.", + "shadows-direction-description": "Elige dónde se proyecta la sombra.", + "shadows-direction-label": "Dirección de la sombra", + "shadows-label": "Sombras paralelas", + "title": "Interfaz de usuario", + "tooltips-description": "Activar o desactivar los avisos emergentes en toda la interfaz.", + "tooltips-label": "Mostrar textos emergentes", + "translucent-widgets-description": "Haz que los botones, pestañas y otros widgets dentro de los paneles sean semitransparentes.", + "translucent-widgets-label": "Widgets translúcidos" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Alfabético", + "automation-change-mode-description": "Elija cómo se seleccionan los fondos de pantalla al cambiar automáticamente.", + "automation-change-mode-label": "Modo de cambio", + "automation-custom-interval-description": "Introduce el tiempo en formato HH:MM (p. ej. 01:30).", + "automation-custom-interval-label": "Intervalo personalizado", + "automation-interval-description": "Con qué frecuencia cambiar los fondos de pantalla automáticamente.", + "automation-interval-label": "Intervalo del fondo de pantalla", + "automation-random-wallpaper-description": "Programa cambios aleatorios del fondo de pantalla a intervalos regulares.", + "automation-scheduled-change-description": "Cambiar automáticamente los fondos de pantalla a intervalos regulares.", + "automation-scheduled-change-label": "Cambio programado", + "look-feel-edge-smoothness-description": "Aplica un efecto suave y difuminado al borde de las transiciones.", + "look-feel-edge-smoothness-label": "Suavizar borde de la transición", + "look-feel-fill-color-description": "Elige un color de relleno que pueda aparecer detrás del fondo de pantalla.", + "look-feel-fill-mode-description": "Selecciona cómo debe escalar la imagen para coincidir con la resolución de tu monitor.", + "look-feel-fill-mode-label": "Modo de relleno", + "look-feel-skip-startup-transition-description": "Omitir la animación del fondo de pantalla al iniciar la shell.", + "look-feel-skip-startup-transition-label": "Omitir transición de inicio", + "look-feel-title": "Apariencia y estilo", + "look-feel-transition-duration-description": "Duración de las animaciones de transición en segundos.", + "look-feel-transition-duration-label": "Duración de la transición", + "look-feel-transition-type-description": "Tipo de animación al cambiar entre fondos de pantalla.", + "look-feel-transition-type-label": "Tipo de transición", + "settings-clear-cache-button": "Borrar caché", + "settings-clear-cache-description": "Limpiar fondos de pantalla redimensionados en caché para liberar espacio en disco.", + "settings-clear-cache-label": "Caché de fondos de pantalla", + "settings-clear-cache-toast": "Caché de fondos de pantalla borrada", + "settings-desc": "Controla cómo se gestionan y muestran los fondos de pantalla.", + "settings-enable-management-description": "Gestiona fondos de pantalla con Noctalia. Desmarca si prefieres usar otra aplicación.", + "settings-enable-management-label": "Activar gestión de fondos de pantalla", + "settings-enable-overview-description": "Aplica un fondo de pantalla difuminado y atenuado a la pantalla de vista general.", + "settings-enable-overview-label": "Activar fondo de pantalla de vista general", + "settings-folder-description": "Ruta a tu carpeta principal de fondos de pantalla.", + "settings-folder-label": "Carpeta de fondos de pantalla", + "settings-hide-wallpaper-filenames-tooltip-hide": "Ocultar nombres de archivo", + "settings-hide-wallpaper-filenames-tooltip-show": "Mostrar nombres de archivo", + "settings-monitor-specific-description": "Establece una carpeta de fondos de pantalla diferente para cada monitor.", + "settings-monitor-specific-label": "Directorios específicos por monitor", + "settings-monitor-specific-tooltip": "Carpeta de fondos de pantalla del monitor", + "settings-overview-blur-strength-description": "Aplica la intensidad del desenfoque a la vista general.", + "settings-overview-blur-strength-label": "Intensidad del desenfoque de la vista general", + "settings-overview-tint-description": "Aplica la intensidad del tinte a la vista general.", + "settings-overview-tint-label": "Intensidad del tinte de la vista general", + "settings-recursive-search-description": "Buscar fondos de pantalla también en subcarpetas del directorio de fondos de pantalla.", + "settings-recursive-search-label": "Buscar en subcarpetas", + "settings-select-monitor-folder": "Seleccionar carpeta de fondos de pantalla del monitor", + "settings-selector-description": "Elige tu fondo de pantalla.", + "settings-selector-position-description": "Elige dónde aparece el panel selector de fondo de pantalla.", + "settings-show-hidden-files-tooltip-hide": "Ocultar archivos ocultos", + "settings-show-hidden-files-tooltip-show": "Mostrar archivos ocultos", + "settings-title": "Configuración del fondo de pantalla", + "settings-use-original-images-description": "Omitir el redimensionamiento de fondos de pantalla antes de mostrarlos. Ahorra espacio en disco y reduce el uso de CPU durante los cambios de fondo de pantalla, pero puede usar más memoria para imágenes muy grandes.", + "settings-use-original-images-label": "Usar imágenes originales", + "settings-view-mode-description": "Elige cómo se muestran los fondos de pantalla desde tu directorio.", + "settings-view-mode-label": "Modo de visualización", + "view-mode-browse": "Explorar directorios", + "view-mode-cycle-tooltip": "Modo de visualización: {mode} (clic para cambiar)", + "view-mode-recursive": "Incluir subdirectorios", + "view-mode-single": "Directorio raíz" + } + }, + "placeholders": { + "add-new-keybind": "Añadir atajo de teclado", + "command-example": "echo \"Hola, mundo\"", + "enter-command": "Introduce el comando a ejecutar (aplicación o script personalizado)", + "enter-ipc-identifier": "Introduce un identificador único para los comandos IPC", + "enter-path": "Introduce la ruta...", + "enter-text-to-collapse": "p. ej. 'no se está reproduciendo nada'. Usa /regex/ para patrones.", + "enter-tooltip": "Introduce texto emergente", + "enter-width-pixels": "Introduce el ancho en píxeles", + "keybind-recording": "Grabando atajo de teclado...", + "search": "Buscar...", + "search-icons": "p. ej. noctalia, niri, battery, cloud", + "search-launcher": "Buscar entradas... o usa > para comandos", + "search-wallhaven": "Buscar en Wallhaven...", + "search-wallpapers": "Escribe para filtrar fondos de pantalla..." + }, + "positions": { + "bottom": "Inferior", + "bottom-center": "Inferior central", + "bottom-left": "Inferior izquierda", + "bottom-right": "Inferior derecha", + "center": "Centro", + "center-left": "Izquierda centrado", + "center-right": "Derecha centrado", + "close-to-bar": "Cerca del botón de la barra", + "follow-bar": "Seguir barra", + "left": "Izquierda", + "right": "Derecha", + "top": "Superior", + "top-center": "Superior central", + "top-left": "Superior izquierda", + "top-right": "Superior derecha" + }, + "session-menu": { + "action-in-seconds": "{action} en {seconds} segundos...", + "cancel-timer": "Cancelar temporizador", + "title": "Menú de sesión" + }, + "setup": { + "all-done": "¡Listo!", + "appearance": { + "subheader": "Elige el modo oscuro y las fuentes de color (fondo de pantalla o predefinidas)." + }, + "customize": { + "header": "Personaliza tu experiencia", + "subheader": "Ajusta la posición de la barra, densidad, escala y más." + }, + "skip-setup": "Omitir la configuración", + "telemetry-wizard-done": "¡Entendido!", + "telemetry-wizard-note": "Tienes el control — activa o desactiva esto cuando quieras en los ajustes", + "telemetry-wizard-subtitle": "Hemos añadido analíticas anónimas para ayudar a mejorar Noctalia", + "telemetry-wizard-title": "Actualización de privacidad", + "wallpaper": { + "choose-dir": "Elige un directorio que contenga tus fondos de pantalla", + "dir-browse": "Seleccionar carpeta", + "dir-description": "Elige la carpeta que contiene tus fondos de pantalla.", + "dir-label": "Directorio de fondos de pantalla", + "dir-select-title": "Seleccionar carpeta de fondos", + "header": "Elige tu fondo", + "no-dir": "No se seleccionó un directorio de fondos", + "no-valid": "No se encontraron imágenes válidas en: {dir}", + "none-in-dir": "No se encontraron fondos en el directorio", + "preview-error": "No se pudo cargar la imagen", + "select-prompt": "Selecciona un fondo abajo", + "subheader": "Define el ambiente con un bonito fondo." + }, + "welcome": "Bienvenido", + "welcome-note": "Solo algunos ajustes básicos para empezar — las opciones al completo están en configuración", + "welcome-subtitle": "Hagamos que tu escritorio sea único", + "welcome-title": "¡Bienvenido a Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "No se detectó ningún reproductor multimedia", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "Aplicación desconocida", + "uptime": "Actividad: {uptime}", + "user-requested": "Solicitado por el usuario", + "welcome-back": "¡Bienvenido de nuevo,", + "widget-settings-title": "Configuración de {widget}" + }, + "system-monitor": { + "core-usage": "Uso del núcleo {id}", + "cpu-temp": "Temperatura de CPU", + "cpu-usage": "Uso de CPU", + "disk": "Disco", + "download-speed": "Velocidad de descarga", + "gpu-temp": "Temperatura de GPU", + "load-average": "Carga promedio", + "title": "Monitor del sistema", + "upload-speed": "Velocidad de subida" + }, + "toast": { + "airplane-mode": { + "description": "Desactivar todas las comunicaciones inalámbricas.", + "title": "Modo avión" + }, + "battery": { + "critical": "Batería crítica", + "critical-desc": "Batería al {percent}% — por favor, conecta el cargador inmediatamente", + "low": "Batería baja", + "low-desc": "Batería al {percent}% — por favor, conecta el cargador" + }, + "bluetooth": { + "address-copied": "Dirección copiada al portapapeles", + "auto-connect-disabled": "Conexión automática desactivada", + "auto-connect-enabled": "Conexión automática activada", + "auto-connecting": "Conectando a {count} dispositivo(s)...", + "confirm-code": "Confirma el código {value} en el otro dispositivo.", + "connect-failed": "Error al conectar con el dispositivo", + "disconnect-failed": "Error al desconectar del dispositivo", + "discoverable-change-failed": "Error al cambiar el estado de visibilidad", + "discoverable-disabled": "Visible desactivado", + "discoverable-enabled": "Visible activado", + "display-code": "Introduce el código {value} en el otro dispositivo.", + "forget-failed": "Error al olvidar el dispositivo", + "pair-failed": "Error al emparejar el dispositivo", + "passkey-required": "El dispositivo requiere una clave de acceso.", + "pincode-required": "El dispositivo requiere código PIN.", + "state-change-failed": "Error al cambiar el estado de Bluetooth" + }, + "clipboard": { + "long-text": "Texto largo", + "unavailable": "Historial del portapapeles no disponible", + "unavailable-desc": "La aplicación 'cliphist' no está instalada. Por favor, instálala para usar las funciones de historial del portapapeles" + }, + "custom-command-failed": { + "description": "El comando falló: {command}\\nCódigo de salida: {code}", + "title": "Comando personalizado falló" + }, + "do-not-disturb": { + "disabled": "'No molestar' desactivado", + "disabled-desc": "Mostrando todas las notificaciones", + "enabled": "'No molestar' activado", + "enabled-desc": "Encontrarás estas notificaciones en tu historial" + }, + "donation-opened": "Página de donación abierta en tu navegador", + "internet-limited": "Conectado sin Internet", + "keyboard-layout": { + "changed": "Distribución de teclado cambiada a {layout}", + "title": "Teclado" + }, + "missing-control-center": { + "description": "El widget del centro de control se eliminó de la barra. Para acceder a él nuevamente desde la barra, debes volver a añadir el widget. También puedes abrirlo haciendo clic derecho en la barra", + "label": "Se eliminó el último widget del centro de control" + }, + "night-light": { + "forced": "Activación forzada", + "normal": "Modo normal", + "not-installed": "wlsunset no está instalado" + }, + "noctalia-performance": { + "disabled": "Modo de rendimiento desactivado", + "enabled": "Modo de rendimiento activado", + "label": "Noctalia Rendimiento" + }, + "power-profile": { + "changed": "Perfil de energía cambiado", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "Se encontró un error al procesar las plantillas", + "title": "Falló el procesamiento de plantillas" + }, + "vpn": { + "connected": "Conectado a '{name}'", + "disconnected": "Desconectado de '{name}'" + }, + "wallpaper-colors": { + "disabled": "Colores del fondo de pantalla desactivados", + "enabled": "Colores del fondo de pantalla activados", + "label": "Generación de colores para fondos de pantalla" + }, + "wifi": { + "connected": "Conectado a '{ssid}'", + "connection-failed": "Error de conexión", + "connection-timeout": "Tiempo de conexión agotado", + "disconnected": "Desconectado de '{ssid}'", + "incorrect-password": "Contraseña incorrecta", + "network-not-found": "Red no encontrada" + } + }, + "tooltips": { + "add-widget": "Añadir widget", + "auto-connect": "Alternar conexión automática para este dispositivo", + "bluetooth-auto-connect-off": "Conexión automática desactivada", + "bluetooth-auto-connect-on": "Conexión automática activada", + "bluetooth-devices": "Dispositivos Bluetooth", + "brightness-at": "Brillo: {brightness}%", + "click-to-start-recording": "Grabadora de pantalla (iniciar grabación)", + "click-to-stop-recording": "Grabadora de pantalla (detener grabación)", + "collapse": "Colapsar barra lateral", + "copy-address": "Copiar dirección", + "delete-notification": "Eliminar notificación", + "dismiss-notification": "Descartar notificación", + "do-not-disturb-enabled": "No molestar", + "expand": "Expandir barra lateral", + "forget-network": "Olvidar red", + "grid-view": "Vista de cuadrícula", + "hidden-files-hide": "Archivos ocultos", + "hide-unnamed-devices": "Ocultar dispositivos sin nombre", + "home": "Directorio home", + "input-muted": "Activar/desactivar silencio de entrada", + "keep-awake": "Mantener despierto", + "keyboard-layout": "Distribución de teclado {layout}", + "list-view": "Vista de lista", + "manage-vpn": "Conexiones VPN", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "Se ha alcanzado el número máximo de widgets", + "microphone-volume-at": "Volumen del micrófono: {volume}%", + "move-to-section": "Mover a {section}", + "mute": "Silenciar", + "next-media": "Siguiente pista", + "next-month": "Mes siguiente", + "night-light-not-installed": "Luz nocturna (no disponible)", + "noctalia-performance-enabled": "Modo de rendimiento Noctalia", + "open-annotation-tool": "Abrir con herramienta de anotación", + "open-control-center": "Centro de control", + "open-notification-history-enable-dnd": "Historial de notificaciones", + "open-settings": "Configuración", + "open-tray-dropdown": "Bandeja del sistema", + "output-muted": "Alternar silencio de salida", + "power-profile": "Perfil de energía {profile}", + "previous-media": "Pista anterior", + "previous-month": "Mes anterior", + "refresh-devices": "Actualizar dispositivos", + "refresh-wallhaven": "Actualizar resultados de Wallhaven", + "refresh-wallpaper-list": "Actualizar lista de fondos de pantalla", + "remove-widget": "Eliminar widget", + "search-close": "Cerrar búsqueda", + "session-menu": "Menú de sesión", + "show-all-devices": "Mostrar todos los dispositivos", + "switch-to-dark-mode": "Modo oscuro", + "switch-to-light-mode": "Modo claro", + "unmute": "Activar el audio", + "up": "Directorio superior", + "volume-at": "Volumen de salida: {volume}%", + "wallpaper-selector": "Selector de fondos de pantalla" + }, + "wallpaper": { + "browse": { + "empty-directory": "Este directorio está vacío.", + "go-root": "Ir a la carpeta raíz de fondos de pantalla", + "go-up": "Ir a la carpeta superior", + "go-up-hint": "Usa el botón de atrás para navegar hacia arriba." + }, + "configure-directory": "Configura tu directorio de fondos de pantalla con imágenes.", + "fill-modes": { + "crop": "Recortar (Rellenar)", + "fit": "Ajustar (Contener)", + "repeat": "Repetir (Mosaico)", + "stretch": "Estirar" + }, + "no-match": "No se encontraron coincidencias.", + "no-wallpaper": "No se encontraron fondos de pantalla.", + "panel": { + "apikey-help": "Se requiere una clave API para acceder a contenido NSFW.", + "apikey-label": "Clave API", + "apikey-managed-by-env": "Gestionado a través de la variable de entorno NOCTALIA_WALLHAVEN_API_KEY.", + "apikey-placeholder": "Introduce tu clave API de Wallhaven", + "appearance-dark-tab": "Oscuro", + "appearance-light-tab": "Claro", + "categories-anime": "Anime", + "categories-label": "Categorías", + "categories-people": "Personas", + "color-extraction-disabled": "Usar extracción de colores del fondo de pantalla", + "color-extraction-enabled": "Usar esquemas de color predefinidos", + "header-devices-apply-all-tooltip": "Elegir fondo de pantalla por monitor", + "header-devices-per-monitor-tooltip": "Aplicar a todos los monitores", + "header-link-light-dark-tooltip": "Mantener fondo de pantalla al cambiar de tema", + "header-separate-light-dark-tooltip": "Cambiar fondo de pantalla automáticamente con el tema", + "header-sun-linked-tooltip": "Separar fondos de pantalla claros y oscuros", + "header-sun-separate-tooltip": "Mismo fondo de pantalla para claro y oscuro", + "order-asc": "Ascendente", + "order-desc": "Descendente", + "order-label": "Orden", + "purity-label": "Filtro de contenido", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Sketchy", + "ratios-all-portrait": "Todos verticales", + "ratios-all-wide": "Todos horizontales", + "ratios-any": "Cualquiera", + "ratios-label": "Relación de aspecto", + "resolution-atleast": "Al menos", + "resolution-exact": "Exacto", + "resolution-label": "Resolución", + "resolution-mode-label": "Modo", + "solid-color-tooltip": "Fondo de color sólido", + "sort-date-asc": "Ordenar por más antiguos primero", + "sort-date-desc": "Ordenar por más reciente primero", + "sort-name-asc": "Ordenar por nombre (A-Z)", + "sort-name-desc": "Ordenar por nombre (Z-A)", + "sort-random": "Ordenar aleatoriamente", + "sorting-date-added": "Fecha de adición", + "sorting-favorites": "Favoritos", + "sorting-label": "Ordenar por", + "sorting-relevance": "Relevancia", + "sorting-toplist": "Mejores", + "sorting-views": "Visualizaciones", + "source-label": "Fuente", + "source-wallhaven": "Wallhaven", + "title": "Selector de fondo de pantalla", + "wallhaven-settings-title": "Configuración de Wallhaven" + }, + "transitions": { + "disc": "Disco", + "fade": "Desvanecer", + "honeycomb": "Panal", + "pixelate": "Pixelar", + "stripes": "Rayas", + "wipe": "Barrido" + }, + "try-different-search": "Intenta con una búsqueda diferente.", + "wallhaven": { + "loading": "Cargando fondos de pantalla...", + "no-results": "No se encontraron fondos de pantalla. Intenta con una búsqueda diferente.", + "page": "{current} de {total}", + "page-prefix": "Página", + "page-suffix": "de {total}" + } + }, + "weather": { + "clear-sky": "Cielo despejado", + "drizzle": "Llovizna", + "fog": "Niebla", + "mainly-clear": "Mayormente despejado", + "overcast": "Nublado", + "partly-cloudy": "Parcialmente nublado", + "rain-showers": "Chubascos", + "snow": "Nieve", + "thunderstorm": "Tormenta" + }, + "widgets": { + "color-picker": { + "palette-description": "Elige entre una amplia gama de colores predefinidos.", + "palette-label": "Paleta", + "palette-theme-colors": "Acceso rápido a la paleta de colores de tu tema.", + "title": "Selector de colores" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm en minúsculas", + "ampm-uppercase": "AM/PM en mayúsculas", + "common-12hour-time-minutes": "Hora en formato de 12 horas con minutos", + "common-24hour-time-minutes": "Hora en formato de 24 horas con minutos", + "common-24hour-time-seconds": "Hora en formato de 24 horas con segundos", + "common-european-date": "Formato de fecha europeo", + "common-iso-date": "Formato de fecha ISO", + "common-us-date": "Formato de fecha estadounidense", + "common-weekday-date": "Día de la semana con fecha", + "common-weekday-month-day": "Día de la semana, mes y día", + "day-abbreviated": "Nombre del día abreviado", + "day-full": "Nombre completo del día", + "day-leading-zero": "Día con cero inicial (01-31)", + "day-no-leading-zero": "Día sin cero inicial (1-31)", + "hour-leading-zero": "Hora con cero inicial (00-23) — formato de 24 horas", + "hour-no-leading-zero": "Hora sin cero inicial (0-23) — formato de 24 horas", + "minute-leading-zero": "Minuto con cero inicial (00-59)", + "minute-no-leading-zero": "Minuto sin cero inicial (0-59)", + "month-abbreviated": "Nombre del mes abreviado", + "month-full": "Nombre del mes completo", + "month-number-leading-zero": "Mes como número con cero inicial (01-12)", + "month-number-no-zero": "Mes como número sin cero inicial (1-12)", + "second-leading-zero": "Segundo con cero inicial (00-59)", + "second-no-leading-zero": "Segundo sin cero inicial (0-59)", + "timezone-abbreviation": "Abreviatura de la zona horaria", + "year-four-digit": "Año como número de cuatro dígitos", + "year-two-digit": "Año como número de dos dígitos (00-99)" + }, + "file-picker": { + "item": "elemento", + "items": "elementos", + "search-placeholder": "Buscar archivos y carpetas...", + "select-current": "Seleccionar Actual", + "select-file": "Seleccionar Archivo", + "select-folder": "Seleccionar Carpeta", + "selected": "Seleccionado:", + "title": "Selector de Archivos" + }, + "file-picker-title": "Seleccionar un archivo", + "icon-picker": { + "title": "Selector de Iconos" + }, + "tooltip-placeholder": "Marcador de posición" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "Identidad Anónima", + "ca-cert": "Certificado CA", + "eap-method": "Método EAP", + "password": "Contraseña de Usuario", + "phase2-auth": "Autenticación de Fase 2", + "username": "Identidad / Nombre de Usuario" + }, + "panel": { + "action-required": "Acción requerida", + "add-hidden-network": "Añadir red oculta", + "add-network": "Añadir red", + "available-interfaces": "Interfaces disponibles", + "available-networks": "Redes disponibles", + "disabled": "Wi-Fi está desactivado", + "disconnecting": "Desconectando...", + "dns": "DNS", + "enable-message": "Activa Wi-Fi para ver las redes disponibles.", + "enter-password": "Ingresa la contraseña...", + "forget": "Olvidar", + "forget-network": "¿Olvidar esta red?", + "forgetting": "Olvidando...", + "hidden-network": "Red oculta", + "interface": "Interfaz de red", + "internet-connected": "Conectado a internet", + "internet-limited": "Sin internet", + "internet-status": "Estado de internet", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "Redes conocidas", + "link-speed": "Velocidad de enlace", + "network-name-ssid": "Nombre de red (SSID)", + "no-ethernet-devices": "No se detectaron dispositivos Ethernet", + "no-networks": "No se encontraron redes Wi-Fi", + "saved": "Guardado", + "scan-again": "Escanear de nuevo", + "searching": "Buscando redes cercanas...", + "security-open": "Abrir", + "security-wep": "WEP", + "security-wpa": "WPA/WPA2/WPA3 Personal", + "security-wpa-ent": "WPA Enterprise", + "security-wpa2-ent": "WPA2 Enterprise", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 Enterprise", + "show-password": "Mostrar contraseña", + "title": "Wi-Fi" + }, + "signal": { + "excellent": "Excelente", + "fair": "Razonable", + "good": "Bueno", + "poor": "Malo", + "weak": "Débil" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/fr.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/fr.json new file mode 100644 index 0000000..8a03b23 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/fr.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "Effacer l'historique", + "close-app": "Fermer {app}", + "connect-vpn": "Se connecter à {name}", + "cycle-visualizer": "Changer de visualization", + "disable-bluetooth": "Désactiver le Bluetooth", + "disable-dnd": "Désactiver le mode 'Ne pas déranger'", + "disable-wifi": "Désactiver le Wi-Fi", + "disconnect-vpn": "Se déconnecter de {name}", + "dock-settings": "Paramètres du dock", + "enable-bluetooth": "Activer le Bluetooth", + "enable-dnd": "Activer le mode 'Ne pas déranger'", + "enable-wifi": "Activer le Wi-Fi", + "launcher-settings": "Paramètres du lanceur", + "lower-to-bottom": "Abaisser au plus bas / Descendre au plus bas", + "open-calendar": "Ouvrir le calendrier", + "open-display-settings": "Paramètres d'affichage", + "open-launcher": "Ouvrir le lanceur", + "open-settings": "Ouvrir les paramètres", + "raise-to-top": "Mettre en avant", + "random-wallpaper": "Fond d'écran aléatoire", + "run-custom-command": "Exécuter une commande personnalisée", + "show-active-only": "Afficher uniquement les actifs", + "show-all": "Tout afficher", + "toggle-mute": "Activer/désactiver le mode muet", + "widget-settings": "Paramètres du widget" + }, + "authentication": { + "error": "Erreur d'authentification", + "failed": "Échec de l'authentification" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Appliquer les couleurs du thème à l'icône de la fenêtre active.", + "hide-mode-description": "Contrôle le comportement du widget lorsqu'aucune fenêtre n'est active.", + "scrolling-mode-description": "Contrôler quand le défilement de texte est activé pour les titres de fenêtre longs.", + "show-app-icon-description": "Afficher l'icône de l'application à côté du titre de la fenêtre.", + "show-app-icon-label": "Afficher l'icône de l'application", + "show-app-text-description": "Afficher le titre de la fenêtre active.", + "show-app-text-label": "Afficher le titre de la fenêtre" + }, + "audio-visualizer": { + "color-name-description": "Sélectionnez la couleur du visualiseur.", + "color-name-label": "Couleur de remplissage", + "height-description": "Largeur de composant personnalisée.", + "hide-when-idle-description": "Si activé, le visualiseur est masqué sauf lorsqu'un lecteur est en lecture.", + "hide-when-idle-label": "Masquer lorsqu'aucun média n'est en lecture", + "width-description": "Largeur personnalisée du composant." + }, + "battery": { + "device-default": "Par défaut (périphérique d'affichage)", + "device-description": "Sélectionnez le périphérique de batterie à afficher.", + "device-label": "Dispositif à pile", + "display-mode-description": "Choisissez comment la batterie est affichée dans la barre.", + "display-mode-graphic": "Batterie graphique", + "display-mode-graphic-clean": "Batterie graphique (sans %)", + "display-mode-icon-always": "Icône - Toujours afficher %", + "display-mode-icon-hover": "Icône - Afficher au survol", + "display-mode-icon-only": "Icône seulement", + "hide-if-idle-description": "Masquer le widget lorsque la batterie n'est ni en charge ni en décharge.", + "hide-if-idle-label": "Masquer en cas d'inactivité", + "hide-if-not-detected-description": "Masquer le widget lorsqu'aucune batterie n'est détectée sur le système.", + "hide-if-not-detected-label": "Caché si non détecté", + "low-battery-threshold-label": "Seuil d'avertissement de batterie faible", + "show-noctalia-performance-description": "Afficher le basculement du mode performance Noctalia dans le panneau batterie.
Désactive les ombres et les animations dans Noctalia pour réduire l'utilisation des ressources.", + "show-noctalia-performance-label": "Afficher le basculement Noctalia Performance", + "show-power-profile-description": "Afficher la sélection du profil d'alimentation dans le panneau batterie.", + "show-power-profile-label": "Afficher les contrôles de profil d'alimentation" + }, + "brightness": { + "apply-all-description": "Changer la luminosité de tous les moniteurs lors de l'utilisation de la molette de défilement.", + "apply-all-label": "Appliquer les modifications de défilement à tous les moniteurs" + }, + "clock": { + "clock-display-description": "Personnalisez l'affichage de votre horloge en ajoutant des jetons de la liste ci-dessous. Pour utiliser le format 12 heures, vous devez inclure le jeton 'AP'.", + "clock-display-label": "Affichage de l'horloge", + "custom-font-description": "Sélectionner une police personnalisée pour l'affichage de l'horloge.", + "custom-font-label": "Police personnalisée", + "custom-font-placeholder": "Sélectionner une police personnalisée...", + "custom-font-search-placeholder": "Rechercher des polices...", + "horizontal-bar-description": "Astuce : Utilisez \\n pour créer un saut de ligne.", + "horizontal-bar-label": "Barre horizontale", + "preview": "Aperçu", + "tooltip-format-description": "Chaîne de format pour l'infobulle affichée lors du survol de l'horloge. Laissez vide pour utiliser l'infobulle par défaut.", + "tooltip-format-label": "Format de l'infobulle", + "use-custom-font-description": "Remplacez la police par défaut par une police personnalisée pour l'horloge.", + "use-custom-font-label": "Utiliser une police personnalisée", + "use-monospaced-font-description": "Lorsque cette option est activée, l'horloge utilisera la police monospace.", + "use-monospaced-font-label": "Utiliser une police monospace", + "vertical-bar-description": "Utilisez un espace pour séparer chaque partie sur une nouvelle ligne.", + "vertical-bar-label": "Barre verticale" + }, + "control-center": { + "browse-file": "Parcourir les fichiers", + "browse-library": "Parcourir la bibliothèque", + "color-selection-description": "Applique les couleurs du thème aux icônes.", + "enable-colorization-description": "Activer la colorisation pour l'icône, en appliquant les couleurs du thème.", + "icon-description": "Sélectionnez une icône de la bibliothèque ou un fichier personnalisé.", + "select-custom-icon": "Sélectionner une icône personnalisée", + "use-distro-logo-description": "Utiliser le logo de votre distribution au lieu d'une icône personnalisée.", + "use-distro-logo-label": "Utiliser le logo de la distribution au lieu de l'icône" + }, + "custom-button": { + "collapse-condition-description": "Si le texte de sortie correspond à cette valeur, le bouton se repliera.", + "collapse-condition-label": "Condition d’effondrement", + "color-selection-description": "Appliquer les couleurs du thème à l'icône et au texte.", + "default-tooltip": "Bouton personnalisé, configurer dans les paramètres", + "display-command-output-description": "Entrez une commande à exécuter à intervalle régulier. La première ligne de sa sortie sera affichée sous forme de texte.", + "display-command-output-label": "Afficher la sortie de la commande", + "display-command-output-stream-description": "Entrez une commande à exécuter en continu.", + "dynamic-text": "Texte dynamique", + "enable-colorization-description": "Activer la colorisation pour l'icône et le texte du bouton personnalisé, en appliquant les couleurs du thème.", + "enable-colorization-label": "Activer la colorisation", + "general-tooltip-text-description": "Texte personnalisé à afficher dans l'infobulle du bouton.", + "general-tooltip-text-label": "Texte d'infobulle personnalisé", + "hide-mode-always-expanded": "Toujours étendu", + "hide-mode-description": "Contrôle la visibilité du widget quand la commande n'a pas de sortie.", + "hide-mode-expand-with-output": "Étendre quand il y a une sortie", + "hide-mode-label": "Mode masqué", + "hide-mode-max-transparent": "Étendu au maximum mais transparent", + "icon-description": "Sélectionnez une icône dans la bibliothèque.", + "ipc-identifier-description": "Identifiant unique pour les commandes IPC. Utilisez cet identifiant avec 'qs -c noctalia-shell ipc call cb [action] [identifiant]' pour contrôler ce bouton via IPC.", + "ipc-identifier-label": "Identifiant IPC", + "left-click-description": "Commande à exécuter quand le bouton est cliqué à gauche.", + "left-click-label": "Clic gauche", + "left-click-update-text": "Mettre à jour le texte affiché au clic gauche", + "max-text-length-horizontal-description": "Nombre maximal de caractères à afficher dans la barre horizontale (0 pour masquer le texte).", + "max-text-length-horizontal-label": "Longueur max du texte (horizontal)", + "max-text-length-vertical-description": "Nombre maximal de caractères à afficher dans la barre verticale (0 pour masquer le texte).", + "max-text-length-vertical-label": "Longueur max du texte (vertical)", + "middle-click-description": "Commande à exécuter quand le bouton est cliqué au milieu.", + "middle-click-label": "Clic du milieu", + "middle-click-update-text": "Mettre à jour le texte affiché lors d'un clic molette", + "parse-json-description": "Analyser la sortie de la commande en tant qu'objet JSON pour définir dynamiquement le texte et l'icône.", + "parse-json-label": "Analyser la sortie en JSON", + "refresh-interval-description": "Intervalle en millisecondes.", + "refresh-interval-label": "Intervalle d'actualisation", + "right-click-description": "Commande à exécuter quand le bouton est cliqué à droite.", + "right-click-label": "Clic droit", + "right-click-update-text": "Mettre à jour le texte affiché lors d'un clic droit", + "show-exec-tooltip-description": "Afficher les infobulles avec les détails de la commande (clic gauche/droit/milieu, molette).", + "show-exec-tooltip-label": "Afficher les infobulles de commande", + "show-icon-description": "Bascule la visibilité de l'icône du widget.", + "show-icon-label": "Afficher l'icône", + "show-text-tooltip-description": "Afficher les infobulles avec la sortie de la commande texte.", + "show-text-tooltip-label": "Afficher les infobulles de texte dynamiques", + "text-stream-description": "Les lignes diffusées depuis la commande seront affichées sous forme de texte sur le bouton.", + "text-stream-label": "Flux", + "wheel-description": "Commande à exécuter lorsque la molette est utilisée.
Utilisez $delta pour le delta de la molette dans la commande.", + "wheel-down": "Molette bas", + "wheel-down-description": "Commande à exécuter lorsque la molette est défilée vers le bas.", + "wheel-down-label": "Commande molette bas", + "wheel-label": "Molette", + "wheel-mode-separate-description": "Activer des commandes séparées pour la molette haut et bas.", + "wheel-mode-separate-label": "Commandes de molette séparées", + "wheel-up": "Molette haut", + "wheel-up-description": "Commande à exécuter lorsque la molette est défilée vers le haut.", + "wheel-up-label": "Commande molette haut", + "wheel-update-text": "Mettre à jour le texte affiché au défilement" + }, + "keyboard-layout": { + "show-icon-description": "Afficher l'icône de disposition du clavier." + }, + "lock-keys": { + "hide-when-off-description": "Masquer l'indicateur lorsque la touche n'est pas active.", + "hide-when-off-label": "Masquer lorsque désactivé", + "show-caps-lock-description": "Afficher l'état du verrouillage majuscule.", + "show-caps-lock-label": "Verr Maj", + "show-num-lock-description": "Afficher l'état du verrouillage numérique.", + "show-num-lock-label": "Verr Num", + "show-scroll-lock-description": "Afficher l'état du verrouillage du défilement.", + "show-scroll-lock-label": "Verr Maj" + }, + "media-mini": { + "compact-mode-description": "Activer une disposition compacte pour le panneau du lecteur multimédia.", + "compact-mode-label": "Mode compact", + "hide-mode-description": "Contrôle le comportement du widget lorsqu'aucun média n'est en cours de lecture.", + "max-width-description": "Définit la taille horizontale maximale du widget. Le widget se rétrécira pour s'adapter à un contenu plus court.", + "no-active-player": "Aucun lecteur actif", + "panel-section-description": "Configurer l'apparence et le comportement du panneau du lecteur multimédia.", + "panel-section-label": "Panneau du lecteur multimédia", + "scrolling-mode-description": "Contrôler quand le défilement de texte est activé pour les titres de piste longs.", + "scrolling-mode-label": "Mode de défilement", + "show-album-art-description": "Afficher la pochette d'album de la piste en cours de lecture.", + "show-album-art-label": "Afficher la pochette d'album", + "show-artist-first-description": "Afficher artiste - titre au lieu de titre - artiste.", + "show-artist-first-label": "Afficher l'artiste en premier", + "show-progress-ring-description": "Afficher un indicateur de progression circulaire montrant la progression de la piste.", + "show-progress-ring-label": "Afficher l'anneau de progression", + "show-visualizer-description": "Afficher un visualiseur audio quand la musique est en cours de lecture.", + "show-visualizer-label": "Afficher le visualiseur", + "use-fixed-width-description": "Lorsque activé, le widget utilisera toujours la largeur maximale au lieu de s'ajuster dynamiquement au contenu.", + "use-fixed-width-label": "Utiliser une largeur fixe", + "visualizer-type-description": "Choisir le style de visualiseur audio à afficher.", + "visualizer-type-label": "Type de visualiseur" + }, + "notification-history": { + "hide-widget-when-zero-description": "Masquer l’icône de notification lorsqu’il n’y a aucune notification.", + "hide-widget-when-zero-label": "Masquer l’icône sans notifications", + "hide-widget-when-zero-unread-description": "Masquer l’icône de notification lorsqu’il n’y a aucune notification non lue.", + "hide-widget-when-zero-unread-label": "Masquer l’icône sans notifications non lues", + "show-unread-badge-description": "Afficher un badge montrant le nombre de notifications non lues.", + "show-unread-badge-label": "Afficher le badge non lu", + "unread-badge-color-description": "Sélectionnez la couleur pour le badge de notification non lue.", + "unread-badge-color-label": "Couleur du badge non lu" + }, + "section-editor": { + "placeholder": "Sélectionnez un widget à ajouter...", + "search-placeholder": "Rechercher des widgets..." + }, + "spacer": { + "width-description": "Largeur de l'espacement en pixels." + }, + "system-monitor": { + "compact-mode-description": "Afficher les statistiques sous forme de mini-graphiques à barres au lieu de valeurs textuelles. Empêche les décalages de mise en page.", + "compact-mode-label": "Mode compact", + "cpu-cores-description": "Afficher l'utilisation des cœurs du CPU individuellement.", + "cpu-cores-label": "Cœurs du CPU", + "cpu-frequency-description": "Afficher la vitesse d'horloge actuelle du CPU en GHz.", + "cpu-frequency-label": "Afficher la fréquence du CPU", + "cpu-temperature-description": "Afficher les lectures de température du CPU si disponibles.", + "cpu-temperature-label": "Température du CPU", + "cpu-usage-description": "Afficher le pourcentage d'utilisation actuel du CPU.", + "cpu-usage-label": "Utilisation du CPU", + "disk-path-description": "Sélectionnez le point de montage du disque à surveiller.", + "disk-path-label": "Chemin du disque", + "gpu-temperature-description": "Afficher les relevés de température du GPU, si disponibles.", + "load-average-description": "Afficher la charge moyenne du système.", + "load-average-label": "Charge moyenne", + "memory-percentage-description": "Afficher l'utilisation de la mémoire en pourcentage au lieu de valeurs absolues.", + "memory-percentage-label": "Mémoire en pourcentage", + "memory-usage-description": "Afficher les informations actuelles d'utilisation de la RAM.", + "memory-usage-label": "Utilisation de la mémoire", + "network-traffic-description": "Afficher les vitesses de téléchargement et de téléversement du réseau.", + "network-traffic-label": "Trafic réseau", + "storage-as-percentage-description": "Afficher l'espace disque en pourcentage au lieu des valeurs absolues.", + "storage-as-percentage-label": "Disque en pourcentage", + "storage-available-description": "Affiche l'espace disque disponible au lieu de l'espace utilisé.", + "storage-available-label": "Espace disque disponible", + "storage-usage-description": "Afficher les informations d'utilisation de l'espace disque.", + "storage-usage-label": "Utilisation du stockage", + "swap-usage-description": "Afficher l'utilisation de la mémoire swap.", + "swap-usage-label": "Utilisation du swap", + "use-monospace-font-description": "Utilisez une police de caractères monospace pour une largeur de caractère uniforme.", + "use-monospace-font-label": "Police monospace", + "use-padding-description": "Remplit les valeurs de texte avec des espaces en début de ligne pour éviter le décalage de la mise en page.", + "use-padding-description-disabled-monospace-font": "Une police monospace est nécessaire pour cette fonctionnalité.", + "use-padding-description-disabled-vertical": "La barre des tâches verticale ne prend pas en charge le remplissage de texte.", + "use-padding-label": "Rembourrage du Texte" + }, + "taskbar": { + "colorize-icons-description": "Appliquer les couleurs du thème aux icônes de la barre des tâches.", + "hide-mode-description": "Contrôle le comportement du widget lorsqu'il n'y a pas de fenêtres correspondantes.", + "hide-mode-label": "Mode de masquage", + "icon-scale-description": "Définit le facteur d'échelle pour les icônes de la barre des tâches.", + "icon-scale-label": "Mise à l'échelle des icônes", + "max-width-description": "Largeur maximale de la barre des tâches en pourcentage de la largeur de l'écran.", + "max-width-label": "Largeur maximale", + "only-active-workspaces-description": "Afficher uniquement les applications des espaces de travail actifs.", + "only-active-workspaces-label": "Seulement depuis les espaces de travail actifs", + "only-same-monitor-description": "Afficher uniquement les applications de l'écran où se trouve la barre.", + "only-same-monitor-label": "Seulement à partir du même écran", + "show-pinned-apps-description": "Afficher les applications épinglées du dock dans la barre des tâches.", + "show-pinned-apps-label": "Afficher les applications épinglées", + "show-title-description": "Afficher les titres des fenêtres dans la barre des tâches.", + "show-title-description-disabled": "La barre des tâches verticale ne prend pas en charge l'affichage des titres.", + "show-title-label": "Afficher le titre", + "smart-width-description": "Ajuster automatiquement la largeur des entrées en fonction du nombre d'entrées.", + "smart-width-label": "Largeur intelligente", + "title-width-description": "Définir la largeur des titres de fenêtres dans la barre des tâches (en pixels).", + "title-width-label": "Largeur du titre", + "title-width-reset-tooltip": "Réinitialiser la largeur du titre" + }, + "tray": { + "chevron-color-description": "Appliquer les couleurs du thème à l'icône chevron du tiroir.", + "chevron-color-label": "Couleur du chevron", + "colorize-icons-description": "Appliquer les couleurs du thème aux icônes de la barre système.", + "colorize-icons-label": "Coloriser les icônes", + "drawer-enabled-description": "Lorsqu'elle est activée, les éléments de la barre système non épinglés sont affichés dans un panneau tiroir.
Lorsqu'elle est désactivée, tous les éléments de la barre système sont affichés en ligne.", + "drawer-enabled-label": "Activer le tiroir", + "hide-passive-description": "Lorsque cette option est activée, les éléments de la barre d'état système ayant le statut 'Passif' seront masqués.", + "hide-passive-label": "Masquer les objets passifs" + }, + "volume": { + "display-mode-description": "Choisissez comment vous souhaitez que cette valeur apparaisse." + }, + "workspace": { + "character-count-description": "Nombre de caractères à afficher des noms d'espaces de travail (1-10).", + "character-count-label": "Nombre de caractères", + "empty-color-description": "Définir la couleur d'arrière-plan pour les espaces de travail vides.", + "empty-color-label": "Couleur d'espace de travail vide", + "enable-scrollwheel-description": "Basculer entre les espaces de travail avec la molette de la souris.", + "enable-scrollwheel-label": "Faites défiler pour changer d'espace de travail", + "focused-color-description": "Définir la couleur d'arrière-plan pour l'espace de travail sélectionné.", + "focused-color-label": "Couleur de l'espace de travail sélectionné", + "follow-focused-screen-description": "Afficher les espaces de travail de l'écran actuellement ciblé, plutôt que de l'écran où se trouve la barre.", + "follow-focused-screen-label": "Suivre l'écran ciblé", + "font-weight-description": "Définir le poids visuel du texte dans l'espace de travail.", + "font-weight-label": "Graisse de la police", + "grouped-border-opacity-description": "Définir le niveau d'opacité des bordures des conteneurs d'espace de travail.", + "grouped-border-opacity-label": "Opacité de la bordure", + "hide-unoccupied-description": "Ne pas afficher les espaces de travail sans fenêtres.", + "hide-unoccupied-label": "Masquer les inoccupés", + "label-mode-description": "Choisir comment les étiquettes d'espace de travail sont affichées.", + "label-mode-label": "Mode d'étiquette", + "occupied-color-description": "Définir la couleur d'arrière-plan pour les espaces de travail occupés.", + "occupied-color-label": "Couleur de l'espace de travail occupé", + "pill-size-description": "Ajustez la taille des pilules d'espace de travail (50%-100%).", + "pill-size-label": "Taille de la pilule", + "reverse-scrolling-description": "Inverser la direction du changement d'espace de travail lors du défilement.", + "reverse-scrolling-label": "Défilement inversé", + "show-applications-description": "Afficher les icônes des applications dans chaque espace de travail.", + "show-applications-hover-description": "Afficher les icônes d'application dans chaque espace de travail lorsque celui-ci est survolé.", + "show-applications-hover-label": "Afficher les applications au survol", + "show-applications-label": "Afficher les applications", + "show-badge-description": "Afficher le badge du numéro d'espace de travail en mode groupé.", + "show-badge-label": "Afficher le badge de l'espace de travail", + "show-labels-only-when-occupied-description": "Afficher uniquement les étiquettes d'espace de travail lorsqu'elles contiennent des fenêtres.", + "show-labels-only-when-occupied-label": "Afficher les étiquettes uniquement lorsque le champ est occupé", + "unfocused-icons-opacity-description": "Définir le niveau d'opacité des icônes d'applications non focalisées.", + "unfocused-icons-opacity-label": "Opacité des icônes non ciblées" + } + }, + "battery": { + "all-batteries": "Toutes les batteries (combinées)", + "battery-health": "État de la batterie", + "battery-level": "Niveau de batterie", + "capacity-level": "Capacité : {level}", + "charging-rate": "Taux de charge : {rate} W", + "discharging-rate": "Taux de décharge : {rate} W", + "health": "État : {percent}%", + "inhibit-idle-description": "Maintient le système éveillé.", + "no-battery-detected": "Aucune batterie détectée", + "plugged-in": "Branché", + "power-profile": "Profil d’alimentation", + "time-left": "Temps restant : {time}", + "time-until-full": "Temps jusqu'à charge complète : {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Appareils disponibles", + "blocked": "Bloqué", + "connected-devices": "Appareils connectés", + "device-address": "Adresse de l’appareil", + "disabled": "Le Bluetooth est désactivé", + "discoverable": "Détectable", + "enable-message": "Activez le Bluetooth pour voir les appareils disponibles.", + "known-devices": "Appareils connus", + "no-devices": "Aucun appareil disponible", + "paired-devices": "Appareils appairés", + "pairing-mode": "Assurez-vous que votre appareil est en mode d'appairage.", + "scanning": "Recherche d'appareils en cours...", + "signal-text-excellent": "Signal : Excellent", + "signal-text-fair": "Signal : Moyen", + "signal-text-good": "Signal : Bon", + "signal-text-poor": "Signal : Faible", + "signal-text-unknown": "Signal : Inconnu", + "signal-text-very-poor": "Signal : Très faible" + } + }, + "changelog": { + "error": { + "fetch-failed": "Impossible de charger les données du journal des modifications. Veuillez réessayer plus tard.", + "rate-limit": "Limite de GitHub atteinte. Réessayez dans quelques minutes." + }, + "panel": { + "buttons-discord": "Rejoindre notre Discord", + "buttons-dismiss": "OK", + "buttons-feedback": "Donner un avis", + "empty": "Les notes de version ne sont pas encore disponibles.", + "highlight-title": "Points importants", + "released": "Publié le {date}", + "subtitle-fresh": "Merci d’avoir installé Noctalia ! Voici ce que contient cette version.", + "subtitle-updated": "Mise à jour depuis {previousVersion}", + "title": "Quoi de neuf dans {version}", + "version": "Version {version}", + "version-new-user": "Nouvelle installation" + } + }, + "common": { + "actions": "Actions", + "active": "Actif", + "add": "Ajouter", + "appearance": "Apparence", + "apply": "Appliquer", + "auto-connect": "Connexion automatique", + "automation": "Automatisation", + "available": "Disponible", + "back": "Retour", + "battery": "Batterie", + "behavior": "Comportement", + "bluetooth": "Bluetooth", + "brightness": "Luminosité", + "browse": "Parcourir", + "calendar": "Calendrier", + "calendar-panel": "Panneau du calendrier", + "cancel": "Annuler", + "cards": "Cartes", + "charging": "Chargement", + "clear": "Effacer", + "clipboard": "Presse-papiers", + "close": "Fermer", + "color-muted": "Muet", + "colors": "Couleurs", + "command": "Commande", + "confirm": "Confirmer", + "connect": "Connecter", + "connected": "Connecté", + "connecting": "Connexion en cours...", + "continue": "Continuer", + "contributors": "Contributeurs", + "copied-to-clipboard": "Copié dans le presse-papier", + "countdown": "Compte à rebours", + "customize": "Personnaliser", + "date": "Date", + "default": "Par défaut", + "delete": "Supprimer", + "devices": "Appareils", + "disabled": "Désactivé", + "discharging": "Décharge", + "disconnect": "Déconnecter", + "disconnected": "Déconnecté", + "disconnecting": "Déconnexion...", + "display-mode": "Mode d'affichage", + "documentation": "Documentation", + "download": "Télécharger", + "duration": "Durée", + "dysfunctional": "Dysfonctionnel", + "edit": "Modifier", + "enabled": "Activé", + "error": "Erreur", + "ethernet": "Ethernet", + "events": "Événements", + "execute": "Exécuter", + "faithful": "Fidèle", + "focus": "Concentration", + "font-weight-bold": "Gras", + "font-weight-light": "Légère", + "font-weight-medium": "Moyen", + "font-weight-regular": "Régulier", + "font-weight-semibold": "Semi-gras", + "frequency": "Fréquence", + "gateway": "Passerelle", + "general": "Général", + "height": "Hauteur", + "hibernate": "Hiberner", + "history": "Historique", + "icon": "Icône", + "idle": "Inactif", + "info": "Info", + "input": "Entrée", + "install": "Installer", + "installed": "Installé", + "interface": "Interface", + "internet": "Internet", + "keybind": "Raccourci clavier", + "language": "Langue", + "loading": "Chargement...", + "local": "Local", + "location": "Emplacement", + "lock": "Verrouiller", + "logout": "Déconnexion", + "look": "Apparence", + "margins": "Marges", + "media": "Médias", + "media-player": "Lecteur multimédia", + "memory": "Mémoire", + "monitors": "Écrans", + "move-down": "Déplacer vers le bas", + "move-up": "Déplacer vers le haut", + "network": "Réseau", + "next": "Suivant", + "night-light": "Éclairage nocturne", + "no": "Non", + "no-results": "Aucun résultat", + "no-summary": "Aucun résumé", + "none": "Aucun", + "not-found": "Non trouvé", + "notifications": "Notifications", + "official": "Officiel", + "on-surface": "Sur la surface", + "output": "Sortie", + "pair": "Appairer", + "paired": "Apparié", + "pairing": "Appairage...", + "panels": "Panneaux", + "password": "Mot de passe", + "pause": "Pause", + "performance": "Performances", + "pin": "Épingle", + "play": "Lire", + "polling": "Interrogation", + "position": "Position", + "previous": "Précédent", + "primary": "Primaire", + "random": "Aléatoire", + "reboot": "Redémarrer", + "reboot-to-uefi": "Redémarrer en UEFI", + "record": "Démarrer l'enregistrement", + "refresh": "Actualiser", + "required": "(requis)", + "reset": "Réinitialiser", + "reset-to-default": "Réinitialiser par Défaut", + "result-count": "{count} résultat", + "result-count-plural": "{count} résultats", + "resume": "Reprendrer", + "save": "Enregistrer", + "scanning": "Analyse en cours...", + "screen-corners": "Coins d'écran", + "search": "Rechercher", + "second": "{count} seconde", + "second-plural": "{count} secondes", + "secondary": "Secondaire", + "security": "Sécurité", + "select": "Sélectionner", + "select-color": "Sélectionner une couleur", + "select-color-description": "Appliquer les couleurs du thème pour mettre en valeur.", + "select-icon-color": "Sélectionner la couleur de l'icône", + "settings": "Paramètres", + "shortcuts": "Raccourcis", + "shutdown": "Éteindre", + "signal": "Signal", + "sound": "Son", + "sources": "Sources", + "start": "Commencer", + "stop": "Arrêtez", + "supporters": "Soutiens", + "suspend": "Mettre en veille", + "templates": "Modèles", + "tertiary": "Tertiaire", + "test": "Test", + "thresholds": "Seuils", + "title": "Titre", + "toast": "Toast", + "trusted": "De confiance", + "uninstall": "Désinstaller", + "unknown": "Inconnu", + "unpair": "Déjumeler", + "unpin": "Détacher", + "update": "Mise à jour", + "upload": "Téléverser", + "username": "Nom d'utilisateur", + "userspace-reboot": "Redémarrer l'espace utilisateur", + "version": "Version", + "vibrant": "Vibrant", + "visualizer": "Visualiseur", + "volume": "Volume", + "volumes": "Volumes", + "wallpaper": "Fond d’écran", + "weather": "Météo", + "weather-loading": "Chargement de la météo...", + "weather-no-location": "Définissez un emplacement dans les Paramètres pour voir la météo", + "week": "Semaine", + "widgets": "Widgets", + "width": "Largeur", + "wifi": "Wi-Fi", + "windows": "Fenêtres", + "yes": "Oui" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Profil d'alimentation", + "tooltip-disabled": "Installez power-profiles-daemon pour utiliser les profils d'alimentation" + }, + "wifi": { + "label-disconnected": "Wi-Fi déconnecté", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Toujours caché", + "always-show": "Toujours afficher", + "force-open": "Forcer l'ouverture", + "on-hover": "Au survol" + }, + "hide-modes": { + "auto-hide": "Masquer automatiquement ", + "hidden": "Masquer quand vide", + "idle": "Masquer quand inactif", + "non-exclusive": "Non exclusif", + "transparent": "Transparent quand vide", + "visible": "Toujours visible" + }, + "launcher": { + "categories": { + "all": "Tout", + "audiovideo": "Audio et vidéo", + "chat": "Conversation", + "development": "Développement", + "education": "Éducation", + "emoji-activity": "Activité", + "emoji-animals": "Animaux", + "emoji-flags": "Drapeaux", + "emoji-food": "Nourriture et boissons", + "emoji-nature": "Nature", + "emoji-objects": "Objets", + "emoji-people": "Personnes et corps", + "emoji-recent": "Récent", + "emoji-symbols": "Symboles", + "emoji-travel": "Voyages et lieux", + "game": "Jeux", + "graphics": "Graphiques", + "misc": "Divers", + "office": "Bureau", + "pinned": "Épinglé", + "system": "Système", + "webbrowser": "Navigateur web" + }, + "providers": { + "applications": "Applications", + "calculator": "Calculatrice", + "calculator-deprecated": "La commande >calc est obsolète et sera bientôt supprimée. Veuillez saisir les expressions mathématiques directement dans la recherche à la place.", + "calculator-description": "Calculatrice — évaluez des expressions mathématiques", + "calculator-enter-expression": "Entrez une expression mathématique", + "calculator-press-enter-to-copy": "Appuyez sur Entrée pour copier le résultat", + "clipboard": "Historique du presse-papiers", + "clipboard-clear-description": "Effacer tout l'historique du presse-papiers", + "clipboard-clear-description-full": "Supprimer tous les éléments de l'historique du presse-papiers", + "clipboard-clear-history": "Effacer l'historique du presse-papiers", + "clipboard-delete": "Supprimer l'entrée du presse-papiers", + "clipboard-history-disabled": "Historique du presse-papiers désactivé", + "clipboard-history-disabled-description": "Activez l'historique du presse-papiers dans les paramètres ou installez cliphist", + "clipboard-loading": "Chargement de l'historique du presse-papiers...", + "clipboard-search-description": "Rechercher dans l'historique du presse-papiers", + "command-description": "Exécuter des commandes shell", + "emoji": "Sélecteur d'émojis", + "emoji-loading": "Chargement des émojis...", + "emoji-loading-description": "Veuillez patienter", + "emoji-no-recent": "Aucun emoji récent pour le moment", + "emoji-search-description": "Rechercher et copier des émojis", + "settings-search-description": "Trouvez et explorez les paramètres", + "windows-search-description": "Rechercher et focaliser les fenêtres ouvertes" + } + }, + "lock-screen": { + "authenticating": "Authentification...", + "password": "Entrez votre mot de passe...", + "restart": "Redémarrer", + "shut-down": "Éteindre" + }, + "notifications": { + "panel": { + "click-to-collapse": "Cliquer pour réduire", + "click-to-expand": "Cliquez pour développer", + "description": "Vos notifications apparaîtront ici à mesure qu'elles arriveront.", + "no-notifications": "Aucune notification" + }, + "range": { + "earlier": "Plus anciennes", + "today": "Aujourd'hui", + "yesterday": "Hier" + }, + "time": { + "diff-d": "il y a 1 jour", + "diff-dd": "il y a {diff} jours", + "diff-h": "il y a 1 heure", + "diff-hh": "il y a {diff} heures", + "diff-m": "il y a 1 minute", + "diff-mm": "il y a {diff} minutes", + "now": "maintenant" + } + }, + "options": { + "bar": { + "density-comfortable": "Confortable", + "density-compact": "Compact", + "density-default": "Défaut", + "density-mini": "Mini", + "density-spacious": "Spacieux", + "display-mode-always-visible": "Toujours Visible", + "type-floating": "Flottante", + "type-framed": "Encadrée", + "type-simple": "Simple" + }, + "control-center": { + "quick-settings-style-classic": "Classique", + "quick-settings-style-modern": "Moderne" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "Confortable", + "compact": "Compact", + "default": "Par défaut" + }, + "launcher-view-mode": { + "grid": "Grille", + "list": "Liste" + }, + "notification-density": { + "compact": "Compact", + "default": "Par défaut" + }, + "scrolling-modes": { + "always": "Défiler toujours", + "hover": "Défiler au survol", + "never": "Ne jamais défiler" + }, + "session-menu-grid-layout": { + "grid": "Grille", + "single-row": "Une seule rangée" + }, + "settings-panel-mode": { + "attached": "Panneau attaché à la barre", + "centered": "Panneau centré", + "window": "Fenêtre séparée" + }, + "visualizer-types": { + "linear": "Linéaire", + "mirrored": "Miroir", + "wave": "Vague" + }, + "visualizer-visibility": { + "with-background": "Avec fond uniquement" + }, + "workspace-labels": { + "index": "Index", + "index-and-name": "Index et Nom", + "name": "Nom" + } + }, + "panels": { + "about": { + "become-supporter": "Devenir un soutien", + "changelog": "Afficher le journal des modifications", + "changelog-on-startup": "Afficher le journal des modifications lors de la mise à jour", + "changelog-on-startup-desc": "Afficher automatiquement le journal des modifications lors de la mise à jour de Noctalia.", + "contributors-description": "Un grand merci à notre {count} super contributeur !", + "contributors-description-plural": "Un grand merci à nos {count} super contributeurs !", + "copy-info": "Copier les informations", + "debug-disabled": "Mode débogage désactivé", + "debug-enabled": "Mode débogage activé", + "info-copied": "Info copiée dans le presse-papiers", + "noctalia-available": "Disponible :", + "noctalia-desc": "Un shell de bureau élégant et minimaliste, conçu avec soin pour Wayland et bâti avec Quickshell.", + "noctalia-git-commit": "Commit Git :", + "noctalia-installed-version": "Version installée :", + "noctalia-latest-version": "Dernière version :", + "noctalia-qs-version": "Noctalia QS version :", + "noctalia-title": "Shell Noctalia", + "privacy-policy": "Politique de confidentialité", + "support": "Soutenez-nous", + "supporter-badge": "Soutien", + "supporters-desc": "Un immense merci à notre incroyable soutien !", + "supporters-desc-plural": "Un grand merci à nos {count} incroyables supporters !", + "supporters-loading": "Chargement des contributeurs...", + "system-board": "Carte mère :", + "system-cpu": "Processeur :", + "system-disk": "Disque :", + "system-gpu": "GPU :", + "system-host": "Hôte :", + "system-install-hint": "Installer fastfetch pour afficher les informations système", + "system-kernel": "Noyau :", + "system-loading": "Chargement des informations système...", + "system-memory": "Mémoire :", + "system-monitor": "Moniteur :", + "system-not-installed": "fastfetch n'est pas installé", + "system-os": "Système d'exploitation :", + "system-packages": "Paquets :", + "system-product": "Produit :", + "system-title": "Informations système", + "system-uptime": "Temps de disponibilité :", + "system-wm": "WM :", + "telemetry-data-copied": "Données de télémétrie copiées dans le presse-papiers", + "telemetry-desc": "Aidez à améliorer Noctalia en partageant des informations système anonymes (résolution d'écran, compositeur, distribution). Envoyé une fois au démarrage, pas de suivi, les données sont automatiquement supprimées après 30 jours.", + "telemetry-enabled": "Envoyer des informations système anonymes", + "telemetry-show-data": "Afficher les données", + "telemetry-title": "Confidentialité", + "title": "À propos", + "up-to-date": "Vous êtes à jour !", + "update-available": "Mise à jour disponible", + "view-commit": "Voir le commit sur GitHub" + }, + "audio": { + "devices-desc": "Configurez les périphériques d'entrée et de sortie audio disponibles.", + "devices-input-device-description": "Sélectionnez le périphérique d'entrée audio souhaité.", + "devices-input-device-label": "Périphérique d'entrée", + "devices-output-device-description": "Sélectionnez le périphérique de sortie audio souhaité.", + "devices-output-device-label": "Périphérique de sortie", + "devices-title": "Périphériques audio", + "external-mixer-description": "Entrez la commande ou le chemin d'accès de l'application à lancer lors de l'activation de la fonctionnalité de mixeur audio externe.", + "external-mixer-label": "Commande de mixeur audio externe", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Définissez vos applications multimédias préférées et ignorées.", + "media-excluded-player-description": "Ajoutez des mots-clés pour les lecteurs que le système doit ignorer. Chaque mot-clé doit être sur une nouvelle ligne.", + "media-excluded-player-label": "Lecteur exclu", + "media-excluded-player-placeholder": "tapez la sous-chaîne et appuyez sur +", + "media-frame-rate-description": "Des fréquences plus élevées sont plus fluides mais utilisent plus de ressources.", + "media-frame-rate-label": "Fréquence d'images", + "media-primary-player-description": "Entrez un mot-clé pour identifier votre lecteur principal.", + "media-primary-player-label": "Lecteur principal", + "media-primary-player-placeholder": "ex : spotify, vlc, mpv", + "media-scrolling-speed-description": "Temps en secondes pour que le titre défile du début à la fin.", + "media-scrolling-speed-label": "Vitesse de défilement", + "media-scrolling-title-description": "Activer le défilement continu pour les titres multimédias longs.", + "media-scrolling-title-label": "Titre défilant", + "media-title": "Lecteurs multimédias", + "on-middle-clicked-description": "Commande à exécuter lorsque le bouton du milieu est cliqué.", + "panel-applications-empty": "Aucune application ne lit actuellement de l'audio", + "spectrum-mirrored-description": "Inverse le spectre pour que les basses fréquences soient au centre. Quand désactivé, les basses fréquences sont à gauche et les hautes à droite.", + "spectrum-mirrored-label": "Spectre miroir", + "title": "Audio", + "visualizer-type-description": "Choisissez un type de visualisation pour la lecture de médias.", + "visualizer-type-label": "Type de visualisation", + "volumes-desc": "Ajustez les contrôles de volume et les niveaux audio.", + "volumes-feedback-sound-file-description": "Chemin du fichier son lu lors du changement de volume.", + "volumes-feedback-sound-file-label": "Fichier audio de retour du volume", + "volumes-feedback-sound-file-select-title": "Sélectionner le fichier audio de retour de volume", + "volumes-input-volume-description": "Niveau du volume d'entrée du microphone.", + "volumes-mute-input-description": "Couper l'entrée audio par défaut (microphone).", + "volumes-mute-input-label": "Couper l'entrée audio", + "volumes-mute-output-description": "Couper la sortie audio principale du système.", + "volumes-mute-output-label": "Couper la sortie audio", + "volumes-output-volume-description": "Niveau de volume de l'ensemble du système.", + "volumes-step-size-description": "Ajustez l'incrément pour les changements de volume (molette de la souris, raccourcis clavier).", + "volumes-step-size-label": "Incrément de volume", + "volumes-volume-feedback-description": "Émettre un son de retour lors du réglage du volume.", + "volumes-volume-feedback-label": "Jouer le son de retour de volume", + "volumes-volume-overdrive-description": "Permet d'augmenter le volume au-delà de 100 %. Cette fonctionnalité peut ne pas être prise en charge par tout le matériel.", + "volumes-volume-overdrive-label": "Autoriser le dépassement du volume" + }, + "bar": { + "appearance-auto-hide-delay-description": "Délai avant que la barre ne se masque après le départ de la souris", + "appearance-auto-hide-delay-label": "Délai de masquage ", + "appearance-auto-hide-exclusive-note": "Remarque : La zone exclusive est automatiquement désactivée lorsque le masquage automatique est actif", + "appearance-auto-show-delay-description": "Délai avant que la barre ne s’affiche lorsque la souris entre en bordure", + "appearance-auto-show-delay-label": "Délai d’apparition ", + "appearance-background-opacity-description": "Définir l'opacité de l'arrière-plan spécifiquement pour la barre.", + "appearance-background-opacity-label": "Opacité de l'arrière-plan de la barre", + "appearance-capsule-color-description": "Choisissez une couleur pour les capsules de la barre, ou n'en utilisez aucune pour la couleur de surface par défaut.", + "appearance-capsule-color-label": "Couleur de la capsule", + "appearance-capsule-opacity-description": "Définir le niveau d'opacité des arrière-plans des widgets quand la capsule est affichée.", + "appearance-capsule-opacity-label": "Opacité de la capsule", + "appearance-content-padding-description": "Ajustez l'espacement entre les bords de la barre et les widgets.", + "appearance-content-padding-label": "Espacement du contenu", + "appearance-density-description": "Ajustez le remplissage de la barre pour un aspect compact ou spacieux.", + "appearance-density-label": "Densité de la barre", + "appearance-desc": "Personnalisez l'apparence et la position de la barre.", + "appearance-display-mode-description": "Choisir quand la barre est visible.", + "appearance-enable-exclusion-zone-inset-description": "Réduisez la zone d'exclusion de 1 pixel physique afin que les fenêtres affleurantes se fondent parfaitement sous le bord de la barre.", + "appearance-enable-exclusion-zone-inset-label": "Zone d'exclusion en retrait", + "appearance-floating-description": "Afficher la barre sous forme de 'pilule' flottante.", + "appearance-floating-label": "Barre flottante", + "appearance-font-scale-description": "Ajuster l'échelle de la taille de police pour le texte affiché dans la barre.", + "appearance-font-scale-label": "Échelle de police", + "appearance-frame-radius": "Rayon interne", + "appearance-frame-settings-description": "Ajuster l'épaisseur du cadre et le rayon interne", + "appearance-frame-settings-label": "Paramètres du cadre", + "appearance-frame-thickness": "Épaisseur", + "appearance-hide-on-overview-description": "Masquer la barre et fermer les panneaux lorsque l'aperçu du compositeur est actif.", + "appearance-hide-on-overview-label": "Masquer la barre dans l'aperçu", + "appearance-margins-description": "Ajustez les marges autour de la barre flottante.", + "appearance-margins-horizontal": "Marge horizontale", + "appearance-margins-vertical": "Marge verticale", + "appearance-outer-corners-description": "Afficher des coins incurvés vers l'extérieur sur la barre.", + "appearance-outer-corners-label": "Coins extérieurs", + "appearance-position-description": "Choisissez où placer la barre sur l'écran.", + "appearance-position-label": "Position de la barre", + "appearance-show-capsule-description": "Afficher les arrière-plans des widgets.", + "appearance-show-capsule-label": "Afficher la capsule", + "appearance-show-on-workspace-switch-description": "Afficher la Bar brièvement et automatiquement lorsque l'espace de travail change.", + "appearance-show-on-workspace-switch-label": "Afficher la barre lors du changement d'espace de travail", + "appearance-show-outline-description": "Affiche une bordure visible autour de chaque widget.", + "appearance-show-outline-label": "Afficher les contours des widgets", + "appearance-type-description": "Choisissez le style de la barre : Simple, Flottante ou Encadrée.", + "appearance-type-label": "Type de barre", + "appearance-use-separate-opacity-description": "Activer l'utilisation d'une valeur d'opacité distincte pour l'arrière-plan de la barre.", + "appearance-use-separate-opacity-label": "Utiliser l'opacité des barres séparément", + "appearance-widget-spacing-description": "Ajustez l'espacement entre chaque widget dans la barre.", + "appearance-widget-spacing-label": "Espacement des widgets", + "behavior-middle-click-command-description": "Commande à exécuter au clic du milieu.", + "behavior-middle-click-command-label": "Commande du clic du Milieu", + "behavior-middle-click-command-placeholder": "niri msg action basculer l'aperçu", + "behavior-middle-click-description": "Choisissez ce que fait le clic du milieu sur les zones vides de la Barre.", + "behavior-middle-click-follow-mouse-description": "Ouvrir le panneau de clic central sélectionné à la position du curseur.", + "behavior-middle-click-follow-mouse-label": "Clic molette suit la souris", + "behavior-middle-click-label": "Action du clic du milieu sur la Barre", + "behavior-right-click-command-description": "Commande à exécuter au clic droit.", + "behavior-right-click-command-label": "Commande du Clic Droit", + "behavior-right-click-command-placeholder": "notify-send \"Clic droit\"", + "behavior-right-click-description": "Choisissez ce que fait un clic droit sur les zones vides de la barre.", + "behavior-right-click-follow-mouse-description": "Ouvre le panneau de clic droit sélectionné à la position du curseur.", + "behavior-right-click-follow-mouse-label": "Clic droit suit la souris", + "behavior-right-click-label": "Action du clic droit sur la Barre", + "behavior-wheel-wrap-description": "Lorsque activé, le défilement continue du dernier élément au premier.", + "behavior-wheel-wrap-label": "Boucler", + "behavior-workspace-scroll-description": "Choisissez ce que fait la molette de la souris sur les zones vides de la barre.", + "behavior-workspace-scroll-label": "Action de la molette de la souris de la Barre", + "behavior-workspace-scroll-option-content": "Contenu", + "behavior-workspace-scroll-option-workspace": "Espace de travail", + "monitor-configure-widgets": "Configurer les widgets", + "monitor-override-settings": "Remplacer les paramètres globaux", + "monitor-override-settings-description": "Utiliser des paramètres personnalisés pour ce moniteur.", + "monitor-reset-all": "Réinitialiser tout", + "monitor-widgets-title": "Configuration du widget - {monitor}", + "monitors-desc": "Afficher la barre sur des moniteurs spécifiques. Par défaut, sur tous si aucun n'est choisi.", + "monitors-desc-new": "Configurez les moniteurs qui affichent la barre et personnalisez les paramètres pour chaque moniteur.", + "monitors-title": "Affichage sur les moniteurs", + "title": "Barre", + "tray-blacklist-description": "Ajouter des règles d'exclusion pour la boîte à miniatures, prend en charge les caractères génériques (*).", + "tray-blacklist-label": "Liste noire", + "tray-blacklist-placeholder": "ex : nm-applet, Fcitx*", + "tray-pin-application": "Épingler l'application", + "tray-unpin-application": "Désépingler l'application", + "use-global-widgets": "Utiliser les widgets globaux", + "widgets-desc": "Faites glisser les widgets pour modifier leur ordre. Utilisez le menu contextuel (clic droit) pour transférer les widgets entre les sections ou les supprimer.", + "widgets-title": "Positionnement des widgets" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Générez des schémas de couleurs à partir de votre fond d'écran. Extrait automatiquement les couleurs pour créer un thème cohérent.", + "color-source-use-wallpaper-colors-label": "Utiliser les couleurs du fond d'écran", + "dark-mode-mode-description": "Active la commutation automatique entre le mode clair et le mode sombre.", + "dark-mode-mode-label": "Programmation du mode sombre", + "dark-mode-mode-manual": "Manuel", + "dark-mode-mode-off": "Éteint", + "dark-mode-switch-description": "Passe à un thème plus sombre pour une visualisation plus facile la nuit.", + "delete-error-description": "Échec de la suppression de {scheme}", + "delete-error-title": "Échec de la suppression", + "delete-success-description": "{scheme} supprimé avec succès", + "delete-success-title": "Jeu de couleurs supprimé", + "download-button": "Télécharger plus", + "download-downloading": "Téléchargement...", + "download-empty": "Aucun jeu de couleurs disponible", + "download-error-api-error": "Erreur API: {status}", + "download-error-description": "Échec du téléchargement de {scheme}", + "download-error-download-failed": "Échec du téléchargement avec le code de sortie: {code}", + "download-error-invalid-response": "Format de réponse API invalide", + "download-error-no-files": "Aucun fichier trouvé pour le jeu de couleurs", + "download-error-parse-failed": "Échec de l'analyse de la réponse API: {error}", + "download-error-rate-limit": "Limite de débit de l'API GitHub dépassée", + "download-error-title": "Échec du téléchargement", + "download-fetching": "Récupération des jeux de couleurs disponibles...", + "download-success-description": "{scheme} téléchargé avec succès", + "download-success-title": "Jeu de couleurs téléchargé", + "download-title": "Télécharger des jeux de couleurs", + "method-description": { + "content": "Schéma Material Design avec extraction de couleurs haute fidélité qui correspond étroitement aux couleurs réelles du contenu source.", + "dysfunctional": "Comme Faithful, mais choisit la deuxième famille de couleurs la plus dominante comme principale.", + "faithful": "Tente de rester proche de la couleur source tout en générant une palette harmonieuse.", + "fruit-salad": "Schéma Material Design qui produit une palette ludique et vibrante avec des teintes diverses et variées.", + "monochrome": "Schéma Material Design utilisant une échelle de gris à teinte unique avec un contenu chromatique minimal.", + "muted": "Génère des tons plus doux et désaturés pour une apparence calme et discrète.", + "rainbow": "Schéma Material Design qui génère des couleurs sur tout le spectre pour une diversité de couleurs maximale.", + "tonal-spot": "Algorithme par défaut de Material Design. Crée des palettes équilibrées en utilisant les variations tonales de la couleur source.", + "vibrant": "Maximise la saturation des couleurs pour des palettes audacieuses et accrocheuses aux tons vifs." + }, + "predefined-desc": "Choisissez parmi une collection de jeux de couleurs prédéfinis.", + "predefined-generate-templates-label": "Générer des modèles pour les schémas prédéfinis", + "predefined-title": "Jeux de couleurs prédéfinis", + "sync-gsettings-description": "Adapter le thème du système à la variante claire ou sombre active.", + "sync-gsettings-label": "Synchroniser le thème système", + "templates-desc": "Appliquer des couleurs aux applications externes.", + "templates-filter-description": "Afficher les modèles d'une catégorie spécifique.", + "templates-filter-label": "Filtrer par catégorie", + "templates-misc-description": "Créez vos propres modèles.", + "templates-misc-label": "Avancé", + "templates-misc-user-templates-description": "N'activez que si vous savez ce que vous faites — consultez notre documentation en ligne.", + "templates-misc-user-templates-label": "Activer les modèles utilisateur", + "templates-none-detected": "Aucun détecté", + "templates-write-path": "Écrit : {filepath}", + "title": "Jeu de couleurs", + "wallpaper-method-description": "Choisissez votre méthode de génération de palette préférée.", + "wallpaper-method-label": "Méthode de génération de palette", + "wallpaper-monitor-source-description": "Sélectionnez le moniteur à utiliser pour extraire les couleurs du fond d'écran.", + "wallpaper-monitor-source-label": "Source de génération des couleurs" + }, + "connections": { + "authentication-required": "Authentification requise", + "bluetooth-auto-connect-description": "Se connecter automatiquement aux appareils appairés de confiance lorsque le Bluetooth est activé.", + "bluetooth-auto-connect-label": "Connecter automatiquement les appareils appairés", + "bluetooth-devices-unnamed": "Les appareils sans nom ne sont pas affichés.", + "bluetooth-discoverable": "Cet appareil est détectable en tant que {hostName} tant que cet onglet de paramètres est ouvert.", + "bluetooth-rssi-polling-description": "Échantillonne périodiquement le RSSI des appareils connectés via bluetoothctl. Peut ne pas être disponible pour tous les appareils ; utilise des ressources minimales lorsqu'il est activé.", + "bluetooth-rssi-polling-interval-description": "Configurer la fréquence de mise à jour de la force du signal pour les appareils connectés.", + "bluetooth-rssi-polling-interval-label": "Intervalle d'interrogation", + "bluetooth-rssi-polling-label": "Interrogation du signal Bluetooth", + "disable-discoverability-description": "Masquer votre appareil des appareils Bluetooth à proximité.", + "disable-discoverability-label": "Désactiver la visibilité de l'appareil", + "hide-unnamed-devices-description": "Masquer les appareils qui n'apparaissent que sous forme d'adresses Bluetooth.", + "hide-unnamed-devices-label": "Masquer les appareils sans nom", + "pin-instructions": "Veuillez saisir le code PIN affiché sur votre appareil.", + "title": "Connexions", + "wifi-header-text": "Utilisez le Wi-Fi pour voir les réseaux disponibles et vous connecter à internet." + }, + "control-center": { + "cards-desc": "Personnalisez les commandes qui apparaissent dans le centre de contrôle et leur ordre d'affichage.", + "desc": "Configurer le positionnement et le comportement du panneau du centre de contrôle.", + "open-at-mouse-description": "Lors d'un clic droit sur une zone vide de la Bar, ouvre le Centre de Contrôle près du curseur. Si désactivé, le réglage de position ci-dessus est utilisé à la place.", + "open-at-mouse-label": "Ouvrir au niveau du curseur lors d'un clic droit", + "position-description": "Choisissez où le panneau du centre de contrôle apparaît lorsqu'il est ouvert.", + "shortcuts-custom-button-command-description": "La commande à exécuter lorsque le bouton est cliqué.", + "shortcuts-custom-button-enable-on-state-logic-description": "Active une deuxième icône et un état 'chaud' basés sur une commande de vérification.", + "shortcuts-custom-button-enable-on-state-logic-label": "Activer la logique d'état activé", + "shortcuts-custom-button-icon-description": "Sélectionnez une icône de la bibliothèque.", + "shortcuts-custom-button-on-clicked-label": "Commande de clic gauche", + "shortcuts-custom-button-on-middle-clicked-description": "Commande à exécuter lors d'un clic central sur le bouton.", + "shortcuts-custom-button-on-state-command-description": "Commande à exécuter pour vérifier si le bouton doit être dans l'état 'activé'. Retourne 0 pour activé, non-zéro pour désactivé.", + "shortcuts-custom-button-on-state-command-label": "Commande de vérification de l'état activé", + "shortcuts-custom-button-on-state-icon-description": "L'icône du bouton lorsqu'il est dans l'état 'activé'.", + "shortcuts-custom-button-on-state-icon-label": "Icône d'état activé", + "shortcuts-custom-button-state-checks-add": "Ajouter une vérification d'état", + "shortcuts-custom-button-state-checks-command": "Commande à exécuter pour cette vérification d'état", + "shortcuts-custom-button-state-checks-label": "Vérifications d'état", + "shortcuts-custom-button-state-checks-remove": "Supprimer", + "shortcuts-custom-button-tooltip-description": "L'infobulle à afficher lors du survol du bouton.", + "shortcuts-custom-button-tooltip-label": "Infobulle", + "shortcuts-title": "Widgets de raccourcis", + "system-monitor-disk-path-description": "Sélectionnez le point de montage du disque que la carte du moniteur système dans le centre de contrôle doit surveiller.", + "system-monitor-disk-path-label": "Chemin du disque du moniteur système", + "title": "Centre de contrôle" + }, + "desktop-widgets": { + "clock-enabled-description": "Afficher un widget d'horloge sur le bureau.", + "clock-enabled-label": "Activer le widget horloge", + "clock-format-label": "Format", + "clock-height-description": "Hauteur du widget horloge en pixels.", + "clock-height-label": "Taille", + "clock-minimal-mode-description": "Utiliser un affichage d'horloge minimaliste avec l'heure et la date.", + "clock-minimal-mode-label": "Mode minimaliste", + "clock-rounded-corners-description": "Utiliser des coins arrondis pour l'arrière-plan du widget.", + "clock-rounded-corners-label": "Coins arrondis", + "clock-show-background-description": "Afficher le conteneur d'arrière-plan pour le widget d'horloge.", + "clock-show-background-label": "Afficher l'arrière-plan", + "clock-show-date-description": "Afficher la date actuelle sous l'heure.", + "clock-show-date-label": "Afficher la date", + "clock-show-seconds-description": "Afficher les secondes dans l'heure.", + "clock-show-seconds-label": "Afficher les secondes", + "clock-style-analog": "Analogique", + "clock-style-binary": "Binaire", + "clock-style-description": "Choisissez le style d'affichage de l'horloge.", + "clock-style-digital": "Numérique", + "clock-style-label": "Style d'horloge", + "clock-style-minimal": "Minimal", + "clock-width-description": "Largeur du widget horloge en pixels.", + "cpu-intensive-note": "Les widgets marqués d'un ! utilisent plus de CPU et devraient être activés avec prudence.", + "edit-mode-button-label": "Entrer en mode édition", + "edit-mode-controls-explanation": "Clic gauche et glisser-déposer : Déplacer ou redimensionner le widget.\nClic droit : Ouvrir les options du menu contextuel.", + "edit-mode-description": "Activer le mode édition pour déplacer et repositionner les widgets du bureau. Une fois activé, les widgets affichent un contour de glissement et peuvent être repositionnés.", + "edit-mode-exit-button": "Quitter le mode édition", + "edit-mode-grid-snap-label": "Aligner sur la grille", + "edit-mode-grid-snap-scale-label": "Échelle d'accrochage à la grille", + "edit-mode-label": "Mode édition", + "enabled-description": "Activer ou désactiver complètement les widgets du bureau.", + "enabled-label": "Activer les widgets de bureau", + "general-desc": "Configurer les widgets qui apparaissent sur votre bureau.", + "general-title": "Widgets de bureau", + "media-player-enabled-description": "Afficher un widget de lecteur multimédia sur le bureau.", + "media-player-enabled-label": "Activer le widget lecteur multimédia", + "media-player-rounded-corners-description": "Activer les coins arrondis sur les bords du widget.", + "media-player-show-album-art-description": "Afficher la pochette de l'album et les informations sur les pistes (titre et artiste).", + "media-player-show-album-art-label": "Afficher la pochette et le titre de l'album", + "media-player-show-background-description": "Afficher le conteneur d'arrière-plan pour le widget lecteur multimédia.", + "media-player-show-buttons-description": "Afficher les boutons de contrôle multimédia (lecture/pause, précédent, suivant) sur le widget lecteur multimédia.", + "media-player-show-buttons-label": "Afficher les boutons", + "media-player-show-visualizer-description": "Afficher la superposition de l'visualiseur audio.", + "media-player-visualizer-type-description": "Choisissez un type de visualisation pour l'arrière-plan du lecteur multimédia de bureau.", + "overview-enabled-description": "Afficher les widgets du bureau lorsque l'aperçu du compositeur est ouvert.", + "overview-enabled-label": "Afficher dans l'aperçu", + "system-stat-layout-bottom": "Bas", + "system-stat-layout-description": "Choisissez comment la légende est affichée par rapport au graphique.", + "system-stat-layout-label": "Disposition", + "system-stat-layout-side": "Côté", + "system-stat-rounded-corners-description": "Utiliser des coins arrondis pour l'arrière-plan du widget.", + "system-stat-rounded-corners-label": "Coins Arrondis", + "system-stat-show-background-description": "Afficher le conteneur d'arrière-plan pour le widget de statistiques système.", + "system-stat-show-background-label": "Afficher l'arrière-plan", + "system-stat-stat-type-description": "Choisis quelle statistique système afficher.", + "system-stat-stat-type-label": "Type de Statistique", + "title": "Widgets de bureau", + "weather-enabled-description": "Afficher un widget météo sur le bureau.", + "weather-enabled-label": "Activer le widget météo", + "weather-show-background-description": "Afficher le conteneur d'arrière-plan pour le widget météo." + }, + "display": { + "monitors-backlight-device-auto-option": "Par défaut", + "monitors-backlight-device-description": "Sélectionnez un appareil de rétroéclairage pour cette sortie.", + "monitors-backlight-device-label": "Appareil de Rétroéclairage", + "monitors-brightness-step-description": "Ajustez l'incrément pour les changements de luminosité (molette de la souris et raccourcis clavier).", + "monitors-brightness-step-label": "Incrément de luminosité", + "monitors-brightness-unavailable-ddc-disabled": "Contrôle de la luminosité indisponible. Activez \"Prise en charge de la luminosité externe\" pour contrôler la luminosité de cet écran.", + "monitors-brightness-unavailable-generic": "Le contrôle de la luminosité n'est pas disponible pour cet écran.", + "monitors-desc": "Ajustez la luminosité pour chaque écran.", + "monitors-enforce-minimum-description": "Résout le problème de l'extinction complète du rétroéclairage sur certains écrans à 0% de luminosité.", + "monitors-enforce-minimum-label": "Imposer une luminosité minimale (1%)", + "monitors-external-brightness-description": "Activer le support DDCUtil pour contrôler la luminosité des écrans externes via le protocole DDC/CI.", + "monitors-external-brightness-label": "Prise en charge de la luminosité externe", + "monitors-global-brightness-description": "Ajuster la luminosité de tous les moniteurs en même temps.", + "monitors-global-brightness-label": "Tous les moniteurs", + "monitors-title": "Paramètres par moniteur", + "night-light-auto-schedule-description": "Basé sur l'heure du coucher et du lever du soleil à {location} — recommandé.", + "night-light-auto-schedule-label": "Planification automatique", + "night-light-desc": "Réduit l'émission de lumière bleue pour vous aider à mieux dormir et réduire la fatigue oculaire.", + "night-light-enable-description": "Applique un filtre de couleur chaude pour réduire l'émission de lumière bleue.", + "night-light-enable-label": "Activer l'éclairage nocturne", + "night-light-force-activation-description": "Ignore la planification et applique le filtre nocturne immédiatement.", + "night-light-force-activation-label": "Forcer l'activation", + "night-light-manual-schedule-description": "Définissez des heures personnalisées pour le lever et le coucher du soleil.", + "night-light-manual-schedule-label": "Planification manuelle", + "night-light-manual-schedule-select-start": "Sélectionner l'heure de début", + "night-light-manual-schedule-select-stop": "Sélectionner l'heure de fin", + "night-light-manual-schedule-sunrise": "Heure du lever du soleil", + "night-light-manual-schedule-sunset": "Heure du coucher du soleil", + "night-light-temperature-day": "Jour", + "night-light-temperature-day-description": "Contrôle la température pendant la journée.", + "night-light-temperature-description": "Réglez la chaleur des couleurs pour la nuit et le jour.", + "night-light-temperature-label": "Température de couleur", + "night-light-temperature-night": "Nuit", + "night-light-temperature-night-description": "Contrôle la température pendant la nuit.", + "title": "Affichage" + }, + "dock": { + "appearance-background-opacity-description": "Ajustez l'opacité de l'arrière-plan du dock.", + "appearance-border-radius-description": "Ajuster le rayon de bordure du dock.", + "appearance-border-radius-label": "Rayon de bordure", + "appearance-colorize-icons-description": "Appliquer les couleurs du thème aux icônes d'applications du dock (applications non focalisées uniquement).", + "appearance-colorize-icons-label": "Coloriser les icônes", + "appearance-dead-opacity-description": "Ajuster l'opacité des icônes d'applications qui ne sont pas en cours d'exécution.", + "appearance-dead-opacity-label": "Opacité morte", + "appearance-desc": "Personnalisez le comportement et l'apparence du dock.", + "appearance-display-auto-hide": "Masquer automatiquement", + "appearance-display-description": "Choisissez comment le dock se comporte.", + "appearance-display-exclusive": "Exclusif", + "appearance-dock-indicator-description": "Afficher un petit indicateur lorsque le dock est masqué.", + "appearance-dock-indicator-label": "Indicateur du dock", + "appearance-floating-distance-description": "Ajustez la distance de flottaison par rapport au bord de l'écran.", + "appearance-floating-distance-label": "Distance de flottaison du dock", + "appearance-group-apps-description": "Regrouper plusieurs fenêtres de la même app en une seule entrée du Dock.", + "appearance-group-apps-label": "Regrouper les mêmes applications", + "appearance-group-click-action-cycle": "Basculer entre les fenêtres", + "appearance-group-click-action-description": "Choisissez ce que fait le clic gauche pour les applications groupées.", + "appearance-group-click-action-label": "Action au clic d'application groupée", + "appearance-group-click-action-list": "Ouvrir la liste des fenêtres", + "appearance-group-context-menu-mode-description": "Choisissez comment le menu contextuel se comporte pour les applications groupées.", + "appearance-group-context-menu-mode-extended": "Étendu", + "appearance-group-context-menu-mode-label": "Mode de menu d'applications groupé", + "appearance-group-context-menu-mode-list": "Liste des fenêtres", + "appearance-group-indicator-style-description": "Choisissez comment les indicateurs d'exécution groupés affichent l'état de la fenêtre active.", + "appearance-group-indicator-style-dots": "Points", + "appearance-group-indicator-style-label": "Style d'indicateur groupé", + "appearance-group-indicator-style-number": "Nombre", + "appearance-hide-show-speed-description": "Ajustez la vitesse de l'animation d'affichage/masquage du dock.", + "appearance-hide-show-speed-label": "Vitesse d'affichage/masquage", + "appearance-icon-size-description": "Ajuster la taille globale du dock.", + "appearance-icon-size-label": "Taille du dock", + "appearance-inactive-indicators-description": "Afficher les pastilles d'indicateur pour toutes les applications, pas seulement celle qui est active.", + "appearance-inactive-indicators-label": "Indicateurs de fonctionnement", + "appearance-indicator-color-description": "Choisissez la couleur de l'indicateur du dock masqué.", + "appearance-indicator-color-label": "Couleur de l'indicateur", + "appearance-indicator-opacity-description": "Ajustez l'opacité de l'indicateur du dock masqué.", + "appearance-indicator-opacity-label": "Opacité de l'indicateur", + "appearance-indicator-thickness-description": "Activez un indicateur du dock masqué plus épais (6 px au lieu de 3 px).", + "appearance-indicator-thickness-label": "Indicateur plus épais", + "appearance-launcher-icon-description": "Choisissez l'icône utilisée pour le Dock Launcher.", + "appearance-launcher-icon-label": "Icône du Lanceur", + "appearance-launcher-position-description": "Choisissez où l'icône du lanceur apparaît dans le dock.", + "appearance-launcher-position-end": "Fin", + "appearance-launcher-position-label": "Position du lanceur", + "appearance-launcher-position-start": "Démarrer", + "appearance-launcher-use-distro-logo-description": "Utilise le logo de ta distribution comme icône du Dock Launcher.", + "appearance-launcher-use-distro-logo-label": "Utiliser le logo de la distribution au lieu de l'icône", + "appearance-pinned-static-description": "Toujours pousser les icônes d'applications épinglées vers la gauche dans un ordre statique.", + "appearance-pinned-static-label": "Applications épinglées statiques", + "appearance-position-description": "Choisissez l'emplacement du Dock à l'écran.", + "appearance-position-label": "Position", + "appearance-show-launcher-icon-description": "Afficher l'icône du lanceur d'applications dans le dock.", + "appearance-show-launcher-icon-label": "Afficher le Lanceur d'applications", + "appearance-sit-on-frame-description": "Aligner le dock à l'intérieur de la bordure du cadre au lieu qu'il se superpose.", + "appearance-sit-on-frame-label": "Le dock est sur le cadre", + "appearance-type-attached": "Ancré", + "appearance-type-description": "Choisissez entre une barre flottante en forme de pilule ou une barre statique attachée au bord.", + "appearance-type-floating": "Flottant", + "appearance-type-label": "Style du dock", + "enabled-description": "Afficher ou masquer complètement le dock.", + "enabled-label": "Activer le dock", + "monitors-desc": "Afficher le dock sur des écrans spécifiques. Par défaut, il s'affiche sur tous les écrans si aucun n'est sélectionné.", + "monitors-only-same-monitor-description": "Afficher uniquement les applications de l'écran où se trouve le dock.", + "monitors-only-same-monitor-label": "Seulement les applications du même écran", + "monitors-title": "Affichage sur les moniteur", + "title": "Dock" + }, + "general": { + "copy-settings": "Copier les paramètres", + "fonts-default-description": "Police principale utilisée dans toute l'interface.", + "fonts-default-label": "Police par défaut", + "fonts-default-placeholder": "Sélectionner la police par défaut...", + "fonts-default-scale-description": "Augmenter ou diminuer la taille du texte standard.", + "fonts-default-scale-label": "Taille de police par défaut", + "fonts-default-search-placeholder": "Rechercher des polices...", + "fonts-desc": "Choisissez les polices utilisées dans toute l'interface.", + "fonts-monospace-description": "Police monospace utilisée pour l'affichage des chiffres et des statistiques.", + "fonts-monospace-label": "Police monospace", + "fonts-monospace-placeholder": "Sélectionner la police monospace...", + "fonts-monospace-scale-description": "Augmenter ou diminuer la taille du texte monospace.", + "fonts-monospace-scale-label": "Taille de police monospace", + "fonts-monospace-search-placeholder": "Rechercher des polices monospace...", + "fonts-title": "Polices", + "keybinds-conflict-description": "La combinaison de touches est déjà attribuée à {action}.", + "keybinds-conflict-title": "Conflit de raccourcis clavier", + "keybinds-description": "Configurez les touches de navigation globales pour les panneaux et le lanceur.", + "keybinds-down": "Déplacer vers le bas", + "keybinds-enter": "Confirmer / Action", + "keybinds-escape": "Fermer / Retour", + "keybinds-left": "Déplacer à gauche", + "keybinds-modifier-description": "Les raccourcis doivent inclure une touche de modification (Ctrl ou Alt).", + "keybinds-modifier-title": "Modificateur requis", + "keybinds-remove": "Supprimer", + "keybinds-right": "Déplacer à droite", + "keybinds-title": "Raccourcis clavier de navigation", + "keybinds-up": "Déplacer vers le haut", + "language-desc": "Choisissez votre langue préférée pour l'application.", + "language-select-auto-detect": "Automatique", + "language-select-description": "Sélectionnez la langue utilisée dans l'interface de l'application.", + "language-select-label": "Langue de l'application", + "launch-setup-wizard": "Assistant de configuration", + "profile-desc": "Modifiez vos informations utilisateur et votre avatar.", + "profile-picture-description": "Votre photo de profil qui apparaît dans toute l'interface.", + "profile-picture-label": "Photo de profil", + "profile-select-avatar": "Sélectionner une image d'avatar", + "profile-title": "Profil", + "profile-tooltip": "Photo de profil", + "reverse-scrolling-description": "Inverser le sens de défilement interprété.", + "reverse-scrolling-label": "Défilement inversé", + "screen-corners-desc": "Personnalisez l'arrondi des coins de l'écran et les effets visuels.", + "screen-corners-radius-description": "Ajustez l'arrondi des coins de l'écran.", + "screen-corners-radius-label": "Rayon des coins de l'écran", + "screen-corners-radius-reset": "Réinitialiser le rayon des coins de l'écran", + "screen-corners-show-corners-description": "Affichez des coins arrondis sur le bord de l'écran.", + "screen-corners-show-corners-label": "Afficher les coins de l'écran", + "screen-corners-solid-black-description": "Utilisez du noir uni au lieu de la couleur de fond de la barre.", + "screen-corners-solid-black-label": "Coins noirs unis", + "screen-corners-title": "Coins de l'écran", + "settings-copied": "Paramètres copiés dans le presse-papier", + "smooth-scrolling-description": "Animer le défilement des listes pour une expérience de molette plus fluide.", + "smooth-scrolling-label": "Défilement fluide", + "tab-basics": "Bases", + "tab-keybinds": "Raccourcis clavier" + }, + "hooks": { + "color-generation-description": "Commande à exécuter lorsque la génération de couleurs est terminée (par ex. après la thématisation basée sur le fond d'écran).", + "color-generation-label": "Couleurs générées", + "color-generation-placeholder": "par ex. notify-send \"Couleurs\" \"Générées\"", + "info-command-info-description": "• Les commandes sont exécutées via le shell (sh -lc)
• Les commandes s'exécutent en arrière-plan (détachées)
• Les boutons de test s'exécutent avec les valeurs actuelles", + "info-parameters-description": "• Wallpaper hook: $1 = chemin du fond d'écran, $2 = nom de l'écran, $3 = thème (sombre/clair)
• Theme toggle hook: $1 = true/false (état du mode sombre)
• Screen lock/unlock hooks: $1 = lock/unlock (état de verrouillage de l'écran)
• Performance mode hooks: Aucun paramètre
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "Paramètres disponibles", + "noctalia-started-description": "Commande à exécuter lorsque Noctalia a fini de charger.", + "noctalia-started-label": "Noctalia démarré", + "noctalia-started-placeholder": "ex : notify-send 'Noctalia chargé'", + "performance-mode-disabled-description": "Commande à exécuter lorsque le mode performance Noctalia est désactivé.", + "performance-mode-disabled-label": "Mode performance désactivé", + "performance-mode-disabled-placeholder": "ex : notify-send \"Performance\" \"Mode désactivé\"", + "performance-mode-enabled-description": "Commande à exécuter lorsque le mode performance Noctalia est activé.", + "performance-mode-enabled-label": "Mode performance activé", + "performance-mode-enabled-placeholder": "ex : notify-send \"Performance\" \"Mode activé\"", + "screen-lock-description": "Commande à exécuter lorsque l'écran est verrouillé.", + "screen-lock-label": "Écran verrouillé", + "screen-lock-placeholder": "ex : notify-send \"Écran\" \"Verrouillé\"", + "screen-unlock-description": "Commande à exécuter lorsque l'écran est déverrouillé.", + "screen-unlock-label": "Écran déverrouillé", + "screen-unlock-placeholder": "ex : notify-send \"Écran\" \"Déverrouillé\"", + "session-description": "Commande à exécuter avant l'arrêt ou le redémarrage. Reçoit le type d'action comme $1 (arrêt/redémarrage).", + "session-label": "Fin de session", + "session-placeholder": "ex : notify-send \"Session\" \"$1\"", + "system-hooks-desc": "Configurez des commandes à exécuter lors d'événements système.", + "system-hooks-enable-description": "Activer ou désactiver toutes les commandes de hook.", + "system-hooks-enable-label": "Activer les hooks", + "system-hooks-title": "Hooks système", + "theme-changed-description": "Commande à exécuter lorsque le thème bascule entre le mode sombre et clair.", + "theme-changed-label": "Thème changé", + "theme-changed-placeholder": "ex : notify-send \"Thème\" \"Basculé\"", + "title": "Hooks", + "wallpaper-changed-description": "Commande à exécuter lorsque le fond d'écran change.", + "wallpaper-changed-label": "Fond d'écran changé", + "wallpaper-changed-placeholder": "ex : notify-send \"Fond d'écran\" \"Changé\"" + }, + "idle": { + "custom-add": "Ajouter une commande", + "custom-description": "Exécute une commande shell après une période d'inactivité.", + "custom-entry-command": "Commande", + "custom-entry-delete": "Supprimer", + "custom-entry-edit": "Modifier la Commande Personnalisée", + "custom-entry-name": "Nom", + "custom-entry-name-placeholder": "ex : Éteindre les lumières", + "custom-entry-new": "Nouvelle Commande Personnalisée", + "custom-entry-timeout": "Temps d'inactivité", + "custom-entry-timeout-format": "{count} seconde", + "custom-entry-timeout-format-plural": "{count} secondes", + "custom-entry-unnamed": "Commande sans nom", + "custom-label": "Commandes d'inactivité personnalisées", + "enable-description": "Éteindre l'écran, verrouiller ou suspendre automatiquement après une période d'inactivité.", + "enable-label": "Activer la gestion de l'inactivité", + "fade-duration-description": "Secondes pour l'animation de fondu au noir avant que chaque action ne se déclenche. Tout mouvement de souris annule le fondu.", + "fade-duration-label": "Durée du fondu", + "lock-description": "Secondes d'inactivité avant l'activation de l'écran de verrouillage.", + "lock-label": "Verrouiller l'écran", + "resume-command-label": "Reprendre la commande", + "screen-off-description": "Secondes d'inactivité avant que les moniteurs ne s'éteignent.", + "screen-off-label": "Éteindre l'écran", + "status-description": "Temps d'inactivité tel que rapporté par le compositeur.", + "status-label": "Temps d'inactivité", + "suspend-description": "Secondes d'inactivité avant que le système ne se suspende.", + "tab-behavior": "Comportement", + "tab-custom": "Personnalisé", + "timeouts-description": "Réglez sur 0 pour désactiver une étape. Les délais d'attente sont mis en pause tant que 'Rester éveillé' est actif.", + "timeouts-label": "Délais d'expiration", + "title": "Inactivité", + "unavailable": "La surveillance native de l'inactivité n'est pas disponible sur ce compositeur." + }, + "indicator": { + "default-value": "Par défaut : {value}", + "system-default": "Par défaut du système" + }, + "launcher": { + "clipboard-desc": "Accédez à et gérez votre historique du presse-papiers depuis le lanceur.", + "execute-desc": "Configurer la façon dont les applications sont lancées.", + "execute-title": "Exécution", + "settings-annotation-tool-description": "Commande à exécuter lorsque l'on clique sur le bouton d'annotation dans l'historique du presse-papiers. L'image sera envoyée à cette commande.", + "settings-annotation-tool-label": "Outil d'annotation", + "settings-annotation-tool-placeholder": "ex : 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Coller automatiquement l'élément sélectionné du presse-papiers. Nécessite wtype.", + "settings-auto-paste-label": "Collage automatique", + "settings-clip-chips-description": "Afficher une barre d'onglets pour filtrer l'historique du presse-papiers par type (Images, Liens, Fichiers, Code, etc.).", + "settings-clip-chips-label": "Activer les puces de catégorie", + "settings-clip-preview-description": "Afficher un aperçu du contenu du presse-papiers lors de l'utilisation de la commande >clip.", + "settings-clip-preview-label": "Activer l'aperçu du presse-papiers", + "settings-clip-smart-icons-description": "Afficher des icônes spécifiques pour les liens, les fichiers, les couleurs et le code au lieu d'une icône de presse-papiers générique.", + "settings-clip-smart-icons-label": "Activer les icônes intelligentes", + "settings-clip-wrap-text-description": "Envelopper le texte dans la liste du presse-papiers au lieu de le tronquer.", + "settings-clip-wrap-text-label": "Envelopper le texte du presse-papiers", + "settings-clipboard-history-description": "Accédez aux éléments précédemment copiés depuis le lanceur.", + "settings-clipboard-history-label": "Activer l'historique du presse-papiers", + "settings-clipboard-watch-image-description": "Chaîne de commande complète passée à wl-paste pour les changements d'image (redémarrage requis).", + "settings-clipboard-watch-image-label": "Commande de Surveillance d'Images", + "settings-clipboard-watch-text-description": "Chaîne de commande complète passée à wl-paste pour les changements de texte (nécessite un redémarrage).", + "settings-clipboard-watch-text-label": "Commande de surveillance de texte", + "settings-custom-launch-prefix-description": "Préfixer les commandes avec un lanceur personnalisé (ex : 'runapp' pour l'intégration systemd).", + "settings-custom-launch-prefix-enabled-description": "Utiliser un préfixe personnalisé pour lancer les applications au lieu de la méthode par défaut.", + "settings-custom-launch-prefix-enabled-label": "Activer le préfixe de lancement personnalisé", + "settings-custom-launch-prefix-label": "Préfixe de lancement personnalisé", + "settings-density-description": "Ajustez la taille des icônes d'application et la densité du Launcher.", + "settings-density-label": "Densité", + "settings-desc": "Personnalisez le comportement et l'apparence du lanceur.", + "settings-enable-session-search-description": "Afficher les actions de session (verrouiller, éteindre, redémarrer, etc.) lors de la recherche dans le Launcher.", + "settings-enable-session-search-label": "Activer la recherche de session", + "settings-enable-settings-search-description": "Afficher les résultats des paramètres lors de la recherche dans le lanceur.", + "settings-enable-settings-search-label": "Activer la recherche dans les paramètres", + "settings-enable-windows-search-description": "Rechercher et focaliser les fenêtres actives.", + "settings-enable-windows-search-label": "Activer la recherche de fenêtres", + "settings-grid-view-description": "Afficher les éléments dans une grille au lieu d'une liste.", + "settings-icon-mode-description": "Utiliser les icônes natives du système au lieu des icônes Tabler.", + "settings-icon-mode-label": "Utiliser les icônes natives", + "settings-ignore-mouse-input-description": "Désactiver l'interaction de la souris et la molette de défilement dans le lanceur.", + "settings-ignore-mouse-input-label": "Ignorer l'entrée de la souris", + "settings-overlay-layer-description": "Affiche le lanceur sur la couche de superposition, au-dessus des fenêtres en plein écran. Lorsqu'il est activé, le lanceur ne s'attachera pas à la barre.", + "settings-overlay-layer-label": "Afficher au-dessus du plein écran", + "settings-position-description": "Choisissez où le panneau du lanceur apparaît.", + "settings-show-categories-description": "Afficher les onglets de catégories pour filtrer les applications.", + "settings-show-categories-label": "Afficher les catégories", + "settings-show-icon-background-description": "Afficher un arrière-plan rectangulaire arrondi derrière les icônes.", + "settings-show-icon-background-label": "Afficher l'arrière-plan de l'icône", + "settings-sort-by-usage-description": "Lorsque cette option est activée, les applications fréquemment lancées apparaissent en premier dans la liste.", + "settings-sort-by-usage-label": "Trier par les plus utilisés", + "settings-terminal-command-description": "Commande pour lancer un terminal. Ex : 'kitty -e' ou 'gnome-terminal --'.", + "settings-terminal-command-label": "Commande du terminal", + "settings-use-app2unit-description": "Utilise une méthode de lancement alternative pour mieux gérer les processus des applications et prévenir les problèmes.", + "settings-use-app2unit-label": "Utiliser App2Unit pour lancer les applications", + "settings-view-mode-description": "Choisis la disposition des entrées du Lanceur.", + "settings-view-mode-label": "Mode d'affichage", + "title": "Lanceur" + }, + "location": { + "auto-locate-description": "Détecte automatiquement votre position périodiquement en utilisant votre adresse IP.", + "auto-locate-label": "Localisation Automatique", + "calendar-cards-desc": "Organiser et activer/désactiver les cartes dans le panneau de calendrier.", + "calendar-cards-title": "Cartes de calendrier", + "calendar-header-label": "En-tête du calendrier", + "calendar-month-label": "Mois calendaire", + "date-time-12hour-format-description": "Afficher l'heure au format 12 heures sur l'écran de verrouillage et dans le calendrier. L'horloge de la barre possède ses propres paramètres.", + "date-time-12hour-format-label": "Utiliser le format horaire de 12 heures", + "date-time-desc": "Personnalisez l'affichage de la date et de l'heure.", + "date-time-first-day-of-week-automatic": "Automatique (utiliser les paramètres régionaux du système)", + "date-time-first-day-of-week-description": "Choisissez quel jour commence la semaine dans le calendrier.", + "date-time-first-day-of-week-label": "Premier jour de la semaine", + "date-time-show-events-description": "Afficher les événements dans le panneau du calendrier.", + "date-time-show-events-label": "Afficher les événements du calendrier", + "date-time-title": "Date & heure", + "date-time-use-analog-description": "Afficher une horloge de style analogique sur la fenêtre du calendrier.", + "date-time-use-analog-label": "Utiliser une horloge de style analogique", + "date-time-week-numbers-description": "Afficher la semaine de l'année (ex : Semaine 38) dans le calendrier.", + "date-time-week-numbers-label": "Afficher les numéros de semaine", + "geolocate-now-button": "Localiser maintenant", + "location-desc": "Obtenez une météo et une planification de l'éclairage nocturne précises en définissant votre emplacement.", + "location-search-description": "ex : Toronto, ON", + "location-search-label": "Rechercher un emplacement", + "location-search-placeholder": "Entrez le nom de l'emplacement", + "location-title": "Votre emplacement", + "weather-desc": "Choisissez votre unité de température préférée.", + "weather-enabled-description": "Afficher les informations météorologiques dans toute l'interface et récupérer les données météo. Lorsque désactivé, tous les éléments météo seront masqués et aucune requête réseau ne sera effectuée.", + "weather-enabled-label": "Activer la météo", + "weather-fahrenheit-description": "Afficher la température en Fahrenheit au lieu de Celsius.", + "weather-fahrenheit-label": "Afficher la température en Fahrenheit (°F)", + "weather-hide-city-description": "Masquer le nom de la ville dans les affichages météo de toute l'interface.", + "weather-hide-city-label": "Cacher le nom de la ville", + "weather-hide-timezone-description": "Masquer l'abréviation du fuseau horaire des affichages météorologiques dans toute l'interface.", + "weather-hide-timezone-label": "Masquer le fuseau horaire", + "weather-show-effects-description": "Afficher des effets visuels supplémentaires (comme la pluie, la neige ou la foudre) sur la carte météo.", + "weather-show-effects-label": "Afficher les effets météorologiques", + "weather-show-in-calendar-description": "Afficher les prévisions météo quotidiennes directement dans votre vue calendrier.", + "weather-show-in-calendar-label": "Afficher la météo dans le calendrier", + "weather-talia-mascot-always-description": "Toujours utiliser les Talia emojis au lieu des tabler icons.", + "weather-talia-mascot-always-label": "Toujours afficher les emojis météo de Talia" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "Lorsque fprintd (authentification par empreinte digitale) est actif, cette option vous permet de vous connecter avec votre mot de passe au lieu d'une empreinte digitale.", + "allow-password-with-fprintd-label": "Autoriser la connexion par mot de passe avec fprintd", + "auto-start-auth-description": "Démarre automatiquement l'authentification par empreinte digitale sans nécessiter de pression sur une touche ou un clic de bouton.", + "auto-start-auth-label": "Authentification au démarrage automatique", + "clock-format-description": "Personnalisez le format de l'horloge en utilisant les jetons de syntaxe de date/heure.", + "clock-format-label": "Format de l'horloge", + "clock-style-analog": "Analogique", + "clock-style-custom": "Personnalisé", + "clock-style-description": "Choisissez le style visuel de l'horloge sur l'écran de verrouillage.", + "clock-style-digital": "Numérique", + "clock-style-label": "Style de l'Horloge", + "compact-lockscreen-description": "Afficher uniquement le champ de saisie de connexion et les commandes système, en masquant les widgets météo et multimédia.", + "compact-lockscreen-label": "Écran de verrouillage compact", + "enable-lockscreen-media-controls-description": "Afficher les commandes interactives de lecture multimédia sur l'écran de verrouillage.", + "enable-lockscreen-media-controls-label": "Commandes Multimédia de l'Écran de Verrouillage", + "lock-on-suspend-description": "Verrouiller automatiquement l'écran lors de la mise en veille du système.", + "lock-on-suspend-label": "Verrouiller à la suspension", + "lock-screen-animations-description": "Activer ou désactiver les animations de l'écran de verrouillage.", + "lock-screen-animations-label": "Animations de l'Écran de Verrouillage", + "lock-screen-blur-strength-description": "Applique un effet de flou au fond d'écran de l'écran de verrouillage.", + "lock-screen-blur-strength-label": "Intensité du flou de l'écran de verrouillage", + "lock-screen-tint-strength-description": "Applique une superposition de teinte au fond d'écran de l'écran de verrouillage.", + "lock-screen-tint-strength-label": "Force de la teinte de l'écran de verrouillage", + "monitors-desc": "Afficher l'écran de verrouillage sur des moniteurs spécifiques. Par défaut, tous sont utilisés si aucun n'est choisi.", + "password-chars-description": "De jolies icônes utilisées pour masquer votre mot de passe.", + "password-chars-label": "Icônes de mot de passe aléatoires", + "show-hibernate-description": "Afficher l’option 'hiberner' dans les commandes d'alimentation.", + "show-hibernate-label": "Afficher l’hibernation", + "show-session-buttons-description": "Autoriser l'accès aux paramètres d'alimentation depuis l'écran de verrouillage.", + "show-session-buttons-label": "Commandes d'alimentation", + "title": "Écran de verrouillage" + }, + "network": { + "bluetooth-description": "Activer la gestion Bluetooth.", + "bluetooth-rssi-polling-description": "Échantillonner périodiquement le RSSI des appareils connectés via bluetoothctl. Peut ne pas être disponible pour tous les appareils ; utilise un minimum de ressources lorsqu'il est activé.", + "bluetooth-rssi-polling-label": "Interrogation du signal Bluetooth", + "desc": "Gérez les connexions Wi-Fi et Bluetooth.", + "wifi-description": "Gérer les réseaux sans fil (nécessite NetworkManager)." + }, + "notifications": { + "duration-critical-urgency-description": "Durée de visibilité des notifications de priorité critique.", + "duration-critical-urgency-label": "Urgence critique", + "duration-desc": "Configurer la durée d'affichage des notifications en fonction de leur niveau d'urgence.", + "duration-low-urgency-description": "Durée de visibilité des notifications de faible priorité.", + "duration-low-urgency-label": "Urgence faible", + "duration-normal-urgency-description": "Durée de visibilité des notifications de priorité normale.", + "duration-normal-urgency-label": "Urgence normale", + "duration-respect-expire-description": "Utiliser le délai d'expiration défini dans la notification.", + "duration-respect-expire-label": "Respecter le délai d'expiration", + "duration-title": "Durée des notifications", + "history-clear-dismiss-description": "Effacer la notification de l'historique lorsqu'elle est ignorée.", + "history-clear-dismiss-label": "Effacer si ignorée", + "history-critical-urgency-description": "Enregistrer les notifications de priorité critique dans l'historique.", + "history-critical-urgency-label": "Enregistrer l'urgence critique dans l'historique", + "history-desc": "Contrôler quelles notifications sont enregistrées dans l'historique en fonction de leur niveau d'urgence.", + "history-low-urgency-description": "Enregistrer les notifications de priorité faible dans l'historique.", + "history-low-urgency-label": "Enregistrer l'urgence faible dans l'historique", + "history-normal-urgency-description": "Enregistrer les notifications de priorité normale dans l'historique.", + "history-normal-urgency-label": "Enregistrer l'urgence normale dans l'historique", + "monitors-desc": "Afficher les notifications sur des moniteurs spécifiques. Par défaut, sur tous si aucun n'est choisi.", + "rules-action-block": "Bloc", + "rules-action-block-desc": "Ignore complètement.", + "rules-action-hide": "Masquer", + "rules-action-hide-desc": "Pas de popup, pas de son, ajoute à l'historique.", + "rules-action-label": "Action", + "rules-action-mute": "Muet", + "rules-action-mute-desc": "Pas de son, affiche toujours le pop-up et dans l'historique.", + "rules-add": "Ajouter une règle", + "rules-delete": "Supprimer la règle", + "rules-description": "Faire correspondre le nom de l'application ou le contenu. Les règles sont vérifiées dans l'ordre, et la première correspondance est appliquée.", + "rules-edit": "Modifier la règle", + "rules-label": "Règles de Filtrage", + "rules-pattern-label": "Motif", + "rules-pattern-placeholder": "firefox, discord, ou /regex/", + "rules-tab": "Règles", + "settings-always-on-top-description": "Afficher les notifications au-dessus des fenêtres en plein écran et des autres applications.", + "settings-background-opacity-description": "Ajuster l'opacité des arrière-plans de notifications.", + "settings-density-description": "Choisissez la densité de la carte de notification.", + "settings-density-label": "Densité", + "settings-desc": "Configurez l'apparence et le comportement des notifications.", + "settings-do-not-disturb-description": "Désactive toutes les fenêtres de notification lorsque cette option est activée.", + "settings-enabled-description": "Activer ou désactiver le démon de notifications, nécessite un redémarrage de Noctalia shell.", + "settings-enabled-label": "Activer les notifications", + "settings-location-description": "Où les notifications apparaissent à l'écran.", + "settings-markdown-description": "Afficher le contenu des notifications en utilisant le formatage Markdown.", + "settings-markdown-label": "Activer Markdown", + "sounds-desc": "Configurer les effets sonores et le volume des notifications.", + "sounds-enabled-description": "Activer les effets sonores pour les notifications entrantes.", + "sounds-enabled-label": "Activer les sons de notification", + "sounds-excluded-apps-description": "Ne pas jouer le son de notification configuré pour les applications spécifiques qui ont leurs propres sons intégrés.", + "sounds-excluded-apps-label": "Applications exclues", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Chemin vers le fichier sonore joué pour les notifications de priorité critique.", + "sounds-files-critical-label": "Son d'urgence critique", + "sounds-files-critical-select-title": "Sélectionner le fichier son d'urgence critique", + "sounds-files-desc": "Configurer des fichiers sonores personnalisés pour différents niveaux d'urgence de notification.", + "sounds-files-low-description": "Chemin vers le fichier sonore joué pour les notifications de priorité basse.", + "sounds-files-low-label": "Son d'urgence basse", + "sounds-files-low-select-title": "Sélectionner le fichier son d'urgence basse", + "sounds-files-normal-description": "Chemin vers le fichier sonore joué pour les notifications de priorité normale.", + "sounds-files-normal-label": "Son d'urgence normale", + "sounds-files-normal-select-title": "Sélectionner le fichier son d'urgence normale", + "sounds-files-placeholder": "Entrer le chemin vers le fichier sonore", + "sounds-files-select-file": "Sélectionner un fichier son", + "sounds-files-title": "Fichiers sonores", + "sounds-files-unified-description": "Chemin vers le fichier sonore joué pour les notifications.", + "sounds-files-unified-label": "Son de notification", + "sounds-files-unified-select-title": "Sélectionner le fichier son de notification", + "sounds-separate-description": "Utiliser différents fichiers sonores pour les notifications de priorité basse, normale et critique.", + "sounds-separate-label": "Utiliser des sons différents par priorité", + "sounds-title": "Paramètres sonores", + "sounds-unavailable-description": "Installez Qt6 Multimedia pour activer les sons de notification.", + "sounds-unavailable-label": "Sons de notification non disponibles", + "sounds-volume-description": "Ajuster le niveau de volume pour les sons de notification.", + "sounds-volume-label": "Volume sonore", + "toast-battery-description": "Afficher un avertissement lorsque le niveau de batterie descend en dessous de ce pourcentage.", + "toast-battery-label": "Avertissement de batterie", + "toast-desc": "Configurer l'apparence et le comportement des notifications toast.", + "toast-keyboard-description": "Afficher une notification lorsque la disposition du clavier change.", + "toast-keyboard-label": "Disposition du clavier", + "toast-media-description": "Afficher une notification toast lorsque l'état de lecture du média change.", + "toast-media-label": "Média" + }, + "osd": { + "always-on-top-description": "Afficher l'OSD au-dessus des fenêtres plein écran et autres couches.", + "always-on-top-label": "Toujours au premier plan", + "background-opacity-description": "Contrôle la transparence de l'arrière-plan de l'OSD.", + "background-opacity-label": "Opacité de l'arrière-plan", + "description": "Configurer les indicateurs à l'écran tels que le volume et la luminosité.", + "duration-auto-hide-description": "Ajuster le délai avant disparition de l'OSD.", + "duration-auto-hide-label": "Masquer après", + "duration-desc": "Temps d'affichage avant masquage automatique de l'OSD.", + "duration-title": "Délai d'auto‑masquage", + "enabled-description": "Afficher en temps réel les changements de volume et de luminosité.", + "enabled-label": "Activer l'affichage à l'écran (OSD)", + "events-desc": "Sélectionnez les événements qui déclenchent l'affichage à l'écran.", + "general-desc": "Configurer la visibilité et le comportement de l'OSD.", + "location-description": "Emplacement des affichages à l'écran.", + "monitors-desc": "Afficher l'OSD sur des moniteurs spécifiques. Par défaut sur tous si aucun n'est choisi.", + "title": "Affichage à l'écran", + "types-brightness-description": "Afficher l'OSD lorsque la luminosité de l'écran change.", + "types-brightness-label": "Luminosité", + "types-custom-text-description": "Afficher l’OSD pour les messages texte personnalisés provenant de l’IPC.", + "types-custom-text-label": "Texte personnalisé", + "types-desc": "Sélectionnez les événements qui déclenchent l'OSD.", + "types-input-volume-description": "Afficher l'OSD lorsque le volume du microphone change.", + "types-input-volume-label": "Volume d'entrée", + "types-lockkey-description": "Afficher l'OSD lorsque Verr Maj, Verr Num ou Arrêt défil est activée ou désactivée.", + "types-lockkey-label": "Touches de verrouillage", + "types-media-description": "Afficher l'OSD lorsque l'état de lecture multimédia change (lecture, pause, saut).", + "types-media-label": "Lecture de média", + "types-title": "Événements de déclenchement OSD", + "types-volume-description": "Afficher l'OSD lorsque le volume de sortie audio change.", + "types-volume-label": "Volume de sortie" + }, + "plugins": { + "auto-update": "Mettre à jour les plugins automatiquement", + "auto-update-description": "Mettre à jour automatiquement tous les plugins au démarrage du shell.", + "available-description": "Parcourir et installer des plugins à partir des sources configurées.", + "available-label": "Plugins disponibles", + "available-no-plugins-description": "Vérifiez les sources de vos plugins ou actualisez la liste.", + "available-no-plugins-label": "Aucun plugin disponible", + "check-for-updates": "Rechercher des mises à jour", + "checking-for-updates": "Recherche de mises à jour...", + "collision-already-installed": "Ce plugin est déjà installé", + "collision-custom-version-exists": "Une version personnalisée de \"{source}\" est déjà installée", + "collision-official-version-exists": "La version officielle de ce plugin est déjà installée", + "development-disable": "Désactiver le mode développeur pour ce plugin", + "development-enable": "Activer le mode de développement pour ce plugin.", + "filter-downloaded": "Téléchargé", + "filter-not-downloaded": "Non téléchargé", + "filter-tags-description": "Filtrer les plugins par catégorie ou état de téléchargement.", + "filter-tags-label": "Mots-clés", + "hot-reload-description": "Recharger automatiquement les plugins lorsque leurs fichiers sont modifiéss. Utile pour le développement de plugins.", + "hot-reload-label": "Rechargement à chaud (mode développement)", + "hot-reloaded": "Plugin rechargé : {name}", + "install-error": "Échec de l'installation : {error}", + "install-incompatible": "{plugin} nécessite Noctalia v{version} ou une version ultérieure", + "install-success": "Installation de {plugin} réussie", + "installed-description": "Gérer et configurer tous les plugins installés localement.", + "installed-label": "Plugins installés", + "installed-no-plugins-description": "Installez les plugins depuis la section \"Disponible\".", + "installed-no-plugins-label": "Aucun plugin installé", + "installing": "Installation de {plugin}...", + "notify-updates": "Notifications de mise à jour des plugins", + "notify-updates-description": "Afficher une notification contextuelle lorsque de nouvelles mises à jour de plugins sont disponibles.", + "open-plugin-page": "Ouvrir la page du plugin", + "open-plugins-tab": "Ouvrir les paramètres des plugins", + "plugin-settings-title": "Paramètres de {plugin}", + "refresh-refreshing": "Actualisation de la liste des plugins...", + "refresh-tooltip": "Actualiser les plugins disponibles", + "settings-error-not-loaded": "Plugin non chargé", + "settings-saved": "Paramètres du plugin enregistrés", + "settings-tooltip": "Paramètres du plugin", + "source-custom": "Source personnalisée", + "sources-add-custom": "Ajouter un dépôt personnalisé", + "sources-add-dialog-description": "Ajouter un dépôt GitHub comme source de plugin.", + "sources-add-dialog-error": "Échec de l'ajout de la source du plugin", + "sources-add-dialog-name": "Nom du dépôt", + "sources-add-dialog-name-placeholder": "Mes plugins personnalisés", + "sources-add-dialog-success": "Source du plugin ajoutée avec succès", + "sources-add-dialog-title": "Ajouter la source du plugin", + "sources-add-dialog-url": "URL du dépôt", + "sources-description": "Gérer les dépôts de plugins.", + "sources-placeholder": "Mon dépôt cool", + "sources-remove-tooltip": "Supprimer la source du plugin", + "title": "Plugins", + "translations-reloaded": "Traductions rechargées : {name}", + "uninstall-dialog-description": "Êtes-vous sûr de vouloir désinstaller {plugin} ? Cette action supprimera toutes les données du plugin.", + "uninstall-dialog-title": "Désinstaller le plugin", + "uninstall-error": "Échec de la désinstallation : {error}", + "uninstall-success": "{plugin} a été désinstallé avec succès", + "uninstalling": "Désinstallation de {plugin}...", + "update-all": "Tout mettre à jour ({count})", + "update-all-success": "Tous les plugins ont été mis à jour avec succès", + "update-available": "Nouvelle mise à jour de plugin disponible", + "update-available-plural": "Mises à jour de plugins disponibles ({count})", + "update-error": "Échec de la mise à jour du plugin : {plugin} : {error}", + "update-pending": "v{current} → v{new} (nécessite Noctalia v{required})", + "update-success": "Mise à jour du plugin {plugin} vers la version {version}", + "update-version": "v{current} → v{new}", + "updating": "Mise à jour..." + }, + "region": { + "title": "Région" + }, + "session-menu": { + "countdown-duration-description": "Définir la durée du compte à rebours avant d'exécuter les actions d'alimentation.", + "countdown-duration-label": "Durée du compte à rebours", + "enable-countdown-description": "Afficher un compte à rebours avant d'exécuter les actions d'alimentation.", + "enable-countdown-label": "Activer le compte à rebours", + "entries-desc": "Personnaliser les actions d'alimentation qui apparaissent dans le menu de session et leur ordre.", + "entries-title": "Actions d'alimentation", + "entry-settings-command-description": "Commande personnalisée à exécuter pour cette action. Laisser vide pour utiliser la commande système par défaut.", + "entry-settings-command-placeholder": "ex : systemctl poweroff", + "entry-settings-default-command-lock": "Écran de verrouillage interne (sans commande)", + "entry-settings-default-command-logout": "Déconnexion interne (sans commande)", + "entry-settings-default-info-description": "Si aucune commande personnalisée n'est spécifiée, la commande système par défaut sera utilisée.", + "entry-settings-default-info-label": "Commande par défaut", + "entry-settings-keybind-description": "Attribuez une combinaison de touches pour déclencher cette action lorsque le menu de session est ouvert.", + "entry-settings-keybind-placeholder": "Appuyez sur Enregistrer pour assigner une touche...", + "entry-settings-keybind-recording": "Enregistrement... Appuyez sur n'importe quelle combinaison de touches", + "entry-settings-title": "Configurer {entry}", + "entry-settings-tooltip": "Configurer la commande", + "general-desc": "Configurer le comportement et l'apparence du panneau du menu de session.", + "large-buttons-layout-description": "Choisissez comment les boutons du menu de session sont affichés.", + "large-buttons-layout-label": "Disposition des grands boutons", + "large-buttons-style-description": "Afficher le menu de session avec de grands boutons dans une disposition en grille.", + "large-buttons-style-label": "Style boutons larges", + "position-description": "Choisissez où le panneau du menu de session apparaît lorsqu'il est ouvert.", + "show-header-description": "Afficher le titre et le bouton de fermeture en haut du menu de session.", + "show-header-label": "Afficher l'en-tête", + "show-keybinds-description": "Afficher les indices de raccourcis clavier sur les options de session.", + "show-keybinds-label": "Afficher les raccourcis clavier" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Garder les widgets de bureau visibles tant que le mode performance Noctalia est activé.", + "noctalia-performance-disable-desktop-widgets-label": "Activer les widgets de bureau en mode performance", + "noctalia-performance-disable-wallpaper-description": "Gardez les fonds d'écran du bureau, de l'aperçu et de l'écran de verrouillage visibles lorsque le mode performance Noctalia est activé.", + "noctalia-performance-disable-wallpaper-label": "Activer le rendu du fond d'écran en mode performance", + "title": "Système" + }, + "system-monitor": { + "critical-color-label": "Couleur critique", + "custom-highlight-colors-title-label": "Couleurs de surbrillance personnalisées", + "disk-available-label": "Espace disque disponible", + "disk-section-label": "Utilisation disque", + "enable-dgpu-monitoring-description": "Attention : Ceci va activer votre GPU dédié (NVIDIA/AMD), ce qui peut avoir un impact significatif sur l'autonomie de la batterie des ordinateurs portables dotés de cartes graphiques hybrides.", + "enable-dgpu-monitoring-label": "Activer la surveillance du GPU dédié", + "external-monitor-description": "Entrez la commande ou le chemin de l'application à lancer lors de l'activation de l'application externe de surveillance du système.", + "external-monitor-label": "Commande du moniteur système externe", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "Configurer le comportement et l'apparence du moniteur système.", + "gpu-section-label": "Température du GPU", + "highlight-colors-section-label": "Couleurs de surlignage", + "polling-interval-label": "Intervalle d'interrogation", + "polling-section-description": "Configurer la fréquence de mise à jour de chaque métrique système.", + "polling-section-label": "Intervalles d'interrogation", + "threshold-critical": "Critique", + "threshold-warning": "Avertissement", + "thresholds-section-description": "Ajustez les seuils d’avertissement/critiques et les intervalles d’interrogation pour chaque métrique système.", + "use-custom-highlight-colors-description": "Lorsque cette option est désactivée, les couleurs de surbrillance par défaut du thème sont utilisées.", + "use-custom-highlight-colors-label": "Utiliser des couleurs de surlignage personnalisées", + "warning-color-label": "Couleur d'avertissement" + }, + "user-interface": { + "allow-panels-without-bar-description": "Quand cette option est activée, les panneaux peuvent s'ouvrir sur n'importe quel écran. Quand elle est désactivée, les panneaux s'ouvriront uniquement sur les écrans qui ont une barre, ce qui peut réduire l'utilisation de la mémoire.", + "allow-panels-without-bar-label": "Autoriser les panneaux sur les écrans sans barre", + "animation-disable-description": "Désactiver toutes les animations pour une expérience plus rapide et plus réactive.", + "animation-disable-label": "Désactiver les animations de l'interface utilisateur", + "animation-speed-description": "Ajuster la vitesse globale de l'animation.", + "animation-speed-label": "Vitesse d'animation", + "animation-speed-reset": "Réinitialiser la vitesse de l'animation", + "appearance-desc": "Personnaliser les éléments visuels tels que les infobulles, les bordures et les ombres.", + "blur-behind-description": "Floute la zone derrière les barres et les panneaux en utilisant le protocole de flou du compositeur.", + "blur-behind-label": "Flou d'arrière-plan", + "box-border-description": "Affiche un contour autour des zones de contenu.", + "box-border-label": "Contour du conteneur", + "box-border-radius-description": "Ajuste de l'arrondi des coins des principales sections de la mise en page, telles que les barres latérales, les cartes et les panneaux de contenu.", + "box-border-radius-label": "Rayon du conteneur", + "control-border-radius-description": "Contrôle la courbure des éléments interactifs, y compris les boutons, les bascules et les champs de texte.", + "control-border-radius-label": "Rayon d'entrée", + "desc": "Personnaliser l'apparence, l'ergonomie et le comportement de l'interface.", + "dim-desktop-description": "Atténuer le bureau lorsque des panneaux ou des menus sont ouverts.", + "dim-desktop-label": "Dim bureau", + "dimmer-opacity-description": "Définir le niveau d'opacité pour l'assombrissement du bureau.", + "dimmer-opacity-label": "Opacité du bureau atténué", + "panel-background-opacity-description": "Définir l'opacité de l'arrière-plan pour tous les panneaux (barre, lanceur, paramètres, etc.).", + "panel-background-opacity-label": "Opacité de fond des panneaux", + "panels-attached-to-bar-description": "Les panneaux se verrouillent sur la barre et les bords de l'écran, créant un aspect homogène avec d'élégants coins inversés.", + "panels-attached-to-bar-label": "Aligner les panneaux aux bords", + "panels-overlay-description": "Garantit que les panneaux et la barre restent visibles, même au-dessus des applications en plein écran.", + "panels-overlay-label": "Garder les panneaux et la barre en haut", + "scaling-description": "Modifie la taille de l'interface utilisateur générale, à l'exception de la barre.", + "scaling-label": "Mise à l'échelle de l'interface", + "scrollbar-always-visible-description": "Garder les barres de défilement visibles chaque fois que le contenu est défilable, au lieu de les afficher uniquement au survol.", + "scrollbar-always-visible-label": "Toujours afficher les barres de défilement", + "settings-panel-header": "Panneau des Paramètres", + "settings-panel-mode-description": "Choisir la disposition des paramètres (peut nécessiter une réouverture).", + "settings-panel-mode-label": "Mode du panneau des paramètres", + "settings-panel-sidebar-card-style-description": "Enveloppe la barre latérale des paramètres dans un arrière-plan rempli avec des coins arrondis.", + "settings-panel-sidebar-card-style-label": "Style de Carte de la Barre Latérale", + "shadows-description": "Active les ombres portées sous les barres et les panneaux.", + "shadows-direction-description": "Choisissez où l'ombre est projetée.", + "shadows-direction-label": "Direction de l’ombre", + "shadows-label": "Ombres portées", + "title": "Interface utilisateur", + "tooltips-description": "Activer ou désactiver les infobulles dans toute l'interface.", + "tooltips-label": "Afficher les infobulles", + "translucent-widgets-description": "Rendre les boutons, onglets et autres widgets à l'intérieur des panneaux semi-transparents.", + "translucent-widgets-label": "Widgets translucides" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Alphabétique", + "automation-change-mode-description": "Choisissez comment les fonds d'écran sont sélectionnés lors du changement automatique.", + "automation-change-mode-label": "Mode de changement", + "automation-custom-interval-description": "Entrez l'heure au format HH:MM (ex : 01:30).", + "automation-custom-interval-label": "Intervalle personnalisé", + "automation-interval-description": "Fréquence de changement automatique des fonds d'écran.", + "automation-interval-label": "Intervalle du fond d'écran", + "automation-random-wallpaper-description": "Planifiez des changements de fond d'écran aléatoires à intervalles réguliers.", + "automation-scheduled-change-description": "Changer automatiquement les fonds d'écran à intervalles réguliers.", + "automation-scheduled-change-label": "Changement programmé", + "look-feel-edge-smoothness-description": "Applique un effet de flou progressif au bord des transitions.", + "look-feel-edge-smoothness-label": "Adoucir le bord de la transition", + "look-feel-fill-color-description": "Choisissez une couleur de remplissage qui peut apparaître derrière le fond d'écran.", + "look-feel-fill-mode-description": "Sélectionnez comment l'image doit être mise à l'échelle pour correspondre à la résolution de votre moniteur.", + "look-feel-fill-mode-label": "Mode de remplissage", + "look-feel-skip-startup-transition-description": "Ignorer l'animation du fond d'écran au démarrage du shell.", + "look-feel-skip-startup-transition-label": "Ignorer la transition de démarrage", + "look-feel-title": "Apparence et ergonomie", + "look-feel-transition-duration-description": "Durée des animations de transition en secondes.", + "look-feel-transition-duration-label": "Durée de la transition", + "look-feel-transition-type-description": "Type d'animation lors du changement de fond d'écran.", + "look-feel-transition-type-label": "Type de transition", + "settings-clear-cache-button": "Vider le cache", + "settings-clear-cache-description": "Effacer les fonds d'écran redimensionnés en cache pour libérer de l'espace disque.", + "settings-clear-cache-label": "Cache des fonds d'écran", + "settings-clear-cache-toast": "Cache de fond d'écran effacé", + "settings-desc": "Contrôlez comment les fonds d'écran sont gérés et affichés.", + "settings-enable-management-description": "Gérez les fonds d'écran avec Noctalia. Décochez si vous préférez utiliser une autre application.", + "settings-enable-management-label": "Activer la gestion des fonds d'écran", + "settings-enable-overview-description": "Applique un fond d'écran flou et atténué à l'écran de présentation.", + "settings-enable-overview-label": "Activer le fond d'écran de l'aperçu", + "settings-folder-description": "Chemin vers votre dossier principal de fonds d'écran.", + "settings-folder-label": "Dossier des fonds d'écran", + "settings-hide-wallpaper-filenames-tooltip-hide": "Masquer les noms de fichiers", + "settings-hide-wallpaper-filenames-tooltip-show": "Afficher les noms de fichiers", + "settings-monitor-specific-description": "Définissez un dossier de fond d'écran différent pour chaque moniteur.", + "settings-monitor-specific-label": "Dossiers spécifiques au moniteur", + "settings-monitor-specific-tooltip": "Dossier des fonds d'écran du moniteur", + "settings-overview-blur-strength-description": "Applique la force du flou à l'aperçu.", + "settings-overview-blur-strength-label": "Force du flou de l'aperçu", + "settings-overview-tint-description": "Applique la force de la teinte à l'aperçu.", + "settings-overview-tint-label": "Force de la teinte de l'aperçu", + "settings-recursive-search-description": "Rechercher également des fonds d'écran dans les sous-dossiers du répertoire des fonds d'écran.", + "settings-recursive-search-label": "Rechercher dans les sous-dossiers", + "settings-select-monitor-folder": "Sélectionner le dossier des fonds d'écran du moniteur", + "settings-selector-description": "Choisissez votre fond d’écran.", + "settings-selector-position-description": "Choisissez l'emplacement d'affichage du panneau de sélection du fond d'écran.", + "settings-show-hidden-files-tooltip-hide": "Masquer les fichiers cachés", + "settings-show-hidden-files-tooltip-show": "Afficher les fichiers cachés", + "settings-title": "Paramètres du fond d'écran", + "settings-use-original-images-description": "Ignorer le redimensionnement des fonds d'écran avant l'affichage. Économise de l'espace disque et réduit l'utilisation du CPU lors des changements de fond d'écran, mais peut utiliser plus de mémoire pour les très grandes images.", + "settings-use-original-images-label": "Utiliser les images originales", + "settings-view-mode-description": "Choisissez comment les fonds d'écran sont affichés depuis votre répertoire.", + "settings-view-mode-label": "Mode d'affichage", + "view-mode-browse": "Parcourir les répertoires", + "view-mode-cycle-tooltip": "Mode d'affichage : {mode} (cliquez pour modifier)", + "view-mode-recursive": "Sous-répertoires aplatis", + "view-mode-single": "Répertoire racine" + } + }, + "placeholders": { + "add-new-keybind": "Ajouter un nouveau raccourci clavier", + "command-example": "echo \"Bonjour le monde\"", + "enter-command": "Entrez la commande à exécuter (application ou script personnalisé)", + "enter-ipc-identifier": "Entrez un identifiant unique pour les commandes IPC", + "enter-path": "Entrez le chemin...", + "enter-text-to-collapse": "ex : 'rien n’est en lecture'. Utilisez /regex/ pour les modèles.", + "enter-tooltip": "Entrer l'infobulle", + "enter-width-pixels": "Entrez la largeur en pixels", + "keybind-recording": "Enregistrement de la combinaison de touches...", + "search": "Rechercher...", + "search-icons": "ex : noctalia, niri, batterie, nuage", + "search-launcher": "Rechercher des entrées... ou utilisez > pour les commandes", + "search-wallhaven": "Rechercher sur Wallhaven...", + "search-wallpapers": "Tapez pour filtrer les fonds d'écran..." + }, + "positions": { + "bottom": "Bas", + "bottom-center": "En bas au centre", + "bottom-left": "En bas à gauche", + "bottom-right": "En bas à droite", + "center": "Centre", + "center-left": "Gauche centre", + "center-right": "Droite centre", + "close-to-bar": "Près du bouton de la barre", + "follow-bar": "Suivre la barre", + "left": "Gauche", + "right": "Droite", + "top": "Haut", + "top-center": "En haut au centre", + "top-left": "En haut à gauche", + "top-right": "En haut à droite" + }, + "session-menu": { + "action-in-seconds": "{action} dans {seconds} secondes...", + "cancel-timer": "Annuler le minuteur", + "title": "Menu de session" + }, + "setup": { + "all-done": "Terminé !", + "appearance": { + "subheader": "Choisissez le mode sombre et les sources de couleur (fond d'écran ou prédéfinies)." + }, + "customize": { + "header": "Personnaliser votre expérience", + "subheader": "Ajustez la position de la barre, la densité, l'échelle et plus encore." + }, + "skip-setup": "Ignorer la configuration", + "telemetry-wizard-done": "Bien reçu !", + "telemetry-wizard-note": "Vous avez le contrôle — activez ou désactivez cette option à tout moment dans les paramètres", + "telemetry-wizard-subtitle": "Nous avons ajouté des analyses anonymes pour aider à améliorer Noctalia", + "telemetry-wizard-title": "Mise à jour de la confidentialité", + "wallpaper": { + "choose-dir": "Choisissez un répertoire contenant vos fonds d'écran", + "dir-browse": "Parcourir le dossier", + "dir-description": "Choisissez le dossier contenant vos fonds d'écran.", + "dir-label": "Répertoire des fonds d'écran", + "dir-select-title": "Sélectionner le dossier des fonds d'écran", + "header": "Choisissez votre fond d'écran", + "no-dir": "Aucun répertoire de fonds d'écran sélectionné", + "no-valid": "Aucun fichier image valide trouvé dans : {dir}", + "none-in-dir": "Aucun fond d'écran trouvé dans le répertoire", + "preview-error": "Échec du chargement de l'image", + "select-prompt": "Sélectionnez un fond ci-dessous", + "subheader": "Définissez l'ambiance avec un joli fond." + }, + "welcome": "Bienvenue", + "welcome-note": "Quelques réglages de base pour démarrer — toutes les options sont dans paramètres", + "welcome-subtitle": "Personnalisons votre bureau pour qu'il soit unique", + "welcome-title": "Bienvenue dans Noctalia !" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "Aucun lecteur multimédia détecté", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "Application inconnue", + "uptime": "Activité : {uptime}", + "user-requested": "Demandé par l'utilisateur", + "welcome-back": "Bon retour,", + "widget-settings-title": "Paramètres de {widget}" + }, + "system-monitor": { + "core-usage": "Utilisation du cœur {id}", + "cpu-temp": "Température du processeur", + "cpu-usage": "Utilisation du processeur", + "disk": "Disque", + "download-speed": "Vitesse de téléchargement", + "gpu-temp": "Température du GPU", + "load-average": "Charge moyenne", + "title": "Moniteur système", + "upload-speed": "Vitesse d’envoi" + }, + "toast": { + "airplane-mode": { + "description": "Désactiver toutes les communications sans fil.", + "title": "Mode avion" + }, + "battery": { + "critical": "Batterie critique", + "critical-desc": "Batterie à {percent}% — veuillez connecter le chargeur immédiatement", + "low": "Batterie faible", + "low-desc": "Batterie à {percent}% — veuillez connecter le chargeur" + }, + "bluetooth": { + "address-copied": "Adresse copiée dans le presse-papiers", + "auto-connect-disabled": "Connexion automatique désactivée", + "auto-connect-enabled": "Connexion automatique activée", + "auto-connecting": "Connexion à {count} appareil(s)...", + "confirm-code": "Confirmer le code {value} sur l'autre appareil.", + "connect-failed": "Échec de la connexion à l’appareil", + "disconnect-failed": "Échec de la déconnexion de l’appareil", + "discoverable-change-failed": "Échec du changement d’état détectable", + "discoverable-disabled": "Détectable désactivé", + "discoverable-enabled": "Détectable activé", + "display-code": "Entrez le code {value} sur l'autre appareil.", + "forget-failed": "Échec de l’oubli de l’appareil", + "pair-failed": "Échec de l’appairage de l’appareil", + "passkey-required": "Clé d'identification requise par l'appareil.", + "pincode-required": "Code PIN requis par l'appareil.", + "state-change-failed": "Échec du changement d’état du Bluetooth" + }, + "clipboard": { + "long-text": "Texte long", + "unavailable": "Historique du presse-papiers indisponible", + "unavailable-desc": "L'application 'cliphist' n'est pas installée. Veuillez l'installer pour utiliser les fonctionnalités d'historique du presse-papiers" + }, + "custom-command-failed": { + "description": "La commande a échoué : {command}\\nCode de sortie : {code}", + "title": "La commande personnalisée a échoué" + }, + "do-not-disturb": { + "disabled": "'Ne pas déranger' désactivé", + "disabled-desc": "Affichage de toutes les notifications", + "enabled": "'Ne pas déranger' activé", + "enabled-desc": "Vous trouverez ces notifications dans votre historique" + }, + "donation-opened": "Page de donation ouverte dans votre navigateur", + "internet-limited": "Connecté sans Internet", + "keyboard-layout": { + "changed": "Disposition du clavier changée vers {layout}", + "title": "Clavier" + }, + "missing-control-center": { + "description": "Le widget du centre de contrôle a été retiré de la barre. Pour y accéder à nouveau depuis la barre, veuillez ré‑ajouter le widget. Vous pouvez aussi l'ouvrir en cliquant avec le bouton droit sur la barre", + "label": "Dernier widget du centre de contrôle supprimé" + }, + "night-light": { + "forced": "Activation forcée", + "normal": "Mode normal", + "not-installed": "wlsunset non installé" + }, + "noctalia-performance": { + "disabled": "Mode performance désactivé", + "enabled": "Mode performance activé", + "label": "Noctalia Performance" + }, + "power-profile": { + "changed": "Profil d'alimentation changé", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "Une erreur s'est produite lors du traitement des modèles", + "title": "Le traitement du modèle a échoué" + }, + "vpn": { + "connected": "Connecté à '{name}'", + "disconnected": "Déconnecté de '{name}'" + }, + "wallpaper-colors": { + "disabled": "Couleurs du fond d'écran désactivées", + "enabled": "Couleurs du fond d'écran activées", + "label": "Génération de couleurs pour fonds d'écran" + }, + "wifi": { + "connected": "Connecté à '{ssid}'", + "connection-failed": "Échec de la connexion", + "connection-timeout": "Délai de connexion dépassé", + "disconnected": "Déconnecté de '{ssid}'", + "incorrect-password": "Mot de passe incorrect", + "network-not-found": "Réseau introuvable" + } + }, + "tooltips": { + "add-widget": "Ajouter un widget", + "auto-connect": "Activer/désactiver la connexion automatique pour cet appareil", + "bluetooth-auto-connect-off": "Connexion automatique désactivée", + "bluetooth-auto-connect-on": "Connexion automatique activée", + "bluetooth-devices": "Appareils Bluetooth", + "brightness-at": "Luminosité : {brightness}%", + "click-to-start-recording": "Enregistreur d'écran (démarrer l'enregistrement)", + "click-to-stop-recording": "Enregistreur d'écran (arrêter l'enregistrement)", + "collapse": "Réduire la barre latérale", + "copy-address": "Copier l'adresse", + "delete-notification": "Supprimer la notification", + "dismiss-notification": "Ignorer la notification", + "do-not-disturb-enabled": "Ne pas déranger", + "expand": "Développer la barre latérale", + "forget-network": "Oublier le réseau", + "grid-view": "Vue en grille", + "hidden-files-hide": "Fichiers cachés", + "hide-unnamed-devices": "Masquer les appareils sans nom", + "home": "Répertoire home", + "input-muted": "Activer/désactiver la sourdine de l'entrée", + "keep-awake": "Rester éveillé", + "keyboard-layout": "Disposition du clavier {layout}", + "list-view": "Vue en liste", + "manage-vpn": "Connexions VPN", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "Nombre maximal de widgets atteint", + "microphone-volume-at": "Volume du microphone : {volume}%", + "move-to-section": "Déplacer : {section}", + "mute": "Muet / Couper le son", + "next-media": "Piste suivante", + "next-month": "Mois suivant", + "night-light-not-installed": "Éclairage nocturne (non disponible)", + "noctalia-performance-enabled": "Mode performance Noctalia", + "open-annotation-tool": "Ouvrir avec l'outil d'annotation", + "open-control-center": "Centre de contrôle", + "open-notification-history-enable-dnd": "Historique des notifications", + "open-settings": "Paramètres", + "open-tray-dropdown": "Barre d'état système", + "output-muted": "Activer/désactiver la sourdine de la sortie", + "power-profile": "{profile} profil d'alimentation", + "previous-media": "Piste précédente", + "previous-month": "Mois précédent", + "refresh-devices": "Actualiser les appareils", + "refresh-wallhaven": "Actualiser les résultats de Wallhaven", + "refresh-wallpaper-list": "Actualiser la liste des fonds d'écran", + "remove-widget": "Supprimer le widget", + "search-close": "Fermer la recherche", + "session-menu": "Menu de session", + "show-all-devices": "Afficher tous les appareils", + "switch-to-dark-mode": "Mode sombre", + "switch-to-light-mode": "Mode clair", + "unmute": "Réactiver le micro", + "up": "Répertoire parent", + "volume-at": "Volume de sortie : {volume}%", + "wallpaper-selector": "Sélecteur de fond d'écran" + }, + "wallpaper": { + "browse": { + "empty-directory": "Ce répertoire est vide.", + "go-root": "Aller à la racine du fond d'écran", + "go-up": "Aller au dossier parent", + "go-up-hint": "Utilisez le bouton de retour pour naviguer vers le haut." + }, + "configure-directory": "Configurez votre répertoire de fonds d'écran avec des images.", + "fill-modes": { + "crop": "Recadrer (Remplir)", + "fit": "Ajuster (Contenir)", + "repeat": "Répéter (Mosaïque)", + "stretch": "Étirer" + }, + "no-match": "Aucun résultat trouvé.", + "no-wallpaper": "Aucun fond d'écran trouvé.", + "panel": { + "apikey-help": "Une clé API est requise pour accéder au contenu NSFW.", + "apikey-label": "Clé API", + "apikey-managed-by-env": "Géré via la variable d'environnement NOCTALIA_WALLHAVEN_API_KEY.", + "apikey-placeholder": "Entrez votre clé API Wallhaven", + "appearance-dark-tab": "Sombre", + "appearance-light-tab": "Clair", + "categories-anime": "Anime", + "categories-label": "Catégories", + "categories-people": "Personnes", + "color-extraction-disabled": "Utiliser l'extraction des couleurs du fond d'écran", + "color-extraction-enabled": "Utiliser les schémas de couleurs prédéfinis", + "header-devices-apply-all-tooltip": "Choisir le fond d'écran par moniteur", + "header-devices-per-monitor-tooltip": "Appliquer à tous les moniteurs", + "header-link-light-dark-tooltip": "Garder le fond d'écran lors du changement de thème", + "header-separate-light-dark-tooltip": "Changer automatiquement le fond d'écran avec le thème", + "header-sun-linked-tooltip": "Séparer les fonds d'écran clairs et sombres", + "header-sun-separate-tooltip": "Même fond d'écran pour clair et sombre", + "order-asc": "Croissant", + "order-desc": "Décroissant", + "order-label": "Ordre", + "purity-label": "Filtre de contenu", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Sketchy", + "ratios-all-portrait": "Portrait", + "ratios-all-wide": "Paysage", + "ratios-any": "Tous", + "ratios-label": "Format d'image", + "resolution-atleast": "Au moins", + "resolution-exact": "Exact", + "resolution-label": "Résolution", + "resolution-mode-label": "Mode", + "solid-color-tooltip": "Fond uni", + "sort-date-asc": "Trier du plus ancien au plus récent", + "sort-date-desc": "Trier du plus récent au plus ancien", + "sort-name-asc": "Trier par nom (A-Z)", + "sort-name-desc": "Trier par nom (Z-A)", + "sort-random": "Trier aléatoirement", + "sorting-date-added": "Date d'ajout", + "sorting-favorites": "Favoris", + "sorting-label": "Trier par", + "sorting-relevance": "Pertinence", + "sorting-toplist": "Top liste", + "sorting-views": "Vues", + "source-label": "Source", + "source-wallhaven": "Wallhaven", + "title": "Sélecteur de fond d'écran", + "wallhaven-settings-title": "Paramètres Wallhaven" + }, + "transitions": { + "disc": "Disque", + "fade": "Fondu", + "honeycomb": "Nid d'abeille", + "pixelate": "Pixéliser", + "stripes": "Rayures", + "wipe": "Balayage" + }, + "try-different-search": "Essayez une autre requête de recherche.", + "wallhaven": { + "loading": "Chargement des fonds d'écran...", + "no-results": "Aucun fond d'écran trouvé. Essayez une autre requête de recherche.", + "page": "{current} sur {total}", + "page-prefix": "Page", + "page-suffix": "de {total}" + } + }, + "weather": { + "clear-sky": "Ciel dégagé", + "drizzle": "Bruine", + "fog": "Brouillard", + "mainly-clear": "Généralement dégagé", + "overcast": "Couvert", + "partly-cloudy": "Partiellement nuageux", + "rain-showers": "Averses de pluie", + "snow": "Neige", + "thunderstorm": "Orage" + }, + "widgets": { + "color-picker": { + "palette-description": "Choisissez parmi une large gamme de couleurs prédéfinies.", + "palette-label": "Palette", + "palette-theme-colors": "Accès rapide à la palette de couleurs de votre thème.", + "title": "Sélecteur de couleurs" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm en minuscules", + "ampm-uppercase": "AM/PM en majuscules", + "common-12hour-time-minutes": "Heure sur 12 heures avec minutes", + "common-24hour-time-minutes": "Heure sur 24 heures avec minutes", + "common-24hour-time-seconds": "Heure sur 24 heures avec secondes", + "common-european-date": "Format de date européen", + "common-iso-date": "Format de date ISO", + "common-us-date": "Format de date américain", + "common-weekday-date": "Jour de la semaine avec date", + "common-weekday-month-day": "Jour de la semaine, mois et jour", + "day-abbreviated": "Nom du jour abrégé", + "day-full": "Nom complet du jour", + "day-leading-zero": "Jour avec zéro initial (01-31)", + "day-no-leading-zero": "Jour sans zéro initial (1-31)", + "hour-leading-zero": "Heure avec zéro initial (00-23) — format 24 heures", + "hour-no-leading-zero": "Heure sans zéro initial (0-23) — format 24 heures", + "minute-leading-zero": "Minute avec zéro initial (00-59)", + "minute-no-leading-zero": "Minute sans zéro initial (0-59)", + "month-abbreviated": "Nom du mois abrégé", + "month-full": "Nom complet du mois", + "month-number-leading-zero": "Mois en chiffre avec zéro initial (01-12)", + "month-number-no-zero": "Mois en chiffre sans zéro initial (1-12)", + "second-leading-zero": "Seconde avec zéro initial (00-59)", + "second-no-leading-zero": "Seconde sans zéro initial (0-59)", + "timezone-abbreviation": "Abréviation du fuseau horaire", + "year-four-digit": "Année sur quatre chiffres", + "year-two-digit": "Année sur deux chiffres (00-99)" + }, + "file-picker": { + "item": "élément", + "items": "éléments", + "search-placeholder": "Rechercher des fichiers et des dossiers...", + "select-current": "Sélectionner Actuel", + "select-file": "Sélectionner un fichier", + "select-folder": "Sélectionner un dossier", + "selected": "Sélectionné:", + "title": "Sélecteur de fichiers" + }, + "file-picker-title": "Sélectionner un fichier", + "icon-picker": { + "title": "Sélecteur d'icônes" + }, + "tooltip-placeholder": "Espace réservé" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "Identité Anonyme", + "ca-cert": "Certificat CA", + "eap-method": "Méthode EAP", + "password": "Mot de passe utilisateur", + "phase2-auth": "Authentification Phase 2", + "username": "Identité / Nom d'utilisateur" + }, + "panel": { + "action-required": "Action requise", + "add-hidden-network": "Ajouter un réseau masqué", + "add-network": "Ajouter un réseau", + "available-interfaces": "Interfaces disponibles", + "available-networks": "Réseaux disponibles", + "disabled": "Le Wi-Fi est désactivé", + "disconnecting": "Déconnexion en cours...", + "dns": "DNS", + "enable-message": "Activez le Wi-Fi pour voir les réseaux disponibles.", + "enter-password": "Entrez le mot de passe...", + "forget": "Oublier", + "forget-network": "Oublier ce réseau ?", + "forgetting": "Oubli en cours...", + "hidden-network": "Réseau masqué", + "interface": "Interface réseau", + "internet-connected": "Connecté à Internet", + "internet-limited": "Pas d'internet", + "internet-status": "État de la connexion Internet", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "Réseaux connus", + "link-speed": "Vitesse de liaison", + "network-name-ssid": "Nom du réseau (SSID)", + "no-ethernet-devices": "Aucun périphérique Ethernet détecté", + "no-networks": "Aucun réseau trouvé", + "saved": "Enregistré", + "scan-again": "Analyser à nouveau", + "searching": "Recherche de réseaux à proximité en cours...", + "security-open": "Ouvrir", + "security-wep": "WEP", + "security-wpa": "WPA/WPA2/WPA3 Personnel", + "security-wpa-ent": "WPA Enterprise", + "security-wpa2-ent": "WPA2 Enterprise", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 Enterprise", + "show-password": "Afficher le mot de passe", + "title": "Wi-Fi" + }, + "signal": { + "excellent": "Excellent", + "fair": "Moyen", + "good": "Bien", + "poor": "Mauvais", + "weak": "Faible" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/hu.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/hu.json new file mode 100644 index 0000000..6d61edb --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/hu.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "Előzmények törlése", + "close-app": "{app} bezárása", + "connect-vpn": "Csatlakozás ehhez: {name}", + "cycle-visualizer": "Váltás a vizualizációk között", + "disable-bluetooth": "Bluetooth letiltása", + "disable-dnd": "Ne zavarjanak letiltása", + "disable-wifi": "Wi-Fi letiltása", + "disconnect-vpn": "Kapcsolat bontása ezzel: {name}", + "dock-settings": "Dokk beállítások", + "enable-bluetooth": "Bluetooth engedélyezése", + "enable-dnd": "Ne zavarjanak engedélyezése", + "enable-wifi": "Wi-Fi engedélyezése", + "launcher-settings": "Indító beállítások", + "lower-to-bottom": "Legaljára süllyesztés", + "open-calendar": "Naptár megnyitása", + "open-display-settings": "Kijelzőbeállítások", + "open-launcher": "Indító megnyitása", + "open-settings": "Beállítások megnyitása", + "raise-to-top": "Felülre emelés", + "random-wallpaper": "Véletlenszerű háttérkép", + "run-custom-command": "Egyéni parancs futtatása", + "show-active-only": "Csak az aktívakat jelenítse meg", + "show-all": "Összes megjelenítése", + "toggle-mute": "Némítás be/ki", + "widget-settings": "Widget beállítások" + }, + "authentication": { + "error": "Hitelesítési hiba", + "failed": "A hitelesítés sikertelen" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Témaszínek alkalmazása az aktív ablak ikonra.", + "hide-mode-description": "A widget viselkedése, ha nincs aktív ablak.", + "scrolling-mode-description": "Mikor engedélyezett a szöveggörgetés a hosszú ablakcímek esetén.", + "show-app-icon-description": "Az alkalmazás ikonjának megjelenítése az ablakcím mellett.", + "show-app-icon-label": "Alkalmazásikon megjelenítése", + "show-app-text-description": "Ablakcím megjelenítése.", + "show-app-text-label": "Ablakcím megjelenítése" + }, + "audio-visualizer": { + "color-name-description": "A vizualizátor színe.", + "color-name-label": "Kitöltési szín", + "height-description": "Egyéni komponensszélesség.", + "hide-when-idle-description": "Ha engedélyezve van, a vizualizátor el van rejtve, kivéve, ha egy lejátszó aktívan játszik.", + "hide-when-idle-label": "Elrejtés, ha nincs média lejátszva", + "width-description": "Egyéni komponens szélessége." + }, + "battery": { + "device-default": "Alapértelmezett (Megjelenítő eszköz)", + "device-description": "Melyik akkumulátor eszközt jelenítse meg.", + "device-label": "Akkumulátor eszköz", + "display-mode-description": "Hogyan jelenjen meg az akkumulátor a sávban.", + "display-mode-graphic": "Grafikus akkumulátor", + "display-mode-graphic-clean": "Grafikus akkumulátor (százalék nélkül)", + "display-mode-icon-always": "Ikon - Mindig jelenítse meg a százalékot", + "display-mode-icon-hover": "Ikon - Rámutatásra megjelenik", + "display-mode-icon-only": "Csak ikon", + "hide-if-idle-description": "Widget elrejtése, ha az akkumulátor nincs töltés alatt vagy lemerülőben.", + "hide-if-idle-label": "Elrejtés tétlenség esetén", + "hide-if-not-detected-description": "Widget elrejtése, ha a rendszer nem észlel akkumulátort.", + "hide-if-not-detected-label": "Elrejtés, ha nincs észlelve", + "low-battery-threshold-label": "Alacsony akkumulátor töltöttségi figyelmeztetési küszöb", + "show-noctalia-performance-description": "A Noctalia teljesítménymód kapcsoló megjelenítése az akkumulátor panelen belül.
Letiltja az árnyékokat és animációkat a Noctaliaban az erőforrásfelhasználás csökkentése érdekében.", + "show-noctalia-performance-label": "Noctalia Teljesítmény kapcsoló megjelenítése", + "show-power-profile-description": "Az energiaprofil kiválasztásának megjelenítése az akkumulátor panelen belül.", + "show-power-profile-label": "Energiaprofil vezérlők" + }, + "brightness": { + "apply-all-description": "A fényerő módosítása minden monitoron a görgő használatával.", + "apply-all-label": "Görgetési változások alkalmazása minden monitorra" + }, + "clock": { + "clock-display-description": "Testre szabhatod az óra kijelzőjét a lenti listában szereplő tokenek hozzáadásával. A 12 órás formátum használatához tartalmaznia kell az „AP” tokent.", + "clock-display-label": "Óra kijelző", + "custom-font-description": "Egyéni betűtípus az óra kijelzőjéhez.", + "custom-font-label": "Egyéni betűtípus", + "custom-font-placeholder": "Egyéni betűtípus kiválasztása…", + "custom-font-search-placeholder": "Betűtípus keresése…", + "horizontal-bar-description": "Tipp: Használd a \\n karaktert sortörés létrehozásához.", + "horizontal-bar-label": "Vízszintes sáv", + "preview": "Előnézet", + "tooltip-format-description": "Formátumsztring az óra fölé húzva megjelenő eszköztipphez. Hagyd üresen az alapértelmezett eszköztipp használatához.", + "tooltip-format-label": "Eszköztipp formátum", + "use-custom-font-description": "Felülbírálja az alapértelmezett betűtípus kiválasztását egy egyéni betűtípussal az órához.", + "use-custom-font-label": "Egyéni betűtípus használata", + "use-monospaced-font-description": "Ha engedélyezve van, az óra a monospace betűtípust fogja használni.", + "use-monospaced-font-label": "Monospace betűtípus használata", + "vertical-bar-description": "Szóköz az egyes részek új sorba való elkülönítéséhez.", + "vertical-bar-label": "Függőleges sáv" + }, + "control-center": { + "browse-file": "Fájl tallózása", + "browse-library": "Könyvtár tallózása", + "color-selection-description": "Témaszínek alkalmazása az ikonokra.", + "enable-colorization-description": "Az ikon színezésének engedélyezése témaszínek alkalmazásával.", + "icon-description": "Ikon kiválasztása a könyvtárból vagy egy egyéni fájlból.", + "select-custom-icon": "Egyéni ikon kiválasztása", + "use-distro-logo-description": "Disztribúció logó használata egy egyéni ikon helyett.", + "use-distro-logo-label": "Disztribúció logó használata ikon helyett" + }, + "custom-button": { + "collapse-condition-description": "Ha a kimeneti szöveg megegyezik ezzel az értékkel, a gomb összecsukódik.", + "collapse-condition-label": "Összecsukási feltétel", + "color-selection-description": "Témák színeinek alkalmazása az ikonra és a szövegre.", + "default-tooltip": "Egyéni gomb, konfigurálás a beállításokban", + "display-command-output-description": "Egy parancs, amelyet rendszeres időközönként futtatni szeretnél. A kimenetének első sora szövegként jelenik meg.", + "display-command-output-label": "Parancs kimenetének megjelenítése", + "display-command-output-stream-description": "Egy parancs, amelyet folyamatosan futtatni szeretnél.", + "dynamic-text": "Dinamikus szöveg", + "enable-colorization-description": "Színezés engedélyezése az egyéni gomb ikonjához és szövegéhez, témaszínek alkalmazásával.", + "enable-colorization-label": "Színezés engedélyezése", + "general-tooltip-text-description": "Egyéni szöveg, ami megjelenik a gomb eszköztippjében.", + "general-tooltip-text-label": "Egyéni eszköztipp szöveg", + "hide-mode-always-expanded": "Mindig kibontva", + "hide-mode-description": "A widget láthatósága, ha a parancsnak nincs kimenete.", + "hide-mode-expand-with-output": "Kibontás, ha van kimenet", + "hide-mode-label": "Elrejtési mód", + "hide-mode-max-transparent": "Maximálisan kibontva, de átlátszó", + "icon-description": "Ikon kiválasztása a könyvtárból.", + "ipc-identifier-description": "Egyedi azonosító az IPC parancsokhoz. Használd ezt az azonosítót a 'qs -c noctalia-shell ipc call cb [akció] [azonosító]' paranccsal, hogy vezéreld ezt a gombot IPC-n keresztül.", + "ipc-identifier-label": "IPC-azonosító", + "left-click-description": "Végrehajtandó parancs a gomb bal egérgombbal történő kattintásakor.", + "left-click-label": "Bal kattintás", + "left-click-update-text": "Megjelenített szöveg frissítése bal kattintásra", + "max-text-length-horizontal-description": "Maximális karakterszám a vízszintes sávban (0 a szöveg elrejtéséhez).", + "max-text-length-horizontal-label": "Maximális szöveghossz (vízszintes)", + "max-text-length-vertical-description": "Maximális karakterszám a függőleges sávban (0 a szöveg elrejtéséhez).", + "max-text-length-vertical-label": "Maximális szöveghossz (függőleges)", + "middle-click-description": "Végrehajtandó parancs a gomb középső egérgombbal történő kattintásakor.", + "middle-click-label": "Középső kattintás", + "middle-click-update-text": "Megjelenített szöveg frissítése középső kattintásra", + "parse-json-description": "A parancs kimenetének JSON objektumként történő értelmezése a szöveg és az ikon dinamikus beállításához.", + "parse-json-label": "Kimenet értelmezése JSON-ként", + "refresh-interval-description": "Intervallum milliszekundumban.", + "refresh-interval-label": "Frissítési intervallum", + "right-click-description": "Végrehajtandó parancs a gomb jobb egérgombbal történő kattintásakor.", + "right-click-label": "Jobb kattintás", + "right-click-update-text": "Megjelenített szöveg frissítése jobb kattintásra", + "show-exec-tooltip-description": "Eszköztippek megjelenítése parancs részleteivel (bal/jobb/középső kattintás, görgő).", + "show-exec-tooltip-label": "Parancsleírások megjelenítése", + "show-icon-description": "A widget ikonjának láthatóságát kapcsolja.", + "show-icon-label": "Ikon megjelenítése", + "show-text-tooltip-description": "Eszköztippek megjelenítése a szöveges parancs kimenetével.", + "show-text-tooltip-label": "Dinamikus szöveges eszköztippek megjelenítése", + "text-stream-description": "A parancsból streamelt sorok szövegként jelennek meg a gombon.", + "text-stream-label": "Stream", + "wheel-description": "Végrehajtandó parancs a görgő használatakor.
Használd a $delta változót a parancsban a görgő elmozdulásához.", + "wheel-down": "Görgő le", + "wheel-down-description": "Végrehajtandó parancs, amikor a görgőt lefelé görgetik.", + "wheel-down-label": "Görgő le parancs", + "wheel-label": "Egérgörgő", + "wheel-mode-separate-description": "Külön parancsok engedélyezése a görgő fel és le mozgásához.", + "wheel-mode-separate-label": "Külön görgő parancsok", + "wheel-up": "Görgő fel", + "wheel-up-description": "Végrehajtandó parancs, amikor a görgőt felfelé görgetik.", + "wheel-up-label": "Görgő fel parancs", + "wheel-update-text": "Megjelenített szöveg frissítése görgetésre" + }, + "keyboard-layout": { + "show-icon-description": "A billentyűzetkiosztás ikon megjelenítése." + }, + "lock-keys": { + "hide-when-off-description": "Az indikátor elrejtése, ha a billentyű nem aktív.", + "hide-when-off-label": "Elrejtés kikapcsolt állapotban", + "show-caps-lock-description": "Caps Lock állapot megjelenítése.", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "Num Lock állapot megjelenítése.", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "Scroll Lock állapot megjelenítése.", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "Helytakarékos elrendezés engedélyezése a médialejátszó panelhez.", + "compact-mode-label": "Kompakt mód", + "hide-mode-description": "A widget viselkedését, ha nincs média lejátszva.", + "max-width-description": "Beállítja a widget maximális vízszintes méretét. A widget zsugorodik, hogy illeszkedjen a rövidebb tartalomhoz.", + "no-active-player": "Nincs aktív lejátszó", + "panel-section-description": "A médialejátszó paneljének megjelenésének és viselkedésének konfigurálása.", + "panel-section-label": "Médialejátszó panel", + "scrolling-mode-description": "Mikor engedélyezett a szöveggörgetés hosszú zenecímek esetén.", + "scrolling-mode-label": "Görgetési mód", + "show-album-art-description": "A borítókép megjelenítése az aktuálisan játszott számhoz.", + "show-album-art-label": "Borító megjelenítése", + "show-artist-first-description": "Előadó - cím megjelenítése cím - előadó helyett.", + "show-artist-first-label": "Előadó megjelenítése elöl", + "show-progress-ring-description": "Jelenítsen meg egy kör alakú folyamatjelzőt, amely mutatja a zene előrehaladását.", + "show-progress-ring-label": "Folyamatgyűrű megjelenítése", + "show-visualizer-description": "Jelenítsen meg egy hangvizualizátor, amikor zene szól.", + "show-visualizer-label": "Megjelenítő mutatása", + "use-fixed-width-description": "Ha engedélyezve van, a widget mindig a maximális szélességet használja a tartalomhoz való dinamikus igazodás helyett.", + "use-fixed-width-label": "Rögzített szélesség használata", + "visualizer-type-description": "A megjelenítendő hangvizualizátor stílusa.", + "visualizer-type-label": "Megjelenítő típusa" + }, + "notification-history": { + "hide-widget-when-zero-description": "Az értesítési ikon elrejtése, ha nincs értesítés.", + "hide-widget-when-zero-label": "Ikon elrejtése, amikor nincs értesítés", + "hide-widget-when-zero-unread-description": "Az értesítési ikon elrejtése, ha nincs olvasatlan értesítés.", + "hide-widget-when-zero-unread-label": "Ikon elrejtése, amikor nincs olvasatlan értesítés", + "show-unread-badge-description": "Jelenítsen meg egy jelvényt, amely mutatja az olvasatlan értesítések számát.", + "show-unread-badge-label": "Olvasatlan jelvény megjelenítése", + "unread-badge-color-description": "Az olvasatlan értesítési jelvény színe.", + "unread-badge-color-label": "Olvasatlan jelvény színe" + }, + "section-editor": { + "placeholder": "Hozzáadás…", + "search-placeholder": "Widget keresése…" + }, + "spacer": { + "width-description": "Távolság szélessége pixelben." + }, + "system-monitor": { + "compact-mode-description": "A statisztikákat mini sávdiagramokként jelenítse meg a szöveges értékek helyett. Megakadályozza az elrendezés eltolódását.", + "compact-mode-label": "Kompakt mód", + "cpu-cores-description": "A CPU magok használatának külön-külön megjelenítése.", + "cpu-cores-label": "CPU magok", + "cpu-frequency-description": "Az aktuális CPU-órajel megjelenítése GHz-ben.", + "cpu-frequency-label": "CPU-frekvencia megjelenítése", + "cpu-temperature-description": "CPU hőmérséklet-adatok megjelenítése, ha elérhetőek.", + "cpu-temperature-label": "CPU-hőmérséklet", + "cpu-usage-description": "Az aktuális CPU-használati százalék megjelenítése.", + "cpu-usage-label": "CPU-használat", + "disk-path-description": "Melyik lemez csatolási pontot figyelje.", + "disk-path-label": "Lemez útvonala", + "gpu-temperature-description": "GPU hőmérséklet-értékek megjelenítése, ha elérhetőek.", + "load-average-description": "Rendszerterhelés átlagának megjelenítése.", + "load-average-label": "Átlagos terhelés", + "memory-percentage-description": "A memória használatának megjelenítése százalékos értékben, abszolút értékek helyett.", + "memory-percentage-label": "Memória százalékban", + "memory-usage-description": "Jelenlegi RAM használati információk megjelenítése.", + "memory-usage-label": "Memóriahasználat", + "network-traffic-description": "Hálózati feltöltési és letöltési sebességek megjelenítése.", + "network-traffic-label": "Hálózati forgalom", + "storage-as-percentage-description": "Lemezterület megjelenítése százalékban abszolút értékek helyett.", + "storage-as-percentage-label": "Lemez százalékban", + "storage-available-description": "A rendelkezésre álló lemezterületet jeleníti meg a használt helyett", + "storage-available-label": "Rendelkezésre álló lemezterület", + "storage-usage-description": "Lemezterület-használati információk megjelenítése.", + "storage-usage-label": "Tárhelyhasználat", + "swap-usage-description": "Cserehely memória használatának megjelenítése.", + "swap-usage-label": "Cserehely használat", + "use-monospace-font-description": "Monospace betűtípus használata az egységes karakter-szélességhez.", + "use-monospace-font-label": "Monospace betűtípus", + "use-padding-description": "Kitölti a szövegértékeket vezető szóközökkel, hogy megakadályozza az elrendezés eltolódását.", + "use-padding-description-disabled-monospace-font": "Ehhez a funkcióhoz monospace betűtípus szükséges.", + "use-padding-description-disabled-vertical": "A függőleges tálca nem támogatja a szöveges kitöltést.", + "use-padding-label": "Szövegkitöltés" + }, + "taskbar": { + "colorize-icons-description": "Témaszínek alkalmazása a tálca ikonjaira.", + "hide-mode-description": "A widget viselkedése, ha nincsenek megfelelő ablakok.", + "hide-mode-label": "Elrejtési mód", + "icon-scale-description": "Beállítja a tálca ikonjainak skálázási tényezőjét.", + "icon-scale-label": "Ikon skálázása", + "max-width-description": "A tálca maximális szélessége a képernyőszélesség százalékában.", + "max-width-label": "Maximális szélesség", + "only-active-workspaces-description": "Csak az aktív munkaterületekről származó alkalmazások megjelenítése.", + "only-active-workspaces-label": "Csak az aktív munkaterületekről", + "only-same-monitor-description": "Csak a sáv helyén lévő monitorról származó alkalmazások megjelenítése.", + "only-same-monitor-label": "Csak ugyanarról a monitorról", + "show-pinned-apps-description": "Rögzített alkalmazások megjelenítése a tálcán.", + "show-pinned-apps-label": "Rögzített alkalmazások megjelenítése", + "show-title-description": "Ablakcímek megjelenítése a tálcán.", + "show-title-description-disabled": "A függőleges tálca nem támogatja a címek megjelenítését.", + "show-title-label": "Cím megjelenítése", + "smart-width-description": "A bejegyzés szélességének automatikus beállítása a bejegyzések száma alapján.", + "smart-width-label": "Intelligens szélesség", + "title-width-description": "Az ablakcímek szélessége a tálcán (képpontokban).", + "title-width-label": "Cím szélessége", + "title-width-reset-tooltip": "Cím szélességének alaphelyzetbe állítása" + }, + "tray": { + "chevron-color-description": "Témaszínek a fiók nyíl ikonjára.", + "chevron-color-label": "Nyíl színe", + "colorize-icons-description": "Témaszínek alkalmazása a tálcaikonokra.", + "colorize-icons-label": "Ikonok színezése", + "drawer-enabled-description": "Ha engedélyezve van, a nem rögzített tálcaelemek egy fiókpanelen jelennek meg.
Letiltva az összes tálcaelem sorban jelenik meg.", + "drawer-enabled-label": "Tálca-fiók engedélyezése", + "hide-passive-description": "Engedélyezve a „Passzív” állapotú tálcaelemek el lesznek rejtve.", + "hide-passive-label": "Passzív elemek elrejtése" + }, + "volume": { + "display-mode-description": "Hogyan jelenjen meg ez az érték." + }, + "workspace": { + "character-count-description": "A munkaterület nevekből megjelenítendő karakterek száma (1-10).", + "character-count-label": "Karakterek száma", + "empty-color-description": "Üres munkaterület háttérszínének beállítása.", + "empty-color-label": "Üres munkaterület színe", + "enable-scrollwheel-description": "Váltás a munkaterületek között az egérgörgővel.", + "enable-scrollwheel-label": "Görgetés a munkaterületek váltásához", + "focused-color-description": "A fókuszált munkaterület háttérszíne.", + "focused-color-label": "Fókuszált munkaterület színe", + "follow-focused-screen-description": "Munkaterületek megjelenítése az aktuálisan fókuszált képernyőről, nem arról a képernyőről, ahol a sáv található.", + "follow-focused-screen-label": "Fókuszált képernyő követése", + "font-weight-description": "A szöveg vizuális súlya a munkaterületen belül.", + "font-weight-label": "Betűvastagság", + "grouped-border-opacity-description": "A munkaterület-konténerek szegélyeinek átlátszósági szintjének beállítása.", + "grouped-border-opacity-label": "Szegély átlátszósága", + "hide-unoccupied-description": "Ne jelenítse meg az ablak nélküli munkaterületeket.", + "hide-unoccupied-label": "Foglalatlan elrejtése", + "label-mode-description": "Hogyan jelenjenek meg a munkaterület címkéi.", + "label-mode-label": "Címke mód", + "occupied-color-description": "A foglalt munkaterület háttérszínének beállítása.", + "occupied-color-label": "Foglalt munkaterület színe", + "pill-size-description": "A munkaterület jelzők mérete (50%-100%).", + "pill-size-label": "Kapszula méret", + "reverse-scrolling-description": "Görgetéskor fordított irányba váltson a munkaterületek között.", + "reverse-scrolling-label": "Görgetés megfordítása", + "show-applications-description": "Alkalmazásikonok megjelenítése minden munkaterületen belül.", + "show-applications-hover-description": "Alkalmazásikonok megjelenítése minden munkaterületen belül, amikor a munkaterület fölé viszed az egeret.", + "show-applications-hover-label": "Alkalmazások megjelenítése rámutatáskor", + "show-applications-label": "Alkalmazások megjelenítése", + "show-badge-description": "Munkaterület számának jelzése csoportosított módban.", + "show-badge-label": "Munkafelület jelvény megjelenítése", + "show-labels-only-when-occupied-description": "Csak akkor jelenítse meg a munkaterület címkéket, ha azok tartalmaznak ablakokat.", + "show-labels-only-when-occupied-label": "Címkék megjelenítése csak foglalt állapotban", + "unfocused-icons-opacity-description": "A nem fókuszált alkalmazásikonok átlátszóságának szintje.", + "unfocused-icons-opacity-label": "Fókuszálatlan ikonok átlátszósága" + } + }, + "battery": { + "all-batteries": "Összes akkumulátor (kombinálva)", + "battery-health": "Akkumulátor állapota", + "battery-level": "Akkumulátor töltöttségi szintje", + "capacity-level": "Kapacitás: {level}", + "charging-rate": "Töltési sebesség: {rate} W", + "discharging-rate": "Merülési sebesség: {rate} W", + "health": "Állapot: {percent}%", + "inhibit-idle-description": "Ébren tartja a rendszert.", + "no-battery-detected": "Nincs akkumulátor érzékelve", + "plugged-in": "Csatlakoztatva", + "power-profile": "Energiaprofil", + "time-left": "Hátralévő idő: {time}", + "time-until-full": "Teljes feltöltésig hátralévő idő: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Elérhető eszközök", + "blocked": "Blokkolva", + "connected-devices": "Csatlakoztatott eszközök", + "device-address": "Eszköz címe", + "disabled": "A Bluetooth le van tiltva", + "discoverable": "Felfedezhető", + "enable-message": "Kapcsold be a Bluetooth-t az elérhető eszközök megtekintéséhez.", + "known-devices": "Ismert eszközök", + "no-devices": "Nincsenek elérhető eszközök", + "paired-devices": "Párosított eszközök", + "pairing-mode": "Győződjön meg arról, hogy eszköze párosítási módban van.", + "scanning": "Eszközök keresése…", + "signal-text-excellent": "Jel: Kiváló", + "signal-text-fair": "Jel: Gyenge", + "signal-text-good": "Jel: Jó", + "signal-text-poor": "Jel: Gyenge", + "signal-text-unknown": "Jel: Ismeretlen", + "signal-text-very-poor": "Jelerősség: Nagyon gyenge" + } + }, + "changelog": { + "error": { + "fetch-failed": "Nem sikerült betölteni a változási napló adatait. Próbálja újra később.", + "rate-limit": "Túllépte a GitHub sebességkorlátját. Próbálja újra néhány perc múlva." + }, + "panel": { + "buttons-discord": "Csatlakozz a Discordunkhoz", + "buttons-dismiss": "OK", + "buttons-feedback": "Adj visszajelzést", + "empty": "A kiadási jegyzetek még nem érhetők el.", + "highlight-title": "Főbb újdonságok", + "released": "Kiadva: {date}", + "subtitle-fresh": "Köszönjük, hogy telepítette a Noctalia-t! Itt van, mi található ebben az összeállításban.", + "subtitle-updated": "Frissítve erről: {previousVersion}", + "title": "Újdonságok a {version} verzióban", + "version": "verzió {version}", + "version-new-user": "Új telepítés" + } + }, + "common": { + "actions": "Műveletek", + "active": "Aktív", + "add": "Hozzáadás", + "appearance": "Megjelenés", + "apply": "Alkalmazás", + "auto-connect": "Automatikus csatlakozás", + "automation": "Automatizálás", + "available": "Elérhető", + "back": "Vissza", + "battery": "Akkumulátor", + "behavior": "Viselkedés", + "bluetooth": "Bluetooth", + "brightness": "Fényerő", + "browse": "Böngészés", + "calendar": "Naptár", + "calendar-panel": "Naptár panel", + "cancel": "Mégse", + "cards": "Kártyák", + "charging": "Töltés", + "clear": "Törlés", + "clipboard": "Vágólap", + "close": "Bezárás", + "color-muted": "Némítva", + "colors": "Színek", + "command": "Parancs", + "confirm": "Megerősítés", + "connect": "Csatlakozás", + "connected": "Csatlakoztatva", + "connecting": "Csatlakozás…", + "continue": "Folytatás", + "contributors": "Közreműködők", + "copied-to-clipboard": "Vágólapra másolva", + "countdown": "Visszaszámlálás", + "customize": "Testreszabás", + "date": "Dátum", + "default": "Alapértelmezett", + "delete": "Törlés", + "devices": "Eszközök", + "disabled": "Letiltva", + "discharging": "Kisütés", + "disconnect": "Kapcsolat bontása", + "disconnected": "Kapcsolat bontva", + "disconnecting": "Kapcsolat bontása…", + "display-mode": "Kijelző mód", + "documentation": "Dokumentáció", + "download": "Letöltés", + "duration": "Időtartam", + "dysfunctional": "Hibás", + "edit": "Szerkesztés", + "enabled": "Engedélyezve", + "error": "Hiba", + "ethernet": "Ethernet", + "events": "Események", + "execute": "Végrehajtás", + "faithful": "Hűséges", + "focus": "Fókusz", + "font-weight-bold": "Félkövér", + "font-weight-light": "Vékony", + "font-weight-medium": "Közepes", + "font-weight-regular": "Normál", + "font-weight-semibold": "Félkövér", + "frequency": "Frekvencia", + "gateway": "Átjáró", + "general": "Általános", + "height": "Magasság", + "hibernate": "Hibernálás", + "history": "Előzmények", + "icon": "Ikon", + "idle": "Tétlen", + "info": "Információ", + "input": "Bemenet", + "install": "Telepítés", + "installed": "Telepítve", + "interface": "Felület", + "internet": "Internet", + "keybind": "Gyorsbillentyű", + "language": "Nyelv", + "loading": "Betöltés…", + "local": "Helyi", + "location": "Helyszín", + "lock": "Zárolás", + "logout": "Kijelentkezés", + "look": "Megjelenés", + "margins": "Margók", + "media": "Média", + "media-player": "Médialejátszó", + "memory": "Memória", + "monitors": "Monitorok", + "move-down": "Lejjebb mozgatás", + "move-up": "Feljebb", + "network": "Hálózat", + "next": "Következő", + "night-light": "Éjszakai fény", + "no": "Nem", + "no-results": "Nincs találat", + "no-summary": "Nincs összefoglaló", + "none": "Nincs", + "not-found": "Nem található", + "notifications": "Értesítések", + "official": "Hivatalos", + "on-surface": "Felületen", + "output": "Kimenet", + "pair": "Párosítás", + "paired": "Párosítva", + "pairing": "Párosítás…", + "panels": "Panelek", + "password": "Jelszó", + "pause": "Szünet", + "performance": "Teljesítmény", + "pin": "Rögzítés", + "play": "Lejátszás", + "polling": "Lekérdezés", + "position": "Pozíció", + "previous": "Előző", + "primary": "Elsődleges", + "random": "Véletlen", + "reboot": "Újraindítás", + "reboot-to-uefi": "Újraindítás UEFI-be", + "record": "Felvétel indítása", + "refresh": "Frissítés", + "required": "(kötelező)", + "reset": "Visszaállítás", + "reset-to-default": "Visszaállítás Alapértelmezettre", + "result-count": "{count} eredmény", + "result-count-plural": "eredmények", + "resume": "Folytatás", + "save": "Mentés", + "scanning": "Szkennelés…", + "screen-corners": "Képernyősarkok", + "search": "Keresés", + "second": "{count} másodperc", + "second-plural": "{count} másodperc", + "secondary": "Másodlagos", + "security": "Biztonság", + "select": "Kiválasztás", + "select-color": "Szín kiválasztása", + "select-color-description": "Témaszínek a kiemeléshez.", + "select-icon-color": "Ikon színének kiválasztása", + "settings": "Beállítások", + "shortcuts": "Gyorsbillentyűk", + "shutdown": "Leállítás", + "signal": "Jelzés", + "sound": "Hang", + "sources": "Források", + "start": "Kezdés", + "stop": "Állj", + "supporters": "Támogatók", + "suspend": "Felfüggesztés", + "templates": "Sablonok", + "tertiary": "Harmadlagos", + "test": "Teszt", + "thresholds": "Küszöbértékek", + "title": "Cím", + "toast": "Értesítő", + "trusted": "Megbízható", + "uninstall": "Eltávolítás", + "unknown": "Ismeretlen", + "unpair": "Párosítás bontása", + "unpin": "Rögzítés megszüntetése", + "update": "Frissítés", + "upload": "Feltöltés", + "username": "Felhasználónév", + "userspace-reboot": "Felhasználói tér újraindítása", + "version": "Verzió", + "vibrant": "Élénk", + "visualizer": "Vizualizáló", + "volume": "Hangerő", + "volumes": "Hangerő", + "wallpaper": "Háttérkép", + "weather": "Időjárás", + "weather-loading": "Időjárás betöltése…", + "weather-no-location": "Állítson be egy helyet a Beállításokban az időjárás megtekintéséhez", + "week": "Hét", + "widgets": "Widgetek", + "width": "Szélesség", + "wifi": "Wi-Fi", + "windows": "Ablakok", + "yes": "Igen" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Energiaprofil", + "tooltip-disabled": "Telepítsd a power-profiles-daemon csomagot az energiaprofilok használatához" + }, + "wifi": { + "label-disconnected": "Wi-Fi leválasztva", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Mindig rejtve legyen", + "always-show": "Mindig megjelenítve legyen", + "force-open": "Felfeszít", + "on-hover": "Rámutatáskor" + }, + "hide-modes": { + "auto-hide": "Automatikus elrejtés", + "hidden": "Elrejtés, ha üres", + "idle": "Elrejtés, ha tétlen", + "non-exclusive": "Nem kizárólagos", + "transparent": "Átlátszó, ha üres", + "visible": "Mindig látható" + }, + "launcher": { + "categories": { + "all": "Összes", + "audiovideo": "Hang és videó", + "chat": "Csevegés", + "development": "Fejlesztés", + "education": "Oktatás", + "emoji-activity": "Tevékenység", + "emoji-animals": "Állatok", + "emoji-flags": "Zászlók", + "emoji-food": "Étel és ital", + "emoji-nature": "Természet", + "emoji-objects": "Tárgyak", + "emoji-people": "Emberek és test", + "emoji-recent": "Legutóbbi", + "emoji-symbols": "Szimbólumok", + "emoji-travel": "Utazás és helyek", + "game": "Játékok", + "graphics": "Grafika", + "misc": "Egyéb", + "office": "Iroda", + "pinned": "Rögzített", + "system": "Rendszer", + "webbrowser": "Webböngésző" + }, + "providers": { + "applications": "Alkalmazások", + "calculator": "Számológép", + "calculator-deprecated": "A >calc parancs elavult, és hamarosan el lesz távolítva. A matematikai kifejezéseket közvetlenül a keresésbe írd be helyette.", + "calculator-description": "Számológép — matematikai kifejezések kiértékelése", + "calculator-enter-expression": "Írjon be egy matematikai kifejezést", + "calculator-press-enter-to-copy": "Nyomd meg az Entert az eredmény másolásához", + "clipboard": "Vágólapelőzmények", + "clipboard-clear-description": "Összes vágólapelőzmény törlése", + "clipboard-clear-description-full": "Az összes elem eltávolítása a vágólapelőzményekből", + "clipboard-clear-history": "Vágólapelőzmények törlése", + "clipboard-delete": "Vágólap bejegyzés törlése", + "clipboard-history-disabled": "Vágólapelőzmények letiltva", + "clipboard-history-disabled-description": "Engedélyezd a vágólapelőzményeket a beállításokban, vagy telepítsd a cliphist csomagot", + "clipboard-loading": "Vágólapelőzmények betöltése…", + "clipboard-search-description": "Keresés a vágólapelőzményekben", + "command-description": "Shell parancsok futtatása", + "emoji": "Emoji választó", + "emoji-loading": "Emojik betöltése…", + "emoji-loading-description": "Kis türelmet", + "emoji-no-recent": "Nincsenek még legutóbbi emojik", + "emoji-search-description": "Emojik keresése és másolása", + "settings-search-description": "Keresés és navigálás a beállításokhoz", + "windows-search-description": "Nyitott ablakok keresése és fókuszálása" + } + }, + "lock-screen": { + "authenticating": "Hitelesítés…", + "password": "Add meg a jelszavadat…", + "restart": "Újraindítás", + "shut-down": "Leállítás" + }, + "notifications": { + "panel": { + "click-to-collapse": "Kattints az összecsukáshoz", + "click-to-expand": "Kattints a kibontáshoz", + "description": "Az értesítések itt jelennek meg, amint megérkeznek.", + "no-notifications": "Nincs értesítés" + }, + "range": { + "earlier": "Korábban", + "today": "Ma", + "yesterday": "Tegnap" + }, + "time": { + "diff-d": "1 napja", + "diff-dd": "{diff} napja", + "diff-h": "1 órája", + "diff-hh": "{diff} órája", + "diff-m": "1 perce", + "diff-mm": "{diff} perce", + "now": "most" + } + }, + "options": { + "bar": { + "density-comfortable": "Kényelmes", + "density-compact": "Kompakt", + "density-default": "Alapértelmezett", + "density-mini": "Mini", + "density-spacious": "Tágas", + "display-mode-always-visible": "Mindig látható", + "type-floating": "Lebegő", + "type-framed": "Keretes", + "type-simple": "Egyszerű" + }, + "control-center": { + "quick-settings-style-classic": "Klasszikus", + "quick-settings-style-modern": "Modern" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "Kényelmes", + "compact": "Kompakt", + "default": "Alapértelmezett" + }, + "launcher-view-mode": { + "grid": "Rács", + "list": "Lista" + }, + "notification-density": { + "compact": "Kompakt", + "default": "Alapértelmezett" + }, + "scrolling-modes": { + "always": "Mindig görgetés", + "hover": "Görgetés rámutatáskor", + "never": "Soha ne görgessen" + }, + "session-menu-grid-layout": { + "grid": "Rács", + "single-row": "Egy sor" + }, + "settings-panel-mode": { + "attached": "Panel a sávhoz rögzítve", + "centered": "Középre igazított panel", + "window": "Külön ablak" + }, + "visualizer-types": { + "linear": "Lineáris", + "mirrored": "Tükrözött", + "wave": "Hullám" + }, + "visualizer-visibility": { + "with-background": "Csak háttérrel" + }, + "workspace-labels": { + "index": "Index", + "index-and-name": "Index és név", + "name": "Név" + } + }, + "panels": { + "about": { + "become-supporter": "Legyél támogató", + "changelog": "Változások megtekintése", + "changelog-on-startup": "Változási napló megjelenítése frissítéskor", + "changelog-on-startup-desc": "Automatikusan jelenítse meg a változási naplót, amikor a Noctalia frissül.", + "contributors-description": "Köszönet a(z) {count} fantasztikus közreműködőnknek!", + "contributors-description-plural": "Köszönet a(z) {count} fantasztikus közreműködőnknek!", + "copy-info": "Információk másolása", + "debug-disabled": "Hibakeresési mód letiltva", + "debug-enabled": "Hibakeresési mód engedélyezve", + "info-copied": "Információ a vágólapra másolva", + "noctalia-available": "Elérhető:", + "noctalia-desc": "Egy elegáns és minimalista asztali felület, Waylandra tervezve, Quickshell-en alapulva.", + "noctalia-git-commit": "Git commit:", + "noctalia-installed-version": "Telepített verzió:", + "noctalia-latest-version": "Legújabb verzió:", + "noctalia-qs-version": "Noctalia QS verzió:", + "noctalia-title": "Noctalia shell", + "privacy-policy": "Adatvédelmi irányelvek", + "support": "Támogass minket", + "supporter-badge": "Támogató", + "supporters-desc": "Hatalmas köszönet csodálatos támogatóinknak!", + "supporters-desc-plural": "Hatalmas köszönet a {count} csodálatos támogatóinknak!", + "supporters-loading": "Támogatók betöltése…", + "system-board": "Alaplap:", + "system-cpu": "CPU:", + "system-disk": "Lemez:", + "system-gpu": "GPU:", + "system-host": "Gépnév:", + "system-install-hint": "Telepítsd a fastfetch programot a rendszerinformációk megtekintéséhez", + "system-kernel": "Kernel:", + "system-loading": "Rendszerinformációk betöltése…", + "system-memory": "Memória:", + "system-monitor": "Monitor:", + "system-not-installed": "A fastfetch nincs telepítve", + "system-os": "Operációs rendszer:", + "system-packages": "Csomagok:", + "system-product": "Termék:", + "system-title": "Rendszerinformációk", + "system-uptime": "Működési idő:", + "system-wm": "WM:", + "telemetry-data-copied": "Telemetriai adatok a vágólapra másolva", + "telemetry-desc": "Segíts a Noctalia fejlesztésében névtelen rendszerinformációk (képernyőfelbontás, kompozitor, disztribúció) megosztásával. Egyszer kerül elküldésre indításkor, nincs nyomon követés, az adatok 30 nap után automatikusan törlődnek.", + "telemetry-enabled": "Névtelen rendszerinformációk küldése", + "telemetry-show-data": "Adatok megtekintése", + "telemetry-title": "Adatvédelem", + "title": "Névjegy", + "up-to-date": "Naprakész vagy!", + "update-available": "Frissítés elérhető", + "view-commit": "Commit megtekintése a GitHubon" + }, + "audio": { + "devices-desc": "A rendelkezésre álló hangbemeneti és kimeneti eszközök konfigurálása.", + "devices-input-device-description": "A kívánt hangbemeneti eszköz.", + "devices-input-device-label": "Bemeneti eszköz", + "devices-output-device-description": "A kívánt hangkimeneti eszköz.", + "devices-output-device-label": "Kimeneti eszköz", + "devices-title": "Hangeszközök", + "external-mixer-description": "A parancs vagy alkalmazás útvonala, amit el kell indítani a külső hangkeverő funkció aktiválásakor.", + "external-mixer-label": "Külső hangkeverő parancs", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "A preferált és figyelmen kívül hagyott médiaalkalmazások.", + "media-excluded-player-description": "Kulcsszavak hozzáadása azokhoz a lejátszókhoz, amelyeket figyelmen kívül szeretnél hagyni. Minden kulcsszó új sorban szerepeljen.", + "media-excluded-player-label": "Kizárt lejátszó", + "media-excluded-player-placeholder": "Írj be egy részszöveget és nyomd meg a + gombot", + "media-frame-rate-description": "A magasabb sebesség simább, de több erőforrást használ.", + "media-frame-rate-label": "Képkockasebesség", + "media-primary-player-description": "Egy kulcsszó a fő lejátszó azonosításához.", + "media-primary-player-label": "Elsődleges lejátszó", + "media-primary-player-placeholder": "pl. spotify, vlc, mpv", + "media-scrolling-speed-description": "A cím másodpercben megadott ideje, amíg a címsor az elejétől a végéig görget.", + "media-scrolling-speed-label": "Görgetési sebesség", + "media-scrolling-title-description": "Folyamatos görgetés a hosszú médiacímekhez.", + "media-scrolling-title-label": "Görgető cím", + "media-title": "Médialejátszók", + "on-middle-clicked-description": "A gomb középső kattintásakor végrehajtandó parancs.", + "panel-applications-empty": "Jelenleg egyetlen alkalmazás sem játszik le hangot", + "spectrum-mirrored-description": "Úgy tükrözi a spektrumot, hogy az alacsony frekvenciák középen legyenek. Kikapcsolva az alacsony frekvenciák balra, a magasak jobbra vannak.", + "spectrum-mirrored-label": "Spektrum tükrözése", + "title": "Hang", + "visualizer-type-description": "Vizualizációs típus a média lejátszásához.", + "visualizer-type-label": "Vizualizációs típus", + "volumes-desc": "Beállítja a hangerőszabályzókat és a hangszinteket.", + "volumes-feedback-sound-file-description": "Annak a hangfájlnak az elérési útja, amely a hangerő változtatásakor játszódik le.", + "volumes-feedback-sound-file-label": "Hangerő visszajelző hangfájl", + "volumes-feedback-sound-file-select-title": "Hangerő visszajelző hangfájl kiválasztása", + "volumes-input-volume-description": "Mikrofon bemeneti hangerőszintje.", + "volumes-mute-input-description": "Elnémítja az alapértelmezett hangbemenetet (mikrofon).", + "volumes-mute-input-label": "Hangbemenet némítása", + "volumes-mute-output-description": "Elnémítja a rendszer fő hangkimenetét.", + "volumes-mute-output-label": "Hangkimenet némítása", + "volumes-output-volume-description": "Rendszerszintű hangerőszint.", + "volumes-step-size-description": "Beállítja a hangerő változtatásának lépésméretét (görgő, gyorsbillentyűk).", + "volumes-step-size-label": "Hangerő lépésmérete", + "volumes-volume-feedback-description": "Hangerő állításakor visszajelző hang lejátszása.", + "volumes-volume-feedback-label": "Hangerő visszajelző hang lejátszása", + "volumes-volume-overdrive-description": "Engedélyezi a hangerő 100% fölé emelését. Előfordulhat, hogy nem minden hardver támogatja.", + "volumes-volume-overdrive-label": "Hangerő túllépésének engedélyezése" + }, + "bar": { + "appearance-auto-hide-delay-description": "Idő, amíg a Bar elrejtőzik, miután az egér elhagyja.", + "appearance-auto-hide-delay-label": "Elrejtési késleltetés", + "appearance-auto-hide-exclusive-note": "Megjegyzés: Az exkluzív zóna automatikusan letiltásra kerül, ha az automatikus elrejtés aktív.", + "appearance-auto-show-delay-description": "Idő, miután a sáv megjelenik, ha az egér a szélre kerül.", + "appearance-auto-show-delay-label": "Megjelenítési Késleltetés", + "appearance-background-opacity-description": "A sáv háttérének átlátszósága.", + "appearance-background-opacity-label": "Sáv háttér átlátszósága", + "appearance-capsule-color-description": "Szín a sávkapszulákhoz, vagy semmi az alapértelmezett felületszínhez.", + "appearance-capsule-color-label": "Kapszula színe", + "appearance-capsule-opacity-description": "Beállítja a widget hátterének átlátszósági szintjét, amikor a kapszula megjelenik.", + "appearance-capsule-opacity-label": "Kapszula átlátszósága", + "appearance-content-padding-description": "A távolság a sáv szélei és a widgetek között.", + "appearance-content-padding-label": "Tartalom távolság", + "appearance-density-description": "A sáv belső margóinak beállítása a kompakt vagy tágas megjelenés érdekében.", + "appearance-density-label": "Sáv sűrűsége", + "appearance-desc": "A sáv megjelenésének és pozíciójának testreszabása.", + "appearance-display-mode-description": "Mikor látható a sáv.", + "appearance-enable-exclusion-zone-inset-description": "Csökkenti a kizárási zónát 1 fizikai pixellel, hogy a sáv széle alá tökéletesen illeszkedjenek a szélén lévő ablakok.", + "appearance-enable-exclusion-zone-inset-label": "Behúzott kizárási zóna", + "appearance-floating-description": "Megjeleníti a sávot lebegő „kapszulaként”.", + "appearance-floating-label": "Lebegő sáv", + "appearance-font-scale-description": "A betűméret skálájának beállítása a sávon megjelenő szöveghez.", + "appearance-font-scale-label": "Betűméret-skála", + "appearance-frame-radius": "Belső sugár", + "appearance-frame-settings-description": "A keret vastagsága és a belső sarok sugara", + "appearance-frame-settings-label": "Keret beállításai", + "appearance-frame-thickness": "Vastagság", + "appearance-hide-on-overview-description": "A sáv elrejtése és a panelek bezárása, amikor a kompozitor áttekintése aktív.", + "appearance-hide-on-overview-label": "Sáv elrejtése az áttekintésben", + "appearance-margins-description": "A lebegő sáv körüli margók.", + "appearance-margins-horizontal": "Vízszintes margó", + "appearance-margins-vertical": "Függőleges margó", + "appearance-outer-corners-description": "Megjelenít kifelé ívelt sarkokat a sávon.", + "appearance-outer-corners-label": "Külső sarkok", + "appearance-position-description": "A sáv képernyőn való elhelyezése.", + "appearance-position-label": "Sáv pozíciója", + "appearance-show-capsule-description": "Widget hátterek megjelenítése.", + "appearance-show-capsule-label": "Kapszula megjelenítése", + "appearance-show-on-workspace-switch-description": "Automatikusan jelenítse meg a sávot egy pillanatra, amikor a munkaterület vált.", + "appearance-show-on-workspace-switch-label": "Sáv megjelenítése munkaterület váltáskor", + "appearance-show-outline-description": "Látható szegély megjelenítése minden widget körül.", + "appearance-show-outline-label": "Widget körvonalak megjelenítése", + "appearance-type-description": "A sáv stílusa: Egyszerű, lebegő vagy keretes.", + "appearance-type-label": "Sáv típusa", + "appearance-use-separate-opacity-description": "Lehetővé teszi egy külön átlátszatlansági érték használatát a sáv hátteréhez.", + "appearance-use-separate-opacity-label": "Külön oszlop átlátszóság használata", + "appearance-widget-spacing-description": "Az egyes widgetek közötti távolság a sávon.", + "appearance-widget-spacing-label": "Widget távolság", + "behavior-middle-click-command-description": "Középső kattintásra végrehajtandó parancs.", + "behavior-middle-click-command-label": "Középső kattintás parancsa", + "behavior-middle-click-command-placeholder": "niri msg action áttekintés váltása", + "behavior-middle-click-description": "Mit csináljon a középső kattintás a sáv üres területein.", + "behavior-middle-click-follow-mouse-description": "Megnyitja a kiválasztott középső kattintás panelt a kurzor pozíciójánál.", + "behavior-middle-click-follow-mouse-label": "Középső kattintás követi az egeret", + "behavior-middle-click-label": "Sáv középső kattintás művelete", + "behavior-right-click-command-description": "Jobb kattintásra végrehajtandó parancs.", + "behavior-right-click-command-label": "Jobb Kattintási Parancs", + "behavior-right-click-command-placeholder": "notify-send \"Jobb kattintás\"", + "behavior-right-click-description": "Mit csináljon a jobb kattintás a sáv üres területein.", + "behavior-right-click-follow-mouse-description": "Megnyitja a kiválasztott jobb egérgombos panelt a kurzor pozíciójánál.", + "behavior-right-click-follow-mouse-label": "Jobb kattintás követi az egeret", + "behavior-right-click-label": "Jobb kattintási művelet a sávon", + "behavior-wheel-wrap-description": "Ha engedélyezve van, a görgetés az utolsó elemtől az elsőig folytatódik.", + "behavior-wheel-wrap-label": "Körbefutás", + "behavior-workspace-scroll-description": "Mit csináljon az egérgörgő a sáv üres területein.", + "behavior-workspace-scroll-label": "Sáv egérgörgő művelet", + "behavior-workspace-scroll-option-content": "Tartalom", + "behavior-workspace-scroll-option-workspace": "Munkaterület", + "monitor-configure-widgets": "Widgetek konfigurálása", + "monitor-override-settings": "Globális beállítások felülírása", + "monitor-override-settings-description": "Egyéni beállítások használata ehhez a monitorhoz.", + "monitor-reset-all": "Összes visszaállítása", + "monitor-widgets-title": "Widget konfiguráció - {monitor}", + "monitors-desc": "Sáv megjelenítése meghatározott monitorokon. Alapértelmezetten minden monitoron, ha nincs kiválasztva.", + "monitors-desc-new": "Mely monitorok jelenítsék meg a sávot, és a beállítások testre szabása monitoronként.", + "monitors-title": "Monitorok megjelenítése", + "title": "Sáv", + "tray-blacklist-description": "Tálca kizárási szabályok, támogatja a helyettesítő karaktereket (*).", + "tray-blacklist-label": "Feketelista", + "tray-blacklist-placeholder": "pl. nm-applet, Fcitx*", + "tray-pin-application": "Alkalmazás rögzítése", + "tray-unpin-application": "Alkalmazás feloldása", + "use-global-widgets": "Globális widgetek használata", + "widgets-desc": "Húzd át a widgeteket a sorrendjük megváltoztatásához. Használd a jobb egérgombos menüt a widgetek szakaszok közötti áthelyezéséhez vagy eltávolításához.", + "widgets-title": "Widgetek elhelyezése" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Színsémák generálása a háttérképből. Automatikusan kinyeri a színeket, hogy egy egységes témát hozzon létre.", + "color-source-use-wallpaper-colors-label": "Háttérkép színeinek használata", + "dark-mode-mode-description": "Engedélyezi az automatikus váltást a világos és sötét mód között.", + "dark-mode-mode-label": "Sötét mód ütemezése", + "dark-mode-mode-manual": "Kézi", + "dark-mode-mode-off": "Ki", + "dark-mode-switch-description": "Sötétebb témára vált a könnyebb éjszakai megtekintés érdekében.", + "delete-error-description": "Nem sikerült törölni a(z) {scheme} sémát", + "delete-error-title": "A törlés sikertelen", + "delete-success-description": "{scheme} sikeresen törölve", + "delete-success-title": "Színséma törölve", + "download-button": "Több letöltése", + "download-downloading": "Letöltés…", + "download-empty": "Nincsenek elérhető színsémák", + "download-error-api-error": "API hiba: {status}", + "download-error-description": "Nem sikerült letölteni a(z) {scheme} sémát", + "download-error-download-failed": "A letöltés sikertelen, kilépési kóddal: {code}", + "download-error-invalid-response": "Érvénytelen API válaszformátum", + "download-error-no-files": "Nem található fájl a sémához", + "download-error-parse-failed": "Nem sikerült értelmezni az API választ: {error}", + "download-error-rate-limit": "A GitHub API sebességkorlátja túl lett lépve", + "download-error-title": "A letöltés sikertelen", + "download-fetching": "Elérhető színsémák lekérése…", + "download-success-description": "Sikeresen letöltve: {scheme}", + "download-success-title": "Színséma letöltve", + "download-title": "Színsémák letöltése", + "method-description": { + "content": "Material Design séma nagy pontosságú színkivonással, amely szorosan illeszkedik a forrás tartalom tényleges színeihez.", + "dysfunctional": "Mint a Faithful, de a második legdominánsabb színcsaládot választja elsődlegesnek.", + "faithful": "Igyekszik közel maradni a forrásszínhez, miközben harmonikus palettát generál.", + "fruit-salad": "Material Design séma, amely játékos, élénk palettát hoz létre változatos árnyalatokkal.", + "monochrome": "Material Design séma egyszínű szürkeskálával, minimális színtartalommal.", + "muted": "Lágyabb, telítetlen színeket generál a nyugodt és visszafogott megjelenés érdekében.", + "rainbow": "Material Design séma, amely a teljes spektrumban színeket generál a maximális színdiverzitás érdekében.", + "tonal-spot": "Alapértelmezett Material Design algoritmus. Kiegyensúlyozott palettákat hoz létre a forrásszín tónusváltozataival.", + "vibrant": "Maximalizálja a színtelítettséget a merész, szemet gyönyörködtető palettákhoz élénk tónusokkal." + }, + "predefined-desc": "Válassz az előre definiált színsémák gyűjteményéből.", + "predefined-generate-templates-label": "Sablonok generálása előre definiált sémákhoz", + "predefined-title": "Előre definiált színsémák", + "sync-gsettings-description": "A rendszertéma igazítása az aktív világos vagy sötét módhoz.", + "sync-gsettings-label": "Rendszertéma szinkronizálása", + "templates-desc": "Színek alkalmazása külső alkalmazásokra.", + "templates-filter-description": "Sablonok megjelenítése egy adott kategóriából.", + "templates-filter-label": "Szűrés kategória szerint", + "templates-misc-description": "Hozzon létre saját sablonokat.", + "templates-misc-label": "Haladó", + "templates-misc-user-templates-description": "Csak akkor engedélyezd, ha tudod, mit csinálsz — lásd az online dokumentációt.", + "templates-misc-user-templates-label": "Felhasználói sablonok engedélyezése", + "templates-none-detected": "Nem észlelt semmit", + "templates-write-path": "Írja: {filepath}", + "title": "Színséma", + "wallpaper-method-description": "A kedvenc palettagenerálási módszerek kiválasztása.", + "wallpaper-method-label": "Paletta generálási módszer", + "wallpaper-monitor-source-description": "Melyik monitor legyen használva a háttérkép színeinek kinyeréséhez.", + "wallpaper-monitor-source-label": "Színgenerálási forrás" + }, + "connections": { + "authentication-required": "Hitelesítés szükséges", + "bluetooth-auto-connect-description": "Automatikus csatlakozás megbízható párosított eszközökhöz, ha a Bluetooth engedélyezve van.", + "bluetooth-auto-connect-label": "Párosított eszközök automatikus csatlakoztatása", + "bluetooth-devices-unnamed": "A névtelen eszközök nem jelennek meg.", + "bluetooth-discoverable": "Ez az eszköz {hostName} néven fedezhető fel, amíg ez a beállítási fül nyitva van.", + "bluetooth-rssi-polling-description": "Rendszeresen mintavételezi a csatlakoztatott eszközök RSSI-jét bluetoothctl-en keresztül. Lehet, hogy nem minden eszközhöz érhető el; engedélyezve minimális erőforrásokat használ.", + "bluetooth-rssi-polling-interval-description": "Milyen gyakran frissüljön a jelerősség a csatlakoztatott eszközök számára.", + "bluetooth-rssi-polling-interval-label": "Lekérdezési intervallum", + "bluetooth-rssi-polling-label": "Bluetooth jelszint lekérdezés", + "disable-discoverability-description": "Eszköz elrejtése a közeli Bluetooth eszközök elől.", + "disable-discoverability-label": "Eszköz láthatóságának letiltása", + "hide-unnamed-devices-description": "Elrejti azokat az eszközöket, amelyek csak Bluetooth-címként jelennek meg.", + "hide-unnamed-devices-label": "Névtelen eszközök elrejtése", + "pin-instructions": "Írja be az eszközén megjelenő PIN-kódot.", + "title": "Kapcsolatok", + "wifi-header-text": "Használd a Wi-Fi-t az elérhető hálózatok megtekintéséhez és az internethez való csatlakozáshoz." + }, + "control-center": { + "cards-desc": "Mely vezérlők jelenjenek meg a vezérlőközpontban és milyen sorrendben.", + "desc": "A vezérlőközpont panel pozicionálásának és viselkedésének konfigurálása.", + "open-at-mouse-description": "Ha a sáv üres területére kattintasz jobb gombbal, megnyílik a kurzornál a vezérlőközpont. Ha ez le van tiltva, akkor a fenti pozícióbeállítás kerül alkalmazásra.", + "open-at-mouse-label": "Megnyitás a kurzornál jobb kattintásra", + "position-description": "Hol jelenjen meg a vezérlőközpont panel megnyitáskor.", + "shortcuts-custom-button-command-description": "A gombra kattintáskor végrehajtandó parancs.", + "shortcuts-custom-button-enable-on-state-logic-description": "Második ikon és „aktív” állapot engedélyezése ellenőrző parancs alapján.", + "shortcuts-custom-button-enable-on-state-logic-label": "Bekapcsolt állapotú logika engedélyezése", + "shortcuts-custom-button-icon-description": "Egy ikon kiválasztása a könyvtárból.", + "shortcuts-custom-button-on-clicked-label": "Bal kattintás parancs", + "shortcuts-custom-button-on-middle-clicked-description": "Parancs, ami végrehajtódik, ha a gombot középső kattintással megnyomják.", + "shortcuts-custom-button-on-state-command-description": "A gomb „bekapcsolt” állapotának ellenőrzéséhez végrehajtandó parancs. 0-t ad vissza a bekapcsolthoz, nem nullát a kikapcsolthoz.", + "shortcuts-custom-button-on-state-command-label": "Bekapcsolt állapot ellenőrző parancs", + "shortcuts-custom-button-on-state-icon-description": "A gomb ikonja „bekapcsolt” állapotban.", + "shortcuts-custom-button-on-state-icon-label": "Bekapcsolt állapotú ikon", + "shortcuts-custom-button-state-checks-add": "Állapotellenőrzés hozzáadása", + "shortcuts-custom-button-state-checks-command": "Az állapotellenőrzéshez végrehajtandó parancs", + "shortcuts-custom-button-state-checks-label": "Állapotellenőrzések", + "shortcuts-custom-button-state-checks-remove": "Eltávolítás", + "shortcuts-custom-button-tooltip-description": "A gomb fölé vitt egérmutatónál megjelenő eszköztipp.", + "shortcuts-custom-button-tooltip-label": "Eszköztipp", + "shortcuts-title": "Parancsikon widgetek", + "system-monitor-disk-path-description": "Melyik lemez csatolási pontot kell figyelnie a rendszerfigyelő kártyának a vezérlőközpontban.", + "system-monitor-disk-path-label": "Rendszerfigyelő lemez elérési útja", + "title": "Vezérlőközpont" + }, + "desktop-widgets": { + "clock-enabled-description": "Óra widget megjelenítése az asztalon.", + "clock-enabled-label": "Óra widget engedélyezése", + "clock-format-label": "Formátum", + "clock-height-description": "Az óra widget magassága pixelben.", + "clock-height-label": "Magasság", + "clock-minimal-mode-description": "Minimális órakijelzés idővel és dátummal.", + "clock-minimal-mode-label": "Minimális mód", + "clock-rounded-corners-description": "A widget háttere legyen lekerekített sarkú.", + "clock-rounded-corners-label": "Lekerekített sarkok", + "clock-show-background-description": "Az óra widget háttérkonténerének megjelenítése.", + "clock-show-background-label": "Háttér megjelenítése", + "clock-show-date-description": "Az aktuális dátum megjelenítése az idő alatt.", + "clock-show-date-label": "Dátum megjelenítése", + "clock-show-seconds-description": "Másodpercek megjelenítése az időben.", + "clock-show-seconds-label": "Másodpercek megjelenítése", + "clock-style-analog": "Analóg", + "clock-style-binary": "Bináris", + "clock-style-description": "Az óra kijelzési stílusa.", + "clock-style-digital": "Digitális", + "clock-style-label": "Óra stílus", + "clock-style-minimal": "Minimális", + "clock-width-description": "Az óra widget szélessége képpontokban.", + "cpu-intensive-note": "! jellel megjelölt widgetek több CPU-t használnak, és körültekintően kell őket engedélyezni.", + "edit-mode-button-label": "Szerkesztési módba lépés", + "edit-mode-controls-explanation": "Bal kattintás és húzás: Widget mozgatása vagy átméretezése. Jobb kattintás: Kontextus menü opcióinak megnyitása.", + "edit-mode-description": "Engedélyezi a szerkesztési módot az asztali widgetek mozgatásához és áthelyezéséhez. Ha engedélyezve van, a widgetek húzható körvonalat mutatnak, és áthelyezhetők.", + "edit-mode-exit-button": "Kilépés a szerkesztési módból", + "edit-mode-grid-snap-label": "Rácshoz igazítás", + "edit-mode-grid-snap-scale-label": "Rácsra illesztés skálája", + "edit-mode-label": "Szerkesztési mód", + "enabled-description": "Asztali widgetek engedlyezése vagy letiltása.", + "enabled-label": "Asztali widgetek engedélyezése", + "general-desc": "Az asztalon megjelenő widgetek konfigurálása.", + "general-title": "Asztali widgetek", + "media-player-enabled-description": "Médiaplayer widget megjelenítése az asztalon.", + "media-player-enabled-label": "Médiaplayer widget engedélyezése", + "media-player-rounded-corners-description": "Engedélyezi a lekerekített sarkokat a widget szélein.", + "media-player-show-album-art-description": "Az album borítójának és a számok adatainak (cím és előadó) megjelenítése.", + "media-player-show-album-art-label": "Album borító és cím megjelenítése", + "media-player-show-background-description": "A médialejátszó widget háttérkonténerének megjelenítése.", + "media-player-show-buttons-description": "Médiavezérlő gombok (lejátszás/szünet, előző, következő) megjelenítése a média lejátszó widgeten.", + "media-player-show-buttons-label": "Gombok megjelenítése", + "media-player-show-visualizer-description": "Audiovizuális megjelenítő fedvény megjelenítése.", + "media-player-visualizer-type-description": "Vizualizációs típus az asztali média lejátszó hátteréhez.", + "overview-enabled-description": "Asztali vezérlők megjelenítése, amíg a kompozitor áttekintője nyitva van.", + "overview-enabled-label": "Megjelenítés áttekintésben", + "system-stat-layout-bottom": "Lent", + "system-stat-layout-description": "Hogyan jelenjen meg a jelmagyarázat a grafikonhoz képest.", + "system-stat-layout-label": "Elrendezés", + "system-stat-layout-side": "Oldal", + "system-stat-rounded-corners-description": "Lekerekített sarkok használata a widget hátteréhez.", + "system-stat-rounded-corners-label": "Lekerekített Sarkok", + "system-stat-show-background-description": "A háttérkonténer megjelenítése a rendszerstatisztika widgetben.", + "system-stat-show-background-label": "Háttér megjelenítése", + "system-stat-stat-type-description": "Mely rendszerstatisztikák jelenjenek meg.", + "system-stat-stat-type-label": "Statisztika Típusa", + "title": "Asztali Widgetek", + "weather-enabled-description": "Időjárás widget megjelenítése az asztalon.", + "weather-enabled-label": "Időjárás widget engedélyezése", + "weather-show-background-description": "Az időjárás widget háttérkonténerének megjelenítése." + }, + "display": { + "monitors-backlight-device-auto-option": "Alapértelmezett", + "monitors-backlight-device-description": "Háttérvilágítási eszköz ehhez a kimenethez.", + "monitors-backlight-device-label": "Háttérvilágítási Eszköz", + "monitors-brightness-step-description": "A lépésméret a fényerő változásokhoz (görgő és gyorsbillentyűk).", + "monitors-brightness-step-label": "Fényerő lépésméret", + "monitors-brightness-unavailable-ddc-disabled": "Fényerő vezérlés nem érhető el. Engedélyezd a „Külső fényerő támogatás\" opciót a kijelző fényerejének szabályozásához.", + "monitors-brightness-unavailable-generic": "Fényerő vezérlés nem érhető el ehhez a kijelzőhöz.", + "monitors-desc": "Fényerőbeállítások minden kijelzőhöz.", + "monitors-enforce-minimum-description": "Megoldja azt a problémát, hogy egyes kijelzők háttérvilágítása teljesen kikapcsol 0%-os fényerőnél.", + "monitors-enforce-minimum-label": "Minimális fényerő (1%) kényszerítése", + "monitors-external-brightness-description": "Engedélyezi a ddcutil támogatást a külső kijelzők fényerejének szabályozásához DDC/CI protokollon keresztül.", + "monitors-external-brightness-label": "Külső fényerő támogatás", + "monitors-global-brightness-description": "Az összes monitor fényerejének egyidejű beállítása.", + "monitors-global-brightness-label": "Összes monitor", + "monitors-title": "Monitorkénti beállítások", + "night-light-auto-schedule-description": "A napnyugta és napkelte időpontja alapján itt: {location} — ajánlott.", + "night-light-auto-schedule-label": "Automatikus ütemezés", + "night-light-desc": "Csökkenti a kék fény kibocsátását, hogy jobban tudj aludni, és csökkentse a szem megerőltetését.", + "night-light-enable-description": "Meleg színszűrő alkalmazása a kék fény kibocsátás csökkentése érdekében.", + "night-light-enable-label": "Éjszakai fény engedélyezése", + "night-light-force-activation-description": "Figyelmen kívül hagyja az ütemezést, és azonnal alkalmazza az éjszakai szűrőt.", + "night-light-force-activation-label": "Kényszerített aktiválás", + "night-light-manual-schedule-description": "Egyéni időpontok a napkeltére és napnyugtára.", + "night-light-manual-schedule-label": "Kézi ütemezés", + "night-light-manual-schedule-select-start": "Kezdési idő kiválasztása", + "night-light-manual-schedule-select-stop": "Leállítási idő kiválasztása", + "night-light-manual-schedule-sunrise": "Napkelte időpontja", + "night-light-manual-schedule-sunset": "Napnyugta időpontja", + "night-light-temperature-day": "Nap", + "night-light-temperature-day-description": "Szabályozza a hőmérsékletet nappal.", + "night-light-temperature-description": "A színhőmérséklet éjszakai és nappali használatra.", + "night-light-temperature-label": "Színhőmérséklet", + "night-light-temperature-night": "Éjszaka", + "night-light-temperature-night-description": "Szabályozza a hőmérsékletet éjszaka.", + "title": "Megjelenés" + }, + "dock": { + "appearance-background-opacity-description": "A dokk hátterének átlátszósága.", + "appearance-border-radius-description": "A dokk szegélyének lekerekítése.", + "appearance-border-radius-label": "Szegély lekerekítés", + "appearance-colorize-icons-description": "A témakör színeinek alkalmazása a dokk-alkalmazás ikonjaira (csak a nem fókuszált alkalmazásokra).", + "appearance-colorize-icons-label": "Ikonok színezése", + "appearance-dead-opacity-description": "A nem futó alkalmazásikonok átlátszósága.", + "appearance-dead-opacity-label": "Inaktív átlátszóság", + "appearance-desc": "A dokk viselkedésének és megjelenésének testreszabása.", + "appearance-display-auto-hide": "Automatikus elrejtés", + "appearance-display-description": "A dokk viselkedése.", + "appearance-display-exclusive": "Exkluzív", + "appearance-dock-indicator-description": "Egy kis jelző megjelenítése, amikor a dokk rejtve van.", + "appearance-dock-indicator-label": "Dokk jelző", + "appearance-floating-distance-description": "A dokk és a képernyő széle közötti távolság.", + "appearance-floating-distance-label": "Dokk lebegési távolság", + "appearance-group-apps-description": "Ugyanazon alkalmazás több ablakának csoportosítása egy dokk bejegyzésbe.", + "appearance-group-apps-label": "Azonos alkalmazások csoportosítása", + "appearance-group-click-action-cycle": "Ablakok váltása", + "appearance-group-click-action-description": "Mit csinál a bal kattintás a csoportosított alkalmazásoknál.", + "appearance-group-click-action-label": "Csoportosított alkalmazás kattintási művelete", + "appearance-group-click-action-list": "Ablaklista megnyitása", + "appearance-group-context-menu-mode-description": "Hogyan viselkedik a helyi menü a csoportosított alkalmazásokhoz.", + "appearance-group-context-menu-mode-extended": "Kiterjesztett", + "appearance-group-context-menu-mode-label": "Csoportosított alkalmazásmenü mód", + "appearance-group-context-menu-mode-list": "Ablaklista", + "appearance-group-indicator-style-description": "Hogyan jelenítsék meg a csoportosított futó indikátorok a fókuszált ablak állapotát.", + "appearance-group-indicator-style-dots": "Pontok", + "appearance-group-indicator-style-label": "Csoportosított jelző stílus", + "appearance-group-indicator-style-number": "Szám", + "appearance-hide-show-speed-description": "A dokk elrejtési/megjelenítési animációjának sebessége.", + "appearance-hide-show-speed-label": "Elrejtési/megjelenítési sebesség", + "appearance-icon-size-description": "A dokk teljes mérete.", + "appearance-icon-size-label": "Dokk mérete", + "appearance-inactive-indicators-description": "Jelenjenek meg jelzőpöttyök minden alkalmazásnál, ne csak az aktuálisan aktívnál.", + "appearance-inactive-indicators-label": "Futásjelzők", + "appearance-indicator-color-description": "A rejtett dokk jelzőjének színe.", + "appearance-indicator-color-label": "Jelző színe", + "appearance-indicator-opacity-description": "A rejtett dokk jelzőjének átlátszósága.", + "appearance-indicator-opacity-label": "Jelző átlátszósága", + "appearance-indicator-thickness-description": "Vastagabb jelző használata a rejtett dokkhoz (3 px helyett 6 px).", + "appearance-indicator-thickness-label": "Vastagabb jelző", + "appearance-launcher-icon-description": "A dokkindítóhoz használt ikon.", + "appearance-launcher-icon-label": "Indító ikon", + "appearance-launcher-position-description": "Hol jelenjen meg az indító ikonja a dokkban.", + "appearance-launcher-position-end": "Vége", + "appearance-launcher-position-label": "Indító pozíciója", + "appearance-launcher-position-start": "Indítás", + "appearance-launcher-use-distro-logo-description": "A disztribúció logójának használata a dokkindító ikonja helyett.", + "appearance-launcher-use-distro-logo-label": "Disztribúció logójának használata ikon helyett", + "appearance-pinned-static-description": "A rögzített alkalmazásikonok mindig statikus sorrendben kerüljenek balra.", + "appearance-pinned-static-label": "Statikus rögzített alkalmazások", + "appearance-position-description": "Hol jelenjen meg a dokk a képernyőn.", + "appearance-position-label": "Pozíció", + "appearance-show-launcher-icon-description": "Az alkalmazásindító ikon megjelenítése a dokkban.", + "appearance-show-launcher-icon-label": "Alkalmazásindító megjelenítése", + "appearance-sit-on-frame-description": "A dokk igazítása a keret szegélyén belülre, ahelyett, hogy a tetején ülne.", + "appearance-sit-on-frame-label": "A dokk a kereten ül", + "appearance-type-attached": "Rögzített", + "appearance-type-description": "Lebegő, kapszula alakú sáv vagy statikus, a szélhez rögzített sáv.", + "appearance-type-floating": "Lebegő", + "appearance-type-label": "Dokk stílus", + "enabled-description": "A dokk teljes megjelenítése vagy elrejtése.", + "enabled-label": "Dokk engedélyezése", + "monitors-desc": "Dokk megjelenítése meghatározott monitorokon. Alapértelmezés szerint mindegyiken, ha nincs kiválasztva.", + "monitors-only-same-monitor-description": "Csak azok az alkalmazások jelenjenek meg, amelyek azon a monitoron futnak, ahol a dokk található.", + "monitors-only-same-monitor-label": "Csak az azonos monitoron lévő alkalmazások", + "monitors-title": "Monitor megjelenítés", + "title": "Dokk" + }, + "general": { + "copy-settings": "Beállítások másolása", + "fonts-default-description": "A felületen használt fő betűtípus.", + "fonts-default-label": "Alapértelmezett betűtípus", + "fonts-default-placeholder": "Alapértelmezett betűtípus kiválasztása…", + "fonts-default-scale-description": "Szabványos szöveg méretének növelése vagy csökkentése.", + "fonts-default-scale-label": "Alapértelmezett betűméret", + "fonts-default-search-placeholder": "Betűtípus keresése…", + "fonts-desc": "Az interfészben használt betűtípusok.", + "fonts-monospace-description": "Betűtípus számok és statisztikák megjelenítéséhez.", + "fonts-monospace-label": "Monospace betűtípus", + "fonts-monospace-placeholder": "Monospace betűtípus kiválasztása…", + "fonts-monospace-scale-description": "Monospace szöveg méretének növelése vagy csökkentése.", + "fonts-monospace-scale-label": "Monospace betűméret", + "fonts-monospace-search-placeholder": "Monospace betűtípus keresése…", + "fonts-title": "Betűtípusok", + "keybinds-conflict-description": "A billentyűkombináció már hozzá van rendelve ehhez: {action}.", + "keybinds-conflict-title": "Gyorsbillentyű ütközés", + "keybinds-description": "Globális navigációs billentyűk beállítása a panelekhez és az indítóhoz.", + "keybinds-down": "Lefelé mozgatás", + "keybinds-enter": "Megerősítés / Művelet", + "keybinds-escape": "Bezárás / Vissza", + "keybinds-left": "Balra mozgatás", + "keybinds-modifier-description": "A gyorsbillentyűknek tartalmazniuk kell egy módosító billentyűt (Ctrl vagy Alt).", + "keybinds-modifier-title": "Módosító szükséges", + "keybinds-remove": "Törlés", + "keybinds-right": "Jobbra mozgatás", + "keybinds-title": "Navigációs Gyorsbillentyűk", + "keybinds-up": "Felfelé mozgatás", + "language-desc": "Az alkalmazás előnyben részesített nyelvét.", + "language-select-auto-detect": "Automatikus", + "language-select-description": "Az alkalmazás felületén használt nyelv.", + "language-select-label": "Alkalmazás nyelve", + "launch-setup-wizard": "Beállítási varázsló", + "profile-desc": "Szerkessze felhasználói adatait és profilképét.", + "profile-picture-description": "A profilkép, amely megjelenik a felületen.", + "profile-picture-label": "Profilkép", + "profile-select-avatar": "Profilkép kiválasztása", + "profile-title": "Profil", + "profile-tooltip": "Profilkép", + "reverse-scrolling-description": "Az értelmezett görgetési irány megfordítása.", + "reverse-scrolling-label": "Görgetés megfordítása", + "screen-corners-desc": "A képernyősarkok lekerekítésének és vizuális effektjeinek testreszabása.", + "screen-corners-radius-description": "A képernyő lekerekített sarkai.", + "screen-corners-radius-label": "Képernyősarkok lekerekítési sugara", + "screen-corners-radius-reset": "Képernyősarkok lekerekítési sugarának visszaállítása", + "screen-corners-show-corners-description": "Lekerekített sarkok megjelenítése a képernyő szélén.", + "screen-corners-show-corners-label": "Képernyősarkok megjelenítése", + "screen-corners-solid-black-description": "Egyszínű fekete szín használata a sáv háttérszíne helyett.", + "screen-corners-solid-black-label": "Tömör fekete sarkok", + "screen-corners-title": "Képernyősarkok", + "settings-copied": "Beállítások a vágólapra másolva", + "smooth-scrolling-description": "Listagörgetés animálása a gördülékenyebb görgőélményért.", + "smooth-scrolling-label": "Simább görgetés", + "tab-basics": "Alapok", + "tab-keybinds": "Gyorsbillentyűk" + }, + "hooks": { + "color-generation-description": "Parancs, amely akkor fut le, amikor a színlétrehozás befejeződik (pl. háttérkép alapú téma alkalmazása után).", + "color-generation-label": "Színek generálva", + "color-generation-placeholder": "pl. notify-send \"Színek\" \"Generálva\"", + "info-command-info-description": "• A parancsok shellen keresztül futnak (sh -lc)
• A parancsok a háttérben futnak (leválasztva)
• A tesztgombok az aktuális értékekkel futnak le", + "info-parameters-description": "• Wallpaper horog: $1 = háttérkép útvonala, $2 = képernyő neve, $3 = téma (sötét/világos)
• Témaváltó horog: $1 = igaz/hamis (sötét mód állapot)
• Képernyő zárolása/feloldása horgok: $1 = zárolás/feloldás (képernyőzár állapot)
• Teljesítménymód horgok: Nincs paraméter
• Munkamenet horog: $1 = művelet (leállítás/újraindítás)", + "info-parameters-label": "Elérhető paraméterek", + "noctalia-started-description": "Parancs, ami akkor fut le, amikor a Noctalia betöltése befejeződött.", + "noctalia-started-label": "Noctalia elindult", + "noctalia-started-placeholder": "pl. notify-send 'Noctalia betöltve'", + "performance-mode-disabled-description": "Parancs, amely akkor fut le, ha a Noctalia teljesítmény mód engedélyezve van.", + "performance-mode-disabled-label": "Teljesítmény mód letiltva", + "performance-mode-disabled-placeholder": "pl. notify-send \"Teljesítmény\" \"Mód letiltva\"", + "performance-mode-enabled-description": "Parancs, amely akkor fut le, ha a Noctalia teljesítmény mód engedélyezve van.", + "performance-mode-enabled-label": "Teljesítmény mód engedélyezve", + "performance-mode-enabled-placeholder": "pl. notify-send \"Teljesítmény\" \"Mód engedélyezve\"", + "screen-lock-description": "Parancs, amely a képernyő zárolásakor fut le.", + "screen-lock-label": "Képernyő zárolva", + "screen-lock-placeholder": "pl. notify-send \"Képernyő\" \"Zárolva\"", + "screen-unlock-description": "Parancs, amely a zárképernyő feloldásakor fut le.", + "screen-unlock-label": "Képernyő feloldva", + "screen-unlock-placeholder": "pl. notify-send \"Képernyő\" \"Feloldva\"", + "session-description": "Parancs, amely a leállítás vagy újraindítás előtt végrehajtásra kerül. A művelet típusát $1-ként kapja meg (leállítás/újraindítás).", + "session-label": "Munkamenet vége", + "session-placeholder": "pl., notify-send \"Munkamenet\" \"$1\"", + "system-hooks-desc": "Parancsok, amelyek a rendszeresemények bekövetkezésekor futnak le.", + "system-hooks-enable-description": "Összes horog parancs engedélyezése vagy letiltása.", + "system-hooks-enable-label": "Horgok engedélyezése", + "system-hooks-title": "Rendszer horgok", + "theme-changed-description": "Parancs, ami akkor fut le, amikor a téma vált a sötét és világos mód között.", + "theme-changed-label": "Téma megváltozott", + "theme-changed-placeholder": "pl. notify-send \"Téma\" \"Átváltva\"", + "title": "Horgok", + "wallpaper-changed-description": "Parancs, amely a háttérkép megváltozásakor fut le.", + "wallpaper-changed-label": "Háttérkép megváltozott", + "wallpaper-changed-placeholder": "pl. notify-send \"Háttérkép\" \"Megváltozott\"" + }, + "idle": { + "custom-add": "Parancs hozzáadása", + "custom-description": "Futtass egy shell parancsot egy inaktivitási időszak után.", + "custom-entry-command": "Parancs", + "custom-entry-delete": "Törlés", + "custom-entry-edit": "Egyéni parancs szerkesztése", + "custom-entry-name": "Név", + "custom-entry-name-placeholder": "pl. Lámpák lekapcsolása", + "custom-entry-new": "Új egyéni parancs", + "custom-entry-timeout": "Inaktivitási idő", + "custom-entry-timeout-format": "{count} másodperc", + "custom-entry-timeout-format-plural": "{count} másodperc", + "custom-entry-unnamed": "Névtelen parancs", + "custom-label": "Egyedi tétlenségi parancsok", + "enable-description": "Automatikusan kikapcsolja a képernyőt, zárolja vagy felfüggeszti egy inaktivitási időszak után.", + "enable-label": "Inaktivitás kezelés engedélyezése", + "fade-duration-description": "Másodpercek a feketére halványuló animációhoz, miután minden művelet elindul. Bármilyen egérmozgás megszakítja a halványítást.", + "fade-duration-label": "Áttűnés időtartama", + "lock-description": "Inaktivitási másodpercek a zárolási képernyő aktiválása előtt.", + "lock-label": "Képernyőzár", + "resume-command-label": "Parancs folytatása", + "screen-off-description": "Inaktivitás másodpercei, miután a monitorok kikapcsolnak.", + "screen-off-label": "Képernyő kikapcsolása", + "status-description": "A kompozitor által jelentett inaktivitási idő.", + "status-label": "Tétlen idő", + "suspend-description": "Másodpercnyi inaktivitás, miután a rendszer felfüggeszti magát.", + "tab-behavior": "Viselkedés", + "tab-custom": "Egyéni", + "timeouts-description": "0 a szakasz letiltásához. Az időtúllépések szünetelnek, amíg az Ébren tartás aktív.", + "timeouts-label": "Időtúllépések", + "title": "Tétlenség", + "unavailable": "Natív tétlenségfigyelés nem érhető el ezen a kompozitoron." + }, + "indicator": { + "default-value": "Alapértelmezett: {value}", + "system-default": "Rendszer alapértelmezés" + }, + "launcher": { + "clipboard-desc": "Hozzáférés és a vágólap előzményeinek kezelése az indítóból.", + "execute-desc": "Alkalmazások indításának konfigurálása.", + "execute-title": "Végrehajtás", + "settings-annotation-tool-description": "Parancs, amely a jegyzetelés gombra kattintva fut a vágólap előzményeiben. A kép a parancsra lesz átirányítva.", + "settings-annotation-tool-label": "Annotációs eszköz", + "settings-annotation-tool-placeholder": "pl. „gradia”, „satty -f -”", + "settings-auto-paste-description": "Automatikusan beilleszti a kiválasztott vágólap elemet. Wtype szükséges hozzá.", + "settings-auto-paste-label": "Automatikus beillesztés", + "settings-clip-chips-description": "Fülmenü megjelenítése a vágólap előzmények típus szerinti szűréséhez (Képek, Linkek, Fájlok, Kód stb.).", + "settings-clip-chips-label": "Kategória chipek engedélyezése", + "settings-clip-preview-description": "A vágólap tartalmának előnézetének megjelenítése a >clip parancs használatakor.", + "settings-clip-preview-label": "Vágólap előnézet engedélyezése", + "settings-clip-smart-icons-description": "Mutasson pontos ikonokat linkekhez, fájlokhoz, színekhez és kódhoz egy általános vágólap ikon helyett.", + "settings-clip-smart-icons-label": "Intelligens ikonok engedélyezése", + "settings-clip-wrap-text-description": "A szöveg tördelése a vágólap listában a csonkítás helyett.", + "settings-clip-wrap-text-label": "Vágólapszöveg becsomagolása", + "settings-clipboard-history-description": "Hozzáférés a korábban másolt elemekhez az indítóból.", + "settings-clipboard-history-label": "Vágólapelőzmények engedélyezése", + "settings-clipboard-watch-image-description": "A wl-paste-nek átadott teljes parancssor képmódosításokhoz (újraindítás szükséges).", + "settings-clipboard-watch-image-label": "Képfigyelő Parancs", + "settings-clipboard-watch-text-description": "A wl-paste-nek átadott teljes parancssor a szövegváltozásokhoz (újraindítást szükséges).", + "settings-clipboard-watch-text-label": "Szövegfigyelő parancs", + "settings-custom-launch-prefix-description": "Előtagolja a parancsokat egyéni indítóval (pl. „runapp\" a systemd integrációhoz).", + "settings-custom-launch-prefix-enabled-description": "Egyéni előtag használata az alkalmazások indításához az alapértelmezett módszer helyett.", + "settings-custom-launch-prefix-enabled-label": "Egyéni indítási előtag engedélyezése", + "settings-custom-launch-prefix-label": "Egyéni indítási előtag", + "settings-density-description": "Az alkalmazásikonok mérete és az indító sűrűsége.", + "settings-density-label": "Sűrűség", + "settings-desc": "Az indító viselkedésének és megjelenésének testreszabása.", + "settings-enable-session-search-description": "Munkamenet-műveletek (zárolás, leállítás, újraindítás stb.) megjelenítése az indítóban való kereséskor.", + "settings-enable-session-search-label": "Munkamenet-keresés engedélyezése", + "settings-enable-settings-search-description": "Beállítások megjelenítése a találatok között az indítón belül.", + "settings-enable-settings-search-label": "Beállítások keresésének engedélyezése", + "settings-enable-windows-search-description": "Aktív ablakok keresése és fókuszálása.", + "settings-enable-windows-search-label": "Ablakkeresés engedélyezése", + "settings-grid-view-description": "Elemek megjelenítése rács elrendezésben lista helyett.", + "settings-icon-mode-description": "Natív rendszerikonok használata a Tabler ikonok helyett.", + "settings-icon-mode-label": "Natív ikonok használata", + "settings-ignore-mouse-input-description": "Az egérműveletek és a görgő letiltása az indítóban.", + "settings-ignore-mouse-input-label": "Egérbemenet figyelmen kívül hagyása", + "settings-overlay-layer-description": "Az indító megjelenítése az átfedő rétegen, a teljes képernyős ablakok felett. Ha engedélyezve van, az indító nem fog a sávhoz csatlakozni.", + "settings-overlay-layer-label": "Megjelenítés teljes képernyő felett", + "settings-position-description": "Hol jelenjen meg az indító panel.", + "settings-show-categories-description": "Kategóriafülek megjelenítése az alkalmazások szűréséhez.", + "settings-show-categories-label": "Kategóriák megjelenítése", + "settings-show-icon-background-description": "Ikonok mögötti lekerekített téglalap háttér megjelenítése.", + "settings-show-icon-background-label": "Ikon háttér megjelenítése", + "settings-sort-by-usage-description": "Ha engedélyezve van, a gyakran indított alkalmazások jelennek meg először a listán.", + "settings-sort-by-usage-label": "Rendezés gyakoriság szerint", + "settings-terminal-command-description": "Parancs a terminál indításához. Pl. „kitty -e” vagy „gnome-terminal --”.", + "settings-terminal-command-label": "Terminál parancs", + "settings-use-app2unit-description": "Alternatív indítási módszert használ az alkalmazásfolyamatok jobb kezelésére és a problémák megelőzésére.", + "settings-use-app2unit-label": "App2Unit használata alkalmazások indításához", + "settings-view-mode-description": "Az indító bejegyzések elrendezése.", + "settings-view-mode-label": "Nézet mód", + "title": "Indító" + }, + "location": { + "auto-locate-description": "Automata helymeghatározás az IP-cím alapján, rendszeres időközönként.", + "auto-locate-label": "Automatikus helymeghatározás", + "calendar-cards-desc": "A kártyák rendezésének engedélyezése/letiltása a naptár panelen.", + "calendar-cards-title": "Naptárkártyák", + "calendar-header-label": "Naptár fejléc", + "calendar-month-label": "Naptár hónap", + "date-time-12hour-format-description": "Idő megjelenítése 12 órás formátumban a zárképernyőn és a naptárban. A sávon lévő óra saját beállításokkal rendelkezik.", + "date-time-12hour-format-label": "12 órás időformátum használata", + "date-time-desc": "Hogyan jelenjen meg a dátum és az idő.", + "date-time-first-day-of-week-automatic": "Automatikus (rendszeri területi beállítás használata)", + "date-time-first-day-of-week-description": "Melyik nap kezdje a hetet a naptárban.", + "date-time-first-day-of-week-label": "A hét első napja", + "date-time-show-events-description": "Események megjelenítése a naptár panelen.", + "date-time-show-events-label": "Naptáresemények megjelenítése", + "date-time-title": "Dátum és idő", + "date-time-use-analog-description": "Analóg stílusú óra megjelenítése a naptár ablakon.", + "date-time-use-analog-label": "Analóg stílusú óra használata", + "date-time-week-numbers-description": "Az év hetének megjelenítése (pl. 38. hét) a naptárban.", + "date-time-week-numbers-label": "Hét számok megjelenítése", + "geolocate-now-button": "Helymeghatározás most", + "location-desc": "Pontos időjárás és éjszakai fény ütemezés elérése a hely beállításával.", + "location-search-description": "pl. Toronto, ON", + "location-search-label": "Hely keresése", + "location-search-placeholder": "Add meg a hely nevét", + "location-title": "A tartózkodási helye", + "weather-desc": "A kívánt hőmérsékleti egység kiválasztása.", + "weather-enabled-description": "Időjárási információk mutatása a felületen, és időjárási adatok bekérése. Ha le van tiltva, minden időjárási elem el lesz rejtve, és nem történik hálózati kérés.", + "weather-enabled-label": "Időjárás engedélyezése", + "weather-fahrenheit-description": "Hőmérséklet megjelenítése Fahrenheitben Celsius helyett.", + "weather-fahrenheit-label": "Hőmérséklet megjelenítése Fahrenheitben (°F)", + "weather-hide-city-description": "Városnevek elrejtése az időjárás kijelzőkről a teljes felületen.", + "weather-hide-city-label": "Városnév elrejtése", + "weather-hide-timezone-description": "Az időzóna rövidítésének elrejtése a felületen megjelenő összes időjárás előrejelzésből.", + "weather-hide-timezone-label": "Időzóna elrejtése", + "weather-show-effects-description": "További vizuális effektek (például eső, hó vagy villámlás) megjelenítése az időjárás kártyán.", + "weather-show-effects-label": "Időjárási effektek megjelenítése", + "weather-show-in-calendar-description": "A napi időjárás-előrejelzés megjelenítése közvetlenül a naptárnézetben.", + "weather-show-in-calendar-label": "Időjárás megjelenítése a naptárban", + "weather-talia-mascot-always-description": "Talia hangulatjelek használata a Tabler ikonok helyett.", + "weather-talia-mascot-always-label": "Mindig jelenítse meg a Talia időjárás hangulatjeleket" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "Ha az fprintd (ujjlenyomat-hitelesítés) aktív, ez az opció lehetővé teszi, hogy továbbra is jelszóval jelentkezz be ujjlenyomat helyett.", + "allow-password-with-fprintd-label": "Jelszavas bejelentkezés engedélyezése fprintd-vel", + "auto-start-auth-description": "Automatikusan elindítja az ujjlenyomat-hitelesítést anélkül, hogy billentyűt kellene megnyomni vagy gombra kellene kattintani.", + "auto-start-auth-label": "Automatikus hitelesítés indításkor", + "clock-format-description": "Az óra formátumának testre szabása dátum/idő szintaxis tokenek használatával.", + "clock-format-label": "Óraformátum", + "clock-style-analog": "Analóg", + "clock-style-custom": "Egyéni", + "clock-style-description": "Az óra vizuális stílusa a zárolási képernyőn.", + "clock-style-digital": "Digitális", + "clock-style-label": "Óra stílus", + "compact-lockscreen-description": "Csak a bejelentkezési beviteli mező és a rendszervezérlők megjelenítése, elrejtve az időjárás és média widgeteket.", + "compact-lockscreen-label": "Kompakt zárolási képernyő", + "enable-lockscreen-media-controls-description": "Interaktív médialejátszási vezérlők megjelenítése a zárolási képernyőn.", + "enable-lockscreen-media-controls-label": "Zárolási Képernyő Médiavezérlői", + "lock-on-suspend-description": "A képernyő automatikus zárolása a rendszer felfüggesztésekor.", + "lock-on-suspend-label": "Zárolás felfüggesztéskor", + "lock-screen-animations-description": "A zárolási képernyő animációinak engedélyezése vagy letiltása.", + "lock-screen-animations-label": "Zárolási Képernyő Animációk", + "lock-screen-blur-strength-description": "Homályosító effektust alkalmaz a zárolási képernyő háttérképére.", + "lock-screen-blur-strength-label": "Zárolási képernyő elmosásának erőssége", + "lock-screen-tint-strength-description": "Színezett átfedést alkalmaz a zárolási képernyő háttérképére.", + "lock-screen-tint-strength-label": "Zárolási képernyő árnyalatának erőssége", + "monitors-desc": "Zárolási képernyő megjelenítése meghatározott monitorokon. Alapértelmezetten mindegyiken megjelenik, ha nincs kiválasztva.", + "password-chars-description": "Aranyos ikonok, amelyek elrejtik a jelszavadat.", + "password-chars-label": "Véletlenszerű jelszó ikonok", + "show-hibernate-description": "A „hibernálás” lehetőség megjelenítése az energiakezelési műveletek között.", + "show-hibernate-label": "Hibernálás megjelenítése", + "show-session-buttons-description": "Engedélyezi a hozzáférést az energiabeállításokhoz a zárolási képernyőről.", + "show-session-buttons-label": "Energiakezelés", + "title": "Zárolási képernyő" + }, + "network": { + "bluetooth-description": "A Bluetooth kezelés aktiválása.", + "bluetooth-rssi-polling-description": "Időnként mintavételezi a csatlakoztatott eszközök RSSI-értékét a bluetoothctl-en keresztül. Nem biztos, hogy minden eszközhöz elérhető; engedélyezve minimális erőforrásokat használ.", + "bluetooth-rssi-polling-label": "Bluetooth jel lekérdezés", + "desc": "Wi-Fi és Bluetooth kapcsolatok kezelése.", + "wifi-description": "Vezeték nélküli hálózatok kezelése (NetworkManager szükséges)." + }, + "notifications": { + "duration-critical-urgency-description": "Meddig maradjanak láthatók a kritikus prioritású értesítések.", + "duration-critical-urgency-label": "Kritikus sürgősség", + "duration-desc": "Az értesítések mennyi ideig maradjanak láthatók a sürgősségi szintjük alapján.", + "duration-low-urgency-description": "Meddig maradjanak láthatók az alacsony prioritású értesítések.", + "duration-low-urgency-label": "Alacsony sürgősség", + "duration-normal-urgency-description": "Meddig maradjanak láthatók a normál prioritású értesítések.", + "duration-normal-urgency-label": "Normál sürgősség", + "duration-respect-expire-description": "Az értesítésben beállított lejárati időtúllépés használata.", + "duration-respect-expire-label": "Lejárati időtúllépés figyelembe vétele", + "duration-title": "Értesítés időtartama", + "history-clear-dismiss-description": "Értesítés törlése az előzményekből elvetéskor.", + "history-clear-dismiss-label": "Elvetéskor törlés", + "history-critical-urgency-description": "Kritikus prioritású értesítések mentése az előzményekbe.", + "history-critical-urgency-label": "Kritikus sürgősség mentése az előzményekbe", + "history-desc": "Mely értesítések kerüljenek mentésre az előzményekbe a sürgősségi szintjük alapján.", + "history-low-urgency-description": "Alacsony prioritású értesítések mentése az előzményekbe.", + "history-low-urgency-label": "Alacsony sürgősség mentése az előzményekbe", + "history-normal-urgency-description": "Normál prioritású értesítések mentése az előzményekbe.", + "history-normal-urgency-label": "Normál sürgősség mentése az előzményekbe", + "monitors-desc": "Értesítés megjelenítése meghatározott monitorokon. Alapértelmezetten mindegyiken, ha nincs kiválasztva.", + "rules-action-block": "Blokk", + "rules-action-block-desc": "Teljesen kihagyja.", + "rules-action-hide": "Elrejtés", + "rules-action-hide-desc": "Nincs felugró ablak, nincs hang, hozzáadja az előzményekhez.", + "rules-action-label": "Művelet", + "rules-action-mute": "Némítás", + "rules-action-mute-desc": "Nincs hang, de megjeleníti az előugró ablakot és az előzményekbe rakja.", + "rules-add": "Szabály hozzáadása", + "rules-delete": "Szabály törlése", + "rules-description": "Alkalmazásnév vagy tartalom szerinti egyezés. A szabályok ellenőrzése sorrendben történik, és az első találat lép érvénybe.", + "rules-edit": "Szabály szerkesztése", + "rules-label": "Szűrőszabályok", + "rules-pattern-label": "Minta", + "rules-pattern-placeholder": "firefox, discord, vagy /regex/", + "rules-tab": "Szabályok", + "settings-always-on-top-description": "Értesítések megjelenítése teljes képernyős ablakok és más rétegek felett.", + "settings-background-opacity-description": "Az értesítési hátterek átlátszóságának beállítása.", + "settings-density-description": "Az értesítési kártya sűrűsége.", + "settings-density-label": "Sűrűség", + "settings-desc": "Az értesítések megjelenésének és viselkedésének konfigurálása.", + "settings-do-not-disturb-description": "Letiltja az összes értesítési felugró ablakot, ha engedélyezve van.", + "settings-enabled-description": "Az értesítési daemon engedélyezése vagy letiltása a Noctalia shell újraindítását igényli.", + "settings-enabled-label": "Értesítések engedélyezése", + "settings-location-description": "Ahol az értesítések megjelennek a képernyőn.", + "settings-markdown-description": "Értesítés tartalmának megjelenítése Markdown formázással.", + "settings-markdown-label": "Markdown engedélyezése", + "sounds-desc": "Értesítési hangeffektusok és hangerő beállítása.", + "sounds-enabled-description": "Hanghatások engedélyezése a bejövő értesítésekhez.", + "sounds-enabled-label": "Értesítési hangok engedélyezése", + "sounds-excluded-apps-description": "A beállított értesítési hang lejátszásának kihagyása bizonyos alkalmazásoknál, amelyek saját beépített hangokkal rendelkeznek.", + "sounds-excluded-apps-label": "Kizárt alkalmazások", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "A kritikus prioritású értesítésekhez lejátszott hangfájl útvonala.", + "sounds-files-critical-label": "Kritikus sürgősségi hang", + "sounds-files-critical-select-title": "Kritikus sürgősségi hangfájl kiválasztása", + "sounds-files-desc": "Egyéni hangfájlok konfigurálása a különböző értesítési sürgősségi szintekhez.", + "sounds-files-low-description": "Az alacsony prioritású értesítésekhez lejátszott hangfájl útvonala.", + "sounds-files-low-label": "Alacsony sürgősségi hang", + "sounds-files-low-select-title": "Alacsony sürgősségi hangfájl kiválasztása", + "sounds-files-normal-description": "A normál prioritású értesítésekhez lejátszott hangfájl útvonala.", + "sounds-files-normal-label": "Normál sürgősségi hang", + "sounds-files-normal-select-title": "Normál sürgősségi hangfájl kiválasztása", + "sounds-files-placeholder": "Add meg a hangfájl útvonalát", + "sounds-files-select-file": "Hangfájl kiválasztása", + "sounds-files-title": "Hangfájlok", + "sounds-files-unified-description": "Az értesítésekhez lejátszott hangfájl útvonala.", + "sounds-files-unified-label": "Értesítési hang", + "sounds-files-unified-select-title": "Értesítési hangfájl kiválasztása", + "sounds-separate-description": "Különböző hangfájlok használata az alacsony, normál és kritikus prioritású értesítésekhez.", + "sounds-separate-label": "Különböző hangok használata prioritás szerint", + "sounds-title": "Hangbeállítások", + "sounds-unavailable-description": "Telepítsd a Qt6 Multimédia csomagot az értesítési hangok engedélyezéséhez.", + "sounds-unavailable-label": "Értesítési hangok nem érhetők el", + "sounds-volume-description": "Az értesítési hangok hangerejének beállítása.", + "sounds-volume-label": "Hangerő", + "toast-battery-description": "Figyelmeztetés megjelenítése, ha az akkumulátor töltöttségi szintje ez alá a százalék alá esik.", + "toast-battery-label": "Akkumulátor figyelmeztetés", + "toast-desc": "Felugró üzenet megjelenésének és viselkedésének beállítása.", + "toast-keyboard-description": "Felugró üzenet megjelenítése a billentyűzetkiosztás változásakor.", + "toast-keyboard-label": "Billentyűzetkiosztás", + "toast-media-description": "Felugró üzenet megjelenítése a média lejátszási állapotának változásakor.", + "toast-media-label": "Média" + }, + "osd": { + "always-on-top-description": "Képernyőkijelzők megjelenítése teljes képernyős ablakok és más rétegek felett.", + "always-on-top-label": "Mindig felül", + "background-opacity-description": "A képernyőkijelzők hátterének átlátszósága.", + "background-opacity-label": "Háttér átlátszatlansága", + "description": "Képernyőkijelzők, például hangerő- és fényerő-átfedések beállítása.", + "duration-auto-hide-description": "Az idő, miután a képernyőkijelző eltűnik.", + "duration-auto-hide-label": "Automatikus elrejtés ennyi idő után", + "duration-desc": "Mennyi ideig marad látható a képernyőkijelző, mielőtt automatikusan elrejti magát.", + "duration-title": "Automatikus elrejtési időtúllépés", + "enabled-description": "A hangerő és a fényerő változásainak megjelenítése valós időben.", + "enabled-label": "Képernyőkijelzés engedélyezése", + "events-desc": "Mely események indítsák el a képernyőkijelzőt.", + "general-desc": "A képernyőkijelzők láthatóságának és viselkedésének konfigurálása.", + "location-description": "A képernyőkijelzők helye.", + "monitors-desc": "Képernyőkijelző megjelenítése meghatározott monitorokon. Alapértelmezés szerint az összes monitoron, ha nincs kiválasztva egy.", + "title": "Képernyőkijelzők", + "types-brightness-description": "Képernyőkijelző megjelenítése a képernyő fényerejének változásakor.", + "types-brightness-label": "Fényerő", + "types-custom-text-description": "Képernyőkijelző megjelenítése egyéni szöveges üzenetekhez IPC-ből.", + "types-custom-text-label": "Egyéni szöveg", + "types-desc": "A képernyőkijelzőt kiváltó eseményeket.", + "types-input-volume-description": "Képernyőkijelző megjelenítése a mikrofon hangerejének változásakor.", + "types-input-volume-label": "Bemeneti hangerő", + "types-lockkey-description": "Képernyőkijelző megjelenítése a Caps Lock, Num Lock vagy Scroll Lock váltásakor.", + "types-lockkey-label": "Zárolási gombok", + "types-media-description": "Képernyőkijelző megjelenítése a média lejátszási állapotának változásakor (lejátszás, szünet, ugrás).", + "types-media-label": "Média lejátszás", + "types-title": "Képernyőkijelzőt kiváltó események", + "types-volume-description": "Képernyőkijelző megjelenítése a hangkimenet hangerejének változásakor.", + "types-volume-label": "Kimeneti hangerő" + }, + "plugins": { + "auto-update": "Bővítmények automatikus frissítése", + "auto-update-description": "Automatikusan frissítse az összes plugint a shell indításakor.", + "available-description": "Bővítmények böngészése és telepítése a beállított forrásokból.", + "available-label": "Elérhető bővítmények", + "available-no-plugins-description": "Ellenőrizze a bővítményforrásokat, vagy frissítse a listát.", + "available-no-plugins-label": "Nincsenek elérhető bővítmények", + "check-for-updates": "Frissítések keresése", + "checking-for-updates": "Frissítések keresése…", + "collision-already-installed": "Ez a bővítmény már telepítve van", + "collision-custom-version-exists": "Egyéni verzió a következő helyről: „{source}” már telepítve van", + "collision-official-version-exists": "A bővítmény hivatalos verziója már telepítve van", + "development-disable": "Fejlesztői mód letiltása ennél a bővítménynél", + "development-enable": "Fejlesztői mód engedélyezése ehhez a beépülőhöz.", + "filter-downloaded": "Letöltött", + "filter-not-downloaded": "Nincs letöltve", + "filter-tags-description": "Bővítmények szűrése kategória vagy letöltési állapot szerint.", + "filter-tags-label": "Címkék", + "hot-reload-description": "Automatikusan újratölti a bővítményeket, amikor a fájljaik megváltoznak. Hasznos a bővítményfejlesztéshez.", + "hot-reload-label": "Gyors újratöltés (fejlesztői mód)", + "hot-reloaded": "Újratöltött bővítmény: {name}", + "install-error": "Sikertelen telepítés: {error}", + "install-incompatible": "A {plugin} bővítményhez Noctalia v{version} vagy újabb verzió szükséges", + "install-success": "Sikeresen telepítve: {plugin}", + "installed-description": "Az összes helyileg telepített bővítmény kezelése és beállítása.", + "installed-label": "Telepített bővítmények", + "installed-no-plugins-description": "Bővítmények telepítése az „Elérhető” szakaszban lehetséges.", + "installed-no-plugins-label": "Nincs telepített bővítmény", + "installing": "{plugin} telepítése…", + "notify-updates": "Bővítményfrissítési értesítések", + "notify-updates-description": "Értesítési előugró ablak megjelenítése, ha új bővítményfrissítések érhetők el.", + "open-plugin-page": "Bővítmény oldal megnyitása", + "open-plugins-tab": "Bővítmények beállításainak megnyitása", + "plugin-settings-title": "{plugin} beállításai", + "refresh-refreshing": "Bővítmények listájának frissítése…", + "refresh-tooltip": "Elérhető bővítmények frissítése", + "settings-error-not-loaded": "Bővítmény nincs betöltve", + "settings-saved": "Bővítmény beállításai mentve", + "settings-tooltip": "Bővítmény beállításai", + "source-custom": "Egyéni forrás", + "sources-add-custom": "Egyéni tároló hozzáadása", + "sources-add-dialog-description": "GitHub tároló hozzáadása bővítményforrásként.", + "sources-add-dialog-error": "Sikertelen bővítményforrás hozzáadás", + "sources-add-dialog-name": "Tároló neve", + "sources-add-dialog-name-placeholder": "Egyéni bővítményeim", + "sources-add-dialog-success": "Bővítményforrás sikeresen hozzáadva", + "sources-add-dialog-title": "Bővítményforrás hozzáadása", + "sources-add-dialog-url": "Tároló URL-címe", + "sources-description": "Bővítménytárolók kezelése.", + "sources-placeholder": "Saját menő tárolóm", + "sources-remove-tooltip": "Bővítményforrás eltávolítása", + "title": "Bővítmények", + "translations-reloaded": "Fordítások újratöltve: {name}", + "uninstall-dialog-description": "Biztosan el szeretnéd távolítani a(z) {plugin} bővítményt? Ez eltávolítja az összes bővítményadatot.", + "uninstall-dialog-title": "Bővítmény eltávolítása", + "uninstall-error": "Sikertelen eltávolítás: {error}", + "uninstall-success": "{plugin} sikeresen eltávolítva", + "uninstalling": "{plugin} eltávolítása…", + "update-all": "Összes frissítése ({count})", + "update-all-success": "Minden bővítmény sikeresen frissítve", + "update-available": "Új bővítményfrissítés elérhető", + "update-available-plural": "Új bővítményfrissítések elérhetők ({count})", + "update-error": "Nem sikerült frissíteni a bővítményt: {plugin}: {error}", + "update-pending": "v{current} → v{new} (szükséges Noctalia v{required})", + "update-success": "A(z) {plugin} frissítve lett a v{version} verzióra", + "update-version": "v{current} → v{new}", + "updating": "Frissítés…" + }, + "region": { + "title": "Régió" + }, + "session-menu": { + "countdown-duration-description": "Mennyi ideig tart a visszaszámlálás az energiagazdálkodási műveletek végrehajtása előtt.", + "countdown-duration-label": "Visszaszámlálás időtartama", + "enable-countdown-description": "Visszaszámlálás megjelenítése az energiagazdálkodási műveletek végrehajtása előtt.", + "enable-countdown-label": "Visszaszámláló engedélyezése", + "entries-desc": "Mely energiagazdálkodási műveletek jelenjenek meg a munkamenet menüben, és milyen sorrendben.", + "entries-title": "Energiagazdálkodási műveletek", + "entry-settings-command-description": "Egyéni parancs a művelet végrehajtásához. Hagyd üresen az alapértelmezett rendszerparancs használatához.", + "entry-settings-command-placeholder": "pl. systemctl poweroff", + "entry-settings-default-command-lock": "Belső zárolási képernyő (nincs parancs)", + "entry-settings-default-command-logout": "Belső kijelentkezés (nincs parancs)", + "entry-settings-default-info-description": "Ha nincs megadva egyéni parancs, az alapértelmezett rendszerparancs kerül felhasználásra.", + "entry-settings-default-info-label": "Alapértelmezett parancs", + "entry-settings-keybind-description": "Rendeljen hozzá billentyűkombinációt ezen művelet elindításához, amikor a munkamenet menü nyitva van.", + "entry-settings-keybind-placeholder": "Nyomd meg a Felvétel gombot egy billentyű hozzárendeléséhez…", + "entry-settings-keybind-recording": "Felvétel… Nyomj meg bármilyen billentyűkombinációt", + "entry-settings-title": "{entry} konfigurálása", + "entry-settings-tooltip": "Parancs konfigurálása", + "general-desc": "A munkamenet menüpanel viselkedésének és megjelenésének konfigurálása.", + "large-buttons-layout-description": "Hogyan jelenjenek meg a munkamenü menügombjai.", + "large-buttons-layout-label": "Nagy gombok elrendezése", + "large-buttons-style-description": "A munkamenet menü megjelenítése nagy gombokkal, rácsos elrendezésben.", + "large-buttons-style-label": "Nagyméretű gombok stílusa", + "position-description": "Hol jelenjen meg a munkamenet menüpanel megnyitáskor.", + "show-header-description": "A cím és a bezárás gomb megjelenítése a munkamenet menü tetején.", + "show-header-label": "Fejléc megjelenítése", + "show-keybinds-description": "Gyorsbillentyű tippek megjelenítése a munkamenet-opcióknál.", + "show-keybinds-label": "Gyorsbillentyűk megjelenítése" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Az asztali widgetek láthatóak maradnak, amíg a Noctalia teljesítmény mód engedélyezve van.", + "noctalia-performance-disable-desktop-widgets-label": "Engedélyezi az asztali widgeteket teljesítmény módban", + "noctalia-performance-disable-wallpaper-description": "Az asztali, áttekintő és zárolási képernyő háttérképek láthatóak maradnak, amíg a Noctalia teljesítmény mód engedélyezve van.", + "noctalia-performance-disable-wallpaper-label": "Engedélyezi a háttérkép renderelését teljesítmény módban", + "title": "Rendszer" + }, + "system-monitor": { + "critical-color-label": "Kritikus szín", + "custom-highlight-colors-title-label": "Egyéni kiemelőszínek", + "disk-available-label": "Rendelkezésre álló lemezterület", + "disk-section-label": "Lemezhasználat", + "enable-dgpu-monitoring-description": "Figyelem: Ez felébreszti a dedikált GPU-t (NVIDIA/AMD), ami jelentősen befolyásolhatja az akkumulátor élettartamát a hibrid grafikával rendelkező laptopokon.", + "enable-dgpu-monitoring-label": "Dedikált GPU figyelés engedélyezése", + "external-monitor-description": "Parancs vagy alkalmazás elérési útja a külső rendszermonitor alkalmazás indításához.", + "external-monitor-label": "Külső rendszermonitor parancs", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "A rendszerfigyelő viselkedésének és megjelenésének beállítása.", + "gpu-section-label": "GPU-hőmérséklet", + "highlight-colors-section-label": "Kiemelőszínek", + "polling-interval-label": "Lekérdezési intervallum", + "polling-section-description": "Milyen gyakran frissüljenek az egyes rendszer metrikák.", + "polling-section-label": "Lekérdezési időközök", + "threshold-critical": "Kritikus", + "threshold-warning": "Figyelmeztetés", + "thresholds-section-description": "A figyelmeztetési/kritikus küszöbértékek és a lekérdezési intervallumok minden rendszermutatóhoz.", + "use-custom-highlight-colors-description": "Ha le van tiltva, a téma alapértelmezett kiemelőszínei kerülnek használatra.", + "use-custom-highlight-colors-label": "Egyéni kiemelőszínek használata", + "warning-color-label": "Figyelmeztetési szín" + }, + "user-interface": { + "allow-panels-without-bar-description": "Ha engedélyezve van, a panelek bármely képernyőn megnyithatók. Ha le van tiltva, a panelek csak olyan képernyőkön nyílnak meg, ahol van sáv, ami csökkentheti a memóriahasználatot.", + "allow-panels-without-bar-label": "Panelek engedélyezése sáv nélküli képernyőkön", + "animation-disable-description": "Az összes animáció letiltása a gyorsabb, reszponzívabb élmény érdekében.", + "animation-disable-label": "A felhasználói felület animációinak letiltása", + "animation-speed-description": "A globális animációs sebesség.", + "animation-speed-label": "Animációs sebesség", + "animation-speed-reset": "Animációs sebesség visszaállítása", + "appearance-desc": "A vizuális elemek (például az eszköztippek), a szegélyek és az árnyékok testreszabása.", + "blur-behind-description": "Elhomályosítja a sávok és panelek mögötti területet a kompozitor elmosási protokolljának használatával.", + "blur-behind-label": "Háttér elmosása", + "box-border-description": "Tartalomterületek köré vázlatot jelenít meg.", + "box-border-label": "Konténer körvonal", + "box-border-radius-description": "A fő elrendezési szakaszok (például oldalsávok, kártyák és tartalompanelek) saroklekerekítése.", + "box-border-radius-label": "Tároló sugár", + "control-border-radius-description": "Az interaktív elemek (gombok, kapcsolók és szövegmezők) görbülete.", + "control-border-radius-label": "Bemeneti sugár", + "desc": "Az interfész megjelenésének, érzetének és viselkedésének testreszabása.", + "dim-desktop-description": "Asztal elsötétítése, ha a panelek vagy menük nyitva vannak.", + "dim-desktop-label": "Asztal elsötétítése", + "dimmer-opacity-description": "Az asztal elsötétítésének átlátszósági szintje.", + "dimmer-opacity-label": "Elsötétített asztal átlátszósága", + "panel-background-opacity-description": "Az összes panel (sáv, indító, beállítások stb.) háttérének átlátszósága.", + "panel-background-opacity-label": "Panel háttér átlátszóság", + "panels-attached-to-bar-description": "A panelek a sávhoz és a képernyő széleihez igazodnak, zökkenőmentes megjelenést biztosítva stílusos fordított sarkokkal.", + "panels-attached-to-bar-label": "Panelek szélekhez illesztése", + "panels-overlay-description": "Biztosítja, hogy a panelek és a sáv láthatóak maradjanak, még teljes képernyős alkalmazások felett is.", + "panels-overlay-label": "Panelek és sáv felül tartása", + "scaling-description": "Megváltoztatja az általános felhasználói felület méretét, kivéve a sávot.", + "scaling-label": "Felület méretezés", + "scrollbar-always-visible-description": "Görgetősávok láthatóan tartása, amikor a tartalom görgethető, ahelyett, hogy csak rámutatáskor jelennének meg.", + "scrollbar-always-visible-label": "Mindig jelenítse meg a görgetősávokat", + "settings-panel-header": "Beállítások panel", + "settings-panel-mode-description": "A beállítások elrendezése (újraindítást igényelhet).", + "settings-panel-mode-label": "Beállítási panel mód", + "settings-panel-sidebar-card-style-description": "Lekerekített sarkokkal burkolja a beállítások oldalsávját egy kitöltött háttérbe.", + "settings-panel-sidebar-card-style-label": "Oldalsáv kártyastílus", + "shadows-description": "Engedélyezi az árnyékokat a sávok és panelek alatt.", + "shadows-direction-description": "Hová vetődjön az árnyék.", + "shadows-direction-label": "Árnyék iránya", + "shadows-label": "Árnyékok", + "title": "Felhasználói felület", + "tooltips-description": "Engedélyezi vagy letiltja az eszköztippeket a felületen.", + "tooltips-label": "Eszköztippek megjelenítése", + "translucent-widgets-description": "Félig átlátszóvá teszi a gombokat, füleket és egyéb widgeteket a paneleken belül.", + "translucent-widgets-label": "Áttetsző widgetek" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Ábécérend", + "automation-change-mode-description": "Hogyan választódnak ki a háttérképek automatikus váltáskor.", + "automation-change-mode-label": "Váltási mód", + "automation-custom-interval-description": "Add meg az időt ÓÓ:PP formátumban (pl. 01:30).", + "automation-custom-interval-label": "Egyéni intervallum", + "automation-interval-description": "Milyen gyakran változzon automatikusan a háttérkép.", + "automation-interval-label": "Háttérkép intervallum", + "automation-random-wallpaper-description": "Ütemezze a véletlenszerű háttérképváltásokat rendszeres időközönként.", + "automation-scheduled-change-description": "Automatikusan változtassa a háttérképeket rendszeres időközönként.", + "automation-scheduled-change-label": "Ütemezett váltás", + "look-feel-edge-smoothness-description": "Lágy, tollas hatást alkalmaz az átmenetek szélén.", + "look-feel-edge-smoothness-label": "Átmeneti él lágyítása", + "look-feel-fill-color-description": "Kitöltési szín, amely a háttérkép mögött megjelenhet.", + "look-feel-fill-mode-description": "Hogyan méretezze át a kép magát a monitor felbontásához.", + "look-feel-fill-mode-label": "Kitöltési mód", + "look-feel-skip-startup-transition-description": "Hagyja ki a háttérkép animációt a shell indításakor.", + "look-feel-skip-startup-transition-label": "Indítási átmenet kihagyása", + "look-feel-title": "Megjelenés és érzet", + "look-feel-transition-duration-description": "Az átmeneti animációk időtartama másodpercben.", + "look-feel-transition-duration-label": "Átmenet időtartama", + "look-feel-transition-type-description": "Animáció típusa háttérképek közötti váltáskor.", + "look-feel-transition-type-label": "Átmenet típusa", + "settings-clear-cache-button": "Gyorsítótár törlése", + "settings-clear-cache-description": "Gyorsítótárazott, átméretezett háttérképek törlése a lemezterület felszabadításáért.", + "settings-clear-cache-label": "Háttérkép gyorsítótár", + "settings-clear-cache-toast": "Háttérkép gyorsítótár törölve", + "settings-desc": "Hogyan legyenek kezelve és megjelenítve a háttérképek.", + "settings-enable-management-description": "Háttérképek kezelése a Noctalia segítségével. Vedd ki a pipát, ha inkább más alkalmazást használnál.", + "settings-enable-management-label": "Háttérkép-kezelés engedélyezése", + "settings-enable-overview-description": "Elmosódott és elsötétített háttérképet alkalmaz az áttekintő képernyőn.", + "settings-enable-overview-label": "Áttekintő háttérkép engedélyezése", + "settings-folder-description": "A fő háttérképek mappa elérési útja.", + "settings-folder-label": "Háttérkép mappa", + "settings-hide-wallpaper-filenames-tooltip-hide": "Fájlnevek elrejtése", + "settings-hide-wallpaper-filenames-tooltip-show": "Fájlnevek megjelenítése", + "settings-monitor-specific-description": "Külön háttérkép mappa beállítása minden monitorhoz.", + "settings-monitor-specific-label": "Monitor-specifikus könyvtárak", + "settings-monitor-specific-tooltip": "Monitor háttérkép mappa", + "settings-overview-blur-strength-description": "Az elmosás erőssége az áttekintésben.", + "settings-overview-blur-strength-label": "Áttekintés elmosásának erőssége", + "settings-overview-tint-description": "Az árnyalat erőssége az áttekintésben.", + "settings-overview-tint-label": "Áttekintés árnyalatának erőssége", + "settings-recursive-search-description": "Háttérképek keresése a háttérkép könyvtár alkönyvtáraiban is.", + "settings-recursive-search-label": "Alkönyvtárak keresése", + "settings-select-monitor-folder": "Monitor háttérkép mappa kiválasztása", + "settings-selector-description": "Háttérkép kiválasztása.", + "settings-selector-position-description": "Hol jelenjen meg a háttérkép választó panel.", + "settings-show-hidden-files-tooltip-hide": "Rejtett fájlok elrejtése", + "settings-show-hidden-files-tooltip-show": "Rejtett fájlok megjelenítése", + "settings-title": "Háttérkép beállítások", + "settings-use-original-images-description": "Háttérképek átméretezésének kihagyása megjelenítés előtt. Helyet takarít meg a lemezen és csökkenti a CPU-használatot a háttérképváltások során, de nagyon nagy képek esetén több memóriát fogyaszthat.", + "settings-use-original-images-label": "Eredeti képek használata", + "settings-view-mode-description": "Hogyan jelenjenek meg a háttérképek a könyvtárból.", + "settings-view-mode-label": "Megtekintési mód", + "view-mode-browse": "Könyvtárak böngészése", + "view-mode-cycle-tooltip": "Nézet mód: {mode} (kattints a módosításhoz)", + "view-mode-recursive": "Egybesimított alkönyvtárak", + "view-mode-single": "Gyökérkönyvtár" + } + }, + "placeholders": { + "add-new-keybind": "Új gyorsbillentyű hozzáadása", + "command-example": "echo \"Helló, világ!\"", + "enter-command": "Végrehajtandó parancs (alkalmazás vagy egyéni parancsfájl)", + "enter-ipc-identifier": "Az IPC parancsok egyedi azonosítója", + "enter-path": "Útvonal megadása…", + "enter-text-to-collapse": "pl. „semmi sem játszik”. /regex/ a mintákhoz.", + "enter-tooltip": "Add meg a buboréksúgót", + "enter-width-pixels": "Add meg a szélességet képpontban", + "keybind-recording": "Gyorsbillentyű rögzítése…", + "search": "Keresés…", + "search-icons": "pl. noctalia, niri, battery, cloud", + "search-launcher": "Keress bejegyzéseket vagy írd be a „>” jelet a parancsokhoz", + "search-wallhaven": "Wallhaven keresése…", + "search-wallpapers": "Írja be a háttérképek szűréséhez…" + }, + "positions": { + "bottom": "Alul", + "bottom-center": "Lent középen", + "bottom-left": "Bal alsó", + "bottom-right": "Jobb alsó", + "center": "Közép", + "center-left": "Bal középen", + "center-right": "Jobb középen", + "close-to-bar": "Közel a sáv gombjához", + "follow-bar": "Követi a sávot", + "left": "Bal", + "right": "Jobb", + "top": "Felül", + "top-center": "Fent középen", + "top-left": "Bal felső", + "top-right": "Jobb felső" + }, + "session-menu": { + "action-in-seconds": "{action} {seconds} másodperc múlva…", + "cancel-timer": "Időzítő megszakítása", + "title": "Munkamenet menü" + }, + "setup": { + "all-done": "Kész!", + "appearance": { + "subheader": "A sötét mód és a színforrások állítása (háttérkép vagy előre meghatározott)." + }, + "customize": { + "header": "Tapasztalat testreszabása", + "subheader": "A sáv pozíciója, sűrűsége, méretezése és egyebek." + }, + "skip-setup": "Beállítás kihagyása", + "telemetry-wizard-done": "Értettem!", + "telemetry-wizard-note": "Te irányítasz — bármikor engedélyezheted vagy letilthatod a beállításokban", + "telemetry-wizard-subtitle": "Névtelen analitikát adtunk hozzá, hogy segítsük a Noctalia fejlesztését", + "telemetry-wizard-title": "Adatvédelmi frissítés", + "wallpaper": { + "choose-dir": "A könyvtár, amely tartalmazza a háttérképeket", + "dir-browse": "Háttérkép mappa böngészése", + "dir-description": "A háttérképeket tartalmazó mappa.", + "dir-label": "Háttérkép könyvtár", + "dir-select-title": "Háttérkép mappa kiválasztása", + "header": "Háttérkép kiválasztása", + "no-dir": "Nincs kiválasztott háttérkép könyvtár", + "no-valid": "Nincsenek érvényes képfájlok a következőben: {dir}", + "none-in-dir": "Nincs háttérkép a könyvtárban", + "preview-error": "A kép betöltése sikertelen", + "select-prompt": "Válassz egy háttérképet alább", + "subheader": "Teremts hangulatot egy gyönyörű háttérrel." + }, + "welcome": "Üdvözlünk", + "welcome-note": "Csak néhány alapvető dolog az induláshoz — a teljes beállítási lehetőség a beállításokban található", + "welcome-subtitle": "Tegyük egyedivé az asztalod", + "welcome-title": "Üdvözöl a Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "Nincs médialejátszó észlelve", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "Ismeretlen alkalmazás", + "uptime": "Működési idő: {uptime}", + "user-requested": "Felhasználó kérte", + "welcome-back": "Üdvözlünk újra,", + "widget-settings-title": "{widget} beállítások" + }, + "system-monitor": { + "core-usage": "Mag {id} használat", + "cpu-temp": "CPU-hőmérséklet", + "cpu-usage": "CPU-használat", + "disk": "Lemez", + "download-speed": "Letöltési sebesség", + "gpu-temp": "GPU-hőmérséklet", + "load-average": "Átlagos terhelés", + "title": "Rendszerfigyelő", + "upload-speed": "Feltöltési sebesség" + }, + "toast": { + "airplane-mode": { + "description": "Az összes vezeték nélküli kommunikáció letiltása.", + "title": "Repülőgép üzemmód" + }, + "battery": { + "critical": "Kritikus akkumulátor", + "critical-desc": "Az akkumulátor {percent}%-on van — azonnal csatlakoztasd a töltőt", + "low": "Alacsony akkumulátor", + "low-desc": "Az akkumulátor {percent}%-on van — csatlakoztasd a töltőt" + }, + "bluetooth": { + "address-copied": "Cím a vágólapra másolva", + "auto-connect-disabled": "Automatikus csatlakozás letiltva", + "auto-connect-enabled": "Automatikus csatlakozás engedélyezve", + "auto-connecting": "Csatlakozás {count} eszközhöz…", + "confirm-code": "Erősítsd meg a {value} kódot a másik eszközön.", + "connect-failed": "Nem sikerült csatlakozni az eszközhöz", + "disconnect-failed": "Nem sikerült leválasztani az eszközről", + "discoverable-change-failed": "Nem sikerült megváltoztatni a felfedezhetőság állapotát", + "discoverable-disabled": "Felfedezhetőség letiltva", + "discoverable-enabled": "Felfedezhetőség engedélyezve", + "display-code": "Add meg a {value} kódot a másik eszközön.", + "forget-failed": "Nem sikerült elfelejteni az eszközt", + "pair-failed": "Nem sikerült párosítani az eszközt", + "passkey-required": "Az eszköz által megkövetelt jelszó.", + "pincode-required": "Az eszköz által megkövetelt PIN.", + "state-change-failed": "Nem sikerült megváltoztatni a Bluetooth állapotát" + }, + "clipboard": { + "long-text": "Hosszú szöveg", + "unavailable": "A vágólapelőzmények nem elérhetők", + "unavailable-desc": "A „cliphist” alkalmazás nincs telepítve. Telepítsd a vágólapelőzmények funkcióinak használatához" + }, + "custom-command-failed": { + "description": "A parancs sikertelen: {command}\\nKilépési kód: {code}", + "title": "Egyéni parancs sikertelen" + }, + "do-not-disturb": { + "disabled": "Ne zavarjanak letiltva", + "disabled-desc": "Minden értesítés megjelenítése", + "enabled": "Ne zavarjanak engedélyezve", + "enabled-desc": "Ezeket az értesítéseket az előzményekben találja" + }, + "donation-opened": "Az adományozási oldal megnyílt a böngésződben", + "internet-limited": "Csatlakoztatva internet nélkül", + "keyboard-layout": { + "changed": "Billentyűzetkiosztás megváltozott: {layout}", + "title": "Billentyűzet" + }, + "missing-control-center": { + "description": "A vezérlőközpont widget eltávolításra került a sávról. A sávról való újbóli eléréshez újra hozzá kell adnia a widgetet. Jobb kattintással is megnyithatja a sávon", + "label": "Utolsó vezérlőközpont widget eltávolítva" + }, + "night-light": { + "forced": "Kényszerített aktiválás", + "normal": "Normál mód", + "not-installed": "wlsunset nincs telepítve" + }, + "noctalia-performance": { + "disabled": "Teljesítmény mód letiltva", + "enabled": "Teljesítmény mód engedélyezve", + "label": "Noctalia Teljesítmény" + }, + "power-profile": { + "changed": "Energia profil megváltozott", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "Hiba történt a sablonok feldolgozása során", + "title": "A sablonfeldolgozás sikertelen" + }, + "vpn": { + "connected": "Csatlakozva a(z) „{name}” hálózathoz", + "disconnected": "Kapcsolat megszakadt a(z) „{name}” hálózattal" + }, + "wallpaper-colors": { + "disabled": "Háttérképszínek letiltva", + "enabled": "Háttérkép színek engedélyezve", + "label": "Háttérkép színek generálása" + }, + "wifi": { + "connected": "Csatlakozva a(z) „{ssid}” hálózathoz", + "connection-failed": "Csatlakozás sikertelen", + "connection-timeout": "Csatlakozási időtúllépés", + "disconnected": "Kapcsolat megszakadt a(z) „{ssid}” hálózattal", + "incorrect-password": "Helytelen jelszó.", + "network-not-found": "Hálózat nem található" + } + }, + "tooltips": { + "add-widget": "Widget hozzáadása", + "auto-connect": "Automatikus csatlakozás váltása ehhez az eszközhöz", + "bluetooth-auto-connect-off": "Automatikus csatlakozás kikapcsolva", + "bluetooth-auto-connect-on": "Automatikus csatlakozás engedélyezve", + "bluetooth-devices": "Bluetooth eszközök", + "brightness-at": "Fényerő: {brightness}%", + "click-to-start-recording": "Képernyőfelvevő (felvétel indítása)", + "click-to-stop-recording": "Képernyőfelvevő (felvétel leállítása)", + "collapse": "Oldalsáv összecsukása", + "copy-address": "Cím másolása", + "delete-notification": "Értesítés törlése", + "dismiss-notification": "Értesítés elvetése", + "do-not-disturb-enabled": "Ne zavarjanak", + "expand": "Oldalsáv kibontása", + "forget-network": "Hálózat elfelejtése", + "grid-view": "Rács nézet", + "hidden-files-hide": "Rejtett fájlok", + "hide-unnamed-devices": "Névtelen eszközök elrejtése", + "home": "Kezdőkönyvtár", + "input-muted": "Bemenet némításának váltása", + "keep-awake": "Ébren tartás", + "keyboard-layout": "{layout} billentyűzetkiosztás", + "list-view": "Lista nézet", + "manage-vpn": "VPN-kapcsolatok", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "Elérte a maximális widgetszámot", + "microphone-volume-at": "Mikrofon hangereje: {volume}%", + "move-to-section": "Ugrás a(z) {section} szakaszhoz", + "mute": "Némítás", + "next-media": "Következő zene", + "next-month": "Következő hónap", + "night-light-not-installed": "Éjszakai fény (nem elérhető)", + "noctalia-performance-enabled": "Noctalia teljesítmény mód", + "open-annotation-tool": "Megnyitás jegyzetelő eszközzel", + "open-control-center": "Vezérlőközpont", + "open-notification-history-enable-dnd": "Értesítési előzmények", + "open-settings": "Beállítások", + "open-tray-dropdown": "Tálca", + "output-muted": "Kimenet némításának váltása", + "power-profile": "{profile} energiaprofil", + "previous-media": "Előző zene", + "previous-month": "Előző hónap", + "refresh-devices": "Eszközök frissítése", + "refresh-wallhaven": "Wallhaven eredmények frissítése", + "refresh-wallpaper-list": "Háttérkép lista frissítése", + "remove-widget": "Widget eltávolítása", + "search-close": "Keresés bezárása", + "session-menu": "Munkamenet menü", + "show-all-devices": "Összes eszköz megjelenítése", + "switch-to-dark-mode": "Sötét mód", + "switch-to-light-mode": "Világos mód", + "unmute": "Némítás feloldása", + "up": "Szülőkönyvtár", + "volume-at": "Kimenet hangereje: {volume}%", + "wallpaper-selector": "Háttérképválasztó" + }, + "wallpaper": { + "browse": { + "empty-directory": "Ez a könyvtár üres.", + "go-root": "Ugrás a háttérkép gyökerébe", + "go-up": "Ugrás a szülő mappába", + "go-up-hint": "A visszagombbal navigálhatsz felfelé." + }, + "configure-directory": "A háttérképkönyvtár képekkel való konfigurálása.", + "fill-modes": { + "crop": "Vágás (Kitöltés)", + "fit": "Illesztés (Tartalmazás)", + "repeat": "Ismétlés (Mozaik)", + "stretch": "Nyújtás" + }, + "no-match": "Nincs találat.", + "no-wallpaper": "Nincs háttérkép találat.", + "panel": { + "apikey-help": "NSFW tartalom eléréséhez API kulcs szükséges.", + "apikey-label": "API kulcs", + "apikey-managed-by-env": "A NOCTALIA_WALLHAVEN_API_KEY környezeti változóval kezelhető.", + "apikey-placeholder": "Add meg a Wallhaven API kulcsod!", + "appearance-dark-tab": "Sötét", + "appearance-light-tab": "Világos", + "categories-anime": "Anime", + "categories-label": "Kategóriák", + "categories-people": "Emberek", + "color-extraction-disabled": "Háttérkép színkivonás használata", + "color-extraction-enabled": "Előre meghatározott színsémák használata", + "header-devices-apply-all-tooltip": "Háttérkép választása monitoronként", + "header-devices-per-monitor-tooltip": "Alkalmazás minden monitorra", + "header-link-light-dark-tooltip": "Háttérkép megtartása téma váltásakor", + "header-separate-light-dark-tooltip": "Háttérkép automatikus váltása témával", + "header-sun-linked-tooltip": "Külön világos és sötét háttérképek", + "header-sun-separate-tooltip": "Ugyanaz a háttérkép világos és sötét módhoz", + "order-asc": "Növekvő", + "order-desc": "Csökkenő", + "order-label": "Sorrend", + "purity-label": "Tartalomszűrő", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Sketchy", + "ratios-all-portrait": "Összes álló", + "ratios-all-wide": "Összes széles", + "ratios-any": "Bármelyik", + "ratios-label": "Képarány", + "resolution-atleast": "Legalább", + "resolution-exact": "Pontos", + "resolution-label": "Felbontás", + "resolution-mode-label": "Mód", + "solid-color-tooltip": "Egyszínű háttér", + "sort-date-asc": "Rendezés: a legrégebbiek elöl", + "sort-date-desc": "Rendezés: a legújabbak elöl", + "sort-name-asc": "Rendezés név szerint (A-Z)", + "sort-name-desc": "Rendezés név szerint (Z-A)", + "sort-random": "Rendezés véletlenszerűen", + "sorting-date-added": "Hozzáadás dátuma", + "sorting-favorites": "Kedvencek", + "sorting-label": "Rendezés", + "sorting-relevance": "Relevancia", + "sorting-toplist": "Toplista", + "sorting-views": "Megtekintések", + "source-label": "Forrás", + "source-wallhaven": "Wallhaven", + "title": "Háttérkép-választó", + "wallhaven-settings-title": "Wallhaven beállítások" + }, + "transitions": { + "disc": "Korong", + "fade": "Halványítás", + "honeycomb": "Méhsejt", + "pixelate": "Pixelezés", + "stripes": "Csíkok", + "wipe": "Törlés" + }, + "try-different-search": "Próbáljon másik keresést.", + "wallhaven": { + "loading": "Háttérképek betöltése…", + "no-results": "Nincs háttérkép. Próbáljon másik keresést.", + "page": "{current} / {total}", + "page-prefix": "Oldal", + "page-suffix": "{total} közül" + } + }, + "weather": { + "clear-sky": "Tiszta égbolt", + "drizzle": "Szitálás", + "fog": "Köd", + "mainly-clear": "Többnyire derült", + "overcast": "Borult", + "partly-cloudy": "Részben felhős", + "rain-showers": "Esőzáporok", + "snow": "Hó", + "thunderstorm": "Zivatar" + }, + "widgets": { + "color-picker": { + "palette-description": "Válassz az előre meghatározott színek széles skálájából.", + "palette-label": "Paletta", + "palette-theme-colors": "Gyors hozzáférés a téma színpalettájához.", + "title": "Színválasztó" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm kisbetűvel", + "ampm-uppercase": "AM/PM nagybetűvel", + "common-12hour-time-minutes": "12 órás formátum percekkel", + "common-24hour-time-minutes": "24 órás formátum percekkel", + "common-24hour-time-seconds": "24 órás formátum másodpercekkel", + "common-european-date": "Európai dátumformátum", + "common-iso-date": "ISO dátumformátum", + "common-us-date": "Amerikai dátumformátum", + "common-weekday-date": "Hét napja dátummal", + "common-weekday-month-day": "Hét napja, hónap és nap", + "day-abbreviated": "Rövidített napnév", + "day-full": "Teljes napnév", + "day-leading-zero": "Nap nullával kezdve (01-31)", + "day-no-leading-zero": "Nap nem nullával kezdve (1-31)", + "hour-leading-zero": "Óra nullával kezdve (00-23) — 24 órás formátum", + "hour-no-leading-zero": "Óra nem nullával kezdve (0-23) — 24 órás formátum", + "minute-leading-zero": "Perc nullával kezdve (00-59)", + "minute-no-leading-zero": "Perc nem nullával kezdve (0-59)", + "month-abbreviated": "Rövidített hónapnév", + "month-full": "Teljes hónapnév", + "month-number-leading-zero": "Hónap számként nullával kezdve (01-12)", + "month-number-no-zero": "Hónap számként nem nullával kezdve (1-12)", + "second-leading-zero": "Másodperc nullával kezdve (00-59)", + "second-no-leading-zero": "Másodperc nem nullával kezdve (0-59)", + "timezone-abbreviation": "Időzóna rövidítése", + "year-four-digit": "Év négy számjeggyel", + "year-two-digit": "Év két számjeggyel (00-99)" + }, + "file-picker": { + "item": "elem", + "items": "elemek", + "search-placeholder": "Fájlok és mappák keresése…", + "select-current": "Aktuális kijelölése", + "select-file": "Fájl kijelölése", + "select-folder": "Mappa kiválasztása", + "selected": "Kiválasztva:", + "title": "Fájlválasztó" + }, + "file-picker-title": "Fájl kiválasztása", + "icon-picker": { + "title": "Ikonválasztó" + }, + "tooltip-placeholder": "Helyőrző" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "Anonim Identitás", + "ca-cert": "CA tanúsítvány", + "eap-method": "EAP-módszer", + "password": "Felhasználói jelszó", + "phase2-auth": "2. fázisú hitelesítés", + "username": "Identitás / Felhasználónév" + }, + "panel": { + "action-required": "Szükseges akció", + "add-hidden-network": "Rejtett hálózat hozzáadása", + "add-network": "Hálózat hozzáadása", + "available-interfaces": "Elérhető interfészek", + "available-networks": "Elérhető hálózatok", + "disabled": "A Wi‑Fi le van tiltva", + "disconnecting": "Leválasztás…", + "dns": "DNS", + "enable-message": "Kapcsold be a Wi‑Fi-t az elérhető hálózatok megtekintéséhez.", + "enter-password": "Add meg a Wi‑Fi jelszót…", + "forget": "Elfelejtés", + "forget-network": "Hálózat elfelejtése", + "forgetting": "Elfelejtés…", + "hidden-network": "Rejtett hálózat", + "interface": "Hálózati interfész", + "internet-connected": "Internet csatlakoztatva", + "internet-limited": "Nincs internet", + "internet-status": "Internet állapota", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "Ismert hálózatok", + "link-speed": "Kapcsolati sebesség", + "network-name-ssid": "Hálózat neve (SSID)", + "no-ethernet-devices": "Nem található Ethernet eszköz", + "no-networks": "Nem található Wi‑Fi-hálózat", + "saved": "Mentve", + "scan-again": "Keresés újra", + "searching": "Hálózatok keresése…", + "security-open": "Megnyitás", + "security-wep": "WEP", + "security-wpa": "WPA/WPA2/WPA3 Személyes", + "security-wpa-ent": "WPA Enterprise", + "security-wpa2-ent": "WPA2 Enterprise", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 Enterprise", + "show-password": "Jelszó megjelenítése", + "title": "Wi‑Fi" + }, + "signal": { + "excellent": "Kiválóság", + "fair": "Közepes", + "good": "Jó", + "poor": "Gyenge", + "weak": "Gyenge" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/it.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/it.json new file mode 100644 index 0000000..50cbaf8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/it.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "Cancella cronologia", + "close-app": "Chiudi {app}", + "connect-vpn": "Connetti a {name}", + "cycle-visualizer": "Cambia visualizzatore", + "disable-bluetooth": "Disattiva Bluetooth", + "disable-dnd": "Disattiva 'Non disturbare'", + "disable-wifi": "Disattiva Wi-Fi", + "disconnect-vpn": "Disconnetti {name}", + "dock-settings": "Impostazioni dock", + "enable-bluetooth": "Attiva Bluetooth", + "enable-dnd": "Attiva 'Non disturbare'", + "enable-wifi": "Attiva Wi-Fi", + "launcher-settings": "Impostazioni launcher", + "lower-to-bottom": "Porta in basso", + "open-calendar": "Apri calendario", + "open-display-settings": "Impostazioni schermo", + "open-launcher": "Apri launcher", + "open-settings": "Apri impostazioni", + "raise-to-top": "Porta in primo piano", + "random-wallpaper": "Sfondo casuale", + "run-custom-command": "Esegui comando personalizzato", + "show-active-only": "Mostra solo attive", + "show-all": "Mostra tutto", + "toggle-mute": "Attiva/disattiva muto", + "widget-settings": "Impostazioni widget" + }, + "authentication": { + "error": "Errore di autenticazione", + "failed": "Autenticazione non riuscita" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Applica i colori del tema all’icona della finestra attiva.", + "hide-mode-description": "Controlla il comportamento del widget quando non c’è alcuna finestra attiva.", + "scrolling-mode-description": "Controlla quando abilitare lo scorrimento del testo per titoli finestra lunghi.", + "show-app-icon-description": "Mostra l’icona dell’applicazione accanto al titolo della finestra.", + "show-app-icon-label": "Mostra icona app", + "show-app-text-description": "Mostra il titolo della finestra attiva.", + "show-app-text-label": "Mostra titolo finestra" + }, + "audio-visualizer": { + "color-name-description": "Seleziona il colore del visualizzatore.", + "color-name-label": "Colore riempimento", + "height-description": "Larghezza componente personalizzata.", + "hide-when-idle-description": "Se attivo, il visualizzatore viene nascosto quando nessun player è in riproduzione.", + "hide-when-idle-label": "Nascondi quando non è in riproduzione nulla", + "width-description": "Larghezza personalizzata del componente." + }, + "battery": { + "device-default": "Predefinito (dispositivo schermo)", + "device-description": "Seleziona quale dispositivo batteria visualizzare.", + "device-label": "Dispositivo batteria", + "display-mode-description": "Scegli come visualizzare la batteria nella barra.", + "display-mode-graphic": "Batteria grafica", + "display-mode-graphic-clean": "Batteria grafica (senza %)", + "display-mode-icon-always": "Icona - Mostra sempre %", + "display-mode-icon-hover": "Icona - Mostra al passaggio", + "display-mode-icon-only": "Solo icona", + "hide-if-idle-description": "Nasconde il widget quando la batteria non sta caricando o scaricando.", + "hide-if-idle-label": "Nascondi quando inattiva", + "hide-if-not-detected-description": "Nasconde il widget quando non viene rilevata alcuna batteria nel sistema.", + "hide-if-not-detected-label": "Nascondi se non rilevata", + "low-battery-threshold-label": "Soglia avviso batteria scarica", + "show-noctalia-performance-description": "Mostra l’interruttore della modalità prestazioni Noctalia nel pannello batteria.
Disattiva ombre e animazioni in Noctalia per ridurre l’uso di risorse.", + "show-noctalia-performance-label": "Mostra interruttore Noctalia Prestazioni", + "show-power-profile-description": "Mostra la selezione del profilo energetico nel pannello batteria.", + "show-power-profile-label": "Mostra controlli profilo energetico" + }, + "brightness": { + "apply-all-description": "Cambia la luminosità su tutti i monitor usando la rotella del mouse.", + "apply-all-label": "Applica modifiche di scorrimento a tutti i monitor" + }, + "clock": { + "clock-display-description": "Personalizza la visualizzazione dell’orologio aggiungendo token dall’elenco qui sotto. Per usare il formato 12 ore devi includere il token 'AP'.", + "clock-display-label": "Visualizzazione orologio", + "custom-font-description": "Seleziona un font personalizzato per la visualizzazione dell’orologio.", + "custom-font-label": "Font personalizzato", + "custom-font-placeholder": "Seleziona font personalizzato...", + "custom-font-search-placeholder": "Cerca font...", + "horizontal-bar-description": "Suggerimento: usa \\n per creare un’interruzione di riga.", + "horizontal-bar-label": "Barra orizzontale", + "preview": "Anteprima", + "tooltip-format-description": "Stringa di formato del tooltip mostrato al passaggio sull’orologio. Lascia vuoto per usare quello predefinito.", + "tooltip-format-label": "Formato tooltip", + "use-custom-font-description": "Sovrascrive la selezione del font predefinito con un font personalizzato per l’orologio.", + "use-custom-font-label": "Usa font personalizzato", + "use-monospaced-font-description": "Se attivo, l’orologio userà un font monospaziato.", + "use-monospaced-font-label": "Usa font monospaziato", + "vertical-bar-description": "Usa uno spazio per separare ogni parte su una nuova riga.", + "vertical-bar-label": "Barra verticale" + }, + "control-center": { + "browse-file": "Sfoglia file", + "browse-library": "Sfoglia libreria", + "color-selection-description": "Applica i colori del tema alle icone.", + "enable-colorization-description": "Abilita la colorazione per l'icona, applicando i colori del tema.", + "icon-description": "Seleziona un’icona dalla libreria o un file personalizzato.", + "select-custom-icon": "Seleziona un’icona personalizzata", + "use-distro-logo-description": "Usa il logo della tua distribuzione invece di un’icona personalizzata.", + "use-distro-logo-label": "Usa logo distro al posto dell’icona" + }, + "custom-button": { + "collapse-condition-description": "Se il testo di output corrisponde a questo valore, il pulsante verrà compresso.", + "collapse-condition-label": "Condizione di compressione", + "color-selection-description": "Applica i colori del tema a icona e testo.", + "default-tooltip": "Pulsante personalizzato, configurarlo nelle impostazioni", + "display-command-output-description": "Inserisci un comando da eseguire a intervalli regolari. La prima riga dell’output verrà mostrata come testo.", + "display-command-output-label": "Mostra output comando", + "display-command-output-stream-description": "Inserisci un comando da eseguire in modo continuo.", + "dynamic-text": "Testo dinamico", + "enable-colorization-description": "Abilita la colorazione di icona e testo del pulsante personalizzato applicando i colori del tema.", + "enable-colorization-label": "Abilita colorazione", + "general-tooltip-text-description": "Testo personalizzato da mostrare nel tooltip del pulsante.", + "general-tooltip-text-label": "Testo tooltip personalizzato", + "hide-mode-always-expanded": "Sempre espanso", + "hide-mode-description": "Controlla la visibilità del widget quando il comando non ha output.", + "hide-mode-expand-with-output": "Espandi quando c’è output", + "hide-mode-label": "Modalità nascondi", + "hide-mode-max-transparent": "Massimo espanso ma trasparente", + "icon-description": "Seleziona un’icona dalla libreria.", + "ipc-identifier-description": "Identificatore univoco per i comandi IPC. Usa questo identificatore con 'qs -c noctalia-shell ipc call cb [action] [identifier]' per controllare questo pulsante via IPC.", + "ipc-identifier-label": "Identificatore IPC", + "left-click-description": "Comando da eseguire al clic sinistro sul pulsante.", + "left-click-label": "Clic sinistro", + "left-click-update-text": "Aggiorna testo visualizzato al clic sinistro", + "max-text-length-horizontal-description": "Numero massimo di caratteri da mostrare in barra orizzontale (0 per nascondere il testo).", + "max-text-length-horizontal-label": "Lunghezza max testo (orizzontale)", + "max-text-length-vertical-description": "Numero massimo di caratteri da mostrare in barra verticale (0 per nascondere il testo).", + "max-text-length-vertical-label": "Lunghezza max testo (verticale)", + "middle-click-description": "Comando da eseguire al clic centrale sul pulsante.", + "middle-click-label": "Clic centrale", + "middle-click-update-text": "Aggiorna testo visualizzato al clic centrale", + "parse-json-description": "Interpreta l’output del comando come oggetto JSON per impostare dinamicamente testo e icona.", + "parse-json-label": "Interpreta output come JSON", + "refresh-interval-description": "Intervallo in millisecondi.", + "refresh-interval-label": "Intervallo aggiornamento", + "right-click-description": "Comando da eseguire al clic destro sul pulsante.", + "right-click-label": "Clic destro", + "right-click-update-text": "Aggiorna testo visualizzato al clic destro", + "show-exec-tooltip-description": "Mostra tooltip con i dettagli dei comandi (clic sinistro/destro/centrale, rotella).", + "show-exec-tooltip-label": "Mostra tooltip comandi", + "show-icon-description": "Attiva/disattiva la visibilità dell’icona del widget.", + "show-icon-label": "Mostra icona", + "show-text-tooltip-description": "Mostra tooltip con l’output del comando di testo.", + "show-text-tooltip-label": "Mostra tooltip testo dinamico", + "text-stream-description": "Le righe in streaming del comando verranno mostrate come testo sul pulsante.", + "text-stream-label": "Flusso", + "wheel-description": "Comando da eseguire quando viene usata la rotella del mouse.
Usa $delta nel comando per il delta della rotella.", + "wheel-down": "Scorri giù", + "wheel-down-description": "Comando da eseguire quando la rotella viene fatta scorrere verso il basso.", + "wheel-down-label": "Comando rotella giù", + "wheel-label": "Rotella mouse", + "wheel-mode-separate-description": "Abilita comandi separati per rotella su e giù.", + "wheel-mode-separate-label": "Comandi rotella separati", + "wheel-up": "Scorri su", + "wheel-up-description": "Comando da eseguire quando la rotella viene fatta scorrere verso l’alto.", + "wheel-up-label": "Comando rotella su", + "wheel-update-text": "Aggiorna testo visualizzato allo scorrimento" + }, + "keyboard-layout": { + "show-icon-description": "Mostra l’icona del layout tastiera." + }, + "lock-keys": { + "hide-when-off-description": "Nasconde l’indicatore quando il tasto non è attivo.", + "hide-when-off-label": "Nascondi quando spento", + "show-caps-lock-description": "Mostra lo stato Caps Lock.", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "Mostra lo stato Num Lock.", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "Mostra lo stato Scroll Lock.", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "Abilita un layout salvaspazio per il pannello lettore multimediale.", + "compact-mode-label": "Modalità compatta", + "hide-mode-description": "Controlla il comportamento del widget quando non è in riproduzione alcun contenuto.", + "max-width-description": "Imposta la dimensione orizzontale massima del widget. Il widget si ridurrà con contenuti più brevi.", + "no-active-player": "Nessun player attivo", + "panel-section-description": "Configura aspetto e comportamento del pannello lettore multimediale.", + "panel-section-label": "Pannello lettore multimediale", + "scrolling-mode-description": "Controlla quando abilitare lo scorrimento del testo per titoli brano lunghi.", + "scrolling-mode-label": "Modalità scorrimento", + "show-album-art-description": "Mostra la copertina dell’album del brano attualmente in riproduzione.", + "show-album-art-label": "Mostra copertina album", + "show-artist-first-description": "Mostra artista - titolo invece di titolo - artista.", + "show-artist-first-label": "Mostra prima artista", + "show-progress-ring-description": "Mostra un indicatore circolare del progresso del brano.", + "show-progress-ring-label": "Mostra anello progresso", + "show-visualizer-description": "Mostra un visualizzatore audio durante la riproduzione musicale.", + "show-visualizer-label": "Mostra visualizzatore", + "use-fixed-width-description": "Se attivo, il widget userà sempre la larghezza massima invece di adattarsi dinamicamente al contenuto.", + "use-fixed-width-label": "Usa larghezza fissa", + "visualizer-type-description": "Scegli lo stile del visualizzatore audio da mostrare.", + "visualizer-type-label": "Tipo visualizzatore" + }, + "notification-history": { + "hide-widget-when-zero-description": "Nasconde l’icona notifiche quando non ci sono notifiche.", + "hide-widget-when-zero-label": "Nascondi icona quando non ci sono notifiche", + "hide-widget-when-zero-unread-description": "Nasconde l’icona notifiche quando non ci sono notifiche non lette.", + "hide-widget-when-zero-unread-label": "Nascondi icona quando non ci sono non lette", + "show-unread-badge-description": "Mostra un badge con il numero di notifiche non lette.", + "show-unread-badge-label": "Mostra badge non lette", + "unread-badge-color-description": "Seleziona il colore del badge notifiche non lette.", + "unread-badge-color-label": "Colore badge non lette" + }, + "section-editor": { + "placeholder": "Seleziona un widget da aggiungere...", + "search-placeholder": "Cerca widget..." + }, + "spacer": { + "width-description": "Larghezza spaziatura in pixel." + }, + "system-monitor": { + "compact-mode-description": "Mostra statistiche come mini grafici a barre invece di valori testuali. Evita spostamenti del layout.", + "compact-mode-label": "Modalità compatta", + "cpu-cores-description": "Mostra l'utilizzo dei core della CPU individualmente.", + "cpu-cores-label": "Core CPU", + "cpu-frequency-description": "Mostra la frequenza corrente della CPU in GHz.", + "cpu-frequency-label": "Mostra frequenza CPU", + "cpu-temperature-description": "Mostra le letture della temperatura CPU se disponibili.", + "cpu-temperature-label": "Temperatura CPU", + "cpu-usage-description": "Mostra la percentuale di utilizzo CPU corrente.", + "cpu-usage-label": "Utilizzo CPU", + "disk-path-description": "Seleziona quale punto di mount disco monitorare.", + "disk-path-label": "Percorso disco", + "gpu-temperature-description": "Mostra le letture della temperatura GPU se disponibili.", + "load-average-description": "Mostra il carico medio del sistema.", + "load-average-label": "Carico medio", + "memory-percentage-description": "Mostra l’uso memoria in percentuale invece che in valori assoluti.", + "memory-percentage-label": "Memoria in percentuale", + "memory-usage-description": "Mostra le informazioni sull’uso RAM corrente.", + "memory-usage-label": "Uso memoria", + "network-traffic-description": "Mostra velocità di upload e download di rete.", + "network-traffic-label": "Traffico di rete", + "storage-as-percentage-description": "Mostra lo spazio disco in percentuale invece che in valori assoluti.", + "storage-as-percentage-label": "Disco in percentuale", + "storage-available-description": "Mostra quanto spazio disco è disponibile invece di quanto è usato.", + "storage-available-label": "Spazio disco disponibile", + "storage-usage-description": "Mostra informazioni sull’uso dello spazio disco.", + "storage-usage-label": "Uso spazio disco", + "swap-usage-description": "Mostra utilizzo della memoria swap.", + "swap-usage-label": "Uso swap", + "use-monospace-font-description": "Usa font monospaziato per larghezza caratteri coerente.", + "use-monospace-font-label": "Font monospaziato", + "use-padding-description": "Aggiunge spazi iniziali ai valori testuali per evitare spostamenti del layout.", + "use-padding-description-disabled-monospace-font": "Per questa funzione è necessario un font monospaziato.", + "use-padding-description-disabled-vertical": "La barra verticale non supporta il padding del testo.", + "use-padding-label": "Padding testo" + }, + "taskbar": { + "colorize-icons-description": "Applica i colori del tema alle icone della taskbar.", + "hide-mode-description": "Controlla il comportamento del widget quando non ci sono finestre corrispondenti.", + "hide-mode-label": "Modalità nascondimento", + "icon-scale-description": "Imposta il fattore di scala per le icone della taskbar.", + "icon-scale-label": "Scala icone", + "max-width-description": "Larghezza massima della taskbar come percentuale della larghezza schermo.", + "max-width-label": "Larghezza massima", + "only-active-workspaces-description": "Mostra solo app delle workspace attive.", + "only-active-workspaces-label": "Solo dalle workspace attive", + "only-same-monitor-description": "Mostra solo app del monitor in cui si trova la barra.", + "only-same-monitor-label": "Solo dallo stesso monitor", + "show-pinned-apps-description": "Mostra nella taskbar le app fissate dal dock.", + "show-pinned-apps-label": "Mostra app fissate", + "show-title-description": "Mostra i titoli finestra nella taskbar.", + "show-title-description-disabled": "La taskbar verticale non supporta la visualizzazione dei titoli.", + "show-title-label": "Mostra titolo", + "smart-width-description": "Regola automaticamente la larghezza delle voci in base al loro numero.", + "smart-width-label": "Larghezza intelligente", + "title-width-description": "Imposta la larghezza dei titoli finestra nella taskbar (in pixel).", + "title-width-label": "Larghezza titolo", + "title-width-reset-tooltip": "Reimposta larghezza titolo" + }, + "tray": { + "chevron-color-description": "Applica i colori del tema all’icona chevron del cassetto.", + "chevron-color-label": "Colore chevron", + "colorize-icons-description": "Applica i colori del tema alle icone tray.", + "colorize-icons-label": "Colora icone", + "drawer-enabled-description": "Se attivo, gli elementi tray non fissati vengono mostrati in un pannello cassetto.
Se disattivo, tutti gli elementi tray vengono mostrati in linea.", + "drawer-enabled-label": "Abilita cassetto", + "hide-passive-description": "Se attivo, gli elementi tray con stato 'Passive' verranno nascosti.", + "hide-passive-label": "Nascondi elementi passivi" + }, + "volume": { + "display-mode-description": "Scegli come vuoi che questo valore venga mostrato." + }, + "workspace": { + "character-count-description": "Numero di caratteri da mostrare nei nomi workspace (1-10).", + "character-count-label": "Numero caratteri", + "empty-color-description": "Imposta il colore di sfondo per workspace vuote.", + "empty-color-label": "Colore workspace vuota", + "enable-scrollwheel-description": "Passa tra workspace usando la rotella del mouse.", + "enable-scrollwheel-label": "Scorri per cambiare workspace", + "focused-color-description": "Imposta il colore di sfondo per la workspace focalizzata.", + "focused-color-label": "Colore workspace focalizzata", + "follow-focused-screen-description": "Mostra workspace dello schermo attualmente focalizzato invece dello schermo dove si trova la barra.", + "follow-focused-screen-label": "Segui schermo focalizzato", + "font-weight-description": "Imposta il peso visivo per il testo all'interno dello spazio di lavoro.", + "font-weight-label": "Peso del carattere", + "grouped-border-opacity-description": "Imposta il livello di opacità dei bordi contenitore workspace.", + "grouped-border-opacity-label": "Opacità bordi", + "hide-unoccupied-description": "Non mostrare workspace senza finestre.", + "hide-unoccupied-label": "Nascondi non occupate", + "label-mode-description": "Scegli come visualizzare le etichette workspace.", + "label-mode-label": "Modalità etichette", + "occupied-color-description": "Imposta il colore di sfondo per workspace occupate.", + "occupied-color-label": "Colore workspace occupata", + "pill-size-description": "Regola la dimensione delle pill workspace.", + "pill-size-label": "Dimensione pill", + "reverse-scrolling-description": "Inverti direzione del cambio workspace quando scorri.", + "reverse-scrolling-label": "Inverti scorrimento", + "show-applications-description": "Mostra icone applicazioni dentro ogni workspace.", + "show-applications-hover-description": "Mostra le icone delle applicazioni all'interno di ogni spazio di lavoro quando lo spazio di lavoro viene sorvolato.", + "show-applications-hover-label": "Mostra applicazioni al passaggio del mouse", + "show-applications-label": "Mostra applicazioni", + "show-badge-description": "Mostra badge numero workspace in modalità raggruppata.", + "show-badge-label": "Mostra badge workspace", + "show-labels-only-when-occupied-description": "Mostra etichette workspace solo quando contengono finestre.", + "show-labels-only-when-occupied-label": "Mostra etichette solo se occupate", + "unfocused-icons-opacity-description": "Imposta il livello di opacità delle icone app non focalizzate.", + "unfocused-icons-opacity-label": "Opacità icone non focalizzate" + } + }, + "battery": { + "all-batteries": "Tutte le batterie (combinate)", + "battery-health": "Stato batteria", + "battery-level": "Livello batteria", + "capacity-level": "Capacità: {level}", + "charging-rate": "Velocità di carica: {rate} W", + "discharging-rate": "Velocità di scarica: {rate} W", + "health": "Salute: {percent}%", + "inhibit-idle-description": "Mantiene il sistema attivo.", + "no-battery-detected": "Nessuna batteria rilevata", + "plugged-in": "Collegato alla corrente", + "power-profile": "Profilo energetico", + "time-left": "Tempo rimanente: {time}", + "time-until-full": "Tempo alla carica completa: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Dispositivi disponibili", + "blocked": "Bloccato", + "connected-devices": "Dispositivi connessi", + "device-address": "Indirizzo dispositivo", + "disabled": "Bluetooth disattivato", + "discoverable": "Visibile", + "enable-message": "Attiva il Bluetooth per vedere i dispositivi disponibili.", + "known-devices": "Dispositivi conosciuti", + "no-devices": "Nessun dispositivo disponibile", + "paired-devices": "Dispositivi associati", + "pairing-mode": "Assicurati che il dispositivo sia in modalità associazione.", + "scanning": "Ricerca dispositivi in corso...", + "signal-text-excellent": "Segnale: Eccellente", + "signal-text-fair": "Segnale: Discreto", + "signal-text-good": "Segnale: Buono", + "signal-text-poor": "Segnale: Scarso", + "signal-text-unknown": "Segnale: Sconosciuto", + "signal-text-very-poor": "Segnale: Molto scarso" + } + }, + "changelog": { + "error": { + "fetch-failed": "Impossibile caricare il changelog. Riprova più tardi.", + "rate-limit": "Limite di richieste GitHub superato. Riprova tra qualche minuto." + }, + "panel": { + "buttons-discord": "Unisciti al nostro Discord", + "buttons-dismiss": "OK", + "buttons-feedback": "Invia feedback", + "empty": "Le note di rilascio non sono ancora disponibili.", + "highlight-title": "Novità principali", + "released": "Rilasciato il {date}", + "subtitle-fresh": "Grazie per aver installato Noctalia! Ecco cosa include questa build.", + "subtitle-updated": "Aggiornato da {previousVersion}", + "title": "Novità della versione {version}", + "version": "Versione {version}", + "version-new-user": "Nuova installazione" + } + }, + "common": { + "actions": "Azioni", + "active": "Attivo", + "add": "Aggiungi", + "appearance": "Aspetto", + "apply": "Applica", + "auto-connect": "Connessione automatica", + "automation": "Automazione", + "available": "Disponibile", + "back": "Indietro", + "battery": "Batteria", + "behavior": "Comportamento", + "bluetooth": "Bluetooth", + "brightness": "Luminosità", + "browse": "Sfoglia", + "calendar": "Calendario", + "calendar-panel": "Pannello calendario", + "cancel": "Annulla", + "cards": "Schede", + "charging": "In carica", + "clear": "Cancella", + "clipboard": "Appunti", + "close": "Chiudi", + "color-muted": "Smorzato", + "colors": "Colori", + "command": "Comando", + "confirm": "Conferma", + "connect": "Connetti", + "connected": "Connesso", + "connecting": "Connessione...", + "continue": "Continua", + "contributors": "Contributori", + "copied-to-clipboard": "Copiato negli appunti", + "countdown": "Conto alla rovescia", + "customize": "Personalizza", + "date": "Data", + "default": "Predefinito", + "delete": "Elimina", + "devices": "Dispositivi", + "disabled": "Disattivato", + "discharging": "In scarica", + "disconnect": "Disconnetti", + "disconnected": "Disconnesso", + "disconnecting": "Disconnessione...", + "display-mode": "Modalità di visualizzazione", + "documentation": "Documentazione", + "download": "Scarica", + "duration": "Durata", + "dysfunctional": "Non funzionante", + "edit": "Modifica", + "enabled": "Attivato", + "error": "Errore", + "ethernet": "Ethernet", + "events": "Eventi", + "execute": "Esegui", + "faithful": "Fedele", + "focus": "Messa a fuoco", + "font-weight-bold": "Grassetto", + "font-weight-light": "Sottile", + "font-weight-medium": "Medio", + "font-weight-regular": "Normale", + "font-weight-semibold": "Semibold", + "frequency": "Frequenza", + "gateway": "Gateway", + "general": "Generale", + "height": "Altezza", + "hibernate": "Ibernazione", + "history": "Cronologia", + "icon": "Icona", + "idle": "Inattivo", + "info": "Info", + "input": "Ingresso", + "install": "Installa", + "installed": "Installato", + "interface": "Interfaccia", + "internet": "Internet", + "keybind": "Scorciatoia", + "language": "Lingua", + "loading": "Caricamento...", + "local": "Locale", + "location": "Posizione", + "lock": "Blocca", + "logout": "Disconnetti sessione", + "look": "Aspetto", + "margins": "Margini", + "media": "Media", + "media-player": "Lettore multimediale", + "memory": "Memoria", + "monitors": "Monitor", + "move-down": "Sposta in basso", + "move-up": "Sposta su", + "network": "Rete", + "next": "Successivo", + "night-light": "Luce notturna", + "no": "No", + "no-results": "Nessun risultato", + "no-summary": "Nessun riepilogo", + "none": "Nessuno", + "not-found": "Non trovato", + "notifications": "Notifiche", + "official": "Ufficiale", + "on-surface": "Su superficie", + "output": "Uscita", + "pair": "Associa", + "paired": "Associato", + "pairing": "Associazione...", + "panels": "Pannelli", + "password": "Password", + "pause": "Pausa", + "performance": "Prestazioni", + "pin": "Fissa", + "play": "Riproduci", + "polling": "Interrogazione", + "position": "Posizione", + "previous": "Precedente", + "primary": "Primario", + "random": "Casuale", + "reboot": "Riavvia", + "reboot-to-uefi": "Riavvia in UEFI", + "record": "Avvia registrazione", + "refresh": "Aggiorna", + "required": "(obbligatorio)", + "reset": "Reimposta", + "reset-to-default": "Ripristina predefiniti", + "result-count": "{count} risultato", + "result-count-plural": "{count} risultati", + "resume": "Riprendi", + "save": "Salva", + "scanning": "Scansione...", + "screen-corners": "Angoli schermo", + "search": "Cerca", + "second": "{count} secondo", + "second-plural": "{count} secondi", + "secondary": "Secondario", + "security": "Sicurezza", + "select": "Seleziona", + "select-color": "Seleziona colore", + "select-color-description": "Applica i colori del tema per evidenziare.", + "select-icon-color": "Seleziona colore icona", + "settings": "Impostazioni", + "shortcuts": "Scorciatoie", + "shutdown": "Spegni", + "signal": "Segnale", + "sound": "Suono", + "sources": "Sorgenti", + "start": "Avvia", + "stop": "Ferma", + "supporters": "Sostenitori", + "suspend": "Sospendi", + "templates": "Template", + "tertiary": "Terziario", + "test": "Prova", + "thresholds": "Soglie", + "title": "Titolo", + "toast": "Notifica popup", + "trusted": "Attendibile", + "uninstall": "Disinstalla", + "unknown": "Sconosciuto", + "unpair": "Rimuovi associazione", + "unpin": "Sblocca", + "update": "Aggiorna", + "upload": "Carica", + "username": "Nome utente", + "userspace-reboot": "Riavvia spazio utente", + "version": "Versione", + "vibrant": "Vivace", + "visualizer": "Visualizzatore", + "volume": "Volume", + "volumes": "Volumi", + "wallpaper": "Sfondo", + "weather": "Meteo", + "weather-loading": "Caricamento meteo...", + "weather-no-location": "Imposta una posizione nelle Impostazioni per vedere il meteo", + "week": "Settimana", + "widgets": "Widget", + "width": "Larghezza", + "wifi": "Wi-Fi", + "windows": "Finestre", + "yes": "Sì" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Profilo energetico", + "tooltip-disabled": "Installa power-profiles-daemon per usare i profili energetici" + }, + "wifi": { + "label-disconnected": "Wi-Fi disconnesso", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Nascondi sempre", + "always-show": "Mostra sempre", + "force-open": "Apri sempre", + "on-hover": "Al passaggio" + }, + "hide-modes": { + "auto-hide": "Nascondi automaticamente", + "hidden": "Nascondi se vuoto", + "idle": "Nascondi quando inattivo", + "non-exclusive": "Non esclusivo", + "transparent": "Trasparente se vuoto", + "visible": "Sempre visibile" + }, + "launcher": { + "categories": { + "all": "Tutte", + "audiovideo": "Audio e video", + "chat": "Chat", + "development": "Sviluppo", + "education": "Formazione", + "emoji-activity": "Attività", + "emoji-animals": "Animali", + "emoji-flags": "Bandiere", + "emoji-food": "Cibo e bevande", + "emoji-nature": "Natura", + "emoji-objects": "Oggetti", + "emoji-people": "Persone e corpo", + "emoji-recent": "Recenti", + "emoji-symbols": "Simboli", + "emoji-travel": "Viaggi e luoghi", + "game": "Giochi", + "graphics": "Grafica", + "misc": "Varie", + "office": "Ufficio", + "pinned": "Fissate", + "system": "Sistema", + "webbrowser": "Browser web" + }, + "providers": { + "applications": "Applicazioni", + "calculator": "Calcolatrice", + "calculator-deprecated": "Il comando >calc è deprecato e verrà rimosso presto. Digita direttamente le espressioni matematiche nella ricerca.", + "calculator-description": "Calcolatrice — valuta espressioni matematiche", + "calculator-enter-expression": "Inserisci un’espressione matematica", + "calculator-press-enter-to-copy": "Premi Invio per copiare il risultato", + "clipboard": "Cronologia appunti", + "clipboard-clear-description": "Cancella tutta la cronologia appunti", + "clipboard-clear-description-full": "Rimuovi tutti gli elementi dalla cronologia appunti", + "clipboard-clear-history": "Cancella cronologia appunti", + "clipboard-delete": "Elimina voce appunti", + "clipboard-history-disabled": "Cronologia appunti disattivata", + "clipboard-history-disabled-description": "Attiva la cronologia appunti nelle impostazioni o installa cliphist", + "clipboard-loading": "Caricamento cronologia appunti...", + "clipboard-search-description": "Cerca nella cronologia appunti", + "command-description": "Esegui comandi shell", + "emoji": "Selettore emoji", + "emoji-loading": "Caricamento emoji...", + "emoji-loading-description": "Attendi prego", + "emoji-no-recent": "Nessuna emoji recente", + "emoji-search-description": "Cerca e copia emoji", + "settings-search-description": "Cerca e vai alle impostazioni", + "windows-search-description": "Cerca e metti a fuoco finestre aperte" + } + }, + "lock-screen": { + "authenticating": "Autenticazione in corso...", + "password": "Inserisci la password...", + "restart": "Riavvia", + "shut-down": "Spegni" + }, + "notifications": { + "panel": { + "click-to-collapse": "Fai clic per comprimere", + "click-to-expand": "Fai clic per espandere", + "description": "Le notifiche appariranno qui quando arrivano.", + "no-notifications": "Nessuna notifica" + }, + "range": { + "earlier": "Precedenti", + "today": "Oggi", + "yesterday": "Ieri" + }, + "time": { + "diff-d": "1 giorno fa", + "diff-dd": "{diff} giorni fa", + "diff-h": "1 ora fa", + "diff-hh": "{diff} ore fa", + "diff-m": "1 minuto fa", + "diff-mm": "{diff} minuti fa", + "now": "ora" + } + }, + "options": { + "bar": { + "density-comfortable": "Comoda", + "density-compact": "Compatta", + "density-default": "Predefinita", + "density-mini": "Mini", + "density-spacious": "Ampia", + "display-mode-always-visible": "Sempre visibile", + "type-floating": "Flottante", + "type-framed": "Incorniciata", + "type-simple": "Semplice" + }, + "control-center": { + "quick-settings-style-classic": "Classico", + "quick-settings-style-modern": "Moderno" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "Comoda", + "compact": "Compatta", + "default": "Predefinita" + }, + "launcher-view-mode": { + "grid": "Griglia", + "list": "Elenco" + }, + "notification-density": { + "compact": "Compatta", + "default": "Predefinita" + }, + "scrolling-modes": { + "always": "Scorri sempre", + "hover": "Scorri al passaggio del mouse", + "never": "Non scorrere mai" + }, + "session-menu-grid-layout": { + "grid": "Griglia", + "single-row": "Singola riga" + }, + "settings-panel-mode": { + "attached": "Pannello agganciato alla barra", + "centered": "Pannello centrato", + "window": "Finestra separata" + }, + "visualizer-types": { + "linear": "Lineare", + "mirrored": "Specchiato", + "wave": "Onda" + }, + "visualizer-visibility": { + "with-background": "Solo con sfondo" + }, + "workspace-labels": { + "index": "Indice", + "index-and-name": "Indice e nome", + "name": "Nome" + } + }, + "panels": { + "about": { + "become-supporter": "Diventa sostenitore", + "changelog": "Visualizza changelog", + "changelog-on-startup": "Mostra il changelog all'aggiornamento", + "changelog-on-startup-desc": "Mostra automaticamente il changelog quando Noctalia viene aggiornato.", + "contributors-description": "Un ringraziamento al nostro {count} contributore fantastico!", + "contributors-description-plural": "Un ringraziamento ai nostri {count} contributori fantastici!", + "copy-info": "Copia info", + "debug-disabled": "Modalità debug disattivata", + "debug-enabled": "Modalità debug attivata", + "info-copied": "Info copiate negli appunti", + "noctalia-available": "Disponibile:", + "noctalia-desc": "Una shell desktop elegante e minimale, progettata con cura per Wayland, costruita con Quickshell.", + "noctalia-git-commit": "Commit Git:", + "noctalia-installed-version": "Versione installata:", + "noctalia-latest-version": "Ultima versione:", + "noctalia-qs-version": "Noctalia QS versione:", + "noctalia-title": "Shell Noctalia", + "privacy-policy": "Informativa privacy", + "support": "Supportaci", + "supporter-badge": "Sostenitore", + "supporters-desc": "Un enorme grazie al nostro fantastico sostenitore!", + "supporters-desc-plural": "Un enorme grazie ai nostri {count} fantastici sostenitori!", + "supporters-loading": "Caricamento sostenitori...", + "system-board": "Scheda madre:", + "system-cpu": "CPU:", + "system-disk": "Disco:", + "system-gpu": "GPU:", + "system-host": "Host:", + "system-install-hint": "Installa fastfetch per visualizzare le informazioni di sistema", + "system-kernel": "Kernel:", + "system-loading": "Caricamento informazioni sistema...", + "system-memory": "Memoria:", + "system-monitor": "Monitor:", + "system-not-installed": "fastfetch non è installato", + "system-os": "SO:", + "system-packages": "Pacchetti:", + "system-product": "Prodotto:", + "system-title": "Informazioni di sistema", + "system-uptime": "Uptime:", + "system-wm": "WM:", + "telemetry-data-copied": "Dati telemetria copiati negli appunti", + "telemetry-desc": "Aiuta a migliorare Noctalia condividendo info di sistema anonime (risoluzione schermo, compositor, distro). Inviate una sola volta all’avvio, nessun tracciamento, dati eliminati automaticamente dopo 30 giorni.", + "telemetry-enabled": "Invia informazioni di sistema anonime", + "telemetry-show-data": "Visualizza dati", + "telemetry-title": "Privacy", + "title": "Informazioni", + "up-to-date": "Sei aggiornato!", + "update-available": "Aggiornamento disponibile", + "view-commit": "Visualizza commit su GitHub" + }, + "audio": { + "devices-desc": "Configura i dispositivi audio di input e output disponibili.", + "devices-input-device-description": "Seleziona il dispositivo audio di input desiderato.", + "devices-input-device-label": "Dispositivo input", + "devices-output-device-description": "Seleziona il dispositivo audio di output desiderato.", + "devices-output-device-label": "Dispositivo output", + "devices-title": "Dispositivi audio", + "external-mixer-description": "Inserisci il comando o il percorso dell’applicazione da avviare quando attivi il mixer audio esterno.", + "external-mixer-label": "Comando mixer audio esterno", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Imposta le applicazioni multimediali preferite e ignorate.", + "media-excluded-player-description": "Aggiungi parole chiave per i player che vuoi ignorare. Ogni parola chiave deve stare su una nuova riga.", + "media-excluded-player-label": "Player escluso", + "media-excluded-player-placeholder": "digita una sottostringa e premi +", + "media-frame-rate-description": "Valori più alti sono più fluidi ma usano più risorse.", + "media-frame-rate-label": "Frame rate", + "media-primary-player-description": "Inserisci una parola chiave per identificare il tuo player principale.", + "media-primary-player-label": "Player principale", + "media-primary-player-placeholder": "es. spotify, vlc, mpv", + "media-scrolling-speed-description": "Tempo in secondi affinché il titolo scorra dall’inizio alla fine.", + "media-scrolling-speed-label": "Velocità scorrimento", + "media-scrolling-title-description": "Abilita lo scorrimento continuo per titoli media lunghi.", + "media-scrolling-title-label": "Titolo scorrevole", + "media-title": "Lettori multimediali", + "on-middle-clicked-description": "Comando da eseguire quando il pulsante riceve clic centrale.", + "panel-applications-empty": "Nessuna applicazione sta riproducendo audio", + "spectrum-mirrored-description": "Specchia lo spettro in modo che le basse frequenze siano al centro. Quando disattivato, le basse frequenze sono a sinistra e le alte a destra.", + "spectrum-mirrored-label": "Spettro speculare", + "title": "Audio", + "visualizer-type-description": "Scegli un tipo di visualizzazione per la riproduzione multimediale.", + "visualizer-type-label": "Tipo visualizzazione", + "volumes-desc": "Regola i controlli del volume e i livelli audio.", + "volumes-feedback-sound-file-description": "Percorso del file audio riprodotto quando si cambia il volume.", + "volumes-feedback-sound-file-label": "File audio di feedback del volume", + "volumes-feedback-sound-file-select-title": "Seleziona file audio di feedback volume", + "volumes-input-volume-description": "Livello volume di input del microfono.", + "volumes-mute-input-description": "Silenzia l’input audio predefinito (microfono).", + "volumes-mute-input-label": "Silenzia input audio", + "volumes-mute-output-description": "Silenzia l’output audio principale del sistema.", + "volumes-mute-output-label": "Silenzia output audio", + "volumes-output-volume-description": "Livello volume di sistema.", + "volumes-step-size-description": "Regola la dimensione del passo per le variazioni volume (rotella mouse, scorciatoie tastiera).", + "volumes-step-size-label": "Passo volume", + "volumes-volume-feedback-description": "Riproduci un suono di feedback quando regoli il volume.", + "volumes-volume-feedback-label": "Riproduci suono feedback volume", + "volumes-volume-overdrive-description": "Consenti di alzare il volume oltre il 100%. Potrebbe non essere supportato da tutto l’hardware.", + "volumes-volume-overdrive-label": "Consenti overdrive volume" + }, + "bar": { + "appearance-auto-hide-delay-description": "Tempo prima che la barra si nasconda dopo l’uscita del mouse", + "appearance-auto-hide-delay-label": "Ritardo nascondi", + "appearance-auto-hide-exclusive-note": "Nota: la zona esclusiva viene disabilitata automaticamente quando il nascondimento automatico è attivo", + "appearance-auto-show-delay-description": "Tempo prima che la barra appaia quando il mouse entra nel bordo", + "appearance-auto-show-delay-label": "Ritardo mostra", + "appearance-background-opacity-description": "Imposta l’opacità sfondo specifica per la barra.", + "appearance-background-opacity-label": "Opacità sfondo barra", + "appearance-capsule-color-description": "Scegli un colore per le capsule barra oppure nessuno per usare il colore superficie predefinito.", + "appearance-capsule-color-label": "Colore capsula", + "appearance-capsule-opacity-description": "Imposta il livello opacità degli sfondi widget quando la capsula è mostrata.", + "appearance-capsule-opacity-label": "Opacità capsula", + "appearance-content-padding-description": "Regola la spaziatura tra i bordi della Bar e i Widget.", + "appearance-content-padding-label": "Spaziatura interna del contenuto", + "appearance-density-description": "Regola il padding della barra per un aspetto compatto o ampio.", + "appearance-density-label": "Densità barra", + "appearance-desc": "Personalizza aspetto e posizione della barra.", + "appearance-display-mode-description": "Scegli quando la barra è visibile.", + "appearance-enable-exclusion-zone-inset-description": "Riduci la zona di esclusione di 1 pixel fisico in modo che le finestre a filo si estendano perfettamente sotto il bordo della barra.", + "appearance-enable-exclusion-zone-inset-label": "Zona di esclusione interna", + "appearance-floating-description": "Mostra la barra come 'pill' flottante.", + "appearance-floating-label": "Barra flottante", + "appearance-font-scale-description": "Regola la scala dimensione font per il testo mostrato nella barra.", + "appearance-font-scale-label": "Scala font", + "appearance-frame-radius": "Raggio interno", + "appearance-frame-settings-description": "Regola spessore frame e raggio angolo interno", + "appearance-frame-settings-label": "Impostazioni frame", + "appearance-frame-thickness": "Spessore", + "appearance-hide-on-overview-description": "Nascondi barra e chiudi pannelli quando l’overview compositor è attiva.", + "appearance-hide-on-overview-label": "Nascondi barra in overview", + "appearance-margins-description": "Regola i margini attorno alla barra flottante.", + "appearance-margins-horizontal": "Margine orizzontale", + "appearance-margins-vertical": "Margine verticale", + "appearance-outer-corners-description": "Mostra angoli curvati verso l’esterno sulla barra.", + "appearance-outer-corners-label": "Angoli esterni", + "appearance-position-description": "Scegli dove posizionare la barra sullo schermo.", + "appearance-position-label": "Posizione barra", + "appearance-show-capsule-description": "Mostra sfondi widget.", + "appearance-show-capsule-label": "Mostra capsula", + "appearance-show-on-workspace-switch-description": "Mostra automaticamente la barra per poco quando cambia la workspace.", + "appearance-show-on-workspace-switch-label": "Mostra barra al cambio workspace", + "appearance-show-outline-description": "Mostra un bordo visibile attorno a ogni widget.", + "appearance-show-outline-label": "Mostra contorni widget", + "appearance-type-description": "Scegli lo stile barra: Semplice, Flottante o Incorniciata.", + "appearance-type-label": "Tipo barra", + "appearance-use-separate-opacity-description": "Abilita per usare un valore opacità separato per lo sfondo barra.", + "appearance-use-separate-opacity-label": "Usa opacità barra separata", + "appearance-widget-spacing-description": "Regola la spaziatura tra i widget nella barra.", + "appearance-widget-spacing-label": "Spaziatura widget", + "behavior-middle-click-command-description": "Comando da eseguire al clic centrale.", + "behavior-middle-click-command-label": "Comando Clic Centrale", + "behavior-middle-click-command-placeholder": "niri msg action commuta panoramica", + "behavior-middle-click-description": "Scegli cosa fa il clic centrale sulle aree vuote della Barra.", + "behavior-middle-click-follow-mouse-description": "Apri il pannello del clic centrale selezionato nella posizione del cursore.", + "behavior-middle-click-follow-mouse-label": "Clic centrale segue il mouse", + "behavior-middle-click-label": "Azione del clic centrale sulla Barra", + "behavior-right-click-command-description": "Comando da eseguire al clic destro.", + "behavior-right-click-command-label": "Comando Clic Destro", + "behavior-right-click-command-placeholder": "notify-send \"Clic destro\"", + "behavior-right-click-description": "Scegli cosa fa il clic destro sulle aree vuote della barra.", + "behavior-right-click-follow-mouse-description": "Apri il pannello del clic destro selezionato nella posizione del cursore.", + "behavior-right-click-follow-mouse-label": "Clic destro segue il mouse", + "behavior-right-click-label": "Azione al clic destro sulla Barra", + "behavior-wheel-wrap-description": "Quando abilitato, lo scorrimento continua dall'ultimo elemento al primo.", + "behavior-wheel-wrap-label": "Circolare", + "behavior-workspace-scroll-description": "Scegli cosa fa la rotellina del mouse sulle aree vuote della barra.", + "behavior-workspace-scroll-label": "Azione rotella mouse della Barra", + "behavior-workspace-scroll-option-content": "Contenuto", + "behavior-workspace-scroll-option-workspace": "Spazio di lavoro", + "monitor-configure-widgets": "Configura widget", + "monitor-override-settings": "Sovrascrivi impostazioni globali", + "monitor-override-settings-description": "Usa impostazioni personalizzate per questo monitor.", + "monitor-reset-all": "Reimposta tutto", + "monitor-widgets-title": "Configurazione Widget - {monitor}", + "monitors-desc": "Mostra barra su monitor specifici. Se non ne scegli nessuno, verranno usati tutti.", + "monitors-desc-new": "Configura quali monitor mostrano la barra e personalizza impostazioni per monitor.", + "monitors-title": "Visualizzazione monitor", + "title": "Barra", + "tray-blacklist-description": "Aggiungi regole esclusione tray, supporta wildcard (*).", + "tray-blacklist-label": "Lista nera", + "tray-blacklist-placeholder": "es. nm-applet, Fcitx*", + "tray-pin-application": "Fissa applicazione", + "tray-unpin-application": "Rimuovi applicazione fissata", + "use-global-widgets": "Usa widget globali", + "widgets-desc": "Trascina i widget per cambiarne l’ordine. Usa il menu clic destro per spostare widget tra sezioni o rimuoverli.", + "widgets-title": "Posizionamento widget" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Genera schemi colore dal tuo sfondo. Estrae automaticamente i colori per creare un tema coerente.", + "color-source-use-wallpaper-colors-label": "Usa colori dello sfondo", + "dark-mode-mode-description": "Abilita il passaggio automatico tra modalità chiara e scura.", + "dark-mode-mode-label": "Programmazione modalità scura", + "dark-mode-mode-manual": "Manuale", + "dark-mode-mode-off": "Disattivata", + "dark-mode-switch-description": "Passa a un tema più scuro per una visione più confortevole di notte.", + "delete-error-description": "Eliminazione di {scheme} non riuscita", + "delete-error-title": "Eliminazione non riuscita", + "delete-success-description": "{scheme} eliminato con successo", + "delete-success-title": "Schema colore eliminato", + "download-button": "Scarica altro", + "download-downloading": "Download in corso...", + "download-empty": "Nessuno schema colore disponibile", + "download-error-api-error": "Errore API: {status}", + "download-error-description": "Download di {scheme} non riuscito", + "download-error-download-failed": "Download fallito con codice uscita: {code}", + "download-error-invalid-response": "Formato risposta API non valido", + "download-error-no-files": "Nessun file trovato per lo schema", + "download-error-parse-failed": "Analisi risposta API non riuscita: {error}", + "download-error-rate-limit": "Limite richieste API GitHub superato", + "download-error-title": "Download non riuscito", + "download-fetching": "Recupero schemi colore disponibili...", + "download-success-description": "{scheme} scaricato con successo", + "download-success-title": "Schema colore scaricato", + "download-title": "Scarica Schemi Colore", + "method-description": { + "content": "Schema Material Design con estrazione colore ad alta fedeltà che rispecchia da vicino i colori reali del contenuto sorgente.", + "dysfunctional": "Simile a Faithful ma sceglie come primario il secondo gruppo colore più dominante.", + "faithful": "Tenta di restare vicino al colore sorgente mantenendo una palette armoniosa.", + "fruit-salad": "Schema Material Design che produce una palette vivace e giocosa con tonalità varie e diversificate.", + "monochrome": "Schema Material Design in scala di grigi a tinta singola con contenuto cromatico minimo.", + "muted": "Genera tonalità più morbide e desaturate per un aspetto calmo e sobrio.", + "rainbow": "Schema Material Design che genera colori su tutto lo spettro per massima diversità cromatica.", + "tonal-spot": "Algoritmo predefinito Material Design. Crea palette bilanciate usando variazioni tonali del colore sorgente.", + "vibrant": "Massimizza la saturazione colore per palette audaci e d’impatto con tonalità vivide." + }, + "predefined-desc": "Scegli da una raccolta di schemi colore predefiniti.", + "predefined-generate-templates-label": "Genera template per schemi predefiniti", + "predefined-title": "Schemi colore predefiniti", + "sync-gsettings-description": "Abbina il tema di sistema alla variante chiara o scura attiva.", + "sync-gsettings-label": "Sincronizza tema di sistema", + "templates-desc": "Applica colori ad applicazioni esterne.", + "templates-filter-description": "Mostra template di una categoria specifica.", + "templates-filter-label": "Filtra per categoria", + "templates-misc-description": "Crea i tuoi template.", + "templates-misc-label": "Avanzato", + "templates-misc-user-templates-description": "Abilita solo se sai cosa stai facendo — consulta la documentazione online.", + "templates-misc-user-templates-label": "Abilita template utente", + "templates-none-detected": "Nessuno rilevato", + "templates-write-path": "Scrive: {filepath}", + "title": "Schema colore", + "wallpaper-method-description": "Scegli il tuo metodo preferito di generazione palette.", + "wallpaper-method-label": "Metodo generazione palette", + "wallpaper-monitor-source-description": "Seleziona quale monitor usare per estrarre i colori dallo sfondo.", + "wallpaper-monitor-source-label": "Sorgente generazione colore" + }, + "connections": { + "authentication-required": "Autenticazione richiesta", + "bluetooth-auto-connect-description": "Connettiti automaticamente ai dispositivi accoppiati fidati quando il Bluetooth è abilitato.", + "bluetooth-auto-connect-label": "Connetti automaticamente i dispositivi accoppiati", + "bluetooth-devices-unnamed": "I dispositivi senza nome non sono mostrati.", + "bluetooth-discoverable": "Questo dispositivo è visibile come {hostName} mentre questa scheda impostazioni è aperta.", + "bluetooth-rssi-polling-description": "Campiona periodicamente l’RSSI dei dispositivi connessi tramite bluetoothctl. Potrebbe non essere disponibile per tutti i dispositivi; usa risorse minime quando attivo.", + "bluetooth-rssi-polling-interval-description": "Configura la frequenza di aggiornamento dell’intensità segnale per i dispositivi connessi.", + "bluetooth-rssi-polling-interval-label": "Intervallo polling", + "bluetooth-rssi-polling-label": "Polling segnale Bluetooth", + "disable-discoverability-description": "Nascondi il tuo dispositivo ai dispositivi Bluetooth vicini.", + "disable-discoverability-label": "Disattiva visibilità dispositivo", + "hide-unnamed-devices-description": "Nascondi dispositivi che appaiono solo come indirizzi Bluetooth.", + "hide-unnamed-devices-label": "Nascondi dispositivi senza nome", + "pin-instructions": "Inserisci il codice PIN mostrato sul tuo dispositivo.", + "title": "Connessioni", + "wifi-header-text": "Usa il Wi-Fi per vedere le reti disponibili e connetterti a internet." + }, + "control-center": { + "cards-desc": "Personalizza quali controlli appaiono nel centro di controllo e in quale ordine.", + "desc": "Configura posizione e comportamento del pannello centro di controllo.", + "open-at-mouse-description": "Quando si fa clic destro su un'area vuota della Bar, apri il Centro di Controllo vicino al cursore. Se disabilitato, viene utilizzata l'impostazione di posizione sopra indicata.", + "open-at-mouse-label": "Apri al cursore con clic destro", + "position-description": "Scegli dove appare il pannello centro di controllo quando viene aperto.", + "shortcuts-custom-button-command-description": "Comando da eseguire quando il pulsante viene cliccato.", + "shortcuts-custom-button-enable-on-state-logic-description": "Abilita una seconda icona e uno stato 'attivo' basato su un comando di controllo.", + "shortcuts-custom-button-enable-on-state-logic-label": "Abilita logica stato attivo", + "shortcuts-custom-button-icon-description": "Seleziona un’icona dalla libreria.", + "shortcuts-custom-button-on-clicked-label": "Comando clic sinistro", + "shortcuts-custom-button-on-middle-clicked-description": "Comando da eseguire al clic centrale del pulsante.", + "shortcuts-custom-button-on-state-command-description": "Comando da eseguire per verificare se il pulsante deve essere nello stato 'attivo'. Restituisce 0 per attivo, valore diverso da 0 per spento.", + "shortcuts-custom-button-on-state-command-label": "Comando controllo stato attivo", + "shortcuts-custom-button-on-state-icon-description": "L’icona del pulsante quando è nello stato 'attivo'.", + "shortcuts-custom-button-on-state-icon-label": "Icona stato attivo", + "shortcuts-custom-button-state-checks-add": "Aggiungi controllo stato", + "shortcuts-custom-button-state-checks-command": "Comando da eseguire per questo controllo stato", + "shortcuts-custom-button-state-checks-label": "Controlli stato", + "shortcuts-custom-button-state-checks-remove": "Rimuovi", + "shortcuts-custom-button-tooltip-description": "Tooltip da mostrare al passaggio del mouse sul pulsante.", + "shortcuts-custom-button-tooltip-label": "Suggerimento", + "shortcuts-title": "Widget scorciatoie", + "system-monitor-disk-path-description": "Seleziona quale punto di mount disco deve monitorare la scheda monitor di sistema nel centro di controllo.", + "system-monitor-disk-path-label": "Percorso disco monitor di sistema", + "title": "Centro di controllo" + }, + "desktop-widgets": { + "clock-enabled-description": "Mostra un widget orologio sul desktop.", + "clock-enabled-label": "Abilita widget orologio", + "clock-format-label": "Formato", + "clock-height-description": "Altezza del widget orologio in pixel.", + "clock-height-label": "Altezza", + "clock-minimal-mode-description": "Usa una visualizzazione orologio minimale con ora e data.", + "clock-minimal-mode-label": "Modalità minimale", + "clock-rounded-corners-description": "Usa angoli arrotondati per lo sfondo del widget.", + "clock-rounded-corners-label": "Angoli arrotondati", + "clock-show-background-description": "Mostra il contenitore sfondo per il widget orologio.", + "clock-show-background-label": "Mostra sfondo", + "clock-show-date-description": "Mostra la data corrente sotto l’ora.", + "clock-show-date-label": "Mostra data", + "clock-show-seconds-description": "Mostra i secondi nell’orario.", + "clock-show-seconds-label": "Mostra secondi", + "clock-style-analog": "Analogico", + "clock-style-binary": "Binario", + "clock-style-description": "Scegli lo stile di visualizzazione dell’orologio.", + "clock-style-digital": "Digitale", + "clock-style-label": "Stile orologio", + "clock-style-minimal": "Minimale", + "clock-width-description": "Larghezza del widget orologio in pixel.", + "cpu-intensive-note": "I widget contrassegnati con ! usano più CPU e vanno abilitati con attenzione.", + "edit-mode-button-label": "Entra in modalità modifica", + "edit-mode-controls-explanation": "Clic sinistro e trascina: sposta o ridimensiona il widget.\nClic destro: apri opzioni menu contestuale.", + "edit-mode-description": "Abilita modalità modifica per spostare e riposizionare i widget desktop. Quando attiva, i widget mostrano un contorno di trascinamento e possono essere riposizionati.", + "edit-mode-exit-button": "Esci da modalità modifica", + "edit-mode-grid-snap-label": "Aggancio griglia", + "edit-mode-grid-snap-scale-label": "Scala di aggancio alla griglia", + "edit-mode-label": "Modalità modifica", + "enabled-description": "Abilita o disabilita completamente i widget desktop.", + "enabled-label": "Abilita widget desktop", + "general-desc": "Configura i widget che appaiono sul desktop.", + "general-title": "Widget desktop", + "media-player-enabled-description": "Mostra un widget lettore multimediale sul desktop.", + "media-player-enabled-label": "Abilita widget lettore multimediale", + "media-player-rounded-corners-description": "Abilita angoli arrotondati sui bordi del widget.", + "media-player-show-album-art-description": "Mostra la copertina album e le informazioni traccia (titolo e artista).", + "media-player-show-album-art-label": "Mostra copertina e titolo", + "media-player-show-background-description": "Mostra il contenitore sfondo.", + "media-player-show-buttons-description": "Mostra pulsanti controllo media (riproduci/pausa, precedente, successivo).", + "media-player-show-buttons-label": "Mostra pulsanti", + "media-player-show-visualizer-description": "Mostra la sovrapposizione visualizzatore audio.", + "media-player-visualizer-type-description": "Scegli un tipo di visualizzazione.", + "overview-enabled-description": "Mostra widget desktop quando l’overview del compositor è aperta.", + "overview-enabled-label": "Mostra in overview", + "system-stat-layout-bottom": "In basso", + "system-stat-layout-description": "Scegli come mostrare la legenda rispetto al grafico.", + "system-stat-layout-label": "Disposizione", + "system-stat-layout-side": "Laterale", + "system-stat-rounded-corners-description": "Usa angoli arrotondati per lo sfondo del widget.", + "system-stat-rounded-corners-label": "Angoli arrotondati", + "system-stat-show-background-description": "Mostra il contenitore sfondo per il widget statistiche sistema.", + "system-stat-show-background-label": "Mostra sfondo", + "system-stat-stat-type-description": "Scegli quale statistica di sistema visualizzare.", + "system-stat-stat-type-label": "Tipo statistica", + "title": "Widget desktop", + "weather-enabled-description": "Mostra un widget meteo sul desktop.", + "weather-enabled-label": "Abilita widget meteo", + "weather-show-background-description": "Mostra il contenitore sfondo per il widget meteo." + }, + "display": { + "monitors-backlight-device-auto-option": "Predefinito", + "monitors-backlight-device-description": "Seleziona un dispositivo di retroilluminazione per questa uscita.", + "monitors-backlight-device-label": "Dispositivo retroilluminazione", + "monitors-brightness-step-description": "Regola la dimensione del passo per le variazioni luminosità (rotella mouse e scorciatoie tastiera).", + "monitors-brightness-step-label": "Passo luminosità", + "monitors-brightness-unavailable-ddc-disabled": "Controllo luminosità non disponibile. Abilita \"Supporto luminosità esterna\" per controllare la luminosità di questo schermo.", + "monitors-brightness-unavailable-generic": "Il controllo luminosità non è disponibile per questo schermo.", + "monitors-desc": "Regola le impostazioni di luminosità per ciascun monitor.", + "monitors-enforce-minimum-description": "Risolve il problema della retroilluminazione che su alcuni schermi si spegne completamente a luminosità 0%.", + "monitors-enforce-minimum-label": "Forza luminosità minima (1%)", + "monitors-external-brightness-description": "Abilita il supporto DDCUtil per controllare la luminosità dei monitor esterni tramite protocollo DDC/CI.", + "monitors-external-brightness-label": "Supporto luminosità esterna", + "monitors-global-brightness-description": "Regola la luminosità di tutti i monitor contemporaneamente.", + "monitors-global-brightness-label": "Tutti i monitor", + "monitors-title": "Impostazioni per monitor", + "night-light-auto-schedule-description": "Basato su orario di alba e tramonto a {location} — consigliato.", + "night-light-auto-schedule-label": "Programmazione automatica", + "night-light-desc": "Riduce l’emissione di luce blu per aiutarti a dormire meglio e ridurre l’affaticamento visivo.", + "night-light-enable-description": "Applica un filtro colore caldo per ridurre l’emissione di luce blu.", + "night-light-enable-label": "Abilita Luce notturna", + "night-light-force-activation-description": "Ignora la programmazione e applica subito il filtro notturno.", + "night-light-force-activation-label": "Forza attivazione", + "night-light-manual-schedule-description": "Imposta orari personalizzati per alba e tramonto.", + "night-light-manual-schedule-label": "Programmazione manuale", + "night-light-manual-schedule-select-start": "Seleziona ora inizio", + "night-light-manual-schedule-select-stop": "Seleziona ora fine", + "night-light-manual-schedule-sunrise": "Ora alba", + "night-light-manual-schedule-sunset": "Ora tramonto", + "night-light-temperature-day": "Giorno", + "night-light-temperature-day-description": "Controlla la temperatura durante il giorno.", + "night-light-temperature-description": "Imposta il calore colore per notte e giorno.", + "night-light-temperature-label": "Temperatura colore", + "night-light-temperature-night": "Notte", + "night-light-temperature-night-description": "Controlla la temperatura durante la notte.", + "title": "Schermo" + }, + "dock": { + "appearance-background-opacity-description": "Regola l’opacità dello sfondo del dock.", + "appearance-border-radius-description": "Regola il raggio bordo del dock.", + "appearance-border-radius-label": "Raggio bordo", + "appearance-colorize-icons-description": "Applica i colori del tema alle icone app del dock (solo app non focalizzate).", + "appearance-colorize-icons-label": "Colora icone", + "appearance-dead-opacity-description": "Regola l’opacità delle icone app non in esecuzione.", + "appearance-dead-opacity-label": "Opacità app non attive", + "appearance-desc": "Personalizza comportamento e aspetto del dock.", + "appearance-display-auto-hide": "Nascondi automaticamente", + "appearance-display-description": "Scegli come si comporta il dock.", + "appearance-display-exclusive": "Esclusivo", + "appearance-dock-indicator-description": "Mostra un piccolo indicatore quando il dock è nascosto.", + "appearance-dock-indicator-label": "Indicatore dock", + "appearance-floating-distance-description": "Imposta la distanza tra il dock e il bordo dello schermo.", + "appearance-floating-distance-label": "Distanza flottante dock", + "appearance-group-apps-description": "Raggruppa più finestre della stessa app in una sola voce dock.", + "appearance-group-apps-label": "Raggruppa stesse app", + "appearance-group-click-action-cycle": "Scorri finestre", + "appearance-group-click-action-description": "Scegli cosa fa il clic sinistro sulle app raggruppate.", + "appearance-group-click-action-label": "Azione clic app raggruppata", + "appearance-group-click-action-list": "Apri elenco finestre", + "appearance-group-context-menu-mode-description": "Scegli come si comporta il menu contestuale per app raggruppate.", + "appearance-group-context-menu-mode-extended": "Esteso", + "appearance-group-context-menu-mode-label": "Modalità menu app raggruppata", + "appearance-group-context-menu-mode-list": "Elenco finestre", + "appearance-group-indicator-style-description": "Scegli come gli indicatori app raggruppate mostrano lo stato della finestra focalizzata.", + "appearance-group-indicator-style-dots": "Punti", + "appearance-group-indicator-style-label": "Stile indicatore raggruppato", + "appearance-group-indicator-style-number": "Numero", + "appearance-hide-show-speed-description": "Regola la velocità dell’animazione mostra/nascondi del dock.", + "appearance-hide-show-speed-label": "Velocità mostra/nascondi", + "appearance-icon-size-description": "Regola la dimensione complessiva del dock.", + "appearance-icon-size-label": "Dimensione dock", + "appearance-inactive-indicators-description": "Mostra indicatori per tutte le app, non solo quella attualmente attiva.", + "appearance-inactive-indicators-label": "Indicatori in esecuzione", + "appearance-indicator-color-description": "Scegli il colore dell'indicatore del dock nascosto.", + "appearance-indicator-color-label": "Colore dell'indicatore", + "appearance-indicator-opacity-description": "Regola l'opacità dell'indicatore del dock nascosto.", + "appearance-indicator-opacity-label": "Opacità dell'indicatore", + "appearance-indicator-thickness-description": "Utilizza un indicatore del dock nascosto più spesso (6px invece di 3px).", + "appearance-indicator-thickness-label": "Indicatore più spesso", + "appearance-launcher-icon-description": "Scegli l'icona usata per il Dock Launcher.", + "appearance-launcher-icon-label": "Icona del Launcher", + "appearance-launcher-position-description": "Scegli dove appare l’icona launcher nel dock.", + "appearance-launcher-position-end": "Fine", + "appearance-launcher-position-label": "Posizione launcher", + "appearance-launcher-position-start": "Inizio", + "appearance-launcher-use-distro-logo-description": "Usa il logo della tua distribuzione come icona del Dock Launcher.", + "appearance-launcher-use-distro-logo-label": "Usa il logo della distro invece dell'icona", + "appearance-pinned-static-description": "Spinge sempre le icone app fissate a sinistra in ordine statico.", + "appearance-pinned-static-label": "App fissate statiche", + "appearance-position-description": "Scegli dove appare il dock sullo schermo.", + "appearance-position-label": "Posizione", + "appearance-show-launcher-icon-description": "Mostra l’icona launcher applicazioni nel dock.", + "appearance-show-launcher-icon-label": "Mostra launcher app", + "appearance-sit-on-frame-description": "Allinea il dock dentro il bordo frame invece di posizionarlo sopra.", + "appearance-sit-on-frame-label": "Dock sul frame", + "appearance-type-attached": "Ancorato", + "appearance-type-description": "Scegli tra pill flottante o barra statica agganciata al bordo.", + "appearance-type-floating": "Flottante", + "appearance-type-label": "Stile dock", + "enabled-description": "Mostra o nascondi completamente il dock.", + "enabled-label": "Abilita dock", + "monitors-desc": "Mostra dock su monitor specifici. Se non ne scegli nessuno, verranno usati tutti.", + "monitors-only-same-monitor-description": "Mostra solo app del monitor in cui si trova il dock.", + "monitors-only-same-monitor-label": "Solo app dello stesso monitor", + "monitors-title": "Visualizzazione monitor", + "title": "Dock" + }, + "general": { + "copy-settings": "Copia impostazioni", + "fonts-default-description": "Font principale usato in tutta l’interfaccia.", + "fonts-default-label": "Font predefinito", + "fonts-default-placeholder": "Seleziona font predefinito...", + "fonts-default-scale-description": "Aumenta o riduci la dimensione del testo standard.", + "fonts-default-scale-label": "Dimensione font predefinito", + "fonts-default-search-placeholder": "Cerca font...", + "fonts-desc": "Scegli i font usati in tutta l’interfaccia.", + "fonts-monospace-description": "Font monospaziato usato per numeri e visualizzazione statistiche.", + "fonts-monospace-label": "Font monospaziato", + "fonts-monospace-placeholder": "Seleziona font monospaziato...", + "fonts-monospace-scale-description": "Aumenta o riduci la dimensione del testo monospaziato.", + "fonts-monospace-scale-label": "Dimensione font monospaziato", + "fonts-monospace-search-placeholder": "Cerca font monospaziato...", + "fonts-title": "Font", + "keybinds-conflict-description": "La combinazione di tasti è già assegnata a {action}.", + "keybinds-conflict-title": "Conflitto scorciatoia", + "keybinds-description": "Configura i tasti di navigazione globali per pannelli e launcher.", + "keybinds-down": "Sposta in basso", + "keybinds-enter": "Conferma / Azione", + "keybinds-escape": "Chiudi / Indietro", + "keybinds-left": "Sposta a sinistra", + "keybinds-modifier-description": "Le scorciatoie devono includere un tasto modificatore (Ctrl o Alt).", + "keybinds-modifier-title": "Modificatore richiesto", + "keybinds-remove": "Rimuovi / Elimina", + "keybinds-right": "Sposta a destra", + "keybinds-title": "Scorciatoie navigazione", + "keybinds-up": "Sposta in alto", + "language-desc": "Scegli la lingua preferita per l’applicazione.", + "language-select-auto-detect": "Automatico", + "language-select-description": "Seleziona la lingua usata nell’interfaccia dell’applicazione.", + "language-select-label": "Lingua applicazione", + "launch-setup-wizard": "Guidata di configurazione", + "profile-desc": "Modifica i dettagli utente e l’avatar.", + "profile-picture-description": "La tua immagine profilo mostrata in tutta l’interfaccia.", + "profile-picture-label": "Immagine profilo", + "profile-select-avatar": "Seleziona immagine avatar", + "profile-title": "Profilo", + "profile-tooltip": "Immagine profilo", + "reverse-scrolling-description": "Inverte la direzione di scorrimento interpretata", + "reverse-scrolling-label": "Inverti scorrimento", + "screen-corners-desc": "Personalizza arrotondamento angoli schermo ed effetti visivi.", + "screen-corners-radius-description": "Regola gli angoli arrotondati dello schermo.", + "screen-corners-radius-label": "Raggio angoli schermo", + "screen-corners-radius-reset": "Reimposta raggio angoli schermo", + "screen-corners-show-corners-description": "Mostra angoli arrotondati sul bordo dello schermo.", + "screen-corners-show-corners-label": "Mostra angoli schermo", + "screen-corners-solid-black-description": "Usa nero pieno invece del colore di sfondo della barra.", + "screen-corners-solid-black-label": "Angoli neri pieni", + "screen-corners-title": "Angoli schermo", + "settings-copied": "Impostazioni copiate negli appunti", + "smooth-scrolling-description": "Anima lo scorrimento delle liste per un'esperienza della rotella più fluida.", + "smooth-scrolling-label": "Scorrimento fluido", + "tab-basics": "Base", + "tab-keybinds": "Scorciatoie" + }, + "hooks": { + "color-generation-description": "Comando da eseguire al completamento della generazione del colore (ad es. dopo la tematizzazione basata sullo sfondo).", + "color-generation-label": "Colori generati", + "color-generation-placeholder": "ad es. notify-send \"Colori\" \"Generati\"", + "info-command-info-description": "• I comandi vengono eseguiti via shell (sh -lc)
• I comandi girano in background (staccati)
• I pulsanti test eseguono con i valori correnti", + "info-parameters-description": "• Wallpaper hook: $1 = percorso dello sfondo, $2 = nome dello schermo, $3 = tema (scuro/chiaro)
• Theme toggle hook: $1 = true/false (stato della modalità scura)
• Screen lock/unlock hooks: $1 = lock/unlock (stato di blocco schermo)
• Performance mode hooks: Nessun parametro
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "Parametri disponibili", + "noctalia-started-description": "Comando da eseguire quando Noctalia ha finito il caricamento.", + "noctalia-started-label": "Noctalia avviata", + "noctalia-started-placeholder": "es. notify-send 'Noctalia caricata'", + "performance-mode-disabled-description": "Comando da eseguire quando la modalità prestazioni Noctalia è disabilitata.", + "performance-mode-disabled-label": "Modalità prestazioni disabilitata", + "performance-mode-disabled-placeholder": "es. notify-send \"Prestazioni\" \"Modalità disabilitata\"", + "performance-mode-enabled-description": "Comando da eseguire quando la modalità prestazioni Noctalia è abilitata.", + "performance-mode-enabled-label": "Modalità prestazioni abilitata", + "performance-mode-enabled-placeholder": "es. notify-send \"Prestazioni\" \"Modalità abilitata\"", + "screen-lock-description": "Comando da eseguire quando lo schermo viene bloccato.", + "screen-lock-label": "Schermo bloccato", + "screen-lock-placeholder": "es. notify-send \"Schermo\" \"Bloccato\"", + "screen-unlock-description": "Comando da eseguire quando la schermata blocco viene sbloccata.", + "screen-unlock-label": "Schermo sbloccato", + "screen-unlock-placeholder": "es. notify-send \"Schermo\" \"Sbloccato\"", + "session-description": "Comando da eseguire prima di spegnimento o riavvio. Riceve il tipo azione in $1 (shutdown/reboot).", + "session-label": "Fine sessione", + "session-placeholder": "es. notify-send \"Sessione\" \"$1\"", + "system-hooks-desc": "Configura comandi da eseguire quando avvengono eventi di sistema.", + "system-hooks-enable-description": "Abilita o disabilita tutti i comandi hook.", + "system-hooks-enable-label": "Abilita hook", + "system-hooks-title": "Hook di sistema", + "theme-changed-description": "Comando da eseguire quando il tema passa tra Modalità Scura e Chiara.", + "theme-changed-label": "Tema cambiato", + "theme-changed-placeholder": "es. notify-send \"Tema\" \"Cambiato\"", + "title": "Hook", + "wallpaper-changed-description": "Comando da eseguire quando cambia lo sfondo.", + "wallpaper-changed-label": "Sfondo cambiato", + "wallpaper-changed-placeholder": "es. notify-send \"Sfondo\" \"Cambiato\"" + }, + "idle": { + "custom-add": "Aggiungi comando", + "custom-description": "Esegui un comando shell dopo un periodo di inattività.", + "custom-entry-command": "Comando", + "custom-entry-delete": "Elimina", + "custom-entry-edit": "Modifica Comando Personalizzato", + "custom-entry-name": "Nome", + "custom-entry-name-placeholder": "es. Spegni le luci", + "custom-entry-new": "Nuovo Comando Personalizzato", + "custom-entry-timeout": "Tempo inattività", + "custom-entry-timeout-format": "{count} secondo", + "custom-entry-timeout-format-plural": "{count} secondi", + "custom-entry-unnamed": "Comando senza nome", + "custom-label": "Comandi inattività personalizzati", + "enable-description": "Spegni automaticamente schermo, blocca o sospendi dopo un periodo di inattività.", + "enable-label": "Abilita gestione inattività", + "fade-duration-description": "Secondi per l’animazione dissolvenza al nero prima che parta ogni azione. Qualsiasi movimento mouse annulla la dissolvenza.", + "fade-duration-label": "Durata dissolvenza", + "lock-description": "Secondi di inattività prima che si attivi la schermata blocco.", + "lock-label": "Schermata blocco", + "resume-command-label": "Riprendi comando", + "screen-off-description": "Secondi di inattività prima che i monitor vengano spenti.", + "screen-off-label": "Spegni schermo", + "status-description": "Tempo di inattività come riportato dal compositor.", + "status-label": "Tempo inattività", + "suspend-description": "Secondi di inattività prima che il sistema venga sospeso.", + "tab-behavior": "Comportamento", + "tab-custom": "Personalizzato", + "timeouts-description": "Imposta a 0 per disabilitare una fase. I timeout vengono sospesi mentre 'Mantieni attivo' è attivo.", + "timeouts-label": "Timeout", + "title": "Inattività", + "unavailable": "Il monitoraggio inattività nativo non è disponibile su questo compositor." + }, + "indicator": { + "default-value": "Default: {value}", + "system-default": "System Default" + }, + "launcher": { + "clipboard-desc": "Accedi e gestisci la cronologia appunti dal launcher.", + "execute-desc": "Configura come vengono avviate le applicazioni.", + "execute-title": "Esecuzione", + "settings-annotation-tool-description": "Comando da eseguire quando fai clic sul pulsante annota nella cronologia appunti. L’immagine verrà passata in pipe a questo comando.", + "settings-annotation-tool-label": "Strumento annotazioni", + "settings-annotation-tool-placeholder": "es. 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Incolla automaticamente l’elemento appunti selezionato. Richiede wtype.", + "settings-auto-paste-label": "Incolla automaticamente", + "settings-clip-chips-description": "Mostra una barra delle schede per filtrare la cronologia degli appunti per tipo (Immagini, Link, File, Codice, ecc.).", + "settings-clip-chips-label": "Abilita chip di categoria", + "settings-clip-preview-description": "Mostra un’anteprima del contenuto appunti quando usi il comando >clip.", + "settings-clip-preview-label": "Abilita anteprima clip", + "settings-clip-smart-icons-description": "Mostra icone specifiche per link, file, colori e codice invece di un'icona generica per gli appunti.", + "settings-clip-smart-icons-label": "Abilita icone intelligenti", + "settings-clip-wrap-text-description": "Va a capo il testo nell’elenco appunti invece di troncarlo.", + "settings-clip-wrap-text-label": "Testo appunti a capo", + "settings-clipboard-history-description": "Accedi agli elementi copiati in precedenza dal launcher.", + "settings-clipboard-history-label": "Abilita cronologia appunti", + "settings-clipboard-watch-image-description": "Stringa comando completa passata a wl-paste per cambi immagine. (richiede riavvio)", + "settings-clipboard-watch-image-label": "Comando monitoraggio immagini", + "settings-clipboard-watch-text-description": "Stringa comando completa passata a wl-paste per cambi testo. (richiede riavvio)", + "settings-clipboard-watch-text-label": "Comando monitoraggio testo", + "settings-custom-launch-prefix-description": "Prefissa i comandi con un launcher personalizzato (es. 'runapp' per integrazione systemd).", + "settings-custom-launch-prefix-enabled-description": "Usa un prefisso personalizzato per avviare applicazioni invece del metodo predefinito.", + "settings-custom-launch-prefix-enabled-label": "Abilita prefisso avvio personalizzato", + "settings-custom-launch-prefix-label": "Prefisso avvio personalizzato", + "settings-density-description": "Regola la dimensione delle icone applicazione e la densità del launcher.", + "settings-density-label": "Densità", + "settings-desc": "Personalizza comportamento e aspetto del launcher.", + "settings-enable-session-search-description": "Mostra azioni sessione (blocco, spegnimento, riavvio, ecc.) durante la ricerca nel launcher.", + "settings-enable-session-search-label": "Abilita ricerca sessione", + "settings-enable-settings-search-description": "Mostra risultati impostazioni durante la ricerca nel launcher.", + "settings-enable-settings-search-label": "Abilita ricerca impostazioni", + "settings-enable-windows-search-description": "Cerca e metti a fuoco finestre attive.", + "settings-enable-windows-search-label": "Abilita ricerca finestre", + "settings-grid-view-description": "Mostra elementi in layout a griglia invece che a elenco.", + "settings-icon-mode-description": "Usa icone di sistema native invece delle icone Tabler.", + "settings-icon-mode-label": "Usa icone native", + "settings-ignore-mouse-input-description": "Disabilita interazione mouse e rotella nel launcher.", + "settings-ignore-mouse-input-label": "Ignora input mouse", + "settings-overlay-layer-description": "Mostra il launcher sul layer overlay, sopra le finestre fullscreen. Se attivo, il launcher non verrà agganciato alla barra.", + "settings-overlay-layer-label": "Mostra sopra fullscreen", + "settings-position-description": "Scegli dove appare il pannello launcher.", + "settings-show-categories-description": "Mostra schede categoria per filtrare le applicazioni.", + "settings-show-categories-label": "Mostra categorie", + "settings-show-icon-background-description": "Mostra uno sfondo a rettangolo arrotondato dietro le icone.", + "settings-show-icon-background-label": "Mostra sfondo icone", + "settings-sort-by-usage-description": "Se attivo, le app avviate più spesso appaiono per prime nell’elenco.", + "settings-sort-by-usage-label": "Ordina per più usate", + "settings-terminal-command-description": "Comando per avviare un terminale. Es. \"kitty -e\" o \"gnome-terminal --\".", + "settings-terminal-command-label": "Comando terminale", + "settings-use-app2unit-description": "Usa un metodo di avvio alternativo per gestire meglio i processi app e prevenire problemi.", + "settings-use-app2unit-label": "Usa App2Unit per avviare applicazioni", + "settings-view-mode-description": "Scegli il layout delle voci launcher.", + "settings-view-mode-label": "Modalità vista", + "title": "Launcher" + }, + "location": { + "auto-locate-description": "Rileva automaticamente la tua posizione periodicamente usando il tuo indirizzo IP.", + "auto-locate-label": "Localizzazione Automatica", + "calendar-cards-desc": "Organizza e abilita/disabilita le schede nel pannello calendario.", + "calendar-cards-title": "Schede calendario", + "calendar-header-label": "Intestazione calendario", + "calendar-month-label": "Mese calendario", + "date-time-12hour-format-description": "Mostra l’ora in formato 12 ore nella schermata blocco e nel calendario. L’orologio della barra ha impostazioni proprie.", + "date-time-12hour-format-label": "Usa formato ora 12 ore", + "date-time-desc": "Personalizza come vengono mostrati data e ora.", + "date-time-first-day-of-week-automatic": "Automatico (usa locale di sistema)", + "date-time-first-day-of-week-description": "Scegli quale giorno inizia la settimana nel calendario.", + "date-time-first-day-of-week-label": "Primo giorno della settimana", + "date-time-show-events-description": "Mostra gli eventi nel pannello calendario.", + "date-time-show-events-label": "Mostra eventi calendario", + "date-time-title": "Data e ora", + "date-time-use-analog-description": "Mostra un orologio in stile analogico nella finestra calendario.", + "date-time-use-analog-label": "Usa orologio stile analogico", + "date-time-week-numbers-description": "Mostra nel calendario la settimana dell’anno (es. settimana 38).", + "date-time-week-numbers-label": "Mostra numeri settimana", + "geolocate-now-button": "Localizza ora", + "location-desc": "Ottieni meteo accurato e pianificazione Luce notturna impostando la tua posizione.", + "location-search-description": "es. Torino, TO", + "location-search-label": "Cerca una posizione", + "location-search-placeholder": "Inserisci il nome della posizione", + "location-title": "La tua posizione", + "weather-desc": "Scegli l’unità di temperatura preferita.", + "weather-enabled-description": "Mostra informazioni meteo in tutta l’interfaccia e recupera dati meteo. Se disattivato, tutti gli elementi meteo saranno nascosti e non verranno effettuate richieste di rete.", + "weather-enabled-label": "Abilita meteo", + "weather-fahrenheit-description": "Mostra la temperatura in Fahrenheit invece che in Celsius.", + "weather-fahrenheit-label": "Mostra temperatura in Fahrenheit (°F)", + "weather-hide-city-description": "Nascondi il nome città dalle visualizzazioni meteo in tutta l’interfaccia.", + "weather-hide-city-label": "Nascondi nome città", + "weather-hide-timezone-description": "Nascondi l’abbreviazione del fuso orario dalle visualizzazioni meteo in tutta l’interfaccia.", + "weather-hide-timezone-label": "Nascondi fuso orario", + "weather-show-effects-description": "Mostra effetti visivi aggiuntivi (come pioggia, neve o fulmini) sulla scheda meteo.", + "weather-show-effects-label": "Mostra effetti meteo", + "weather-show-in-calendar-description": "Mostra la previsione meteo giornaliera direttamente nella vista calendario.", + "weather-show-in-calendar-label": "Mostra meteo nel calendario", + "weather-talia-mascot-always-description": "Usa sempre le Talia emojis invece delle tabler icons.", + "weather-talia-mascot-always-label": "Mostra sempre le emoji meteo di Talia" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "Quando fprintd (autenticazione impronta digitale) è attivo, questa opzione ti consente comunque di accedere con la password invece che con l’impronta.", + "allow-password-with-fprintd-label": "Consenti accesso con password con fprintd", + "auto-start-auth-description": "Avvia automaticamente l’autenticazione con impronta senza richiedere pressione tasti o clic su pulsante.", + "auto-start-auth-label": "Avvio automatico autenticazione", + "clock-format-description": "Personalizza il formato orologio usando token di sintassi data/ora.", + "clock-format-label": "Formato orologio", + "clock-style-analog": "Analogico", + "clock-style-custom": "Personalizzato", + "clock-style-description": "Scegli lo stile visivo dell’orologio nella schermata blocco.", + "clock-style-digital": "Digitale", + "clock-style-label": "Stile orologio", + "compact-lockscreen-description": "Mostra solo input di accesso e controlli di sistema, nascondendo i widget meteo e media.", + "compact-lockscreen-label": "Schermata blocco compatta", + "enable-lockscreen-media-controls-description": "Mostra i controlli interattivi di riproduzione multimediale sulla schermata di blocco.", + "enable-lockscreen-media-controls-label": "Controlli Multimediali della Schermata di Blocco", + "lock-on-suspend-description": "Blocca automaticamente lo schermo quando il sistema viene sospeso.", + "lock-on-suspend-label": "Blocca alla sospensione", + "lock-screen-animations-description": "Abilita o disabilita le animazioni della schermata blocco.", + "lock-screen-animations-label": "Animazioni schermata blocco", + "lock-screen-blur-strength-description": "Applica un effetto sfocatura allo sfondo della schermata blocco.", + "lock-screen-blur-strength-label": "Intensità sfocatura schermata blocco", + "lock-screen-tint-strength-description": "Applica una sovrapposizione colorata allo sfondo della schermata blocco.", + "lock-screen-tint-strength-label": "Intensità tinta schermata blocco", + "monitors-desc": "Mostra la schermata blocco su monitor specifici. Se non ne scegli nessuno, verranno usati tutti.", + "password-chars-description": "Icone carine usate per nascondere la password.", + "password-chars-label": "Icone password casuali", + "show-hibernate-description": "Mostra l’opzione 'ibernazione' nei controlli alimentazione.", + "show-hibernate-label": "Mostra ibernazione", + "show-session-buttons-description": "Consenti accesso alle impostazioni di alimentazione dalla schermata blocco.", + "show-session-buttons-label": "Controlli alimentazione", + "title": "Schermata blocco" + }, + "network": { + "bluetooth-description": "Attiva la gestione Bluetooth.", + "bluetooth-rssi-polling-description": "Campiona periodicamente l’RSSI dei dispositivi connessi tramite bluetoothctl. Potrebbe non essere disponibile per tutti i dispositivi; usa risorse minime quando attivo.", + "bluetooth-rssi-polling-label": "Polling segnale Bluetooth", + "desc": "Gestisci connessioni Wi-Fi e Bluetooth.", + "wifi-description": "Gestisci reti wireless (richiede NetworkManager)." + }, + "notifications": { + "duration-critical-urgency-description": "Per quanto tempo restano visibili le notifiche a priorità critica.", + "duration-critical-urgency-label": "Urgenza critica", + "duration-desc": "Configura per quanto tempo le notifiche restano visibili in base al livello di urgenza.", + "duration-low-urgency-description": "Per quanto tempo restano visibili le notifiche a priorità bassa.", + "duration-low-urgency-label": "Urgenza bassa", + "duration-normal-urgency-description": "Per quanto tempo restano visibili le notifiche a priorità normale.", + "duration-normal-urgency-label": "Urgenza normale", + "duration-respect-expire-description": "Usa il timeout di scadenza impostato nella notifica.", + "duration-respect-expire-label": "Rispetta timeout di scadenza", + "duration-title": "Durata notifiche", + "history-clear-dismiss-description": "Rimuove la notifica dalla cronologia quando viene ignorata.", + "history-clear-dismiss-label": "Cancella quando ignorata", + "history-critical-urgency-description": "Salva in cronologia le notifiche a priorità critica.", + "history-critical-urgency-label": "Salva in cronologia urgenza critica", + "history-desc": "Controlla quali notifiche vengono salvate in cronologia in base al livello di urgenza.", + "history-low-urgency-description": "Salva in cronologia le notifiche a priorità bassa.", + "history-low-urgency-label": "Salva in cronologia urgenza bassa", + "history-normal-urgency-description": "Salva in cronologia le notifiche a priorità normale.", + "history-normal-urgency-label": "Salva in cronologia urgenza normale", + "monitors-desc": "Mostra notifiche su monitor specifici. Se non ne scegli nessuno, verranno usati tutti.", + "rules-action-block": "Blocco", + "rules-action-block-desc": "Salta completamente.", + "rules-action-hide": "Nascondi", + "rules-action-hide-desc": "Nessun popup, nessun suono, aggiunge alla cronologia.", + "rules-action-label": "Azione", + "rules-action-mute": "Silenzia", + "rules-action-mute-desc": "Nessun suono, mostra comunque il pop-up e nella cronologia.", + "rules-add": "Aggiungi regola", + "rules-delete": "Elimina regola", + "rules-description": "Corrisponde al nome dell'app o al contenuto. Le regole vengono controllate in ordine e la prima corrispondenza viene applicata.", + "rules-edit": "Modifica regola", + "rules-label": "Regole di Filtro", + "rules-pattern-label": "Modello", + "rules-pattern-placeholder": "firefox, discord, o /regex/", + "rules-tab": "Regole", + "settings-always-on-top-description": "Mostra le notifiche sopra finestre fullscreen e altri layer.", + "settings-background-opacity-description": "Regola l’opacità degli sfondi notifica.", + "settings-density-description": "Scegli la densità delle schede notifica.", + "settings-density-label": "Densità", + "settings-desc": "Configura aspetto e comportamento delle notifiche.", + "settings-do-not-disturb-description": "Disabilita tutti i popup notifica quando attivo.", + "settings-enabled-description": "Abilita o disabilita il demone notifiche; richiede riavvio della shell Noctalia.", + "settings-enabled-label": "Abilita notifiche", + "settings-location-description": "Dove appaiono le notifiche sullo schermo.", + "settings-markdown-description": "Renderizza il contenuto delle notifiche usando formattazione Markdown.", + "settings-markdown-label": "Abilita Markdown", + "sounds-desc": "Configura effetti sonori notifiche e volume.", + "sounds-enabled-description": "Abilita effetti sonori per le notifiche in arrivo.", + "sounds-enabled-label": "Abilita suoni notifiche", + "sounds-excluded-apps-description": "Salta la riproduzione del suono notifica configurato per applicazioni specifiche che hanno già suoni integrati.", + "sounds-excluded-apps-label": "Applicazioni escluse", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Percorso del file audio riprodotto per notifiche a priorità critica.", + "sounds-files-critical-label": "Suono urgenza critica", + "sounds-files-critical-select-title": "Seleziona file suono urgenza critica", + "sounds-files-desc": "Configura file audio personalizzati per diversi livelli di urgenza notifica.", + "sounds-files-low-description": "Percorso del file audio riprodotto per notifiche a priorità bassa.", + "sounds-files-low-label": "Suono urgenza bassa", + "sounds-files-low-select-title": "Seleziona file suono urgenza bassa", + "sounds-files-normal-description": "Percorso del file audio riprodotto per notifiche a priorità normale.", + "sounds-files-normal-label": "Suono urgenza normale", + "sounds-files-normal-select-title": "Seleziona file suono urgenza normale", + "sounds-files-placeholder": "Inserisci percorso file audio", + "sounds-files-select-file": "Seleziona file audio", + "sounds-files-title": "File audio", + "sounds-files-unified-description": "Percorso del file audio riprodotto per le notifiche.", + "sounds-files-unified-label": "Suono notifica", + "sounds-files-unified-select-title": "Seleziona file suono notifica", + "sounds-separate-description": "Usa file audio diversi per notifiche a priorità bassa, normale e critica.", + "sounds-separate-label": "Usa suoni diversi per priorità", + "sounds-title": "Impostazioni suono", + "sounds-unavailable-description": "Installa Qt6 Multimedia per abilitare i suoni notifica.", + "sounds-unavailable-label": "Suoni notifiche non disponibili", + "sounds-volume-description": "Regola il livello volume per i suoni notifica.", + "sounds-volume-label": "Volume suono", + "toast-battery-description": "Mostra un avviso quando il livello batteria scende sotto questa percentuale.", + "toast-battery-label": "Avviso batteria", + "toast-desc": "Configura aspetto e comportamento dei toast.", + "toast-keyboard-description": "Mostra un toast quando cambia il layout tastiera.", + "toast-keyboard-label": "Layout tastiera", + "toast-media-description": "Mostra un toast quando cambia lo stato riproduzione media.", + "toast-media-label": "Media" + }, + "osd": { + "always-on-top-description": "Mostra OSD sopra finestre fullscreen e altri layer.", + "always-on-top-label": "Sempre in primo piano", + "background-opacity-description": "Controlla la trasparenza dello sfondo OSD.", + "background-opacity-label": "Opacità sfondo", + "description": "Configura indicatori a schermo come overlay volume e luminosità.", + "duration-auto-hide-description": "Regola il tempo prima che OSD scompaia.", + "duration-auto-hide-label": "Nascondi automaticamente dopo", + "duration-desc": "Per quanto tempo OSD resta visibile prima di nascondersi automaticamente.", + "duration-title": "Timeout nascondimento automatico", + "enabled-description": "Mostra in tempo reale le variazioni di volume e luminosità.", + "enabled-label": "Abilita on-screen display", + "events-desc": "Seleziona quali eventi attivano OSD.", + "general-desc": "Configura visibilità e comportamento di OSD.", + "location-description": "Dove appaiono gli indicatori a schermo.", + "monitors-desc": "Mostra OSD su monitor specifici. Se non ne scegli nessuno, verranno usati tutti.", + "title": "On-screen display", + "types-brightness-description": "Mostra OSD quando cambia la luminosità schermo.", + "types-brightness-label": "Luminosità", + "types-custom-text-description": "Mostra OSD per messaggi testuali personalizzati da IPC.", + "types-custom-text-label": "Testo personalizzato", + "types-desc": "Seleziona gli eventi che attivano OSD.", + "types-input-volume-description": "Mostra OSD quando cambia il volume microfono.", + "types-input-volume-label": "Volume input", + "types-lockkey-description": "Mostra OSD quando Caps Lock, Num Lock o Scroll Lock vengono attivati/disattivati.", + "types-lockkey-label": "Tasti lock", + "types-media-description": "Mostra OSD quando cambia lo stato riproduzione media (play, pausa, salta).", + "types-media-label": "Riproduzione media", + "types-title": "Eventi attivazione OSD", + "types-volume-description": "Mostra OSD quando cambia il volume output audio.", + "types-volume-label": "Volume output" + }, + "plugins": { + "auto-update": "Aggiorna automaticamente plugin", + "auto-update-description": "Aggiorna automaticamente tutti i plugin all’avvio della shell.", + "available-description": "Sfoglia e installa plugin dalle sorgenti configurate.", + "available-label": "Plugin disponibili", + "available-no-plugins-description": "Controlla le sorgenti plugin o aggiorna l’elenco.", + "available-no-plugins-label": "Nessun plugin disponibile", + "check-for-updates": "Controlla aggiornamenti", + "checking-for-updates": "Controllo aggiornamenti...", + "collision-already-installed": "Questo plugin è già installato", + "collision-custom-version-exists": "È già installata una versione personalizzata da \"{source}\"", + "collision-official-version-exists": "La versione ufficiale di questo plugin è già installata", + "development-disable": "Disabilita modalità sviluppo per questo plugin", + "development-enable": "Abilita modalità sviluppo per questo plugin", + "filter-downloaded": "Scaricati", + "filter-not-downloaded": "Non scaricati", + "filter-tags-description": "Filtra plugin per categoria o stato download.", + "filter-tags-label": "Tag", + "hot-reload-description": "Ricarica automaticamente i plugin quando i loro file cambiano. Utile per sviluppo plugin.", + "hot-reload-label": "Hot reload (modalità dev)", + "hot-reloaded": "Plugin ricaricato: {name}", + "install-error": "Installazione non riuscita: {error}", + "install-incompatible": "{plugin} richiede Noctalia v{version} o superiore", + "install-success": "{plugin} installato con successo", + "installed-description": "Gestisci e configura tutti i plugin installati localmente.", + "installed-label": "Plugin installati", + "installed-no-plugins-description": "Installa plugin dalla sezione \"Disponibili\".", + "installed-no-plugins-label": "Nessun plugin installato", + "installing": "Installazione {plugin}...", + "notify-updates": "Notifiche di aggiornamento dei plugin", + "notify-updates-description": "Mostra un popup di notifica quando sono disponibili nuovi aggiornamenti per i plugin.", + "open-plugin-page": "Apri pagina plugin", + "open-plugins-tab": "Apri impostazioni plugin", + "plugin-settings-title": "Impostazioni {plugin}", + "refresh-refreshing": "Aggiornamento elenco plugin...", + "refresh-tooltip": "Aggiorna plugin disponibili", + "settings-error-not-loaded": "Plugin non caricato", + "settings-saved": "Impostazioni plugin salvate", + "settings-tooltip": "Impostazioni plugin", + "source-custom": "Sorgente personalizzata", + "sources-add-custom": "Aggiungi repository personalizzato", + "sources-add-dialog-description": "Aggiungi un repository GitHub come sorgente plugin.", + "sources-add-dialog-error": "Aggiunta sorgente plugin non riuscita", + "sources-add-dialog-name": "Nome repository", + "sources-add-dialog-name-placeholder": "I miei plugin personalizzati", + "sources-add-dialog-success": "Sorgente plugin aggiunta con successo", + "sources-add-dialog-title": "Aggiungi sorgente plugin", + "sources-add-dialog-url": "URL repository", + "sources-description": "Gestisci repository plugin.", + "sources-placeholder": "Il mio fantastico repository", + "sources-remove-tooltip": "Rimuovi sorgente plugin", + "title": "Plugin", + "translations-reloaded": "Traduzioni ricaricate: {name}", + "uninstall-dialog-description": "Sei sicuro di voler disinstallare {plugin}? Verranno rimossi tutti i dati del plugin.", + "uninstall-dialog-title": "Disinstalla plugin", + "uninstall-error": "Disinstallazione non riuscita: {error}", + "uninstall-success": "{plugin} disinstallato con successo", + "uninstalling": "Disinstallazione {plugin}...", + "update-all": "Aggiorna tutti ({count})", + "update-all-success": "Tutti i plugin aggiornati con successo", + "update-available": "Nuovo aggiornamento plugin disponibile", + "update-available-plural": "Aggiornamenti plugin disponibili ({count})", + "update-error": "Aggiornamento plugin non riuscito: {plugin}: {error}", + "update-pending": "v{current} → v{new} (richiede Noctalia v{required})", + "update-success": "{plugin} aggiornato a v{version}", + "update-version": "v{current} → v{new}", + "updating": "Aggiornamento..." + }, + "region": { + "title": "Regione" + }, + "session-menu": { + "countdown-duration-description": "Imposta quanto dura il timer conto alla rovescia prima di eseguire azioni di alimentazione.", + "countdown-duration-label": "Durata conto alla rovescia", + "enable-countdown-description": "Mostra un timer conto alla rovescia prima di eseguire azioni di alimentazione.", + "enable-countdown-label": "Abilita timer conto alla rovescia", + "entries-desc": "Personalizza quali azioni di alimentazione appaiono nel menu sessione e in quale ordine.", + "entries-title": "Azioni alimentazione", + "entry-settings-command-description": "Comando personalizzato da eseguire per questa azione. Lascia vuoto per usare il comando di sistema predefinito.", + "entry-settings-command-placeholder": "es. systemctl poweroff", + "entry-settings-default-command-lock": "Schermata blocco interna (nessun comando)", + "entry-settings-default-command-logout": "Logout interno (nessun comando)", + "entry-settings-default-info-description": "Se non viene specificato un comando personalizzato, verrà usato il comando di sistema predefinito.", + "entry-settings-default-info-label": "Comando predefinito", + "entry-settings-keybind-description": "Assegna una combinazione tasti per attivare questa azione quando il menu sessione è aperto.", + "entry-settings-keybind-placeholder": "Premi Registra per assegnare un tasto...", + "entry-settings-keybind-recording": "Registrazione... Premi qualsiasi combinazione tasti", + "entry-settings-title": "Configura {entry}", + "entry-settings-tooltip": "Configura comando", + "general-desc": "Configura comportamento e aspetto del pannello menu sessione.", + "large-buttons-layout-description": "Scegli come vengono mostrati i pulsanti grandi del menu sessione.", + "large-buttons-layout-label": "Layout pulsanti grandi", + "large-buttons-style-description": "Mostra il menu sessione con pulsanti grandi in layout a griglia.", + "large-buttons-style-label": "Stile pulsanti grandi", + "position-description": "Scegli dove appare il pannello menu sessione quando viene aperto.", + "show-header-description": "Mostra titolo e pulsante chiusura in alto nel menu sessione.", + "show-header-label": "Mostra intestazione", + "show-keybinds-description": "Mostra suggerimenti scorciatoie nelle opzioni sessione.", + "show-keybinds-label": "Mostra scorciatoie" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Mantenere visibili i widget del desktop mentre la modalità prestazioni Noctalia è abilitato.", + "noctalia-performance-disable-desktop-widgets-label": "Abilita i widget desktop in modalità prestazioni", + "noctalia-performance-disable-wallpaper-description": "Mantieni visibili gli sfondi del desktop, della panoramica e della schermata di blocco mentre la modalità prestazioni Noctalia è abilitato.", + "noctalia-performance-disable-wallpaper-label": "Abilita il rendering dello sfondo in modalità prestazioni", + "title": "Sistema" + }, + "system-monitor": { + "critical-color-label": "Colore critico", + "custom-highlight-colors-title-label": "Colori evidenziazione personalizzati", + "disk-available-label": "Disco disponibile", + "disk-section-label": "Utilizzo disco", + "enable-dgpu-monitoring-description": "Attenzione: questo riattiverà la GPU discreta (NVIDIA/AMD), il che può influire significativamente sulla durata batteria nei portatili con grafica ibrida.", + "enable-dgpu-monitoring-label": "Abilita monitoraggio GPU discreta", + "external-monitor-description": "Inserisci il comando o il percorso dell’applicazione da avviare quando attivi il monitor di sistema esterno.", + "external-monitor-label": "Comando monitor di sistema esterno", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "Configura comportamento e aspetto del monitor di sistema.", + "gpu-section-label": "Temperatura GPU", + "highlight-colors-section-label": "Colori evidenziazione", + "polling-interval-label": "Intervallo polling", + "polling-section-description": "Configura la frequenza di aggiornamento di ogni metrica di sistema.", + "polling-section-label": "Intervalli polling", + "threshold-critical": "Critico", + "threshold-warning": "Avviso", + "thresholds-section-description": "Regola soglie avviso/critiche per ogni metrica di sistema.", + "use-custom-highlight-colors-description": "Quando disattivato, vengono usati i colori evidenziazione predefiniti del tema.", + "use-custom-highlight-colors-label": "Usa colori evidenziazione personalizzati", + "warning-color-label": "Colore avviso" + }, + "user-interface": { + "allow-panels-without-bar-description": "Se attivo, i pannelli possono aprirsi su qualsiasi schermo. Se disattivo, i pannelli si aprono solo sugli schermi con barra, riducendo l’uso memoria.", + "allow-panels-without-bar-label": "Consenti pannelli su schermi senza barra", + "animation-disable-description": "Disabilita tutte le animazioni per un’esperienza più veloce e reattiva.", + "animation-disable-label": "Disabilita animazioni UI", + "animation-speed-description": "Regola la velocità globale delle animazioni.", + "animation-speed-label": "Velocità animazione", + "animation-speed-reset": "Reimposta velocità animazione", + "appearance-desc": "Personalizza elementi visivi come tooltip, bordi e ombre.", + "blur-behind-description": "Sfoca l'area dietro le barre e i pannelli utilizzando il protocollo di sfocatura del compositore.", + "blur-behind-label": "Sfocatura dello sfondo", + "box-border-description": "Mostra un contorno attorno alle aree contenuto.", + "box-border-label": "Contorno contenitore", + "box-border-radius-description": "Regola l’arrotondamento degli angoli delle principali sezioni layout, come sidebar, schede e pannelli contenuto.", + "box-border-radius-label": "Raggio contenitore", + "control-border-radius-description": "Controlla la curvatura degli elementi interattivi, inclusi pulsanti, interruttori e campi testo.", + "control-border-radius-label": "Raggio input", + "desc": "Personalizza aspetto, stile e comportamento dell’interfaccia.", + "dim-desktop-description": "Oscura il desktop quando pannelli o menu sono aperti.", + "dim-desktop-label": "Oscura desktop", + "dimmer-opacity-description": "Imposta il livello di opacità dell’oscuramento desktop.", + "dimmer-opacity-label": "Opacità desktop oscurato", + "panel-background-opacity-description": "Imposta l’opacità sfondo per tutti i pannelli (barra, launcher, impostazioni, ecc...).", + "panel-background-opacity-label": "Opacità sfondo pannelli", + "panels-attached-to-bar-description": "I pannelli si agganciano alla barra e ai bordi schermo, creando un aspetto continuo con eleganti angoli invertiti.", + "panels-attached-to-bar-label": "Aggancia pannelli ai bordi", + "panels-overlay-description": "Assicura che pannelli e barra restino visibili anche sopra applicazioni fullscreen.", + "panels-overlay-label": "Mantieni pannelli e barra in primo piano", + "scaling-description": "Cambia la dimensione dell’interfaccia utente generale, esclusa la barra.", + "scaling-label": "Scala interfaccia", + "scrollbar-always-visible-description": "Mantieni le barre di scorrimento visibili ogni volta che il contenuto è scorrevole, invece di mostrarle solo al passaggio del mouse.", + "scrollbar-always-visible-label": "Mostra sempre le barre di scorrimento", + "settings-panel-header": "Pannello impostazioni", + "settings-panel-mode-description": "Scegli layout impostazioni (potrebbe richiedere riapertura).", + "settings-panel-mode-label": "Modalità pannello impostazioni", + "settings-panel-sidebar-card-style-description": "Avvolge la barra laterale delle impostazioni in uno sfondo pieno con angoli arrotondati.", + "settings-panel-sidebar-card-style-label": "Stile barra laterale", + "shadows-description": "Abilita ombre esterne sotto barre e pannelli.", + "shadows-direction-description": "Scegli la direzione in cui viene proiettata l’ombra.", + "shadows-direction-label": "Direzione ombra", + "shadows-label": "Ombre esterne", + "title": "Interfaccia utente", + "tooltips-description": "Abilita o disabilita i tooltip in tutta l’interfaccia.", + "tooltips-label": "Mostra tooltip", + "translucent-widgets-description": "Rendi semitrasparenti i pulsanti, le schede e gli altri widget all'interno dei pannelli.", + "translucent-widgets-label": "Widget traslucidi" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Alfabetico", + "automation-change-mode-description": "Scegli come selezionare gli sfondi durante il cambio automatico.", + "automation-change-mode-label": "Modalità cambio", + "automation-custom-interval-description": "Inserisci il tempo come HH:MM (es. 01:30).", + "automation-custom-interval-label": "Intervallo personalizzato", + "automation-interval-description": "Quanto spesso cambiare automaticamente gli sfondi.", + "automation-interval-label": "Intervallo sfondo", + "automation-random-wallpaper-description": "Pianifica cambi sfondo casuali a intervalli regolari.", + "automation-scheduled-change-description": "Cambia automaticamente gli sfondi a intervalli regolari.", + "automation-scheduled-change-label": "Cambio pianificato", + "look-feel-edge-smoothness-description": "Applica un effetto morbido e sfumato al bordo delle transizioni.", + "look-feel-edge-smoothness-label": "Ammorbidisci bordo transizione", + "look-feel-fill-color-description": "Scegli un colore di riempimento che potrebbe apparire dietro lo sfondo.", + "look-feel-fill-mode-description": "Seleziona come l’immagine deve scalare per adattarsi alla risoluzione del monitor.", + "look-feel-fill-mode-label": "Modalità riempimento", + "look-feel-skip-startup-transition-description": "Salta l’animazione sfondo all’avvio della shell.", + "look-feel-skip-startup-transition-label": "Salta transizione avvio", + "look-feel-title": "Aspetto e stile", + "look-feel-transition-duration-description": "Durata delle animazioni di transizione in secondi.", + "look-feel-transition-duration-label": "Durata transizione", + "look-feel-transition-type-description": "Tipo animazione durante il passaggio tra sfondi.", + "look-feel-transition-type-label": "Tipo transizione", + "settings-clear-cache-button": "Svuota cache", + "settings-clear-cache-description": "Cancella gli sfondi ridimensionati in cache per liberare spazio su disco.", + "settings-clear-cache-label": "Cache degli sfondi", + "settings-clear-cache-toast": "Cache dello sfondo cancellata", + "settings-desc": "Controlla come gli sfondi vengono gestiti e visualizzati.", + "settings-enable-management-description": "Gestisci gli sfondi con Noctalia. Disattiva se preferisci usare un’altra applicazione.", + "settings-enable-management-label": "Abilita gestione sfondi", + "settings-enable-overview-description": "Applica uno sfondo sfocato e attenuato alla schermata overview.", + "settings-enable-overview-label": "Abilita sfondo overview", + "settings-folder-description": "Percorso della tua cartella sfondi principale.", + "settings-folder-label": "Cartella sfondi", + "settings-hide-wallpaper-filenames-tooltip-hide": "Nascondi nomi file", + "settings-hide-wallpaper-filenames-tooltip-show": "Mostra nomi file", + "settings-monitor-specific-description": "Imposta una cartella sfondi diversa per ogni monitor.", + "settings-monitor-specific-label": "Cartelle specifiche per monitor", + "settings-monitor-specific-tooltip": "Cartella sfondi monitor", + "settings-overview-blur-strength-description": "Applica intensità sfocatura all’overview.", + "settings-overview-blur-strength-label": "Intensità sfocatura overview", + "settings-overview-tint-description": "Applica intensità tinta all’overview.", + "settings-overview-tint-label": "Intensità tinta overview", + "settings-recursive-search-description": "Cerca sfondi anche nelle sottocartelle della cartella sfondi.", + "settings-recursive-search-label": "Cerca nelle sottocartelle", + "settings-select-monitor-folder": "Seleziona cartella sfondi monitor", + "settings-selector-description": "Scegli il tuo sfondo.", + "settings-selector-position-description": "Scegli dove appare il pannello selettore sfondo.", + "settings-show-hidden-files-tooltip-hide": "Nascondi file nascosti", + "settings-show-hidden-files-tooltip-show": "Mostra file nascosti", + "settings-title": "Impostazioni sfondo", + "settings-use-original-images-description": "Salta il ridimensionamento degli sfondi prima della visualizzazione. Risparmia spazio su disco e riduce l'utilizzo della CPU durante i cambi di sfondo, ma potrebbe usare più memoria per immagini molto grandi.", + "settings-use-original-images-label": "Usa le immagini originali", + "settings-view-mode-description": "Scegli come visualizzare gli sfondi dalla tua cartella.", + "settings-view-mode-label": "Modalità visualizzazione", + "view-mode-browse": "Sfoglia cartelle", + "view-mode-cycle-tooltip": "Modalità visualizzazione: {mode} (clic per cambiare)", + "view-mode-recursive": "Sottocartelle appiattite", + "view-mode-single": "Cartella radice" + } + }, + "placeholders": { + "add-new-keybind": "Aggiungi una nuova scorciatoia", + "command-example": "echo \"Ciao Mondo\"", + "enter-command": "Inserisci un comando da eseguire (app o script personalizzato)", + "enter-ipc-identifier": "Inserisci un identificatore univoco per i comandi IPC", + "enter-path": "Inserisci percorso...", + "enter-text-to-collapse": "es. 'non è in riproduzione'. Usa /regex/ per i pattern.", + "enter-tooltip": "Inserisci tooltip", + "enter-width-pixels": "Inserisci larghezza in pixel", + "keybind-recording": "Registrazione scorciatoia...", + "search": "Cerca...", + "search-icons": "es. noctalia, niri, batteria, nuvola", + "search-launcher": "Cerca voci... oppure usa > per i comandi", + "search-wallhaven": "Cerca su Wallhaven...", + "search-wallpapers": "Digita per filtrare gli sfondi..." + }, + "positions": { + "bottom": "In basso", + "bottom-center": "In basso al centro", + "bottom-left": "In basso a sinistra", + "bottom-right": "In basso a destra", + "center": "Centro", + "center-left": "Centro sinistra", + "center-right": "Centro destra", + "close-to-bar": "Vicino al pulsante della barra", + "follow-bar": "Segui la barra", + "left": "Sinistra", + "right": "Destra", + "top": "In alto", + "top-center": "In alto al centro", + "top-left": "In alto a sinistra", + "top-right": "In alto a destra" + }, + "session-menu": { + "action-in-seconds": "{action} tra {seconds} secondi...", + "cancel-timer": "Annulla timer", + "title": "Menu sessione" + }, + "setup": { + "all-done": "Tutto fatto!", + "appearance": { + "subheader": "Scegli la modalità scura e le sorgenti colore (sfondo o predefinite)." + }, + "customize": { + "header": "Personalizza la tua esperienza", + "subheader": "Regola posizione barra, densità, scala e altro." + }, + "skip-setup": "Salta configurazione", + "telemetry-wizard-done": "Capito!", + "telemetry-wizard-note": "Hai il controllo — abilita o disabilita questa opzione in qualsiasi momento nelle impostazioni", + "telemetry-wizard-subtitle": "Abbiamo aggiunto analisi anonime per migliorare Noctalia", + "telemetry-wizard-title": "Aggiornamento privacy", + "wallpaper": { + "choose-dir": "Scegli una cartella che contenga le immagini degli sfondi", + "dir-browse": "Sfoglia cartella sfondi", + "dir-description": "Scegli la cartella che contiene i tuoi sfondi.", + "dir-label": "Cartella sfondi", + "dir-select-title": "Seleziona cartella sfondi", + "header": "Scegli il tuo sfondo", + "no-dir": "Nessuna cartella sfondi selezionata", + "no-valid": "Nessun file immagine valido trovato in: {dir}", + "none-in-dir": "Nessuno sfondo trovato nella cartella", + "preview-error": "Impossibile caricare l'immagine", + "select-prompt": "Seleziona uno sfondo qui sotto", + "subheader": "Imposta l’atmosfera con uno sfondo bellissimo." + }, + "welcome": "Benvenuto", + "welcome-note": "Solo poche basi per iniziare: tutte le opzioni sono nelle impostazioni", + "welcome-subtitle": "Rendiamo il tuo desktop davvero tuo", + "welcome-title": "Benvenuto in Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "Nessun lettore multimediale rilevato", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "App sconosciuta", + "uptime": "Attivo da: {uptime}", + "user-requested": "Richiesto dall'utente", + "welcome-back": "Bentornato,", + "widget-settings-title": "Impostazioni {widget}" + }, + "system-monitor": { + "core-usage": "Utilizzo del core {id}", + "cpu-temp": "Temperatura CPU", + "cpu-usage": "Uso CPU", + "disk": "Disco", + "download-speed": "Velocità download", + "gpu-temp": "Temperatura GPU", + "load-average": "Carico medio", + "title": "Monitor di sistema", + "upload-speed": "Velocità upload" + }, + "toast": { + "airplane-mode": { + "description": "Disabilita tutte le comunicazioni wireless.", + "title": "Modalità aereo" + }, + "battery": { + "critical": "Batteria critica", + "critical-desc": "Batteria al {percent}% — collega subito il caricatore", + "low": "Batteria scarica", + "low-desc": "Batteria al {percent}% — collega il caricatore" + }, + "bluetooth": { + "address-copied": "Address copied to clipboard", + "auto-connect-disabled": "Connessione automatica disabilitata", + "auto-connect-enabled": "Connessione automatica abilitata", + "auto-connecting": "Connessione a {count} dispositivo/i...", + "confirm-code": "Confirm code {value} on the other device.", + "connect-failed": "Failed to connect to device", + "disconnect-failed": "Failed to disconnect from device", + "discoverable-change-failed": "Failed to change discoverable state", + "discoverable-disabled": "Discoverable disabled", + "discoverable-enabled": "Discoverable enabled", + "display-code": "Enter code {value} on the other device.", + "forget-failed": "Failed to forget device", + "pair-failed": "Failed to pair device", + "passkey-required": "Passkey required by the device.", + "pincode-required": "PIN code required by the device.", + "state-change-failed": "Failed to change Bluetooth state" + }, + "clipboard": { + "long-text": "Long text", + "unavailable": "Clipboard history unavailable", + "unavailable-desc": "The 'cliphist' application is not installed. Please install it to use clipboard history features" + }, + "custom-command-failed": { + "description": "Comando fallito: {command}\\nCodice di uscita: {code}", + "title": "Comando personalizzato fallito" + }, + "do-not-disturb": { + "disabled": "'Non disturbare' disattivato", + "disabled-desc": "Showing all notifications", + "enabled": "'Non disturbare' attivato", + "enabled-desc": "You'll find these notifications in your history" + }, + "donation-opened": "Donation page opened in your browser", + "internet-limited": "Connected without internet", + "keyboard-layout": { + "changed": "Keyboard layout changed to {layout}", + "title": "Keyboard" + }, + "missing-control-center": { + "description": "The control center widget has been removed from the bar. To access it from the bar again, you will need to re-add the widget. You can open it with right clicking on the bar too", + "label": "Last control center widget removed" + }, + "night-light": { + "forced": "Forced activation", + "normal": "Normal mode", + "not-installed": "wlsunset not installed" + }, + "noctalia-performance": { + "disabled": "Modalità prestazioni disabilitata", + "enabled": "Modalità prestazioni abilitata", + "label": "Noctalia Prestazioni" + }, + "power-profile": { + "changed": "Power profile changed", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "Encountered an error while processing templates", + "title": "Templating processing failed" + }, + "vpn": { + "connected": "Connected to '{name}'", + "disconnected": "Disconnected from '{name}'" + }, + "wallpaper-colors": { + "disabled": "Wallpaper colors disabled", + "enabled": "Wallpaper colors enabled", + "label": "Wallpaper colors" + }, + "wifi": { + "connected": "Connected to '{ssid}'", + "connection-failed": "Connection failed", + "connection-timeout": "Connection timeout", + "disconnected": "Disconnected from '{ssid}'", + "incorrect-password": "Incorrect password", + "network-not-found": "Network not found" + } + }, + "tooltips": { + "add-widget": "Aggiungi widget", + "auto-connect": "Attiva/disattiva la connessione automatica per questo dispositivo", + "bluetooth-auto-connect-off": "Connessione automatica disattivata", + "bluetooth-auto-connect-on": "Connessione automatica attiva", + "bluetooth-devices": "Dispositivi Bluetooth", + "brightness-at": "Luminosità: {brightness}%", + "click-to-start-recording": "Registratore schermo (avvia registrazione)", + "click-to-stop-recording": "Registratore schermo (ferma registrazione)", + "collapse": "Comprimi barra laterale", + "copy-address": "Copia indirizzo", + "delete-notification": "Elimina notifica", + "dismiss-notification": "Ignora notifica", + "do-not-disturb-enabled": "Non disturbare", + "expand": "Espandi barra laterale", + "forget-network": "Dimentica rete", + "grid-view": "Vista a griglia", + "hidden-files-hide": "File nascosti", + "hide-unnamed-devices": "Nascondi dispositivi senza nome", + "home": "Cartella home", + "input-muted": "Attiva/disattiva muto input", + "keep-awake": "Mantieni attivo", + "keyboard-layout": "Layout tastiera {layout}", + "list-view": "Vista elenco", + "manage-vpn": "Connessioni VPN", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "Numero massimo di widget raggiunto", + "microphone-volume-at": "Volume microfono: {volume}%", + "move-to-section": "Sposta in {section}", + "mute": "Muto", + "next-media": "Brano successivo", + "next-month": "Mese successivo", + "night-light-not-installed": "Luce notturna (non disponibile)", + "noctalia-performance-enabled": "Modalità prestazioni Noctalia", + "open-annotation-tool": "Apri con strumento annotazioni", + "open-control-center": "Centro di controllo", + "open-notification-history-enable-dnd": "Cronologia notifiche", + "open-settings": "Impostazioni", + "open-tray-dropdown": "Area di notifica", + "output-muted": "Attiva/disattiva muto output", + "power-profile": "Profilo energetico {profile}", + "previous-media": "Brano precedente", + "previous-month": "Mese precedente", + "refresh-devices": "Aggiorna dispositivi", + "refresh-wallhaven": "Aggiorna risultati Wallhaven", + "refresh-wallpaper-list": "Aggiorna elenco sfondi", + "remove-widget": "Rimuovi widget", + "search-close": "Chiudi ricerca", + "session-menu": "Menu sessione", + "show-all-devices": "Mostra tutti i dispositivi", + "switch-to-dark-mode": "Modalità scura", + "switch-to-light-mode": "Modalità chiara", + "unmute": "Riattiva audio", + "up": "Cartella superiore", + "volume-at": "Volume uscita: {volume}%", + "wallpaper-selector": "Selettore sfondi" + }, + "wallpaper": { + "browse": { + "empty-directory": "Questa cartella è vuota.", + "go-root": "Vai alla cartella radice degli sfondi", + "go-up": "Vai alla cartella superiore", + "go-up-hint": "Usa il pulsante indietro per salire di livello." + }, + "configure-directory": "Configura la cartella degli sfondi con le immagini.", + "fill-modes": { + "crop": "Ritaglia (Riempi)", + "fit": "Adatta (Contieni)", + "repeat": "Ripeti (Affianca)", + "stretch": "Allunga" + }, + "no-match": "Nessuna corrispondenza trovata.", + "no-wallpaper": "Nessuno sfondo trovato.", + "panel": { + "apikey-help": "Una chiave API è richiesta per accedere ai contenuti NSFW.", + "apikey-label": "Chiave API", + "apikey-managed-by-env": "Gestita tramite la variabile d’ambiente NOCTALIA_WALLHAVEN_API_KEY.", + "apikey-placeholder": "Inserisci la tua chiave API Wallhaven", + "appearance-dark-tab": "Scuro", + "appearance-light-tab": "Chiaro", + "categories-anime": "Anime", + "categories-label": "Categorie", + "categories-people": "Persone", + "color-extraction-disabled": "Usa estrazione colori dallo sfondo", + "color-extraction-enabled": "Usa schemi colore predefiniti", + "header-devices-apply-all-tooltip": "Scegli sfondo per monitor", + "header-devices-per-monitor-tooltip": "Applica a tutti i monitor", + "header-link-light-dark-tooltip": "Mantieni lo sfondo quando cambi tema", + "header-separate-light-dark-tooltip": "Cambia automaticamente lo sfondo con il tema", + "header-sun-linked-tooltip": "Separare sfondi chiari e scuri", + "header-sun-separate-tooltip": "Stesso sfondo per chiaro e scuro", + "order-asc": "Crescente", + "order-desc": "Decrescente", + "order-label": "Ordine", + "purity-label": "Filtro contenuti", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Sketchy", + "ratios-all-portrait": "Tutti verticali", + "ratios-all-wide": "Tutti orizzontali", + "ratios-any": "Qualsiasi", + "ratios-label": "Rapporto d’aspetto", + "resolution-atleast": "Almeno", + "resolution-exact": "Esatta", + "resolution-label": "Risoluzione", + "resolution-mode-label": "Modalità", + "solid-color-tooltip": "Sfondo a colore uniforme", + "sort-date-asc": "Ordina dal più vecchio al più recente", + "sort-date-desc": "Ordina dal più recente al più vecchio", + "sort-name-asc": "Ordina per nome (A-Z)", + "sort-name-desc": "Ordina per nome (Z-A)", + "sort-random": "Ordina casualmente", + "sorting-date-added": "Data aggiunta", + "sorting-favorites": "Preferiti", + "sorting-label": "Ordina per", + "sorting-relevance": "Rilevanza", + "sorting-toplist": "Top list", + "sorting-views": "Visualizzazioni", + "source-label": "Sorgente", + "source-wallhaven": "Wallhaven", + "title": "Selettore sfondi", + "wallhaven-settings-title": "Impostazioni Wallhaven" + }, + "transitions": { + "disc": "Disco", + "fade": "Dissolvenza", + "honeycomb": "Nido d’ape", + "pixelate": "Pixelato", + "stripes": "Strisce", + "wipe": "Scorrimento" + }, + "try-different-search": "Prova una ricerca diversa.", + "wallhaven": { + "loading": "Caricamento sfondi...", + "no-results": "Nessuno sfondo trovato. Prova una ricerca diversa.", + "page": "{current} di {total}", + "page-prefix": "Pagina", + "page-suffix": "di {total}" + } + }, + "weather": { + "clear-sky": "Cielo sereno", + "drizzle": "Pioviggine", + "fog": "Nebbia", + "mainly-clear": "Prevalentemente sereno", + "overcast": "Coperto", + "partly-cloudy": "Parzialmente nuvoloso", + "rain-showers": "Rovesci di pioggia", + "snow": "Neve", + "thunderstorm": "Temporale" + }, + "widgets": { + "color-picker": { + "palette-description": "Scegli da un’ampia gamma di colori predefiniti.", + "palette-label": "Tavolozza", + "palette-theme-colors": "Accesso rapido alla tavolozza colori del tema.", + "title": "Selettore colore" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm in minuscolo", + "ampm-uppercase": "AM/PM in maiuscolo", + "common-12hour-time-minutes": "Ora a 12 ore con minuti", + "common-24hour-time-minutes": "Ora a 24 ore con minuti", + "common-24hour-time-seconds": "Ora a 24 ore con secondi", + "common-european-date": "Formato data europeo", + "common-iso-date": "Formato data ISO", + "common-us-date": "Formato data USA", + "common-weekday-date": "Giorno della settimana con data", + "common-weekday-month-day": "Giorno settimana, mese e giorno", + "day-abbreviated": "Nome giorno abbreviato", + "day-full": "Nome giorno completo", + "day-leading-zero": "Giorno con zero iniziale (01-31)", + "day-no-leading-zero": "Giorno senza zero iniziale (1-31)", + "hour-leading-zero": "Ora con zero iniziale (00-23) — formato 24 ore", + "hour-no-leading-zero": "Ora senza zero iniziale (0-23) — formato 24 ore", + "minute-leading-zero": "Minuto con zero iniziale (00-59)", + "minute-no-leading-zero": "Minuto senza zero iniziale (0-59)", + "month-abbreviated": "Nome mese abbreviato", + "month-full": "Nome mese completo", + "month-number-leading-zero": "Mese come numero con zero iniziale (01-12)", + "month-number-no-zero": "Mese come numero senza zero iniziale (1-12)", + "second-leading-zero": "Secondo con zero iniziale (00-59)", + "second-no-leading-zero": "Secondo senza zero iniziale (0-59)", + "timezone-abbreviation": "Abbreviazione fuso orario", + "year-four-digit": "Anno a quattro cifre", + "year-two-digit": "Anno a due cifre (00-99)" + }, + "file-picker": { + "item": "elemento", + "items": "elementi", + "search-placeholder": "Cerca file e cartelle...", + "select-current": "Seleziona corrente", + "select-file": "Seleziona file", + "select-folder": "Seleziona cartella", + "selected": "Selezionato:", + "title": "Selettore file" + }, + "file-picker-title": "Seleziona un file", + "icon-picker": { + "title": "Selettore icone" + }, + "tooltip-placeholder": "Segnaposto" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "Identità Anonima", + "ca-cert": "Certificato CA", + "eap-method": "Metodo EAP", + "password": "Password Utente", + "phase2-auth": "Autenticazione Fase 2", + "username": "Identità / Nome Utente" + }, + "panel": { + "action-required": "Azione richiesta", + "add-hidden-network": "Aggiungi rete nascosta", + "add-network": "Aggiungi rete", + "available-interfaces": "Interfacce disponibili", + "available-networks": "Reti disponibili", + "disabled": "Wi‑Fi disattivato", + "disconnecting": "Disconnessione in corso...", + "dns": "DNS", + "enable-message": "Attiva il Wi‑Fi per vedere le reti disponibili.", + "enter-password": "Inserisci password Wi‑Fi...", + "forget": "Dimentica", + "forget-network": "Dimentica questa rete", + "forgetting": "Dimenticando...", + "hidden-network": "Rete nascosta", + "interface": "Interfaccia di rete", + "internet-connected": "Internet connesso", + "internet-limited": "Nessun accesso a Internet", + "internet-status": "Stato Internet", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "Reti conosciute", + "link-speed": "Velocità collegamento", + "network-name-ssid": "Nome rete (SSID)", + "no-ethernet-devices": "Nessun dispositivo Ethernet rilevato", + "no-networks": "Nessuna rete Wi‑Fi trovata", + "saved": "Salvata", + "scan-again": "Scansiona di nuovo", + "searching": "Ricerca reti in corso...", + "security-open": "Apri", + "security-wep": "WEP", + "security-wpa": "WPA/WPA2/WPA3 Personale", + "security-wpa-ent": "WPA Enterprise", + "security-wpa2-ent": "WPA2 Enterprise", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 Enterprise", + "show-password": "Mostra password", + "title": "Wi‑Fi" + }, + "signal": { + "excellent": "Eccellente", + "fair": "Discreto", + "good": "Buono", + "poor": "Scarso", + "weak": "Debole" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/ja.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/ja.json new file mode 100644 index 0000000..1a23f99 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/ja.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "履歴を削除する", + "close-app": "{app} を閉じる", + "connect-vpn": "{name} に接続", + "cycle-visualizer": "ビジュアライザーを切り替える", + "disable-bluetooth": "Bluetooth を無効化", + "disable-dnd": "おやすみモードを無効化", + "disable-wifi": "Wi-Fi を無効化", + "disconnect-vpn": "{name} から切断", + "dock-settings": "ドック設定", + "enable-bluetooth": "Bluetooth を有効化", + "enable-dnd": "おやすみモードを有効化", + "enable-wifi": "Wi-Fi を有効化", + "launcher-settings": "ランチャー設定", + "lower-to-bottom": "一番下に移動", + "open-calendar": "カレンダーを開く", + "open-display-settings": "ディスプレイ設定", + "open-launcher": "ランチャーを開く", + "open-settings": "設定を開く", + "raise-to-top": "一番上に移動", + "random-wallpaper": "壁紙をランダムに変更", + "run-custom-command": "カスタムコマンドを実行", + "show-active-only": "アクティブのみ表示", + "show-all": "すべて表示", + "toggle-mute": "ミュート切り替え", + "widget-settings": "ウィジェット設定" + }, + "authentication": { + "error": "認証エラー", + "failed": "認証失敗" + }, + "bar": { + "active-window": { + "colorize-icons-description": "アクティブウィンドウのアイコンにテーマカラーを適用します。", + "hide-mode-description": "アクティブウィンドウがない場合のウィジェットの動作を制御します。", + "scrolling-mode-description": "長いウィンドウタイトルのスクロール動作を制御します。", + "show-app-icon-description": "ウィンドウタイトルの横にアプリアイコンを表示します。", + "show-app-icon-label": "アプリアイコンを表示", + "show-app-text-description": "アクティブなウィンドウのタイトルを表示します。", + "show-app-text-label": "ウィンドウタイトルを表示" + }, + "audio-visualizer": { + "color-name-description": "ビジュアライザーの色を選択します。", + "color-name-label": "塗りつぶし色", + "height-description": "カスタムコンポーネントの幅。", + "hide-when-idle-description": "有効にすると、再生中のプレーヤーがない場合にビジュアライザーを隠します。", + "hide-when-idle-label": "メディア停止時は隠す", + "width-description": "コンポーネントの幅をカスタム設定します。" + }, + "battery": { + "device-default": "デフォルト(表示デバイス)", + "device-description": "表示するバッテリーデバイスを選択します。", + "device-label": "バッテリーデバイス", + "display-mode-description": "バッテリーをバーにどのように表示するかを選択します。", + "display-mode-graphic": "グラフィックバッテリー", + "display-mode-graphic-clean": "グラフィカルバッテリー (パーセント表示なし)", + "display-mode-icon-always": "アイコン - 常に%を表示", + "display-mode-icon-hover": "アイコン - ホバー時に表示", + "display-mode-icon-only": "アイコンのみ", + "hide-if-idle-description": "バッテリーが充電または放電していないときは、ウィジェットを非表示にします。", + "hide-if-idle-label": "アイドル時に隠す", + "hide-if-not-detected-description": "システムでバッテリーが検出されない場合は、ウィジェットを非表示にします。", + "hide-if-not-detected-label": "検出されなければ隠れる", + "low-battery-threshold-label": "低バッテリー警告の閾値", + "show-noctalia-performance-description": "バッテリーパネルに Noctalia パフォーマンスモードのトグルを表示します。
Noctalia のシャドウとアニメーションを無効にしてリソース使用量を削減します。", + "show-noctalia-performance-label": "Noctalia パフォーマンストグルを表示", + "show-power-profile-description": "バッテリーパネルに電源プロファイルの選択項目を表示します。", + "show-power-profile-label": "電源プロファイルコントロールを表示" + }, + "brightness": { + "apply-all-description": "スクロールホイール使用時に、すべてのモニターの輝度を変更します。", + "apply-all-label": "スクロールの変更をすべてのモニターに適用" + }, + "clock": { + "clock-display-description": "以下のトークンを使用して時計の表示をカスタマイズします。12時間表記にするには AP トークンを含める必要があります。", + "clock-display-label": "時計の表示形式", + "custom-font-description": "時計表示用のカスタムフォントを選択します。", + "custom-font-label": "カスタムフォント", + "custom-font-placeholder": "カスタムフォントを選択...", + "custom-font-search-placeholder": "フォントを検索...", + "horizontal-bar-description": "ヒント:\\n を使用すると改行できます。", + "horizontal-bar-label": "水平バー", + "preview": "プレビュー", + "tooltip-format-description": "時計にマウスを合わせたときに表示されるツールチップのフォーマット文字列。デフォルトのツールチップを使用するには空のままにします。", + "tooltip-format-label": "ツールチップ形式", + "use-custom-font-description": "デフォルトのフォント設定を上書きし、時計にカスタムフォントを使用します。", + "use-custom-font-label": "カスタムフォントを使用", + "use-monospaced-font-description": "有効にすると、時計に等幅フォントを使用します。", + "use-monospaced-font-label": "等幅フォントを使用", + "vertical-bar-description": "各パーツを新しい行に分けるにはスペースを使用してください。", + "vertical-bar-label": "垂直バー" + }, + "control-center": { + "browse-file": "ファイルを参照", + "browse-library": "ライブラリを参照", + "color-selection-description": "アイコンに適用するテーマの色を選択します。", + "enable-colorization-description": "アイコンの色付けを有効にし、テーマの色を適用します。", + "icon-description": "ライブラリまたはカスタムファイルからアイコンを選択します。", + "select-custom-icon": "カスタムアイコンを選択", + "use-distro-logo-description": "カスタムアイコンの代わりに、ディストリビューションのロゴを使用します。", + "use-distro-logo-label": "ディストリビューションのロゴの使用" + }, + "custom-button": { + "collapse-condition-description": "出力テキストがこの値と一致する場合、ボタンを折りたたみます。", + "collapse-condition-label": "折りたたみ条件", + "color-selection-description": "テーマカラーをアイコンとテキストに適用します。", + "default-tooltip": "カスタムボタン、設定で構成", + "display-command-output-description": "定期的に実行するコマンドを入力します。出力の最初の行がテキストとして表示されます。", + "display-command-output-label": "コマンド出力を表示", + "display-command-output-stream-description": "継続的に実行するコマンドを入力します。", + "dynamic-text": "動的テキスト", + "enable-colorization-description": "カスタムボタンのアイコンとテキストのカラーリングを有効にし、テーマカラーを適用します。", + "enable-colorization-label": "カラーリングを有効化", + "general-tooltip-text-description": "ボタンのツールチップに表示するカスタムテキスト。", + "general-tooltip-text-label": "カスタムツールチップテキスト", + "hide-mode-always-expanded": "常に展開", + "hide-mode-description": "コマンドの出力がない場合のウィジェットの表示を制御します。", + "hide-mode-expand-with-output": "出力がある時のみ展開", + "hide-mode-label": "非表示モード", + "hide-mode-max-transparent": "最大展開(透明)", + "icon-description": "ライブラリからアイコンを選択します。", + "ipc-identifier-description": "IPCコマンドの一意の識別子。この識別子を「qs -c noctalia-shell ipc call cb [アクション] [識別子]」で使用して、IPC経由でこのボタンを制御します。", + "ipc-identifier-label": "IPC識別子", + "left-click-description": "ボタンを左クリックした際に実行するコマンド。", + "left-click-label": "左クリック", + "left-click-update-text": "左クリック時に表示テキストを更新", + "max-text-length-horizontal-description": "水平バーに表示する最大文字数(0でテキストを非表示)。", + "max-text-length-horizontal-label": "テキストの最大長(水平)", + "max-text-length-vertical-description": "垂直バーに表示する最大文字数(0でテキストを非表示)。", + "max-text-length-vertical-label": "テキストの最大長(垂直)", + "middle-click-description": "ボタンを中クリックした際に実行するコマンド。", + "middle-click-label": "中クリック", + "middle-click-update-text": "中クリック時に表示テキストを更新", + "parse-json-description": "コマンド出力を JSON オブジェクトとして解析し、テキストやアイコンを動的に設定します。", + "parse-json-label": "出力を JSON として解析", + "refresh-interval-description": "間隔(ミリ秒)。", + "refresh-interval-label": "更新間隔", + "right-click-description": "ボタンを右クリックした際に実行するコマンド。", + "right-click-label": "右クリック", + "right-click-update-text": "右クリック時に表示テキストを更新", + "show-exec-tooltip-description": "コマンドの詳細を含むツールチップを表示 (左/右/中央クリック、ホイール)。", + "show-exec-tooltip-label": "コマンドのツールチップを表示", + "show-icon-description": "ウィジェットのアイコンの表示を切り替えます。", + "show-icon-label": "アイコンを表示", + "show-text-tooltip-description": "テキストコマンドの出力を含むツールチップを表示します。", + "show-text-tooltip-label": "動的なテキストツールチップを表示", + "text-stream-description": "コマンドからストリーミングされた行を、ボタン上のテキストとして表示します。", + "text-stream-label": "ストリーム", + "wheel-description": "スクロールホイール操作時に実行するコマンド。
コマンド内で $delta を使用して回転量を取得できます。", + "wheel-down": "下にスクロール", + "wheel-down-description": "スクロールホイールを下方向に回転させた際に実行するコマンド。", + "wheel-down-label": "ホイール下回転時のコマンド", + "wheel-label": "スクロールホイール", + "wheel-mode-separate-description": "ホイールの上回転と下回転で別々のコマンドを有効にします。", + "wheel-mode-separate-label": "ホイール操作を分離", + "wheel-up": "上にスクロール", + "wheel-up-description": "スクロールホイールを上方向に回転させた際に実行するコマンド。", + "wheel-up-label": "ホイール上回転時のコマンド", + "wheel-update-text": "スクロール時に表示テキストを更新" + }, + "keyboard-layout": { + "show-icon-description": "キーボードレイアウトのアイコンを表示します。" + }, + "lock-keys": { + "hide-when-off-description": "キーがアクティブでないときインジケーターを非表示にします。", + "hide-when-off-label": "オフの時に非表示", + "show-caps-lock-description": "Caps Lock の状態を表示します。", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "Num Lock の状態を表示します。", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "Scroll Lock の状態を表示します。", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "メディアプレーヤーパネルの省スペースレイアウトを有効にする。", + "compact-mode-label": "コンパクトモード", + "hide-mode-description": "再生中のメディアがない場合のウィジェットの動作を制御します。", + "max-width-description": "ウィジェットの最大幅を設定します。内容が短い場合は自動的に縮小します。", + "no-active-player": "アクティブなプレーヤーなし", + "panel-section-description": "メディアプレーヤーのパネルの表示と動作を設定します。", + "panel-section-label": "メディアプレーヤーパネル", + "scrolling-mode-description": "長いトラックタイトルのスクロール動作を制御します。", + "scrolling-mode-label": "スクロールモード", + "show-album-art-description": "現在再生中のトラックのアルバムアートを表示します。", + "show-album-art-label": "アルバムアートを表示", + "show-artist-first-description": "「アーティスト名 - タイトル」の順で表示します。", + "show-artist-first-label": "アーティストを先に表示", + "show-progress-ring-description": "トラックの再生位置を示す円形の進捗インジケーターを表示します。", + "show-progress-ring-label": "プログレスリングを表示", + "show-visualizer-description": "音楽再生時にオーディオビジュアライザーを表示します。", + "show-visualizer-label": "ビジュアライザーを表示", + "use-fixed-width-description": "有効にすると、内容に合わせて自動調整せず、常に最大幅を使用します。", + "use-fixed-width-label": "固定幅を使用", + "visualizer-type-description": "表示するオーディオビジュアライザーのスタイルを選択します。", + "visualizer-type-label": "ビジュアライザーの種類" + }, + "notification-history": { + "hide-widget-when-zero-description": "通知がない場合に通知アイコンを非表示にします。", + "hide-widget-when-zero-label": "通知なしでアイコンを非表示", + "hide-widget-when-zero-unread-description": "未読の通知がない場合に通知アイコンを非表示にします。", + "hide-widget-when-zero-unread-label": "未読なしでアイコンを非表示", + "show-unread-badge-description": "未読通知の数を表示するバッジを表示します。", + "show-unread-badge-label": "未読バッジを表示", + "unread-badge-color-description": "未読の通知バッジの色を選択してください。", + "unread-badge-color-label": "未読バッジの色" + }, + "section-editor": { + "placeholder": "追加するウィジェットを選択...", + "search-placeholder": "ウィジェットを検索..." + }, + "spacer": { + "width-description": "スペースの幅(ピクセル)。" + }, + "system-monitor": { + "compact-mode-description": "テキスト値の代わりに、ミニ棒グラフとして統計を表示します。レイアウトのずれを防ぎます。", + "compact-mode-label": "コンパクトモード", + "cpu-cores-description": "CPUコアの使用状況を個別に表示します。", + "cpu-cores-label": "CPUコア", + "cpu-frequency-description": "現在のCPUクロック速度をGHzで表示します。", + "cpu-frequency-label": "CPU周波数を表示", + "cpu-temperature-description": "利用可能な場合、CPU 温度を表示します。", + "cpu-temperature-label": "CPU 温度", + "cpu-usage-description": "現在の CPU 使用率を表示します。", + "cpu-usage-label": "CPU 使用率", + "disk-path-description": "監視するディスクのマウントポイントを選択します。", + "disk-path-label": "ディスクパス", + "gpu-temperature-description": "利用可能な場合、GPU 温度を表示します。", + "load-average-description": "システム負荷平均を表示します。", + "load-average-label": "負荷平均", + "memory-percentage-description": "メモリ使用量を絶対値ではなくパーセンテージで表示します。", + "memory-percentage-label": "メモリをパーセントで表示", + "memory-usage-description": "現在の RAM 使用量を表示します。", + "memory-usage-label": "メモリ使用量", + "network-traffic-description": "ネットワークのアップロード・ダウンロード速度を表示します。", + "network-traffic-label": "ネットワークトラフィック", + "storage-as-percentage-description": "ディスク容量を絶対値ではなくパーセンテージで表示する。", + "storage-as-percentage-label": "ディスクをパーセンテージで", + "storage-available-description": "使用量ではなく、利用可能なディスク容量を表示します。", + "storage-available-label": "利用可能なディスク容量", + "storage-usage-description": "ストレージの使用状況を表示します。", + "storage-usage-label": "ストレージ使用量", + "swap-usage-description": "スワップメモリの使用状況を表示します。", + "swap-usage-label": "スワップ使用量", + "use-monospace-font-description": "タイプライター体で翻訳します。", + "use-monospace-font-label": "等幅フォント", + "use-padding-description": "レイアウトのずれを防ぐため、テキスト値に先頭のスペースを埋め込みます。", + "use-padding-description-disabled-monospace-font": "この機能には等幅フォントが必要です。", + "use-padding-description-disabled-vertical": "縦のタスクバーはパディングテキストをサポートしていません。", + "use-padding-label": "テキストのパディング" + }, + "taskbar": { + "colorize-icons-description": "タスクバーのアイコンにテーマカラーを適用します。", + "hide-mode-description": "一致するウィンドウがない場合のウィジェットの動作を制御します。", + "hide-mode-label": "非表示モード", + "icon-scale-description": "タスクバーのアイコンの拡大率を設定します。", + "icon-scale-label": "アイコンのスケーリング", + "max-width-description": "タスクバーの最大幅を、画面幅に対するパーセンテージで設定します。", + "max-width-label": "最大幅", + "only-active-workspaces-description": "アクティブなワークスペースのアプリのみを表示します。", + "only-active-workspaces-label": "アクティブなワークスペースのみ", + "only-same-monitor-description": "バーが配置されているディスプレイ上のアプリのみを表示します。", + "only-same-monitor-label": "同じディスプレイのみ", + "show-pinned-apps-description": "ドックにピン留めされたアプリをタスクバーに表示します。", + "show-pinned-apps-label": "ピン留めアプリを表示", + "show-title-description": "タスクバーにウィンドウタイトルを表示します。", + "show-title-description-disabled": "垂直タスクバーではタイトルを表示できません。", + "show-title-label": "タイトルを表示", + "smart-width-description": "項目の数に基づいて、項目の幅を自動的に調整します。", + "smart-width-label": "幅の自動調整", + "title-width-description": "タスクバーのウィンドウタイトルの幅を設定します(ピクセル単位)。", + "title-width-label": "タイトルの幅", + "title-width-reset-tooltip": "タイトルの幅をリセット" + }, + "tray": { + "chevron-color-description": "ドロワーのシェブロンアイコンにテーマの色を適用します。", + "chevron-color-label": "矢印の色", + "colorize-icons-description": "トレイのアイコンにテーマカラーを適用します。", + "colorize-icons-label": "テーマカラーの適用", + "drawer-enabled-description": "有効にすると、ピン留めされていないトレイ項目をドロワーパネル内に表示します。
無効にすると、すべての項目をインラインで表示します。", + "drawer-enabled-label": "ドロワーを有効化", + "hide-passive-description": "有効にすると、ステータスが「パッシブ」のトレイ項目を非表示にします。", + "hide-passive-label": "パッシブ項目を非表示" + }, + "volume": { + "display-mode-description": "値の表示方法を選択します。" + }, + "workspace": { + "character-count-description": "ワークスペース名から表示する文字数 (1-10)。", + "character-count-label": "文字数", + "empty-color-description": "空のWorkspaceの背景色を設定します。", + "empty-color-label": "空のワークスペースの色", + "enable-scrollwheel-description": "マウスホイールを使用してワークスペースを切り替えます。", + "enable-scrollwheel-label": "スクロールでワークスペースを切り替え", + "focused-color-description": "フォーカスされたWorkspaceの背景色を設定します。", + "focused-color-label": "フォーカスされたワークスペースの色", + "follow-focused-screen-description": "バーが配置されている画面ではなく、現在フォーカスされている画面のワークスペースを表示します。", + "follow-focused-screen-label": "フォーカス中の画面に追従", + "font-weight-description": "ワークスペース内のテキストの視覚的ウェイトを設定します。", + "font-weight-label": "フォントの太さ", + "grouped-border-opacity-description": "ワークスペースコンテナのボーダーの不透明度を設定します。", + "grouped-border-opacity-label": "境界線の不透明度", + "hide-unoccupied-description": "ウィンドウがないワークスペースを表示しません。", + "hide-unoccupied-label": "空のワークスペースを隠す", + "label-mode-description": "ワークスペースラベルの表示方法を選択します。", + "label-mode-label": "ラベルモード", + "occupied-color-description": "使用中のWorkspaceの背景色を設定します。", + "occupied-color-label": "使用中のワークスペースの色", + "pill-size-description": "ワークスペースピルのサイズを調整します (50%-100%)。", + "pill-size-label": "ピルサイズ", + "reverse-scrolling-description": "スクロール時のワークスペース切り替え方向を反転する。", + "reverse-scrolling-label": "スクロール方向を反転", + "show-applications-description": "各ワークスペース内にアプリアイコンを表示します。", + "show-applications-hover-description": "ワークスペースにカーソルを合わせたときに、各ワークスペース内にアプリケーションアイコンを表示します。", + "show-applications-hover-label": "ホバー時にアプリケーションを表示", + "show-applications-label": "アプリアイコンを表示", + "show-badge-description": "グループ化モードでワークスペース番号バッジを表示します。", + "show-badge-label": "ワークスペースバッジを表示", + "show-labels-only-when-occupied-description": "ウィンドウが開いているワークスペースにのみラベルを表示します。", + "show-labels-only-when-occupied-label": "ウィンドウがある時のみラベルを表示", + "unfocused-icons-opacity-description": "フォーカスされていないアプリのアイコンの不透明度を設定します。", + "unfocused-icons-opacity-label": "フォーカスされていないアイコンの不透明度" + } + }, + "battery": { + "all-batteries": "すべてのバッテリー(合計)", + "battery-health": "バッテリーの状態", + "battery-level": "バッテリー残量", + "capacity-level": "容量:{level}", + "charging-rate": "充電速度:{rate} W", + "discharging-rate": "放電速度:{rate} W", + "health": "状態:{percent}%", + "inhibit-idle-description": "システムがスリープ状態になるのを防ぎます。", + "no-battery-detected": "バッテリーが見つかりません", + "plugged-in": "電源接続中", + "power-profile": "電源プロファイル", + "time-left": "残り時間:{time}", + "time-until-full": "充電完了まで:{time}" + }, + "bluetooth": { + "panel": { + "available-devices": "利用可能なデバイス", + "blocked": "ブロック中", + "connected-devices": "接続済みデバイス", + "device-address": "デバイスアドレス", + "disabled": "Bluetooth は無効です", + "discoverable": "発見可能", + "enable-message": "利用可能なデバイスを表示するには Bluetooth を有効にしてください。", + "known-devices": "登録済みデバイス", + "no-devices": "利用可能なデバイスはありません", + "paired-devices": "ペアリングされたデバイス", + "pairing-mode": "デバイスがペアリングモードになっていることを確認してください。", + "scanning": "デバイスをスキャン中...", + "signal-text-excellent": "信号:良好", + "signal-text-fair": "合図:可", + "signal-text-good": "信号:良し", + "signal-text-poor": "信号:弱い", + "signal-text-unknown": "信号:不明", + "signal-text-very-poor": "信号:非常に悪い" + } + }, + "changelog": { + "error": { + "fetch-failed": "変更履歴データを読み込めませんでした。後ほど再試行してください。", + "rate-limit": "GitHub のレート制限を超えました。数分待ってから再試行してください。" + }, + "panel": { + "buttons-discord": "Discord に参加", + "buttons-dismiss": "OK", + "buttons-feedback": "フィードバックください", + "empty": "リリースノートはまだ利用できません。", + "highlight-title": "ハイライト", + "released": "{date} リリース", + "subtitle-fresh": "Noctalia をインストールしていただきありがとうございます!今回のビルド内容は以下の通りです。", + "subtitle-updated": "{previousVersion} からアップデートされました", + "title": "バージョン {version} の新機能", + "version": "バージョン {version}", + "version-new-user": "新規インストール" + } + }, + "common": { + "actions": "アクション", + "active": "アクティブ", + "add": "追加", + "appearance": "外観", + "apply": "適用", + "auto-connect": "自動接続", + "automation": "自動化", + "available": "利用可能", + "back": "戻る", + "battery": "バッテリー", + "behavior": "動作", + "bluetooth": "Bluetooth", + "brightness": "明るさ", + "browse": "閲覧", + "calendar": "カレンダー", + "calendar-panel": "カレンダーパネル", + "cancel": "キャンセル", + "cards": "カード", + "charging": "充電中", + "clear": "クリア", + "clipboard": "クリップボード", + "close": "閉じる", + "color-muted": "ミュート", + "colors": "色", + "command": "コマンド", + "confirm": "確認", + "connect": "接続", + "connected": "接続済み", + "connecting": "接続中...", + "continue": "続ける", + "contributors": "貢献者", + "copied-to-clipboard": "クリップボードにコピーしました", + "countdown": "カウントダウン", + "customize": "カスタマイズ", + "date": "日付", + "default": "デフォルト", + "delete": "削除", + "devices": "デバイス", + "disabled": "無効", + "discharging": "放電中", + "disconnect": "切断", + "disconnected": "切断済み", + "disconnecting": "切断中...", + "display-mode": "表示モード", + "documentation": "ドキュメント", + "download": "ダウンロード", + "duration": "期間", + "dysfunctional": "機能不全", + "edit": "編集", + "enabled": "有効", + "error": "エラー", + "ethernet": "イーサネット", + "events": "イベント", + "execute": "実行", + "faithful": "忠実", + "focus": "集中", + "font-weight-bold": "太字", + "font-weight-light": "ライト", + "font-weight-medium": "中", + "font-weight-regular": "標準", + "font-weight-semibold": "セミボールド", + "frequency": "頻度", + "gateway": "ゲートウェイ", + "general": "一般", + "height": "高さ", + "hibernate": "ハイバネート", + "history": "履歴", + "icon": "アイコン", + "idle": "アイドル状態", + "info": "情報", + "input": "入力", + "install": "インストール", + "installed": "インストール済み", + "interface": "インターフェース", + "internet": "インターネット", + "keybind": "キーバインド", + "language": "言語", + "loading": "読み込み中...", + "local": "ローカル", + "location": "場所", + "lock": "ロック", + "logout": "ログアウト", + "look": "外観", + "margins": "余白", + "media": "メディア", + "media-player": "メディアプレーヤー", + "memory": "メモリ", + "monitors": "モニター", + "move-down": "下へ移動", + "move-up": "上へ移動", + "network": "ネットワーク", + "next": "次へ", + "night-light": "夜間モード", + "no": "いいえ", + "no-results": "結果なし", + "no-summary": "要約なし", + "none": "なし", + "not-found": "見つかりませんでした", + "notifications": "通知", + "official": "公式", + "on-surface": "表面上", + "output": "出力", + "pair": "ペアリング", + "paired": "ペアリング済み", + "pairing": "ペアリング中...", + "panels": "パネル", + "password": "パスワード", + "pause": "一時停止", + "performance": "パフォーマンス", + "pin": "ピン留め", + "play": "再生", + "polling": "ポーリング", + "position": "位置", + "previous": "前へ", + "primary": "プライマリ", + "random": "ランダム", + "reboot": "再起動", + "reboot-to-uefi": "UEFIで再起動", + "record": "録画を開始", + "refresh": "更新", + "required": "(必須)", + "reset": "リセット", + "reset-to-default": "デフォルトにリセット", + "result-count": "{count} 結果", + "result-count-plural": "{count} 結果", + "resume": "再開", + "save": "保存", + "scanning": "スキャン中...", + "screen-corners": "画面の隅", + "search": "検索", + "second": "{count} 秒", + "second-plural": "{count} 秒", + "secondary": "セカンダリー", + "security": "セキュリティ", + "select": "選択", + "select-color": "色を選択", + "select-color-description": "強調のためにテーマの色を適用します。", + "select-icon-color": "アイコンの色を選択", + "settings": "設定", + "shortcuts": "ショートカット", + "shutdown": "シャットダウン", + "signal": "信号", + "sound": "サウンド", + "sources": "ソース", + "start": "開始", + "stop": "停止", + "supporters": "サポーター", + "suspend": "サスペンド", + "templates": "テンプレート", + "tertiary": "第三", + "test": "テスト", + "thresholds": "しきい値", + "title": "タイトル", + "toast": "トースト", + "trusted": "信頼済み", + "uninstall": "アンインストール", + "unknown": "不明", + "unpair": "ペアリング解除", + "unpin": "ピン留め解除", + "update": "アップデート", + "upload": "アップロード", + "username": "ユーザー名", + "userspace-reboot": "ユーザースペースを再起動", + "version": "バージョン", + "vibrant": "鮮やか", + "visualizer": "ビジュアライザー", + "volume": "ボリューム", + "volumes": "ボリューム", + "wallpaper": "壁紙", + "weather": "天気", + "weather-loading": "天気を読み込み中...", + "weather-no-location": "天気を表示するには、設定で場所を設定してください", + "week": "週", + "widgets": "ウィジェット", + "width": "幅", + "wifi": "Wi-Fi", + "windows": "ウィンドウ", + "yes": "はい" + }, + "control-center": { + "power-profile": { + "tooltip-action": "電源プロファイル", + "tooltip-disabled": "電源プロファイルを使用するには power-profiles-daemon をインストールしてください" + }, + "wifi": { + "label-disconnected": "Wi-Fi 未接続", + "label-ethernet": "イーサネット" + } + }, + "display-modes": { + "always-hide": "常に隠れる", + "always-show": "常に表示する", + "force-open": "こじ開ける", + "on-hover": "マウスオーバー時" + }, + "hide-modes": { + "auto-hide": "自動非表示", + "hidden": "空の場合に非表示", + "idle": "アイドル時に非表示", + "non-exclusive": "非独占", + "transparent": "空の時は透明", + "visible": "常に表示" + }, + "launcher": { + "categories": { + "all": "すべて", + "audiovideo": "音声・ビデオ", + "chat": "チャット", + "development": "開発", + "education": "教育", + "emoji-activity": "アクティビティ", + "emoji-animals": "動物", + "emoji-flags": "旗", + "emoji-food": "食べ物・飲み物", + "emoji-nature": "自然", + "emoji-objects": "物", + "emoji-people": "人と体", + "emoji-recent": "最近使用", + "emoji-symbols": "記号", + "emoji-travel": "旅行と場所", + "game": "ゲーム", + "graphics": "グラフィックス", + "misc": "その他", + "office": "オフィス", + "pinned": "ピン留め", + "system": "システム", + "webbrowser": "ウェブブラウザ" + }, + "providers": { + "applications": "アプリケーション", + "calculator": "電卓", + "calculator-deprecated": ">calc コマンドは非推奨となり、まもなく削除されます。代わりに、検索で数式を直接入力してください。", + "calculator-description": "電卓 — 数式を計算します", + "calculator-enter-expression": "数式を入力", + "calculator-press-enter-to-copy": "結果をコピーするにはEnterキーを押してください", + "clipboard": "クリップボード履歴", + "clipboard-clear-description": "クリップボード履歴をすべて消去", + "clipboard-clear-description-full": "クリップボード履歴からすべての項目を削除します", + "clipboard-clear-history": "クリップボード履歴を消去", + "clipboard-delete": "クリップボードの項目を削除", + "clipboard-history-disabled": "クリップボード履歴は無効です", + "clipboard-history-disabled-description": "設定で履歴を有効にするか、cliphist をインストールしてください", + "clipboard-loading": "クリップボード履歴を読み込み中...", + "clipboard-search-description": "クリップボード履歴を検索", + "command-description": "シェルコマンドを実行する", + "emoji": "絵文字選択", + "emoji-loading": "絵文字を読み込み中...", + "emoji-loading-description": "お待ちください", + "emoji-no-recent": "まだ最近使った絵文字はありません", + "emoji-search-description": "絵文字を検索してコピー", + "settings-search-description": "設定を検索して移動", + "windows-search-description": "開いているウィンドウを検索してフォーカスする" + } + }, + "lock-screen": { + "authenticating": "認証中...", + "password": "パスワードを入力...", + "restart": "再起動", + "shut-down": "シャットダウン" + }, + "notifications": { + "panel": { + "click-to-collapse": "クリックで折りたたむ", + "click-to-expand": "クリックして展開", + "description": "通知が届くとここに表示されます。", + "no-notifications": "通知なし" + }, + "range": { + "earlier": "それ以前", + "today": "今日", + "yesterday": "昨日" + }, + "time": { + "diff-d": "1日前", + "diff-dd": "{diff}日前", + "diff-h": "1時間前", + "diff-hh": "{diff}時間前", + "diff-m": "1分前", + "diff-mm": "{diff}分前", + "now": "今" + } + }, + "options": { + "bar": { + "density-comfortable": "ゆったり", + "density-compact": "コンパクト", + "density-default": "標準", + "density-mini": "ミニ", + "density-spacious": "広々とした", + "display-mode-always-visible": "常に表示", + "type-floating": "フローティング", + "type-framed": "フレーム", + "type-simple": "シンプル" + }, + "control-center": { + "quick-settings-style-classic": "クラシック", + "quick-settings-style-modern": "モダン" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "快適", + "compact": "コンパクト", + "default": "デフォルト" + }, + "launcher-view-mode": { + "grid": "グリッド", + "list": "リスト" + }, + "notification-density": { + "compact": "コンパクト", + "default": "デフォルト" + }, + "scrolling-modes": { + "always": "常にスクロール", + "hover": "ホバー時にスクロール", + "never": "スクロールしない" + }, + "session-menu-grid-layout": { + "grid": "グリッド", + "single-row": "単一行" + }, + "settings-panel-mode": { + "attached": "バーに吸着", + "centered": "中央に配置", + "window": "独立したウィンドウ" + }, + "visualizer-types": { + "linear": "リニア", + "mirrored": "ミラー", + "wave": "ウェーブ" + }, + "visualizer-visibility": { + "with-background": "背景がある場合のみ" + }, + "workspace-labels": { + "index": "番号", + "index-and-name": "番号 + 名前", + "name": "名前" + } + }, + "panels": { + "about": { + "become-supporter": "サポーターになる", + "changelog": "変更履歴を見る", + "changelog-on-startup": "アップデート時に変更履歴を表示", + "changelog-on-startup-desc": "Noctaliaが更新されたときに、自動的に変更履歴を表示する。", + "contributors-description": "{count}人の素晴らしいコントリビューターに感謝!", + "contributors-description-plural": "{count}人の素晴らしいコントリビューターに感謝!", + "copy-info": "情報をコピー", + "debug-disabled": "デバッグモードが無効になりました", + "debug-enabled": "デバッグモードが有効です", + "info-copied": "クリップボードにコピーしました", + "noctalia-available": "利用可能:", + "noctalia-desc": "Wayland のために丁寧に作り込まれた、Quickshell 製の洗練されたミニマルなデスクトップシェル。", + "noctalia-git-commit": "Git コミット:", + "noctalia-installed-version": "現在のバージョン:", + "noctalia-latest-version": "最新のバージョン:", + "noctalia-qs-version": "Noctalia QS バージョン:", + "noctalia-title": "Noctalia shell", + "privacy-policy": "プライバシーポリシー", + "support": "支援する", + "supporter-badge": "サポーター", + "supporters-desc": "素晴らしいサポーターに心からの感謝を!", + "supporters-desc-plural": "{count}名の素晴らしいサポーターの皆様に心より感謝いたします!", + "supporters-loading": "サポーターを読み込み中...", + "system-board": "マザーボード:", + "system-cpu": "CPU:", + "system-disk": "ディスク:", + "system-gpu": "GPU:", + "system-host": "ホスト:", + "system-install-hint": "fastfetch をインストールしてシステム情報を表示する", + "system-kernel": "カーネル:", + "system-loading": "システム情報を読み込み中...", + "system-memory": "メモリ:", + "system-monitor": "モニター:", + "system-not-installed": "fastfetch がインストールされていません", + "system-os": "OS:", + "system-packages": "パッケージ:", + "system-product": "製品:", + "system-title": "システム情報", + "system-uptime": "稼働時間:", + "system-wm": "WM:", + "telemetry-data-copied": "テレメトリーデータをクリップボードにコピーしました。", + "telemetry-desc": "匿名システム情報(画面解像度、コンポジター、ディストリビューション)を共有して、Noctaliaの改善にご協力ください。起動時に一度送信され、追跡は行われず、データは30日後に自動的に削除されます。", + "telemetry-enabled": "匿名でシステム情報を送信する", + "telemetry-show-data": "データの表示", + "telemetry-title": "プライバシー", + "title": "バージョン情報", + "up-to-date": "最新の状態です!", + "update-available": "アップデートがあります", + "view-commit": "GitHubでコミットを見る" + }, + "audio": { + "devices-desc": "利用可能なオーディオ入出力デバイスを設定します。", + "devices-input-device-description": "使用する入力デバイスを選択します。", + "devices-input-device-label": "入力デバイス", + "devices-output-device-description": "使用する出力デバイスを選択します。", + "devices-output-device-label": "出力デバイス", + "devices-title": "オーディオデバイス", + "external-mixer-description": "外部オーディオミキサー機能を使用する際に起動するコマンド、またはアプリケーションパスを入力します。", + "external-mixer-label": "外部ミキサーのコマンド", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "優先するメディアアプリや、除外設定を行います。", + "media-excluded-player-description": "システムに無視させたいプレーヤーのキーワードを追加します。1行につき1つのキーワードを入力してください。", + "media-excluded-player-label": "除外するプレーヤー", + "media-excluded-player-placeholder": "文字列を入力して + を押す", + "media-frame-rate-description": "値を高くすると滑らかになりますが、リソース消費が増加します。", + "media-frame-rate-label": "フレームレート", + "media-primary-player-description": "メインのプレーヤーを識別するためのキーワードを入力します。", + "media-primary-player-label": "優先プレーヤー", + "media-primary-player-placeholder": "例:spotify, vlc, mpv", + "media-scrolling-speed-description": "タイトルが端から端までスクロールするのにかかる時間(秒)。", + "media-scrolling-speed-label": "スクロール速度", + "media-scrolling-title-description": "長いメディアタイトルを連続スクロールさせます。", + "media-scrolling-title-label": "タイトルのスクロール", + "media-title": "メディアプレーヤー", + "on-middle-clicked-description": "ボタンを中クリックしたときに実行するコマンド。", + "panel-applications-empty": "現在音声を再生中のアプリケーションはありません", + "spectrum-mirrored-description": "スペクトルをミラーリングし、低周波を中央に配置します。オフの場合、低周波は左、高周波は右になります。", + "spectrum-mirrored-label": "ミラースペクトル", + "title": "サウンド", + "visualizer-type-description": "メディア再生の可視化タイプを選択してください。", + "visualizer-type-label": "視覚化の種類", + "volumes-desc": "音量コントロールとレベルを調整します。", + "volumes-feedback-sound-file-description": "音量変更時に再生されるサウンドファイルのパス。", + "volumes-feedback-sound-file-label": "音量フィードバックのサウンドファイル", + "volumes-feedback-sound-file-select-title": "音量フィードバックのサウンドファイルを選択", + "volumes-input-volume-description": "マイクの音量レベル。", + "volumes-mute-input-description": "デフォルトの音声入力(マイク)をミュートします。", + "volumes-mute-input-label": "入力(マイク)をミュート", + "volumes-mute-output-description": "システムのメイン音声出力をミュートします。", + "volumes-mute-output-label": "出力をミュート", + "volumes-output-volume-description": "システム全体の音量レベル。", + "volumes-step-size-description": "音量の変化量(スクロールホイールやショートカットキー)を調整します。", + "volumes-step-size-label": "ボリューム調整ステップ", + "volumes-volume-feedback-description": "音量を調整するときにフィードバック音を再生する。", + "volumes-volume-feedback-label": "音量フィードバックサウンドを再生", + "volumes-volume-overdrive-description": "音量を100%以上に上げることを許可します。すべてのハードウェアでサポートされているとは限りません。", + "volumes-volume-overdrive-label": "100%以上の音量を許可" + }, + "bar": { + "appearance-auto-hide-delay-description": "マウスカーソルがBarから離れた後、Barが非表示になるまでの時間", + "appearance-auto-hide-delay-label": "非表示遅延", + "appearance-auto-hide-exclusive-note": "注:自動非表示が有効な場合、排他ゾーンは自動的に無効になります。", + "appearance-auto-show-delay-description": "マウスが端に到達したときにバーが表示されるまでの時間。", + "appearance-auto-show-delay-label": "表示遅延", + "appearance-background-opacity-description": "バーの背景の不透明度を特に設定します。", + "appearance-background-opacity-label": "バーの背景の不透明度", + "appearance-capsule-color-description": "バーのカプセルの色を選択するか、デフォルトの表面色には色を使用しない。", + "appearance-capsule-color-label": "カプセルの色", + "appearance-capsule-opacity-description": "カプセル表示時のウィジェット背景の不透明度を設定します。", + "appearance-capsule-opacity-label": "カプセルの不透明度", + "appearance-content-padding-description": "バーの端とウィジェット間のパディングを調整します。", + "appearance-content-padding-label": "コンテンツパディング", + "appearance-density-description": "バーの余白を調整し、コンパクトまたはゆったりとした外観にします。", + "appearance-density-label": "バーの密度", + "appearance-desc": "バーの外観や位置をカスタマイズします。", + "appearance-display-mode-description": "バーの表示タイミングを選択してください。", + "appearance-enable-exclusion-zone-inset-description": "排他ゾーンを1物理ピクセル縮小し、バーの端の下にぴったりと重なるようにウィンドウが表示されるようにします。", + "appearance-enable-exclusion-zone-inset-label": "インセット除外領域", + "appearance-floating-description": "バーを浮かせて、カプセル型で表示します。", + "appearance-floating-label": "フローティングバー", + "appearance-font-scale-description": "バーに表示されるテキストのフォントサイズスケールを調整します。", + "appearance-font-scale-label": "フォントスケール", + "appearance-frame-radius": "内側の半径", + "appearance-frame-settings-description": "フレームの太さと内側の半径を調整します", + "appearance-frame-settings-label": "フレーム設定", + "appearance-frame-thickness": "太さ", + "appearance-hide-on-overview-description": "コンポジタの概要がアクティブなときは、バーを非表示にしてパネルを閉じます。", + "appearance-hide-on-overview-label": "概要のバーを非表示にする", + "appearance-margins-description": "フローティングバーの周囲の余白を調整します。", + "appearance-margins-horizontal": "水平マージン", + "appearance-margins-vertical": "垂直マージン", + "appearance-outer-corners-description": "バーの角に、外向きの丸み(装飾)を表示します。", + "appearance-outer-corners-label": "外側のコーナー", + "appearance-position-description": "画面上のバーの配置場所を選択します。", + "appearance-position-label": "バーの位置", + "appearance-show-capsule-description": "ウィジェットの背景を表示します。", + "appearance-show-capsule-label": "カプセルを表示", + "appearance-show-on-workspace-switch-description": "ワークスペースが変更されたときに、Barを自動的に短時間表示します。", + "appearance-show-on-workspace-switch-label": "ワークスペース切り替え時にバーを表示", + "appearance-show-outline-description": "すべてのウィジェットの周囲に枠線を表示します。", + "appearance-show-outline-label": "ウィジェットの枠線を表示", + "appearance-type-description": "バーのスタイルを選択:シンプル、フローティング、またはフレーム。", + "appearance-type-label": "バーの種類", + "appearance-use-separate-opacity-description": "バーの背景に別の不透明度の値を使用できるようにする。", + "appearance-use-separate-opacity-label": "バーの不透明度を個別に設定", + "appearance-widget-spacing-description": "バー内の各ウィジェット間の間隔を調整します。", + "appearance-widget-spacing-label": "ウィジェットの間隔", + "behavior-middle-click-command-description": "中央クリックで実行するコマンド。", + "behavior-middle-click-command-label": "中央クリックコマンド", + "behavior-middle-click-command-placeholder": "niri msg action 概要の切り替え", + "behavior-middle-click-description": "バーの空いている領域で中央クリックが何をするかを選択します。", + "behavior-middle-click-follow-mouse-description": "選択した中央クリックパネルをカーソル位置で開く。", + "behavior-middle-click-follow-mouse-label": "中央クリックでマウスを追従", + "behavior-middle-click-label": "バーの中央クリックアクション", + "behavior-right-click-command-description": "右クリック時に実行するコマンド。", + "behavior-right-click-command-label": "右クリックコマンド", + "behavior-right-click-command-placeholder": "notify-send \"右クリック\"", + "behavior-right-click-description": "バーの空いている領域で右クリックが何をするかを選択します。", + "behavior-right-click-follow-mouse-description": "選択した右クリックパネルをカーソル位置に開く。", + "behavior-right-click-follow-mouse-label": "右クリックでマウスに追従", + "behavior-right-click-label": "バーの右クリックアクション", + "behavior-wheel-wrap-description": "有効にすると、スクロールは最後の項目から最初の項目へ続きます。", + "behavior-wheel-wrap-label": "ループ", + "behavior-workspace-scroll-description": "バーの空いている領域でマウスホイールが何をするか選択します。", + "behavior-workspace-scroll-label": "バーのマウスホイールアクション", + "behavior-workspace-scroll-option-content": "コンテンツ", + "behavior-workspace-scroll-option-workspace": "ワークスペース", + "monitor-configure-widgets": "ウィジェットを設定", + "monitor-override-settings": "グローバル設定を上書き", + "monitor-override-settings-description": "このモニターにカスタム設定を使用する。", + "monitor-reset-all": "すべてリセット", + "monitor-widgets-title": "ウィジェット設定 - {monitor}", + "monitors-desc": "バーを表示するディスプレイを選択します。未選択の場合は全てに表示されます。", + "monitors-desc-new": "どのモニターにバーを表示するかを設定し、モニターごとに設定をカスタマイズします。", + "monitors-title": "表示するディスプレイ", + "title": "バー", + "tray-blacklist-description": "トレイから除外するルールを追加します。ワイルドカード (*) に対応しています。", + "tray-blacklist-label": "ブラックリスト", + "tray-blacklist-placeholder": "例:nm-applet, Fcitx*", + "tray-pin-application": "アプリをピン留め", + "tray-unpin-application": "ピン留めを解除", + "use-global-widgets": "グローバルウィジェットを使用する", + "widgets-desc": "ウィジェットをドラッグして並べ替えます。右クリックメニューから、セクション間の移動や削除が行えます。", + "widgets-title": "ウィジェットの配置" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "壁紙から配色を生成します。色を自動的に抽出し、まとまりのあるテーマを作成します。", + "color-source-use-wallpaper-colors-label": "壁紙の色を使用", + "dark-mode-mode-description": "ライトモードとダークモードの自動切り替えを有効にします。", + "dark-mode-mode-label": "ダークモードのスケジュール", + "dark-mode-mode-manual": "手動", + "dark-mode-mode-off": "オフ", + "dark-mode-switch-description": "夜間でも見やすいように、暗いテーマに切り替えます。", + "delete-error-description": "{scheme} の削除に失敗しました", + "delete-error-title": "削除失敗", + "delete-success-description": "{scheme} を削除しました", + "delete-success-title": "配色を削除しました", + "download-button": "さらにダウンロード", + "download-downloading": "ダウンロード中...", + "download-empty": "利用可能な配色がありません", + "download-error-api-error": "API エラー:{status}", + "download-error-description": "{scheme} のダウンロードに失敗しました", + "download-error-download-failed": "ダウンロードに失敗しました(終了コード:{code})", + "download-error-invalid-response": "無効なAPIレスポンス形式", + "download-error-no-files": "スキームのファイルが見つかりません", + "download-error-parse-failed": "APIレスポンスの解析に失敗しました:{error}", + "download-error-rate-limit": "GitHub APIのレート制限を超えました", + "download-error-title": "ダウンロード失敗", + "download-fetching": "利用可能な配色を取得中...", + "download-success-description": "{scheme} をダウンロードしました", + "download-success-title": "配色をダウンロードしました", + "download-title": "配色のダウンロード", + "method-description": { + "content": "ソースコンテンツの実際の色と厳密に一致する、高忠実度の色抽出を備えたMaterial Designスキーム。", + "dysfunctional": "Faithfulのように、2番目に支配的な色族をプライマリとして選択します。", + "faithful": "調和のとれたパレットを生成しながら、ソースカラーにできるだけ近い色を維持しようとします。", + "fruit-salad": "多様で多彩な色合いを持つ、遊び心のある鮮やかなパレットを生み出すMaterial Designスキーム。", + "monochrome": "単一色相のグレースケールと最小限の彩度コンテンツを使用した Material Design スキーム。", + "muted": "穏やかで控えめな外観のために、より柔らかく、彩度を落とした色調を生成します。", + "rainbow": "最大の色多様性を実現するために、全スペクトルにわたって色を生成するマテリアルデザインスキーム。", + "tonal-spot": "Material Designのデフォルトアルゴリズム。ソースカラーの色調変化を使用して、バランスの取れたパレットを作成します。", + "vibrant": "色の彩度を最大化し、鮮やかな色調で大胆で目を引くパレットを実現します。" + }, + "predefined-desc": "あらかじめ用意された配色から選択します。", + "predefined-generate-templates-label": "プリセット配色のテンプレートを生成する", + "predefined-title": "プリセット配色", + "sync-gsettings-description": "システムテーマをアクティブなライトまたはダークのバリアントに合わせます。", + "sync-gsettings-label": "システムテーマを同期", + "templates-desc": "外部アプリケーションに配色を適用します。", + "templates-filter-description": "特定のカテゴリのテンプレートを表示します。", + "templates-filter-label": "カテゴリで絞り込む", + "templates-misc-description": "独自のテンプレートを作成。", + "templates-misc-label": "詳細設定", + "templates-misc-user-templates-description": "操作方法を理解している場合のみ有効にしてください。オンラインドキュメントを参照してください。", + "templates-misc-user-templates-label": "ユーザーテンプレートを有効化", + "templates-none-detected": "検出されませんでした", + "templates-write-path": "書き込み先:{filepath}", + "title": "配色", + "wallpaper-method-description": "お好みのパレット生成方法を選択してください。", + "wallpaper-method-label": "パレット生成方法", + "wallpaper-monitor-source-description": "壁紙の色を抽出するために使用するモニターを選択してください。", + "wallpaper-monitor-source-label": "色の生成元" + }, + "connections": { + "authentication-required": "認証が必要です", + "bluetooth-auto-connect-description": "Bluetoothが有効な場合、信頼できるペアリング済みデバイスに自動的に接続します。", + "bluetooth-auto-connect-label": "ペアリング済みデバイスを自動接続", + "bluetooth-devices-unnamed": "名前のないデバイスは表示されません。", + "bluetooth-discoverable": "このデバイスは、この設定タブが開いている間、{hostName} として検出可能です。", + "bluetooth-rssi-polling-description": "bluetoothctl を介して接続されたデバイスの RSSI を定期的にサンプリングします。すべてのデバイスで利用できるとは限りません。有効にすると最小限のリソースを使用します。", + "bluetooth-rssi-polling-interval-description": "接続されたデバイスの信号強度を更新する頻度を設定します。", + "bluetooth-rssi-polling-interval-label": "ポーリング間隔", + "bluetooth-rssi-polling-label": "Bluetooth信号ポーリング", + "disable-discoverability-description": "近くのBluetoothデバイスからデバイスを非表示にする。", + "disable-discoverability-label": "デバイスの可視性を無効にする", + "hide-unnamed-devices-description": "Bluetoothアドレスとしてのみ表示されるデバイスを非表示にする。", + "hide-unnamed-devices-label": "名前のないデバイスを非表示にする", + "pin-instructions": "デバイスに表示されているPINコードを入力してください。", + "title": "接続", + "wifi-header-text": "利用可能なネットワークを表示し、インターネットに接続するにはWi-Fiを使用してください。" + }, + "control-center": { + "cards-desc": "コントロールセンターに表示する項目と、その順序をカスタマイズします。", + "desc": "コントロールセンターの位置や挙動を設定します。", + "open-at-mouse-description": "バーの空いている領域を右クリックすると、カーソル付近にコントロールセンターを開きます。無効になっている場合、代わりに上記の配置設定が使用されます。", + "open-at-mouse-label": "右クリックでカーソル位置に開く", + "position-description": "コントロールセンターを開いたときの表示位置を選択します。", + "shortcuts-custom-button-command-description": "ボタンをクリックした際に実行するコマンド。", + "shortcuts-custom-button-enable-on-state-logic-description": "チェックコマンドに基づいて、別のアイコンや「ON」状態を有効にします。", + "shortcuts-custom-button-enable-on-state-logic-label": "ON 状態の判定ロジックを有効化", + "shortcuts-custom-button-icon-description": "ライブラリからアイコンを選択してください。", + "shortcuts-custom-button-on-clicked-label": "左クリック時のコマンド", + "shortcuts-custom-button-on-middle-clicked-description": "ボタンが中央クリックされたときに実行するコマンド。", + "shortcuts-custom-button-on-state-command-description": "ボタンを「ON」状態にするか判定するコマンド。終了コード 0 でON、それ以外でOFFとなります。", + "shortcuts-custom-button-on-state-command-label": "ON 状態の判定コマンド", + "shortcuts-custom-button-on-state-icon-description": "「ON」状態のときに表示するアイコン。", + "shortcuts-custom-button-on-state-icon-label": "ON 状態のアイコン", + "shortcuts-custom-button-state-checks-add": "状態チェックを追加", + "shortcuts-custom-button-state-checks-command": "この状態チェックで実行するコマンド", + "shortcuts-custom-button-state-checks-label": "状態チェック", + "shortcuts-custom-button-state-checks-remove": "削除", + "shortcuts-custom-button-tooltip-description": "ボタンにカーソルを合わせた際に表示するツールチップ。", + "shortcuts-custom-button-tooltip-label": "ツールチップ", + "shortcuts-title": "ショートカットウィジェット", + "system-monitor-disk-path-description": "コントロールセンターのシステムモニターカードが監視するディスクのマウントポイントを選択します。", + "system-monitor-disk-path-label": "システムモニターのディスクパス", + "title": "コントロールセンター" + }, + "desktop-widgets": { + "clock-enabled-description": "デスクトップに時計ウィジェットを表示します。", + "clock-enabled-label": "時計ウィジェットを有効化", + "clock-format-label": "フォーマット", + "clock-height-description": "時計ウィジェットの高さをピクセル単位で設定します。", + "clock-height-label": "高さ", + "clock-minimal-mode-description": "時刻と日付のみのミニマルな時計表示を使用します。", + "clock-minimal-mode-label": "ミニマルモード", + "clock-rounded-corners-description": "ウィジェットの背景を角丸にしてください。", + "clock-rounded-corners-label": "角を丸くする", + "clock-show-background-description": "時計ウィジェットの背景コンテナを表示します。", + "clock-show-background-label": "背景を表示", + "clock-show-date-description": "時刻の下に現在の日付を表示します。", + "clock-show-date-label": "日付を表示", + "clock-show-seconds-description": "時刻に秒を表示します。", + "clock-show-seconds-label": "秒を表示", + "clock-style-analog": "アナログ", + "clock-style-binary": "バイナリ", + "clock-style-description": "時計の表示スタイルを選択します。", + "clock-style-digital": "デジタル", + "clock-style-label": "時計のスタイル", + "clock-style-minimal": "ミニマル", + "clock-width-description": "時計ウィジェットの幅をピクセル単位で設定します。", + "cpu-intensive-note": "! が付いているWidgetはより多くのCPUを使用するため、注意して有効にしてください。", + "edit-mode-button-label": "編集モードに入る", + "edit-mode-controls-explanation": "左クリックでドラッグ:ウィジェットの移動またはサイズ変更。\n右クリック:コンテキストメニューオプションを開く。", + "edit-mode-description": "編集モードを有効にして、デスクトップウィジェットの移動や配置変更を行います。有効にするとウィジェットに枠線が表示され、位置を変更できるようになります。", + "edit-mode-exit-button": "編集モードを終了", + "edit-mode-grid-snap-label": "グリッドにスナップ", + "edit-mode-grid-snap-scale-label": "グリッドスナップスケール", + "edit-mode-label": "編集モード", + "enabled-description": "デスクトップウィジェット全体を有効または無効にします。", + "enabled-label": "デスクトップウィジェットを有効化", + "general-desc": "デスクトップに表示されるウィジェットを設定します。", + "general-title": "デスクトップウィジェット", + "media-player-enabled-description": "デスクトップにメディアプレーヤーウィジェットを表示します。", + "media-player-enabled-label": "メディアプレーヤーウィジェットを有効化", + "media-player-rounded-corners-description": "ウィジェットの端を丸くする。", + "media-player-show-album-art-description": "アルバムのアートワークとトラック情報(タイトルとアーティスト)を表示してください。", + "media-player-show-album-art-label": "アルバムアートとタイトルを表示", + "media-player-show-background-description": "メディアプレーヤーウィジェットの背景コンテナを表示します。", + "media-player-show-buttons-description": "メディアプレーヤーウィジェットにメディアコントロールボタン(再生/一時停止、前へ、次へ)を表示します。", + "media-player-show-buttons-label": "ボタンを表示", + "media-player-show-visualizer-description": "オーディオビジュアライザーのオーバーレイを表示する。", + "media-player-visualizer-type-description": "デスクトップメディアプレーヤーの背景に使用するビジュアライザーの種類を選択します。", + "overview-enabled-description": "コンポジターの概要が開いている間、デスクトップウィジェットを表示する。", + "overview-enabled-label": "概要に表示", + "system-stat-layout-bottom": "下", + "system-stat-layout-description": "凡例がグラフに対してどのように表示されるかを選択します。", + "system-stat-layout-label": "レイアウト", + "system-stat-layout-side": "側", + "system-stat-rounded-corners-description": "ウィジェットの背景に丸い角を使用する。", + "system-stat-rounded-corners-label": "角丸", + "system-stat-show-background-description": "システム統計ウィジェットの背景コンテナを表示します。", + "system-stat-show-background-label": "背景を表示", + "system-stat-stat-type-description": "表示するシステム統計を選択してください。", + "system-stat-stat-type-label": "統計の種類", + "title": "デスクトップウィジェット", + "weather-enabled-description": "デスクトップに天気ウィジェットを表示します。", + "weather-enabled-label": "天気ウィジェットを有効化", + "weather-show-background-description": "天気ウィジェットの背景コンテナを表示します。" + }, + "display": { + "monitors-backlight-device-auto-option": "デフォルト", + "monitors-backlight-device-description": "この出力のバックライトデバイスを選択してください。", + "monitors-backlight-device-label": "バックライトデバイス", + "monitors-brightness-step-description": "明るさの変化量(スクロールホイールやショートカットキー)を調整します。", + "monitors-brightness-step-label": "明るさの調整ステップ", + "monitors-brightness-unavailable-ddc-disabled": "明るさ調整を利用できません。このディスプレイを操作するには「外部ディスプレイの明るさ制御」を有効にしてください。", + "monitors-brightness-unavailable-generic": "このディスプレイでは明るさ調整を利用できません。", + "monitors-desc": "各ディスプレイの明るさ設定を調整します。", + "monitors-enforce-minimum-description": "明るさを 0% にした際に、一部のディスプレイでバックライトが完全に消えてしまう問題を回避します。", + "monitors-enforce-minimum-label": "最低輝度を確保 (1%)", + "monitors-external-brightness-description": "DDC/CI プロトコルを使用して外部ディスプレイの明るさを制御するため、DDCUtil を有効にします。", + "monitors-external-brightness-label": "外部ディスプレイの明るさ制御", + "monitors-global-brightness-description": "すべてのモニターの明るさを一度に調整します。", + "monitors-global-brightness-label": "すべてのモニター", + "monitors-title": "ディスプレイごとの設定", + "night-light-auto-schedule-description": "{location} の日の出・日の入り時刻に合わせます(推奨)。", + "night-light-auto-schedule-label": "自動スケジュール", + "night-light-desc": "ブルーライトを抑えて、睡眠の改善と目の疲れの軽減を助けます。", + "night-light-enable-description": "暖色のフィルターを適用し、ブルーライトを軽減します。", + "night-light-enable-label": "夜間モードの有効化", + "night-light-force-activation-description": "スケジュールを無視して、すぐに夜間フィルターを適用します。", + "night-light-force-activation-label": "常に有効にする", + "night-light-manual-schedule-description": "日の出と日の入りの時間を自由に設定します。", + "night-light-manual-schedule-label": "手動スケジュール設定", + "night-light-manual-schedule-select-start": "開始時間を選択", + "night-light-manual-schedule-select-stop": "終了時間を選択", + "night-light-manual-schedule-sunrise": "日の出の時間", + "night-light-manual-schedule-sunset": "日の入りの時間", + "night-light-temperature-day": "日中", + "night-light-temperature-day-description": "日中の色温度を調整します。", + "night-light-temperature-description": "日中と夜間の色温度(暖かさ)を設定します。", + "night-light-temperature-label": "色温度", + "night-light-temperature-night": "夜間", + "night-light-temperature-night-description": "夜間の色温度を調整します。", + "title": "ディスプレイ" + }, + "dock": { + "appearance-background-opacity-description": "ドックの背景の不透明度を調整します。", + "appearance-border-radius-description": "ドックの角の丸みを調整します。", + "appearance-border-radius-label": "角の丸み", + "appearance-colorize-icons-description": "ドックのアプリアイコンにテーマカラーを適用します(非フォーカス時のみ)。", + "appearance-colorize-icons-label": "テーマカラーの適用", + "appearance-dead-opacity-description": "起動していないアプリアイコンの不透明度を調整します。", + "appearance-dead-opacity-label": "未実行時の不透明度", + "appearance-desc": "ドックの動作と外観をカスタマイズします。", + "appearance-display-auto-hide": "自動的に隠す", + "appearance-display-description": "ドックの表示方法を選択します。", + "appearance-display-exclusive": "領域を確保", + "appearance-dock-indicator-description": "ドックが非表示のときに小さなインジケーターを表示します。", + "appearance-dock-indicator-label": "ドックインジケーター", + "appearance-floating-distance-description": "画面端からドックまでの距離を設定します。", + "appearance-floating-distance-label": "画面端からの距離", + "appearance-group-apps-description": "同じアプリの複数のウィンドウを1つのDockエントリにグループ化します。", + "appearance-group-apps-label": "同じアプリをグループ化", + "appearance-group-click-action-cycle": "ウィンドウを切り替える", + "appearance-group-click-action-description": "グループ化されたアプリの左クリック動作を選択します。", + "appearance-group-click-action-label": "グループ化されたアプリのクリック時の動作", + "appearance-group-click-action-list": "ウィンドウリストを開く", + "appearance-group-context-menu-mode-description": "グループ化されたアプリのコンテキストメニューの動作を選択してください。", + "appearance-group-context-menu-mode-extended": "拡張", + "appearance-group-context-menu-mode-label": "グループ化されたアプリメニューモード", + "appearance-group-context-menu-mode-list": "ウィンドウリスト", + "appearance-group-indicator-style-description": "グループ化された実行中のインジケーターが、フォーカスされたウィンドウの状態をどのように表示するかを選択します。", + "appearance-group-indicator-style-dots": "ドット", + "appearance-group-indicator-style-label": "グループ化されたインジケータースタイル", + "appearance-group-indicator-style-number": "数値", + "appearance-hide-show-speed-description": "ドックの表示・非表示アニメーションの速度を調整します。", + "appearance-hide-show-speed-label": "表示・非表示の速度", + "appearance-icon-size-description": "ドック全体のサイズを調整します。", + "appearance-icon-size-label": "ドックのサイズ", + "appearance-inactive-indicators-description": "現在アクティブなアプリだけでなく、すべてのアプリにインジケーターを表示します。", + "appearance-inactive-indicators-label": "実行インジケーター", + "appearance-indicator-color-description": "隠れたドックのインジケーターの色を選択します。", + "appearance-indicator-color-label": "インジケーターの色", + "appearance-indicator-opacity-description": "隠れたドックのインジケーターの不透明度を調整します。", + "appearance-indicator-opacity-label": "インジケーターの不透明度", + "appearance-indicator-thickness-description": "隠れたドックのインジケーターを太く表示します (3pxの代わりに6px)。", + "appearance-indicator-thickness-label": "太いインジケーター", + "appearance-launcher-icon-description": "Dock Launcher に使用するアイコンを選択してください。", + "appearance-launcher-icon-label": "ランチャーアイコン", + "appearance-launcher-position-description": "ランチャーアイコンをドックのどこに表示するかを選択します。", + "appearance-launcher-position-end": "終了", + "appearance-launcher-position-label": "ランチャーの位置", + "appearance-launcher-position-start": "開始", + "appearance-launcher-use-distro-logo-description": "ディストリビューションのロゴをDock Launcherアイコンとして使用します。", + "appearance-launcher-use-distro-logo-label": "アイコンの代わりにディストロのロゴを使用する", + "appearance-pinned-static-description": "ピン留めされたアプリアイコンを常に左側に寄せて固定します。", + "appearance-pinned-static-label": "ピン留めアプリの固定配置", + "appearance-position-description": "ドックを画面上のどこに表示するかを選択してください。", + "appearance-position-label": "位置", + "appearance-show-launcher-icon-description": "ドックにアプリケーションランチャーアイコンを表示する。", + "appearance-show-launcher-icon-label": "アプリランチャーを表示", + "appearance-sit-on-frame-description": "ドックをフレームの境界の内側に配置し、上部に重ならないようにします。", + "appearance-sit-on-frame-label": "ドックはフレームに配置", + "appearance-type-attached": "ドッキング", + "appearance-type-description": "浮動するピル型、または端に固定された静的バーのどちらかを選択してください。", + "appearance-type-floating": "フローティング", + "appearance-type-label": "ドックスタイル", + "enabled-description": "ドックを完全に表示または非表示にします。", + "enabled-label": "ドックの有効化", + "monitors-desc": "ドックを表示するディスプレイを選択します。未選択の場合は全てに表示されます。", + "monitors-only-same-monitor-description": "ドックが配置されているディスプレイ上のアプリのみを表示します。", + "monitors-only-same-monitor-label": "同じディスプレイのアプリのみ", + "monitors-title": "表示するディスプレイ", + "title": "ドック" + }, + "general": { + "copy-settings": "設定をコピー", + "fonts-default-description": "インターフェース全体で使用されるメインフォント。", + "fonts-default-label": "メインフォント", + "fonts-default-placeholder": "フォントを選択...", + "fonts-default-scale-description": "標準テキストのサイズを拡大・縮小します。", + "fonts-default-scale-label": "メインフォントのサイズ", + "fonts-default-search-placeholder": "フォントを検索...", + "fonts-desc": "インターフェース全体で使用するフォントを選択します。", + "fonts-monospace-description": "数字や統計情報の表示に使用される等幅フォント。", + "fonts-monospace-label": "等幅フォント", + "fonts-monospace-placeholder": "等幅フォントを選択...", + "fonts-monospace-scale-description": "等幅テキストのサイズを拡大・縮小します。", + "fonts-monospace-scale-label": "等幅フォントのサイズ", + "fonts-monospace-search-placeholder": "等幅フォントを検索...", + "fonts-title": "フォント", + "keybinds-conflict-description": "このキーの組み合わせはすでに{action}に割り当てられています。", + "keybinds-conflict-title": "キーバインドの競合", + "keybinds-description": "パネルとランチャーのグローバルナビゲーションキーを設定します。", + "keybinds-down": "下に移動", + "keybinds-enter": "確認 / アクション", + "keybinds-escape": "閉じる / 戻る", + "keybinds-left": "左へ移動", + "keybinds-modifier-description": "ショートカットには修飾キー (Ctrl または Alt) を含める必要があります。", + "keybinds-modifier-title": "修飾キーが必要です", + "keybinds-remove": "削除", + "keybinds-right": "右へ移動", + "keybinds-title": "ナビゲーションのキーバインド", + "keybinds-up": "上へ移動", + "language-desc": "アプリケーションで使用する言語を選択します。", + "language-select-auto-detect": "自動検出", + "language-select-description": "アプリケーションのインターフェースで使用する言語を選択します。", + "language-select-label": "表示言語", + "launch-setup-wizard": "セットアップウィザード", + "profile-desc": "ユーザーの詳細とアバターを編集します。", + "profile-picture-description": "インターフェース全体に表示されるプロフィール画像。", + "profile-picture-label": "プロフィール画像", + "profile-select-avatar": "アバター画像を選択", + "profile-title": "プロフィール", + "profile-tooltip": "プロフィール写真", + "reverse-scrolling-description": "解釈されたスクロール方向を反転。", + "reverse-scrolling-label": "スクロール方向を反転", + "screen-corners-desc": "画面の角の丸みや視覚効果をカスタマイズします。", + "screen-corners-radius-description": "画面の角の丸みを調整します。", + "screen-corners-radius-label": "角の丸み", + "screen-corners-radius-reset": "角の丸みをリセット", + "screen-corners-show-corners-description": "画面の端に丸い角(装飾)を表示します。", + "screen-corners-show-corners-label": "画面の角を表示する", + "screen-corners-solid-black-description": "バーの背景色の代わりに、黒一色を使用します。", + "screen-corners-solid-black-label": "角を黒く塗りつぶす", + "screen-corners-title": "画面の角", + "settings-copied": "設定をクリップボードにコピーしました", + "smooth-scrolling-description": "リストのスクロールをアニメーション化して、よりスムーズなホイール操作を実現します。", + "smooth-scrolling-label": "スムーズスクロール", + "tab-basics": "基本", + "tab-keybinds": "キーバインド" + }, + "hooks": { + "color-generation-description": "色の生成が完了したときに実行されるコマンド(例:壁紙ベースのテーマ適用後など)。", + "color-generation-label": "色が生成されました", + "color-generation-placeholder": "例: notify-send \"色\" \"生成済み\"", + "info-command-info-description": "• コマンドはシェル (sh -lc) 経由で実行されます
• コマンドはバックグラウンドで実行されます
• テストボタンは現在の設定値を使って実行されます", + "info-parameters-description": "• Wallpaper hook: $1 = 壁紙のパス, $2 = 画面名, $3 = テーマ (ダーク/ライト)
• Theme toggle hook: $1 = true/false (ダークモードの状態)
• Screen lock/unlock hooks: $1 = lock/unlock (画面ロックの状態)
• Performance mode hooks: パラメータなし
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "利用可能なパラメータ(引数)", + "noctalia-started-description": "Noctaliaのロードが完了したときに実行するコマンド。", + "noctalia-started-label": "Noctalia が起動しました", + "noctalia-started-placeholder": "例:notify-send 'Noctalia 読み込み完了'", + "performance-mode-disabled-description": "Noctalia パフォーマンスモードが無効になった際に実行されるコマンド。", + "performance-mode-disabled-label": "パフォーマンスモード無効時", + "performance-mode-disabled-placeholder": "例:notify-send \"Performance\" \"Mode disabled\"", + "performance-mode-enabled-description": "Noctalia パフォーマンスモードが有効になった際に実行されるコマンド。", + "performance-mode-enabled-label": "パフォーマンスモード有効時", + "performance-mode-enabled-placeholder": "例:notify-send \"Performance\" \"Mode enabled\"", + "screen-lock-description": "画面がロックされた際に実行されるコマンド。", + "screen-lock-label": "画面ロック時", + "screen-lock-placeholder": "例:notify-send \"Screen\" \"Locked\"", + "screen-unlock-description": "画面ロックが解除された際に実行されるコマンド。", + "screen-unlock-label": "画面ロック解除時", + "screen-unlock-placeholder": "例:notify-send \"Screen\" \"Unlocked\"", + "session-description": "シャットダウンまたは再起動の前に実行されるコマンド。アクションタイプを $1 (シャットダウン/再起動) として受け取ります。", + "session-label": "セッション終了", + "session-placeholder": "例:notify-send \"Session\" \"$1\"", + "system-hooks-desc": "システムイベントが発生した際に実行するコマンドを設定します。", + "system-hooks-enable-description": "すべてのフックコマンドの有効・無効を切り替えます。", + "system-hooks-enable-label": "フックの有効化", + "system-hooks-title": "システムフック", + "theme-changed-description": "ダークモードとライトモードが切り替わった際に実行されるコマンド。", + "theme-changed-label": "テーマ変更時", + "theme-changed-placeholder": "例:notify-send \"Theme\" \"Toggled\"", + "title": "フック", + "wallpaper-changed-description": "壁紙が変更された際に実行されるコマンド。", + "wallpaper-changed-label": "壁紙変更時", + "wallpaper-changed-placeholder": "例:notify-send \"Wallpaper\" \"Changed\"" + }, + "idle": { + "custom-add": "コマンドを追加", + "custom-description": "一定期間の非アクティブ状態の後、シェルコマンドを実行します。", + "custom-entry-command": "コマンド", + "custom-entry-delete": "削除", + "custom-entry-edit": "カスタムコマンドを編集", + "custom-entry-name": "名前", + "custom-entry-name-placeholder": "例:ライトを消す", + "custom-entry-new": "新しいカスタムコマンド", + "custom-entry-timeout": "非アクティブ時間", + "custom-entry-timeout-format": "{count} 秒", + "custom-entry-timeout-format-plural": "{count} 秒", + "custom-entry-unnamed": "名前のないコマンド", + "custom-label": "カスタムアイドルコマンド", + "enable-description": "一定時間操作がない場合、自動的に画面をオフにする、ロックする、またはサスペンドする。", + "enable-label": "アイドル管理を有効にする", + "fade-duration-description": "各アクションが実行される前の、画面が暗転するアニメーションの秒数。マウスを動かすとフェードはキャンセルされます。", + "fade-duration-label": "フェード時間", + "lock-description": "ロック画面がアクティブになるまでの非アクティブ秒数。", + "lock-label": "画面をロック", + "resume-command-label": "コマンドを再開", + "screen-off-description": "モニターがオフになるまでの非アクティブな秒数。", + "screen-off-label": "画面をオフにする", + "status-description": "コンポジターによって報告されたアイドル時間。", + "status-label": "アイドル時間", + "suspend-description": "システムがサスペンドするまでの非アクティブな時間(秒)。", + "tab-behavior": "動作", + "tab-custom": "カスタム", + "timeouts-description": "ステージを無効にするには0に設定します。スリープ防止がアクティブな間、タイムアウトは一時停止されます。", + "timeouts-label": "タイムアウト", + "title": "アイドル", + "unavailable": "このCompositorでは、ネイティブのアイドル監視は利用できません。" + }, + "indicator": { + "default-value": "デフォルト:{value}", + "system-default": "システムのデフォルト" + }, + "launcher": { + "clipboard-desc": "ランチャーからクリップボードの履歴にアクセスして管理できます。", + "execute-desc": "アプリケーションの起動方法を設定します。", + "execute-title": "実行", + "settings-annotation-tool-description": "クリップボード履歴のアノテーションボタンをクリックしたときに実行するコマンド。画像はこのコマンドにパイプされます。", + "settings-annotation-tool-label": "注釈ツール", + "settings-annotation-tool-placeholder": "例:'gradia', 'satty -f -'", + "settings-auto-paste-description": "選択されたクリップボードの項目を自動的に貼り付けます。wtypeが必要です。", + "settings-auto-paste-label": "自動貼り付け", + "settings-clip-chips-description": "タブバーを表示して、クリップボードの履歴をタイプ別(画像、リンク、ファイル、コードなど)にフィルタリングします。", + "settings-clip-chips-label": "カテゴリチップを有効にする", + "settings-clip-preview-description": ">clip コマンド使用時に、クリップボードの内容をプレビュー表示します。", + "settings-clip-preview-label": "クリップボードのプレビューを有効化", + "settings-clip-smart-icons-description": "一般的なクリップボードアイコンの代わりに、リンク、ファイル、色、コードに固有のアイコンを表示します。", + "settings-clip-smart-icons-label": "スマートアイコンを有効にする", + "settings-clip-wrap-text-description": "クリップボードのリストでテキストを切り捨てる代わりに、折り返して表示する。", + "settings-clip-wrap-text-label": "クリップボードのテキストを折り返す", + "settings-clipboard-history-description": "以前コピーした項目にランチャーからアクセスできるようにします。", + "settings-clipboard-history-label": "クリップボード履歴を有効化", + "settings-clipboard-watch-image-description": "画像変更のためにwl-pasteに渡される完全なコマンド文字列(再起動が必要)。", + "settings-clipboard-watch-image-label": "画像監視コマンド", + "settings-clipboard-watch-text-description": "テキスト変更のために wl-paste に渡される完全なコマンド文字列(再起動が必要)。", + "settings-clipboard-watch-text-label": "テキスト監視コマンド", + "settings-custom-launch-prefix-description": "コマンドの先頭にカスタムランチャーを付与します(例:systemd 連携用の 'runapp')。", + "settings-custom-launch-prefix-enabled-description": "デフォルトの方法の代わりに、カスタムプレフィックスを使用してアプリを起動します。", + "settings-custom-launch-prefix-enabled-label": "カスタム起動プレフィックスを有効化", + "settings-custom-launch-prefix-label": "カスタム起動プレフィックス", + "settings-density-description": "アプリケーションアイコンのサイズとLauncherの密度を調整します。", + "settings-density-label": "密度", + "settings-desc": "ランチャーの挙動と外観をカスタマイズします。", + "settings-enable-session-search-description": "Launcherで検索する際に、セッションアクション(ロック、シャットダウン、再起動など)を表示する。", + "settings-enable-session-search-label": "セッション検索を有効にする", + "settings-enable-settings-search-description": "ランチャーで検索する際に設定結果を表示する。", + "settings-enable-settings-search-label": "設定検索を有効にする", + "settings-enable-windows-search-description": "アクティブなウィンドウを検索してフォーカスします。", + "settings-enable-windows-search-label": "ウィンドウ検索を有効にする", + "settings-grid-view-description": "リスト形式の代わりに、グリッド(格子状)レイアウトで項目を表示します。", + "settings-icon-mode-description": "Tablerアイコンではなく、ネイティブのシステムアイコンを使用します。", + "settings-icon-mode-label": "ネイティブアイコンを使用", + "settings-ignore-mouse-input-description": "ランチャーでのマウス操作とスクロールホイールを無効にする。", + "settings-ignore-mouse-input-label": "マウス入力を無視する", + "settings-overlay-layer-description": "ランチャーをオーバーレイレイヤーに、全画面ウィンドウの上に表示します。有効にすると、ランチャーはバーにアタッチされません。", + "settings-overlay-layer-label": "フルスクリーンより上に表示", + "settings-position-description": "ランチャーパネルの表示位置を選択します。", + "settings-show-categories-description": "アプリケーションを絞り込むためのカテゴリタブを表示します。", + "settings-show-categories-label": "カテゴリを表示", + "settings-show-icon-background-description": "アイコンの背面に角丸の長方形の背景を表示する。", + "settings-show-icon-background-label": "アイコンの背景を表示", + "settings-sort-by-usage-description": "有効にすると、よく使うアプリがリストの上位に表示されます。", + "settings-sort-by-usage-label": "使用頻度順に並べ替え", + "settings-terminal-command-description": "ターミナルを起動するためのコマンド(例:'kitty -e' or 'gnome-terminal --')。", + "settings-terminal-command-label": "ターミナル起動コマンド", + "settings-use-app2unit-description": "アプリのプロセス管理を改善し、問題を回避するために代替の起動方法を使用します。", + "settings-use-app2unit-label": "App2Unit を使用して起動", + "settings-view-mode-description": "ランチャーのエントリのレイアウトを選択してください。", + "settings-view-mode-label": "表示モード", + "title": "ランチャー" + }, + "location": { + "auto-locate-description": "IPアドレスを使用して、定期的に現在地を自動検出します。", + "auto-locate-label": "自動検出", + "calendar-cards-desc": "カレンダーパネルのカードを整理し、有効・無効を切り替えます。", + "calendar-cards-title": "カレンダーカード", + "calendar-header-label": "カレンダーヘッダー", + "calendar-month-label": "カレンダー(月表示)", + "date-time-12hour-format-description": "ロック画面とカレンダーで時刻を12時間表記で表示します。バーの時計には独自の設定があります。", + "date-time-12hour-format-label": "12時間表記を使用", + "date-time-desc": "日付と時刻の表示形式をカスタマイズします。", + "date-time-first-day-of-week-automatic": "自動(システムのロケールを使用)", + "date-time-first-day-of-week-description": "カレンダーの週の始まりの曜日を選択します。", + "date-time-first-day-of-week-label": "週の始まり", + "date-time-show-events-description": "カレンダーパネルにイベント(予定)を表示します。", + "date-time-show-events-label": "カレンダーのイベントを表示", + "date-time-title": "日付と時刻", + "date-time-use-analog-description": "カレンダーウィンドウにアナログスタイルの時計を表示します。", + "date-time-use-analog-label": "アナログ時計を使用", + "date-time-week-numbers-description": "カレンダーに週番号(例:第38週)を表示します。", + "date-time-week-numbers-label": "週番号を表示", + "geolocate-now-button": "今すぐ検索", + "location-desc": "場所を設定して、正確な天気情報や夜間モードのスケジュールを取得します。", + "location-search-description": "例:東京、日本", + "location-search-label": "場所を検索", + "location-search-placeholder": "地名を入力", + "location-title": "場所の設定", + "weather-desc": "温度の単位などを選択します。", + "weather-enabled-description": "インターフェース全体に天気情報を表示し、データを取得します。無効にすると、すべての天気要素が非表示になり、ネットワーク通信も行われません。", + "weather-enabled-label": "天気を有効化", + "weather-fahrenheit-description": "気温を摂氏 (°C) ではなく華氏 (°F) で表示します。", + "weather-fahrenheit-label": "華氏 (°F) で表示", + "weather-hide-city-description": "インターフェース全体の天気表示から都市名を隠してください。", + "weather-hide-city-label": "都市名を隠す", + "weather-hide-timezone-description": "インターフェース全体の天気表示からタイムゾーンの略称を非表示にする。", + "weather-hide-timezone-label": "タイムゾーンを隠す", + "weather-show-effects-description": "天気カードに視覚効果(雨、雪、雷など)を追加表示します。", + "weather-show-effects-label": "天気エフェクトを表示", + "weather-show-in-calendar-description": "カレンダービューに毎日の天気予報を直接表示します。", + "weather-show-in-calendar-label": "カレンダー", + "weather-talia-mascot-always-description": "tabler icons の代わりに常に Talia emojis を使用してください。", + "weather-talia-mascot-always-label": "Taliaの天気絵文字を常に表示する" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "fprintd(指紋認証)が有効な場合、このオプションを使用すると、指紋の代わりにパスワードでログインできます。", + "allow-password-with-fprintd-label": "fprintd でパスワードログインを許可する", + "auto-start-auth-description": "たとえば、キーを押したりボタンをクリックしたりしなくても、指紋認証が自動的に開始されます。", + "auto-start-auth-label": "自動認証を開始", + "clock-format-description": "日付/時刻の構文トークンを使用して時計のフォーマットをカスタマイズします。", + "clock-format-label": "時計の形式", + "clock-style-analog": "アナログ", + "clock-style-custom": "カスタム", + "clock-style-description": "ロック画面の時計の表示スタイルを選択してください。", + "clock-style-digital": "デジタル", + "clock-style-label": "時計のスタイル", + "compact-lockscreen-description": "天気やメディアウィジェットを隠し、ログイン入力とシステムコントロールのみを表示します。", + "compact-lockscreen-label": "コンパクトなロック画面", + "enable-lockscreen-media-controls-description": "ロック画面にインタラクティブなメディア再生コントロールを表示する。", + "enable-lockscreen-media-controls-label": "ロック画面のメディアコントロール", + "lock-on-suspend-description": "システムのサスペンド時に、自動的に画面をロックします。", + "lock-on-suspend-label": "サスペンド時にロック", + "lock-screen-animations-description": "ロック画面のアニメーションを有効または無効にする。", + "lock-screen-animations-label": "ロック画面のアニメーション", + "lock-screen-blur-strength-description": "ロック画面の壁紙にぼかし効果を適用します。", + "lock-screen-blur-strength-label": "ロック画面のぼかし強度", + "lock-screen-tint-strength-description": "ロック画面の壁紙に色合いのオーバーレイを適用します。", + "lock-screen-tint-strength-label": "ロック画面の着色強度", + "monitors-desc": "ロック画面を特定のモニターに表示します。何も選択されていない場合、デフォルトですべてに表示されます。", + "password-chars-description": "パスワードを隠すためのかわいいアイコン。", + "password-chars-label": "ランダムなパスワードアイコン", + "show-hibernate-description": "電源操作に「ハイバネート(休止状態)」のオプションを表示します。", + "show-hibernate-label": "ハイバネートを表示", + "show-session-buttons-description": "ロック画面から電源操作を行えるようにします。", + "show-session-buttons-label": "電源操作", + "title": "ロック画面" + }, + "network": { + "bluetooth-description": "Bluetooth の管理を有効にします。", + "bluetooth-rssi-polling-description": "bluetoothctl 経由で接続されたデバイスのRSSIを定期的にサンプリングします。 すべてのデバイスで利用できるとは限りません。 有効にすると、最小限のリソースを使用します。", + "bluetooth-rssi-polling-label": "Bluetooth信号ポーリング", + "desc": "Wi-Fi と Bluetooth 接続を管理します。", + "wifi-description": "ワイヤレスネットワークを管理します(NetworkManager が必要です)。" + }, + "notifications": { + "duration-critical-urgency-description": "緊急度「重大」の通知が表示され続ける時間。", + "duration-critical-urgency-label": "緊急度:重大", + "duration-desc": "緊急度レベルに基づいて、通知の表示時間を設定します。", + "duration-low-urgency-description": "緊急度「低」の通知が表示され続ける時間。", + "duration-low-urgency-label": "緊急度:低", + "duration-normal-urgency-description": "緊急度「通常」の通知が表示され続ける時間。", + "duration-normal-urgency-label": "緊急度:通常", + "duration-respect-expire-description": "通知自体に設定された有効期限(タイムアウト)を使用します。", + "duration-respect-expire-label": "通知指定の期限に従う", + "duration-title": "通知の表示時間", + "history-clear-dismiss-description": "通知を閉じるときに履歴から削除する。", + "history-clear-dismiss-label": "却下時にクリア", + "history-critical-urgency-description": "緊急優先度の通知を履歴に保存します。", + "history-critical-urgency-label": "緊急度:重大を履歴に保存", + "history-desc": "緊急度レベルに基づいて、履歴に保存する通知を制御します。", + "history-low-urgency-description": "低優先度の通知を履歴に保存します。", + "history-low-urgency-label": "低緊急度を履歴に保存", + "history-normal-urgency-description": "通常優先度の通知を履歴に保存します。", + "history-normal-urgency-label": "通常緊急度を履歴に保存", + "monitors-desc": "通知を表示するディスプレイを選択します。未選択の場合は全てに表示されます。", + "rules-action-block": "ブロック", + "rules-action-block-desc": "完全にスキップします。", + "rules-action-hide": "非表示", + "rules-action-hide-desc": "ポップアップなし、音なし、履歴に追加されます。", + "rules-action-label": "アクション", + "rules-action-mute": "ミュート", + "rules-action-mute-desc": "音は出ないが、ポップアップは表示され、履歴にも残る。", + "rules-add": "ルールを追加", + "rules-delete": "ルールを削除", + "rules-description": "アプリ名またはコンテンツに一致。ルールは順番にチェックされ、最初の一致が適用されます。", + "rules-edit": "ルールを編集", + "rules-label": "フィルター規則", + "rules-pattern-label": "パターン", + "rules-pattern-placeholder": "firefox, discord, または /regex/", + "rules-tab": "ルール", + "settings-always-on-top-description": "全画面ウィンドウや他のレイヤーよりも手前に通知を表示します。", + "settings-background-opacity-description": "通知の背景の不透明度を調整します。", + "settings-density-description": "通知カードの密度を選択してください。", + "settings-density-label": "密度", + "settings-desc": "通知の外観と挙動を設定します。", + "settings-do-not-disturb-description": "有効にすると、すべての通知ポップアップを無効にします。", + "settings-enabled-description": "通知デーモンを有効化・無効化します(Noctalia shellの再起動が必要です)。", + "settings-enabled-label": "通知を有効化", + "settings-location-description": "画面上の通知の表示位置。", + "settings-markdown-description": "Markdown形式を使用して通知コンテンツを表示します。", + "settings-markdown-label": "Markdown を有効にする", + "sounds-desc": "通知音の効果と音量を設定します。", + "sounds-enabled-description": "通知受信時のの効果音を有効にします。", + "sounds-enabled-label": "通知音を有効化", + "sounds-excluded-apps-description": "独自の通知音を持つ特定のアプリケーションについて、設定した通知音の再生をスキップします。", + "sounds-excluded-apps-label": "除外するアプリケーション", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "緊急度「重大」の通知で再生する音声ファイルのパス。", + "sounds-files-critical-label": "緊急度「重大」の通知音", + "sounds-files-critical-select-title": "緊急度「重大」の通知音ファイルを選択", + "sounds-files-desc": "通知の緊急度レベルごとにカスタム音声ファイルを設定します。", + "sounds-files-low-description": "緊急度「低」の通知で再生する音声ファイルのパス。", + "sounds-files-low-label": "緊急度「低」の通知音", + "sounds-files-low-select-title": "緊急度「低」の通知音ファイルを選択", + "sounds-files-normal-description": "緊急度「通常」の通知で再生する音声ファイルのパス。", + "sounds-files-normal-label": "緊急度「通常」の通知音", + "sounds-files-normal-select-title": "緊急度「通常」の通知音ファイルを選択", + "sounds-files-placeholder": "音声ファイルへのパスを入力", + "sounds-files-select-file": "音声ファイルを選択", + "sounds-files-title": "音声ファイル", + "sounds-files-unified-description": "通知で再生する音声ファイルへのパス。", + "sounds-files-unified-label": "通知音", + "sounds-files-unified-select-title": "通知音ファイルを選択", + "sounds-separate-description": "緊急度(低、通常、重大)ごとに異なる音声ファイルを使用します。", + "sounds-separate-label": "緊急度ごとに異なる通知音を使用", + "sounds-title": "通知音設定", + "sounds-unavailable-description": "通知音を有効にするには、Qt6 Multimedia をインストールしてください。", + "sounds-unavailable-label": "通知音を利用できません", + "sounds-volume-description": "通知音の音量レベルを調整します。", + "sounds-volume-label": "通知音量", + "toast-battery-description": "バッテリー残量がこの割合を下回った場合に警告を表示します。", + "toast-battery-label": "バッテリー警告", + "toast-desc": "トースト通知の外観と挙動を設定します。", + "toast-keyboard-description": "キーボードレイアウトの変更時にトーストを表示します。", + "toast-keyboard-label": "キーボードレイアウト", + "toast-media-description": "メディアの再生状態が変化したときにトーストを表示します。", + "toast-media-label": "メディア" + }, + "osd": { + "always-on-top-description": "全画面ウィンドウや他のレイヤーよりも手前に OSD を表示します。", + "always-on-top-label": "常に最前面に表示", + "background-opacity-description": "OSD の背景の不透明度を調整します。", + "background-opacity-label": "背景の不透明度", + "description": "音量や輝度のオーバーレイなど、オンスクリーン表示を設定します。", + "duration-auto-hide-description": "OSD が消えるまでの時間を調整します。", + "duration-auto-hide-label": "自動的に隠すまでの時間", + "duration-desc": "OSD が自動的に隠れるまでの表示継続時間。", + "duration-title": "自動非表示のタイムアウト", + "enabled-description": "音量や輝度の変更をリアルタイムで表示します。", + "enabled-label": "オンスクリーンディスプレイを有効に (OSD)", + "events-desc": "画面表示をトリガーするイベントを選択してください。", + "general-desc": "OSDの表示と動作を設定します。", + "location-description": "OSD の表示位置を選択します。", + "monitors-desc": "OSD を表示するディスプレイを選択します。未選択の場合は全てに表示されます。", + "title": "オンスクリーンディスプレイ", + "types-brightness-description": "画面の輝度が変更された時に OSD を表示します。", + "types-brightness-label": "明るさ", + "types-custom-text-description": "IPC 経由のカスタムテキストメッセージを OSD に表示します。", + "types-custom-text-label": "カスタムテキスト", + "types-desc": "OSD を表示するトリガー(イベント)を選択します。", + "types-input-volume-description": "マイク音量が変更された時に OSD を表示します。", + "types-input-volume-label": "入力音量", + "types-lockkey-description": "Caps Lock、Num Lock、Scroll Lock の切り替え時に OSD を表示します。", + "types-lockkey-label": "ロックキー", + "types-media-description": "メディア再生状態(再生、一時停止、スキップ)が変化したときにOSDを表示します。", + "types-media-label": "メディア再生", + "types-title": "OSD のトリガーイベント", + "types-volume-description": "出力音量が変更された時に OSD を表示します。", + "types-volume-label": "出力音量" + }, + "plugins": { + "auto-update": "プラグインを自動更新", + "auto-update-description": "シェル起動時にすべてのプラグインを自動的に更新します。", + "available-description": "設定されたソースからプラグインを参照してインストールします。", + "available-label": "利用可能なプラグイン", + "available-no-plugins-description": "プラグインのソースを確認するか、リストを更新してください。", + "available-no-plugins-label": "利用可能なプラグインはありません", + "check-for-updates": "アップデートを確認", + "checking-for-updates": "アップデートを確認しています...", + "collision-already-installed": "このプラグインはすでにインストールされています", + "collision-custom-version-exists": "「{source}」 発 の カスタム版 は 既に インストール されています", + "collision-official-version-exists": "このプラグインの公式バージョンはすでにインストールされています", + "development-disable": "このプラグインの開発者モードを無効にする", + "development-enable": "このプラグインの開発モードを有効にする。", + "filter-downloaded": "ダウンロード済み", + "filter-not-downloaded": "未ダウンロード", + "filter-tags-description": "カテゴリーまたはダウンロード状況でプラグインを絞り込む。", + "filter-tags-label": "タグ", + "hot-reload-description": "ファイルの変更時にプラグインを自動的にリロードします。プラグイン開発に便利です。", + "hot-reload-label": "ホットリロード(開発モード)", + "hot-reloaded": "プラグインをリロードしました:{name}", + "install-error": "インストールに失敗しました:{error}", + "install-incompatible": "{plugin} には Noctalia v{version} 以降が必要です", + "install-success": "{plugin} をインストールしました", + "installed-description": "ローカルにインストールされたすべてのプラグインを管理・設定します。", + "installed-label": "インストール済みプラグイン", + "installed-no-plugins-description": "「利用可能」セクションからプラグインをインストールしてください。", + "installed-no-plugins-label": "インストールされているプラグインはありません", + "installing": "{plugin} をインストール中...", + "notify-updates": "プラグインの更新通知", + "notify-updates-description": "新しいプラグインのアップデートが利用可能な場合に通知ポップアップを表示します。", + "open-plugin-page": "プラグインページを開く", + "open-plugins-tab": "プラグイン設定を開く", + "plugin-settings-title": "{plugin} の設定", + "refresh-refreshing": "プラグインリストを更新中...", + "refresh-tooltip": "利用可能なプラグインを更新", + "settings-error-not-loaded": "プラグインは読み込まれていません", + "settings-saved": "プラグイン設定を保存しました", + "settings-tooltip": "プラグイン設定", + "source-custom": "カスタムソース", + "sources-add-custom": "カスタムリポジトリを追加", + "sources-add-dialog-description": "プラグインソースとして GitHub リポジトリを追加します。", + "sources-add-dialog-error": "プラグインソースの追加に失敗しました", + "sources-add-dialog-name": "リポジトリ名", + "sources-add-dialog-name-placeholder": "マイカスタムプラグイン", + "sources-add-dialog-success": "プラグインソースを追加しました", + "sources-add-dialog-title": "プラグインソースを追加", + "sources-add-dialog-url": "リポジトリの URL", + "sources-description": "プラグインリポジトリを管理します。", + "sources-placeholder": "マイリポジトリ", + "sources-remove-tooltip": "プラグインソースを削除", + "title": "プラグイン", + "translations-reloaded": "翻訳をリロードしました:{name}", + "uninstall-dialog-description": "本当に {plugin} をアンインストールしますか?すべてのプラグインデータが削除されます。", + "uninstall-dialog-title": "プラグインのアンインストール", + "uninstall-error": "アンインストールに失敗しました:{error}", + "uninstall-success": "{plugin} をアンインストールしました", + "uninstalling": "{plugin} をアンインストール中...", + "update-all": "すべてアップデート ({count})", + "update-all-success": "すべてのプラグインをアップデートしました", + "update-available": "新しいプラグインアップデートがあります", + "update-available-plural": "新しいプラグインアップデートがあります({count}個)", + "update-error": "プラグインのアップデートに失敗しました:{plugin}:{error}", + "update-pending": "v{current} → v{new} (Noctalia v{required}が必要です)", + "update-success": "{plugin} を v{version} にアップデートしました", + "update-version": "v{current} → v{new}", + "updating": "更新中..." + }, + "region": { + "title": "地域" + }, + "session-menu": { + "countdown-duration-description": "カウントダウンタイマーが電源操作を実行するまでの時間を設定します。", + "countdown-duration-label": "カウントダウンの時間", + "enable-countdown-description": "電源操作を実行する前にカウントダウンタイマーを表示します。", + "enable-countdown-label": "カウントダウンタイマーの有効化", + "entries-desc": "セッションメニューに表示される電源操作と、その表示順序をカスタマイズします。", + "entries-title": "電源操作", + "entry-settings-command-description": "このアクションのために実行するカスタムコマンド。空欄にするとデフォルトのシステムコマンドを使用します。", + "entry-settings-command-placeholder": "例:systemctl poweroff", + "entry-settings-default-command-lock": "内部ロック画面(コマンドなし)", + "entry-settings-default-command-logout": "内部ログアウト(コマンドなし)", + "entry-settings-default-info-description": "カスタムコマンドが指定されていない場合、デフォルトのシステムコマンドが使用されます。", + "entry-settings-default-info-label": "デフォルトのコマンド", + "entry-settings-keybind-description": "セッションメニューが開いているときに、このアクションをトリガーするキーの組み合わせを割り当てます。", + "entry-settings-keybind-placeholder": "キーを割り当てるには、記録を押してください...", + "entry-settings-keybind-recording": "録音中... 任意のキーの組み合わせを押してください", + "entry-settings-title": "「{entry}」の設定", + "entry-settings-tooltip": "コマンドを設定", + "general-desc": "セッションメニューパネルの動作と外観を設定します。", + "large-buttons-layout-description": "セッションメニューのボタンの表示方法を選択してください。", + "large-buttons-layout-label": "大きなボタンのレイアウト", + "large-buttons-style-description": "セッションメニューを、グリッド配置の大きなボタンで表示します。", + "large-buttons-style-label": "大きなボタンのスタイル", + "position-description": "セッションメニューパネルの表示位置を選択します。", + "show-header-description": "セッションメニューの上部にタイトルと閉じるボタンを表示します。", + "show-header-label": "ヘッダーを表示", + "show-keybinds-description": "セッションオプションにキーバインドのヒントを表示する。", + "show-keybinds-label": "キーバインドを表示" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Noctalia パフォーマンスモードが有効な間、デスクトップウィジェットを表示したままにする。", + "noctalia-performance-disable-desktop-widgets-label": "パフォーマンスモードでデスクトップウィジェットを有効にする", + "noctalia-performance-disable-wallpaper-description": "Noctalia パフォーマンスモードが有効な間、デスクトップ、概要、ロック画面の壁紙を表示したままにします。", + "noctalia-performance-disable-wallpaper-label": "パフォーマンスモードで壁紙のレンダリングを有効にする", + "title": "システム" + }, + "system-monitor": { + "critical-color-label": "危険時の色", + "custom-highlight-colors-title-label": "カスタムハイライト色", + "disk-available-label": "利用可能なディスク容量", + "disk-section-label": "ストレージ使用量", + "enable-dgpu-monitoring-description": "警告: これにより外部GPU (NVIDIA/AMD) が起動状態になるため、ハイブリッドグラフィックス搭載のノート PC ではバッテリー駆動時間に大きな影響を与える可能性があります。", + "enable-dgpu-monitoring-label": "外部GPUモニタリングを有効にする", + "external-monitor-description": "外部システムモニターアプリケーションを起動する際に実行するコマンドまたはアプリケーションパスを入力してください。", + "external-monitor-label": "外部システムモニターコマンド", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "システムモニターの動作と外観を設定します。", + "gpu-section-label": "GPU 温度", + "highlight-colors-section-label": "ハイライト色", + "polling-interval-label": "ポーリング間隔", + "polling-section-description": "各システムメトリクスの更新頻度を設定します。", + "polling-section-label": "ポーリング間隔", + "threshold-critical": "重大", + "threshold-warning": "警告", + "thresholds-section-description": "各システムメトリックの警告/危険しきい値とポーリング間隔を調整します。", + "use-custom-highlight-colors-description": "無効にすると、テーマのデフォルト強調色が使用されます。", + "use-custom-highlight-colors-label": "カスタム強調色を使用する", + "warning-color-label": "警告時の色" + }, + "user-interface": { + "allow-panels-without-bar-description": "有効にすると、どの画面でもパネルを開けるようになります。無効にすると、バーがある画面でのみパネルが開き、メモリ使用量を抑えられます。", + "allow-panels-without-bar-label": "バーのない画面でのパネル表示を許可", + "animation-disable-description": "すべてのアニメーションを無効にし、応答性と速度を向上させます。", + "animation-disable-label": "UI アニメーションの無効化", + "animation-speed-description": "全体のアニメーション速度を調整します。", + "animation-speed-label": "アニメーション速度", + "animation-speed-reset": "アニメーション速度をリセット", + "appearance-desc": "ツールチップ、境界線、影などの視覚要素をカスタマイズします。", + "blur-behind-description": "バーとパネルの背後の領域を、コンポジターのぼかしプロトコルを使用してぼかします。", + "blur-behind-label": "背景ぼかし", + "box-border-description": "コンテンツ領域の周りにアウトラインを表示します。", + "box-border-label": "コンテナの輪郭", + "box-border-radius-description": "サイドバー、カード、コンテンツパネルなど、主要なレイアウト部分の角の丸みを調整します。", + "box-border-radius-label": "コンテナの角の丸み", + "control-border-radius-description": "ボタン、トグル、テキストフィールドなど、操作可能な要素の角の丸みを制御します。", + "control-border-radius-label": "入力要素の角の丸み", + "desc": "インターフェースの外観や操作感、挙動をカスタマイズします。", + "dim-desktop-description": "パネルやメニューが開いている間、デスクトップを暗くします。", + "dim-desktop-label": "デスクトップを暗くする", + "dimmer-opacity-description": "デスクトップを暗くした際の不透明度を設定します。", + "dimmer-opacity-label": "デスクトップを暗くした際の不透明度", + "panel-background-opacity-description": "すべてのパネル(バー、ランチャー、設定など)の背景の不透明度を設定します。", + "panel-background-opacity-label": "パネル背景の不透明度", + "panels-attached-to-bar-description": "パネルをバーや画面の端に固定します。スタイリッシュな逆アール(角の装飾)により、シームレスな外観にします。", + "panels-attached-to-bar-label": "パネルを画面の端に吸着", + "panels-overlay-description": "全画面アプリケーション上でも、パネルとバーが表示され続けるようにします。", + "panels-overlay-label": "パネルとバーを最前面に保持", + "scaling-description": "バーを除く、ユーザーインターフェース全体のサイズを変更します。", + "scaling-label": "インターフェースのスケール調整", + "scrollbar-always-visible-description": "コンテンツがスクロール可能な場合、スクロールバーを常に表示し、ホバー時のみ表示するのをやめる。", + "scrollbar-always-visible-label": "常にスクロールバーを表示", + "settings-panel-header": "設定パネル", + "settings-panel-mode-description": "設定画面の表示形式を選択します (再起動が必要な場合があります)。", + "settings-panel-mode-label": "設定パネルの表示モード", + "settings-panel-sidebar-card-style-description": "設定サイドバーを、角が丸い塗りつぶされた背景で囲みます。", + "settings-panel-sidebar-card-style-label": "サイドバーのカードスタイル", + "shadows-description": "バーやパネルの下にドロップシャドウ(影)を表示します。", + "shadows-direction-description": "影を落とす場所を選択してください。", + "shadows-direction-label": "影の方向", + "shadows-label": "ドロップシャドウ", + "title": "ユーザーインターフェース", + "tooltips-description": "インターフェース全体のツールチップの有効・無効を切り替えます。", + "tooltips-label": "ツールチップを表示", + "translucent-widgets-description": "パネル内のボタン、タブ、その他のウィジェットを半透明にする。", + "translucent-widgets-label": "半透明のウィジェット" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "アルファベット順", + "automation-change-mode-description": "自動変更時に壁紙を選択する方法を選択します。", + "automation-change-mode-label": "変更モード", + "automation-custom-interval-description": "時間を HH:MM 形式で入力してください(例:01:30)。", + "automation-custom-interval-label": "カスタム間隔", + "automation-interval-description": "壁紙を自動的に切り替える頻度。", + "automation-interval-label": "変更間隔", + "automation-random-wallpaper-description": "一定間隔でランダムに壁紙を変更します。", + "automation-scheduled-change-description": "一定間隔で壁紙を自動的に変更します。", + "automation-scheduled-change-label": "スケジュール変更", + "look-feel-edge-smoothness-description": "切り替え時の境界に、柔らかいぼかし効果(フェザー)を適用します。", + "look-feel-edge-smoothness-label": "境界のぼかし", + "look-feel-fill-color-description": "壁紙の背景に表示される塗りつぶし色を選択します。", + "look-feel-fill-mode-description": "画像をディスプレイの解像度に合わせる方法を選択してください。", + "look-feel-fill-mode-label": "配置方法", + "look-feel-skip-startup-transition-description": "シェル起動時に壁紙アニメーションをスキップする。", + "look-feel-skip-startup-transition-label": "起動時のトランジションをスキップ", + "look-feel-title": "表示と演出", + "look-feel-transition-duration-description": "切り替えアニメーションにかかる時間(秒)。", + "look-feel-transition-duration-label": "切り替え時間", + "look-feel-transition-type-description": "壁紙切り替え時のアニメーションの種類。", + "look-feel-transition-type-label": "切り替え効果", + "settings-clear-cache-button": "キャッシュをクリア", + "settings-clear-cache-description": "キャッシュされたリサイズ済み壁紙を削除してディスク容量を解放します。", + "settings-clear-cache-label": "壁紙キャッシュ", + "settings-clear-cache-toast": "壁紙キャッシュをクリアしました", + "settings-desc": "壁紙の管理方法や表示方法を制御します。", + "settings-enable-management-description": "Noctalia で壁紙を管理します。他のアプリを使用する場合はチェックを外してください。", + "settings-enable-management-label": "壁紙管理を有効化", + "settings-enable-overview-description": "オーバービュー画面の壁紙に、ぼかしと暗さしを適用します。", + "settings-enable-overview-label": "オーバービューの壁紙効果", + "settings-folder-description": "メインの壁紙フォルダのパス。", + "settings-folder-label": "壁紙フォルダ", + "settings-hide-wallpaper-filenames-tooltip-hide": "ファイル名を隠す", + "settings-hide-wallpaper-filenames-tooltip-show": "ファイル名を表示", + "settings-monitor-specific-description": "ディスプレイごとに異なる壁紙フォルダを設定します。", + "settings-monitor-specific-label": "ディスプレイ別の壁紙フォルダ", + "settings-monitor-specific-tooltip": "ディスプレイ別の壁紙フォルダ", + "settings-overview-blur-strength-description": "概要にぼかしの強度を適用します。", + "settings-overview-blur-strength-label": "概要のぼかし強度", + "settings-overview-tint-description": "概要に色合いの強さを適用します。", + "settings-overview-tint-label": "オーバービューの着色強度", + "settings-recursive-search-description": "壁紙フォルダのサブフォルダ内も検索対象にします。", + "settings-recursive-search-label": "サブフォルダも検索", + "settings-select-monitor-folder": "ディスプレイ別の壁紙フォルダを選択", + "settings-selector-description": "壁紙を選択します。", + "settings-selector-position-description": "壁紙選択パネルの表示位置を選択します。", + "settings-show-hidden-files-tooltip-hide": "隠しファイルを隠す", + "settings-show-hidden-files-tooltip-show": "隠しファイルを表示", + "settings-title": "壁紙設定", + "settings-use-original-images-description": "壁紙を表示前にリサイズするのをスキップします。ディスク容量を節約し、壁紙変更時のCPU使用率を削減しますが、非常に大きな画像ではより多くのメモリを使用する可能性があります。", + "settings-use-original-images-label": "元の画像を使用", + "settings-view-mode-description": "ディレクトリから壁紙をどのように表示するかを選択します。", + "settings-view-mode-label": "表示モード", + "view-mode-browse": "ディレクトリを参照", + "view-mode-cycle-tooltip": "表示モード:{mode} (クリックして変更)", + "view-mode-recursive": "フラット化されたサブディレクトリ", + "view-mode-single": "ルートディレクトリ" + } + }, + "placeholders": { + "add-new-keybind": "新しいキーバインドを追加", + "command-example": "echo \"Hello World\"", + "enter-command": "実行するコマンドを入力(アプリまたはスクリプト)", + "enter-ipc-identifier": "IPCコマンドの一意の識別子を入力してください", + "enter-path": "パスを入力してください...", + "enter-text-to-collapse": "例:'nothing is playing'。正規表現 (/.../) も使用できます。", + "enter-tooltip": "ツールチップを入力", + "enter-width-pixels": "幅を入力(ピクセル)", + "keybind-recording": "キーバインドを記録中...", + "search": "検索...", + "search-icons": "例:noctalia, niri, battery, cloud", + "search-launcher": "項目を検索...(または > でコマンド実行)", + "search-wallhaven": "Wallhaven で検索...", + "search-wallpapers": "壁紙を検索して絞り込む..." + }, + "positions": { + "bottom": "下", + "bottom-center": "下部中央", + "bottom-left": "左下", + "bottom-right": "右下", + "center": "中央に配置する", + "center-left": "左側中央", + "center-right": "右側中央", + "close-to-bar": "バーのボタン付近", + "follow-bar": "バーに追従", + "left": "左", + "right": "右", + "top": "上", + "top-center": "上部中央", + "top-left": "左上", + "top-right": "右上" + }, + "session-menu": { + "action-in-seconds": "{seconds} 秒後に {action}...", + "cancel-timer": "タイマーをキャンセル", + "title": "セッションメニュー" + }, + "setup": { + "all-done": "完了!", + "appearance": { + "subheader": "ダークモードと色のソース(壁紙または事前定義)を選択します。" + }, + "customize": { + "header": "自分好みにカスタマイズ", + "subheader": "バーの位置や密度、インターフェースのスケールなどを調整します。" + }, + "skip-setup": "セットアップをスキップ", + "telemetry-wizard-done": "了解しました!", + "telemetry-wizard-note": "設定でいつでも有効または無効にできます。", + "telemetry-wizard-subtitle": "Noctaliaの改善に役立つ匿名分析を追加しました", + "telemetry-wizard-title": "プライバシーアップデート", + "wallpaper": { + "choose-dir": "壁紙画像が含まれているフォルダを選択します", + "dir-browse": "壁紙フォルダを参照", + "dir-description": "壁紙画像が含まれているフォルダを選択します。", + "dir-label": "壁紙フォルダ", + "dir-select-title": "壁紙フォルダを選択", + "header": "壁紙を選択", + "no-dir": "壁紙フォルダが選択されていません", + "no-valid": "有効な画像ファイルが見つかりませんでした:{dir}", + "none-in-dir": "ディレクトリに壁紙が見つかりませんでした", + "preview-error": "画像を読み込めませんでした", + "select-prompt": "以下から壁紙を選択します", + "subheader": "美しい背景で雰囲気を演出しましょう。" + }, + "welcome": "ようこそ", + "welcome-note": "まずは基本設定から始めましょう。詳細なオプションは「設定」にあります。", + "welcome-subtitle": "デスクトップをあなただけのものにしましょう", + "welcome-title": "Noctaliaへようこそ!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "メディアプレーヤーが見つかりません", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "不明なアプリ", + "uptime": "稼働時間:{uptime}", + "user-requested": "ユーザーの要求", + "welcome-back": "おかえりなさい、", + "widget-settings-title": "{widget} の設定" + }, + "system-monitor": { + "core-usage": "コア {id} 使用率", + "cpu-temp": "CPU温度", + "cpu-usage": "CPU使用率", + "disk": "ディスク", + "download-speed": "ダウンロード速度", + "gpu-temp": "GPU温度", + "load-average": "負荷平均", + "title": "システムモニター", + "upload-speed": "アップロード速度" + }, + "toast": { + "airplane-mode": { + "description": "すべてのワイヤレス通信を無効にする。", + "title": "機内モード" + }, + "battery": { + "critical": "バッテリー残量低下", + "critical-desc": "バッテリー残量 {percent}% — すぐに充電器を接続してください", + "low": "バッテリー残量低下", + "low-desc": "バッテリー残量 {percent}% — 充電器を接続してください" + }, + "bluetooth": { + "address-copied": "アドレスをクリップボードにコピーしました", + "auto-connect-disabled": "自動接続が無効です", + "auto-connect-enabled": "自動接続が有効です", + "auto-connecting": "{count} 台のデバイスに接続中...", + "confirm-code": "別のデバイスでコード{value}を確認してください。", + "connect-failed": "デバイスに接続できませんでした", + "disconnect-failed": "デバイスから切断できませんでした", + "discoverable-change-failed": "検出可能な状態の変更に失敗しました", + "discoverable-disabled": "検出不能を無効にしました", + "discoverable-enabled": "検出可能が有効", + "display-code": "別のデバイスに{value}のコードを入力してください。", + "forget-failed": "デバイスの削除に失敗しました", + "pair-failed": "デバイスのペアリングに失敗しました", + "passkey-required": "デバイスでパスキーが必要です。", + "pincode-required": "デバイスがPINコードを要求しています。", + "state-change-failed": "Bluetoothの状態変更に失敗しました" + }, + "clipboard": { + "long-text": "長いテキスト", + "unavailable": "クリップボード履歴を利用できません", + "unavailable-desc": "cliphist がインストールされていません。クリップボード履歴機能を使用するにはインストールしてください" + }, + "custom-command-failed": { + "description": "コマンドが失敗しました: {command}\\n終了コード: {code}", + "title": "カスタムコマンドが失敗しました" + }, + "do-not-disturb": { + "disabled": "おやすみモードを無効化", + "disabled-desc": "すべての通知を表示します", + "enabled": "おやすみモードを有効化", + "enabled-desc": "通知は履歴に保存されます" + }, + "donation-opened": "寄付ページがブラウザで開かれました", + "internet-limited": "インターネット未接続", + "keyboard-layout": { + "changed": "キーボードレイアウトを {layout} に変更しました", + "title": "キーボード" + }, + "missing-control-center": { + "description": "コントロールセンターウィジェットがバーから削除されました。再度アクセスするには、ウィジェットを追加し直す必要があります。バーを右クリックして開くことも可能です", + "label": "最後のコントロールセンターが削除されました" + }, + "night-light": { + "forced": "強制的に有効化", + "normal": "通常モード", + "not-installed": "wlsunset がインストールされていません" + }, + "noctalia-performance": { + "disabled": "パフォーマンスモード:無効", + "enabled": "パフォーマンスモード:有効", + "label": "Noctalia パフォーマンス" + }, + "power-profile": { + "changed": "電源プロファイルを変更しました", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "テンプレートの処理中にエラーが発生しました", + "title": "テンプレート処理に失敗しました" + }, + "vpn": { + "connected": "{name} に接続しました", + "disconnected": "{name} から切断しました" + }, + "wallpaper-colors": { + "disabled": "壁紙配色の生成:無効", + "enabled": "壁紙配色の生成:有効", + "label": "壁紙配色の生成" + }, + "wifi": { + "connected": "{ssid} に接続しました", + "connection-failed": "接続に失敗しました", + "connection-timeout": "接続タイムアウト", + "disconnected": "{ssid} から切断しました", + "incorrect-password": "パスワードが違います", + "network-not-found": "ネットワークが見つかりません" + } + }, + "tooltips": { + "add-widget": "ウィジェットを追加", + "auto-connect": "このデバイスの自動接続を切り替える", + "bluetooth-auto-connect-off": "自動接続が無効です", + "bluetooth-auto-connect-on": "自動接続が有効です", + "bluetooth-devices": "Bluetooth デバイス", + "brightness-at": "画面の明るさ:{brightness}%", + "click-to-start-recording": "画面録画(録画開始)", + "click-to-stop-recording": "画面録画(録画停止)", + "collapse": "サイドバーを折りたたむ", + "copy-address": "アドレスをコピー", + "delete-notification": "通知を削除", + "dismiss-notification": "通知を閉じる", + "do-not-disturb-enabled": "おやすみモード", + "expand": "サイドバーを展開", + "forget-network": "ネットワーク設定を削除", + "grid-view": "グリッド表示", + "hidden-files-hide": "隠しファイル", + "hide-unnamed-devices": "名前のないデバイスを隠す", + "home": "ホームディレクトリ", + "input-muted": "入力ミュート切り替え", + "keep-awake": "スリープ防止", + "keyboard-layout": "キーボードレイアウト:{layout}", + "list-view": "リスト表示", + "manage-vpn": "VPN 接続", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "ウィジェットの最大数に達しました", + "microphone-volume-at": "マイク音量:{volume}%", + "move-to-section": "{section} に移動", + "mute": "ミュート", + "next-media": "次のトラック", + "next-month": "翌月", + "night-light-not-installed": "夜間モード(利用不可)", + "noctalia-performance-enabled": "Noctalia パフォーマンスモード", + "open-annotation-tool": "注釈ツールで開く", + "open-control-center": "コントロールセンター", + "open-notification-history-enable-dnd": "通知履歴", + "open-settings": "設定", + "open-tray-dropdown": "システムトレイ", + "output-muted": "出力ミュート切り替え", + "power-profile": "電源プロファイル:{profile}", + "previous-media": "前のトラック", + "previous-month": "前月", + "refresh-devices": "デバイスを更新", + "refresh-wallhaven": "Wallhaven の検索結果を更新", + "refresh-wallpaper-list": "壁紙リストを更新", + "remove-widget": "ウィジェットを削除", + "search-close": "検索を閉じる", + "session-menu": "セッションメニュー", + "show-all-devices": "すべてのデバイスを表示", + "switch-to-dark-mode": "ダークモード", + "switch-to-light-mode": "ライトモード", + "unmute": "ミュート解除", + "up": "親ディレクトリ", + "volume-at": "出力音量:{volume}%", + "wallpaper-selector": "壁紙選択パネル" + }, + "wallpaper": { + "browse": { + "empty-directory": "このディレクトリは空です。", + "go-root": "壁紙のルートに移動", + "go-up": "親フォルダーへ移動", + "go-up-hint": "戻るボタンを使って上に移動します。" + }, + "configure-directory": "画像ファイルが含まれる壁紙フォルダを設定してください。", + "fill-modes": { + "crop": "拡大する", + "fit": "画面に合わせる", + "repeat": "繰り返し(タイル)", + "stretch": "引き伸ばして表示" + }, + "no-match": "一致する項目がありません。", + "no-wallpaper": "壁紙が見つかりません。", + "panel": { + "apikey-help": "成人向けコンテンツにアクセスするには、APIキーが必要です。", + "apikey-label": "APIキー", + "apikey-managed-by-env": "NOCTALIA_WALLHAVEN_API_KEY 環境変数経由で管理されます。", + "apikey-placeholder": "Wallhaven APIキーを入力してください", + "appearance-dark-tab": "ダーク", + "appearance-light-tab": "ライト", + "categories-anime": "アニメ", + "categories-label": "カテゴリ", + "categories-people": "人物", + "color-extraction-disabled": "壁紙の色抽出を使用する", + "color-extraction-enabled": "事前定義された配色スキームを使用する", + "header-devices-apply-all-tooltip": "モニターごとに壁紙を選択", + "header-devices-per-monitor-tooltip": "すべてのモニターに適用", + "header-link-light-dark-tooltip": "テーマ切り替え時に壁紙を維持", + "header-separate-light-dark-tooltip": "テーマに合わせて壁紙を自動切り替え", + "header-sun-linked-tooltip": "ライトとダークの壁紙を分ける", + "header-sun-separate-tooltip": "ライトとダークで同じ壁紙", + "order-asc": "昇順", + "order-desc": "降順", + "order-label": "順序", + "purity-label": "コンテンツフィルタ", + "purity-nsfw": "R-18", + "purity-sfw": "全年齢", + "purity-sketchy": "際どい", + "ratios-all-portrait": "すべての縦長", + "ratios-all-wide": "すべてのワイド", + "ratios-any": "すべて", + "ratios-label": "アスペクト比", + "resolution-atleast": "以上", + "resolution-exact": "完全一致", + "resolution-label": "解像度", + "resolution-mode-label": "モード", + "solid-color-tooltip": "無地", + "sort-date-asc": "古い順に並べ替え", + "sort-date-desc": "新しい順に並べ替え", + "sort-name-asc": "名前でソート (A-Z)", + "sort-name-desc": "名前順に並べ替え (Z-A)", + "sort-random": "ランダムで並べ替え", + "sorting-date-added": "追加日", + "sorting-favorites": "お気に入り数", + "sorting-label": "並べ替え", + "sorting-relevance": "関連度", + "sorting-toplist": "トップリスト", + "sorting-views": "閲覧数", + "source-label": "取得元", + "source-wallhaven": "Wallhaven", + "title": "壁紙選択パネル", + "wallhaven-settings-title": "Wallhaven 設定" + }, + "transitions": { + "disc": "ディスク", + "fade": "フェード", + "honeycomb": "ハニカム", + "pixelate": "ピクセル化", + "stripes": "ストライプ", + "wipe": "ワイプ" + }, + "try-different-search": "別の検索条件を試してください。", + "wallhaven": { + "loading": "壁紙を読み込み中...", + "no-results": "壁紙が見つかりません。別の検索条件を試してください。", + "page": "{current} / {total}", + "page-prefix": "ページ", + "page-suffix": "{total}件中" + } + }, + "weather": { + "clear-sky": "快晴", + "drizzle": "霧雨", + "fog": "霧", + "mainly-clear": "晴れ", + "overcast": "曇り", + "partly-cloudy": "晴れ時々曇り", + "rain-showers": "雨", + "snow": "雪", + "thunderstorm": "雷雨" + }, + "widgets": { + "color-picker": { + "palette-description": "豊富なプリセットカラーから選択します。", + "palette-label": "パレット", + "palette-theme-colors": "テーマのカラーパレット。", + "title": "色選択" + }, + "datetime-tokens": { + "ampm-lowercase": "午前/午後(英語表記では小文字)", + "ampm-uppercase": "午前/午後(英語表記では大文字)", + "common-12hour-time-minutes": "12時間表記(分を含む)", + "common-24hour-time-minutes": "24時間表記(分を含む)", + "common-24hour-time-seconds": "24時間表記(秒を含む)", + "common-european-date": "ヨーロッパ形式の日付(日.月.年)", + "common-iso-date": "ISO 形式の日付(年-月-日)", + "common-us-date": "米国形式の日付(月/日/年)", + "common-weekday-date": "曜日と日付", + "common-weekday-month-day": "曜日、月、日", + "day-abbreviated": "曜日の短縮名(例:火)", + "day-full": "曜日の完全名(例:月曜日)", + "day-leading-zero": "ゼロ埋めありの日付 (01-31)", + "day-no-leading-zero": "ゼロ埋めなしの日付 (1-31)", + "hour-leading-zero": "ゼロ埋めありの時間 (00-23) — 24時間表記", + "hour-no-leading-zero": "ゼロ埋めなしの時間 (0-23) — 24時間表記", + "minute-leading-zero": "ゼロ埋めありの分 (00-59)", + "minute-no-leading-zero": "ゼロ埋めなしの分 (0-59)", + "month-abbreviated": "月の短縮名(例:1月/Jan)", + "month-full": "月の完全名(例:1月/January)", + "month-number-leading-zero": "ゼロ埋めありの月 (01-12)", + "month-number-no-zero": "ゼロ埋めなしの月 (1-12)", + "second-leading-zero": "ゼロ埋めありの秒 (00-59)", + "second-no-leading-zero": "ゼロ埋めなしの秒 (0-59)", + "timezone-abbreviation": "タイムゾーンの略称", + "year-four-digit": "4桁の西暦", + "year-two-digit": "2桁の西暦 (00-99)" + }, + "file-picker": { + "item": "項目", + "items": "項目", + "search-placeholder": "ファイルとフォルダを検索...", + "select-current": "現在のフォルダを選択する", + "select-file": "ファイルを選択する", + "select-folder": "フォルダを選択する", + "selected": "選択済み:", + "title": "ファイル選択" + }, + "file-picker-title": "ファイルを選択してください", + "icon-picker": { + "title": "アイコン選択" + }, + "tooltip-placeholder": "プレースホルダー" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "匿名ID", + "ca-cert": "CA証明書", + "eap-method": "EAP方式", + "password": "ユーザーパスワード", + "phase2-auth": "フェーズ2認証", + "username": "ID / ユーザー名" + }, + "panel": { + "action-required": "アクションが必要", + "add-hidden-network": "非表示のネットワークを追加", + "add-network": "ネットワークを追加", + "available-interfaces": "利用可能なインターフェース", + "available-networks": "利用可能なネットワーク", + "disabled": "Wi-Fi は無効です", + "disconnecting": "切断中...", + "dns": "DNS", + "enable-message": "利用可能なネットワークを表示するには Wi-Fi を有効にしてください。", + "enter-password": "パスワードを入力...", + "forget": "削除", + "forget-network": "このネットワーク設定を削除しますか?", + "forgetting": "削除中...", + "hidden-network": "隠しネットワーク", + "interface": "ネットワークインターフェース", + "internet-connected": "インターネット接続", + "internet-limited": "インターネットなし", + "internet-status": "インターネット接続状況", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "保存済みネットワーク", + "link-speed": "リンク速度", + "network-name-ssid": "ネットワーク名 (SSID)", + "no-ethernet-devices": "イーサネットデバイスが検出されませんでした", + "no-networks": "ネットワークが見つかりません", + "saved": "保存済み", + "scan-again": "再スキャン", + "searching": "近くのネットワークを検索中...", + "security-open": "開く", + "security-wep": "WEP", + "security-wpa": "WPA/WPA2/WPA3 パーソナル", + "security-wpa-ent": "WPAエンタープライズ", + "security-wpa2-ent": "WPA2エンタープライズ", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3エンタープライズ", + "show-password": "パスワードを表示", + "title": "Wi-Fi" + }, + "signal": { + "excellent": "優秀", + "fair": "普通", + "good": "良い", + "poor": "不良", + "weak": "弱い" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/ko-KR.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/ko-KR.json new file mode 100644 index 0000000..31d81f3 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/ko-KR.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "기록 지우기", + "close-app": "{app} 닫기", + "connect-vpn": "{name} 연결", + "cycle-visualizer": "비주얼라이저 순환", + "disable-bluetooth": "블루투스 비활성화", + "disable-dnd": "방해 금지 모드 끄기", + "disable-wifi": "Wi-Fi 비활성화", + "disconnect-vpn": "{name} 연결 해제", + "dock-settings": "독 설정", + "enable-bluetooth": "블루투스 활성화", + "enable-dnd": "방해 금지 모드 켜기", + "enable-wifi": "Wi-Fi 활성화", + "launcher-settings": "런처 설정", + "lower-to-bottom": "맨 아래로 내리기", + "open-calendar": "달력 열기", + "open-display-settings": "디스플레이 설정", + "open-launcher": "런처 열기", + "open-settings": "설정 열기", + "raise-to-top": "맨 위로 올리기", + "random-wallpaper": "랜덤 배경화면", + "run-custom-command": "사용자 지정 명령 실행", + "show-active-only": "활성 항목만 표시", + "show-all": "모두 표시", + "toggle-mute": "음소거 전환", + "widget-settings": "위젯 설정" + }, + "authentication": { + "error": "인증 오류", + "failed": "인증 실패" + }, + "bar": { + "active-window": { + "colorize-icons-description": "활성 창 아이콘에 테마 색상을 적용합니다.", + "hide-mode-description": "활성 창이 없을 때 위젯의 동작 방식을 제어합니다.", + "scrolling-mode-description": "창 제목이 길 때 텍스트 스크롤 활성화 시점을 제어합니다.", + "show-app-icon-description": "창 제목 옆에 애플리케이션 아이콘을 표시합니다.", + "show-app-icon-label": "앱 아이콘 표시", + "show-app-text-description": "활성 창의 제목을 표시합니다.", + "show-app-text-label": "창 제목 표시" + }, + "audio-visualizer": { + "color-name-description": "비주얼라이저의 색상을 선택합니다.", + "color-name-label": "색으로 채우기", + "height-description": "사용자 지정 구성 요소 너비.", + "hide-when-idle-description": "활성화하면 재생기가 재생 중이지 않을 때 비주얼라이저를 숨깁니다.", + "hide-when-idle-label": "미디어 재생 중이 아닐 때 숨기기", + "width-description": "구성 요소의 너비를 사용자 지정합니다." + }, + "battery": { + "device-default": "기본값 (디스플레이 장치)", + "device-description": "표시할 배터리 장치를 선택합니다.", + "device-label": "배터리 장치", + "display-mode-description": "배터리가 바에 어떻게 표시될지 선택하세요.", + "display-mode-graphic": "그래픽 배터리", + "display-mode-graphic-clean": "그래픽 배터리 (퍼센트 없음)", + "display-mode-icon-always": "아이콘 - 항상 % 표시", + "display-mode-icon-hover": "아이콘 - 호버 시 표시", + "display-mode-icon-only": "아이콘만", + "hide-if-idle-description": "배터리가 충전 중이거나 방전 중이 아닐 때 위젯을 숨깁니다.", + "hide-if-idle-label": "유휴 상태일 때 숨기기", + "hide-if-not-detected-description": "시스템에서 배터리가 감지되지 않으면 위젯을 숨깁니다.", + "hide-if-not-detected-label": "감지되지 않으면 숨기기", + "low-battery-threshold-label": "배터리 부족 경고 임계값", + "show-noctalia-performance-description": "배터리 패널 내부에 Noctalia 성능 모드 토글을 표시합니다.
리소스 사용량을 줄이기 위해 Noctalia의 그림자와 애니메이션을 비활성화합니다.", + "show-noctalia-performance-label": "Noctalia 성능 토글 표시", + "show-power-profile-description": "배터리 패널 내부에 전원 프로필 선택 항목을 표시합니다.", + "show-power-profile-label": "전원 프로필 제어 표시" + }, + "brightness": { + "apply-all-description": "스크롤 휠을 사용할 때 모든 모니터의 밝기를 변경합니다.", + "apply-all-label": "스크롤 변경 사항을 모든 모니터에 적용" + }, + "clock": { + "clock-display-description": "아래 목록에서 토큰을 추가하여 시계 표시를 사용자 지정하세요. 12시간 형식을 사용하려면 'AP' 토큰을 포함해야 합니다.", + "clock-display-label": "시계 표시", + "custom-font-description": "시계 표시에 사용할 사용자 지정 글꼴을 선택합니다.", + "custom-font-label": "사용자 지정 글꼴", + "custom-font-placeholder": "사용자 지정 글꼴 선택...", + "custom-font-search-placeholder": "글꼴 검색...", + "horizontal-bar-description": "팁: 줄바꿈을 하려면 \\n을 사용하세요.", + "horizontal-bar-label": "수평 바", + "preview": "미리보기", + "tooltip-format-description": "시계 위에 마우스를 올렸을 때 표시되는 툴팁의 형식 문자열입니다. 기본 툴팁을 사용하려면 비워 두세요.", + "tooltip-format-label": "툴팁 형식", + "use-custom-font-description": "시계의 기본 글꼴 대신 사용자 지정 글꼴을 사용합니다.", + "use-custom-font-label": "사용자 지정 글꼴 사용", + "use-monospaced-font-description": "활성화하면 시계가 고정폭 글꼴을 사용합니다.", + "use-monospaced-font-label": "고정폭 글꼴 사용", + "vertical-bar-description": "각 부분을 새 줄로 나누려면 공백을 사용하세요.", + "vertical-bar-label": "수직 바" + }, + "control-center": { + "browse-file": "파일 찾아보기", + "browse-library": "라이브러리 찾아보기", + "color-selection-description": "아이콘에 테마 색상을 적용합니다.", + "enable-colorization-description": "아이콘 색상화를 활성화하고 테마 색상을 적용합니다.", + "icon-description": "라이브러리 또는 사용자 지정 파일에서 아이콘을 선택합니다.", + "select-custom-icon": "사용자 지정 아이콘 선택", + "use-distro-logo-description": "사용자 지정 아이콘 대신 배포판 로고를 사용합니다.", + "use-distro-logo-label": "아이콘 대신 배포판 로고 사용" + }, + "custom-button": { + "collapse-condition-description": "출력 텍스트가 이 값과 일치하면 버튼이 접힙니다.", + "collapse-condition-label": "접히는 조건", + "color-selection-description": "아이콘과 텍스트에 테마 색상을 적용합니다.", + "default-tooltip": "사용자 지정 버튼, 설정에서 구성", + "display-command-output-description": "정기적인 간격으로 실행할 명령을 입력합니다. 출력의 첫 번째 줄이 텍스트로 표시됩니다.", + "display-command-output-label": "명령 출력 표시", + "display-command-output-stream-description": "지속적으로 실행할 명령을 입력합니다.", + "dynamic-text": "동적 텍스트", + "enable-colorization-description": "사용자 지정 버튼 아이콘과 텍스트의 색상화를 활성화하여 테마 색상을 적용합니다.", + "enable-colorization-label": "색상화 활성화", + "general-tooltip-text-description": "버튼의 툴팁에 표시할 사용자 지정 텍스트.", + "general-tooltip-text-label": "사용자 지정 도구 설명 텍스트", + "hide-mode-always-expanded": "항상 펼침", + "hide-mode-description": "명령에 출력이 없을 때 위젯의 가시성을 제어합니다.", + "hide-mode-expand-with-output": "출력이 있을 때 펼침", + "hide-mode-label": "숨기기 모드", + "hide-mode-max-transparent": "최대로 펼치지만 투명하게", + "icon-description": "라이브러리에서 아이콘을 선택합니다.", + "ipc-identifier-description": "IPC 명령을 위한 고유 식별자입니다. IPC를 통해 이 버튼을 제어하려면 'qs -c noctalia-shell ipc call cb [action] [identifier]'와 함께 이 식별자를 사용하세요.", + "ipc-identifier-label": "IPC 식별자", + "left-click-description": "버튼을 왼쪽 클릭했을 때 실행할 명령입니다.", + "left-click-label": "왼쪽 클릭", + "left-click-update-text": "왼쪽 클릭 시 표시된 텍스트 업데이트", + "max-text-length-horizontal-description": "수평 바에 표시할 최대 문자 수 (텍스트를 숨기려면 0).", + "max-text-length-horizontal-label": "최대 텍스트 길이 (수평)", + "max-text-length-vertical-description": "수직 바에 표시할 최대 문자 수 (텍스트를 숨기려면 0).", + "max-text-length-vertical-label": "최대 텍스트 길이 (수직)", + "middle-click-description": "버튼을 가운데 클릭했을 때 실행할 명령입니다.", + "middle-click-label": "가운데 클릭", + "middle-click-update-text": "가운데 클릭 시 표시된 텍스트 업데이트", + "parse-json-description": "명령 출력을 JSON 객체로 파싱하여 텍스트와 아이콘을 동적으로 설정합니다.", + "parse-json-label": "출력을 JSON으로 파싱", + "refresh-interval-description": "밀리초 단위 간격.", + "refresh-interval-label": "새로고침 간격", + "right-click-description": "버튼을 오른쪽 클릭했을 때 실행할 명령입니다.", + "right-click-label": "오른쪽 클릭", + "right-click-update-text": "오른쪽 클릭 시 표시된 텍스트 업데이트", + "show-exec-tooltip-description": "명령 세부 정보가 포함된 툴팁 표시 (왼쪽/오른쪽/가운데 클릭, 휠).", + "show-exec-tooltip-label": "명령어 도구 설명 표시", + "show-icon-description": "위젯 아이콘의 가시성을 전환합니다.", + "show-icon-label": "아이콘 표시", + "show-text-tooltip-description": "텍스트 명령어의 출력으로 툴팁을 표시합니다.", + "show-text-tooltip-label": "동적 텍스트 툴팁 표시", + "text-stream-description": "명령에서 스트리밍된 줄이 버튼에 텍스트로 표시됩니다.", + "text-stream-label": "스트림", + "wheel-description": "스크롤 휠을 사용했을 때 실행할 명령입니다.
명령에서 스크롤 휠 델타 값으로 $delta를 사용하세요.", + "wheel-down": "스크롤 휠 아래", + "wheel-down-description": "스크롤 휠을 아래로 내렸을 때 실행할 명령입니다.", + "wheel-down-label": "휠 다운 명령", + "wheel-label": "스크롤 휠", + "wheel-mode-separate-description": "휠 업과 다운에 대해 별도의 명령을 활성화합니다.", + "wheel-mode-separate-label": "별도 휠 명령", + "wheel-up": "스크롤 휠 위", + "wheel-up-description": "스크롤 휠을 위로 올렸을 때 실행할 명령입니다.", + "wheel-up-label": "휠 업 명령", + "wheel-update-text": "스크롤 시 표시된 텍스트 업데이트" + }, + "keyboard-layout": { + "show-icon-description": "키보드 레이아웃 아이콘을 표시합니다." + }, + "lock-keys": { + "hide-when-off-description": "키가 활성화되지 않았을 때 표시기를 숨깁니다.", + "hide-when-off-label": "꺼져 있을 때 숨기기", + "show-caps-lock-description": "Caps Lock 상태를 표시합니다.", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "Num Lock 상태를 표시합니다.", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "Scroll Lock 상태를 표시합니다.", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "미디어 재생기 패널을 위한 공간 절약형 레이아웃을 활성화합니다.", + "compact-mode-label": "소형 모드", + "hide-mode-description": "미디어가 재생 중이 아닐 때 위젯의 동작 방식을 제어합니다.", + "max-width-description": "위젯의 최대 수평 크기를 설정합니다. 내용이 짧으면 위젯 크기가 줄어듭니다.", + "no-active-player": "활성 재생기 없음", + "panel-section-description": "미디어 재생기 패널의 모양과 동작을 구성합니다.", + "panel-section-label": "미디어 재생기 패널", + "scrolling-mode-description": "트랙 제목이 길 때 텍스트 스크롤 활성화 시점을 제어합니다.", + "scrolling-mode-label": "스크롤 모드", + "show-album-art-description": "현재 재생 중인 트랙의 앨범 아트를 표시합니다.", + "show-album-art-label": "앨범 아트 표시", + "show-artist-first-description": "제목 - 아티스트 대신 아티스트 - 제목 순으로 표시합니다.", + "show-artist-first-label": "아티스트 먼저 표시", + "show-progress-ring-description": "트랙 진행 상황을 보여주는 원형 진행 표시기를 표시합니다.", + "show-progress-ring-label": "진행 링 표시", + "show-visualizer-description": "음악이 재생 중일 때 오디오 비주얼라이저를 표시합니다.", + "show-visualizer-label": "비주얼라이저 표시", + "use-fixed-width-description": "활성화하면 위젯이 내용에 따라 동적으로 조정되지 않고 항상 최대 너비를 사용합니다.", + "use-fixed-width-label": "고정 너비 사용", + "visualizer-type-description": "표시할 오디오 비주얼라이저 스타일을 선택합니다.", + "visualizer-type-label": "비주얼라이저 유형" + }, + "notification-history": { + "hide-widget-when-zero-description": "알림이 없을 때 알림 아이콘을 숨깁니다.", + "hide-widget-when-zero-label": "알림이 없을 때 아이콘 숨기기", + "hide-widget-when-zero-unread-description": "읽지 않은 알림이 없을 때 알림 아이콘을 숨깁니다.", + "hide-widget-when-zero-unread-label": "읽지 않은 알림이 없을 때 아이콘 숨기기", + "show-unread-badge-description": "읽지 않은 알림의 수를 보여주는 배지를 표시합니다.", + "show-unread-badge-label": "읽지 않은 배지 표시", + "unread-badge-color-description": "읽지 않은 알림 배지의 색상을 선택하세요.", + "unread-badge-color-label": "읽지 않은 뱃지 색상" + }, + "section-editor": { + "placeholder": "추가할 위젯을 선택하세요...", + "search-placeholder": "위젯 검색..." + }, + "spacer": { + "width-description": "픽셀 단위의 간격 너비입니다." + }, + "system-monitor": { + "compact-mode-description": "텍스트 값 대신 미니 막대 차트로 통계를 표시합니다. 레이아웃이 변경되는 것을 방지합니다.", + "compact-mode-label": "소형 모드", + "cpu-cores-description": "CPU 코어 사용량을 개별적으로 표시합니다.", + "cpu-cores-label": "CPU 코어", + "cpu-frequency-description": "현재 CPU 클럭 속도를 GHz 단위로 표시합니다.", + "cpu-frequency-label": "CPU 주파수 표시", + "cpu-temperature-description": "가능한 경우 CPU 온도를 표시합니다.", + "cpu-temperature-label": "CPU 온도", + "cpu-usage-description": "현재 CPU 사용량을 백분율로 표시합니다.", + "cpu-usage-label": "CPU 사용량", + "disk-path-description": "모니터링할 디스크 마운트 지점을 선택합니다.", + "disk-path-label": "디스크 경로", + "gpu-temperature-description": "가능한 경우 GPU 온도를 표시합니다.", + "load-average-description": "시스템 로드 평균을 표시합니다.", + "load-average-label": "로드 평균", + "memory-percentage-description": "메모리 사용량을 절대값 대신 백분율로 표시합니다.", + "memory-percentage-label": "메모리 백분율 표시", + "memory-usage-description": "현재 램 사용량 정보를 표시합니다.", + "memory-usage-label": "메모리 사용량", + "network-traffic-description": "네트워크 업로드 및 다운로드 속도를 표시합니다.", + "network-traffic-label": "네트워크 트래픽", + "storage-as-percentage-description": "디스크 공간을 절대값 대신 백분율로 표시합니다.", + "storage-as-percentage-label": "디스크 백분율", + "storage-available-description": "사용된 공간 대신 사용 가능한 디스크 공간을 표시합니다.", + "storage-available-label": "사용 가능한 디스크 공간", + "storage-usage-description": "디스크 공간 사용량 정보를 표시합니다.", + "storage-usage-label": "저장소 사용량", + "swap-usage-description": "스왑 메모리 사용량을 표시합니다.", + "swap-usage-label": "스왑 사용량", + "use-monospace-font-description": "일관된 문자 너비를 위해 고정폭 글꼴을 사용합니다.", + "use-monospace-font-label": "고정폭 글꼴", + "use-padding-description": "레이아웃 이동을 방지하기 위해 텍스트 값에 선행 공백을 채웁니다.", + "use-padding-description-disabled-monospace-font": "이 기능에는 고정폭 글꼴이 필요합니다.", + "use-padding-description-disabled-vertical": "세로 작업 표시줄은 패딩 텍스트를 지원하지 않습니다.", + "use-padding-label": "텍스트 패딩" + }, + "taskbar": { + "colorize-icons-description": "작업 표시줄 아이콘에 테마 색상을 적용합니다.", + "hide-mode-description": "일치하는 창이 없을 때 위젯의 동작 방식을 제어합니다.", + "hide-mode-label": "숨기기 모드", + "icon-scale-description": "작업 표시줄 아이콘의 배율을 설정합니다.", + "icon-scale-label": "아이콘 배율", + "max-width-description": "작업 표시줄의 최대 너비를 화면 너비의 백분율로 설정합니다.", + "max-width-label": "최대 너비", + "only-active-workspaces-description": "활성 작업 공간의 앱만 표시합니다.", + "only-active-workspaces-label": "활성 작업 공간의 앱만", + "only-same-monitor-description": "바가 위치한 모니터의 앱만 표시합니다.", + "only-same-monitor-label": "동일 모니터의 앱만", + "show-pinned-apps-description": "독에 고정된 앱을 작업 표시줄에 표시합니다.", + "show-pinned-apps-label": "고정된 앱 표시", + "show-title-description": "작업 표시줄에 창 제목을 표시합니다.", + "show-title-description-disabled": "수직 작업 표시줄은 제목 표시를 지원하지 않습니다.", + "show-title-label": "제목 표시", + "smart-width-description": "항목 수에 따라 항목 너비를 자동으로 조정합니다.", + "smart-width-label": "스마트 너비", + "title-width-description": "작업 표시줄의 창 제목 너비를 설정합니다 (픽셀 단위).", + "title-width-label": "제목 너비", + "title-width-reset-tooltip": "제목 너비 초기화" + }, + "tray": { + "chevron-color-description": "드로어 셰브론 아이콘에 테마 색상을 적용합니다.", + "chevron-color-label": "화살표 색상", + "colorize-icons-description": "트레이 아이콘에 테마 색상을 적용합니다.", + "colorize-icons-label": "아이콘 색상화", + "drawer-enabled-description": "활성화하면 고정되지 않은 트레이 항목이 서랍 패널에 표시됩니다.
비활성화하면 모든 트레이 항목이 인라인으로 표시됩니다.", + "drawer-enabled-label": "서랍 활성화", + "hide-passive-description": "활성화하면 '패시브' 상태의 트레이 항목이 숨겨집니다.", + "hide-passive-label": "패시브 항목 숨기기" + }, + "volume": { + "display-mode-description": "이 값이 어떻게 표시될지 선택하세요." + }, + "workspace": { + "character-count-description": "작업 공간 이름에서 표시할 문자 수 (1-10).", + "character-count-label": "문자 수", + "empty-color-description": "빈 작업 공간의 배경색을 설정합니다.", + "empty-color-label": "빈 작업 공간 색상", + "enable-scrollwheel-description": "마우스 스크롤 휠을 사용하여 작업 공간을 전환합니다.", + "enable-scrollwheel-label": "스크롤로 작업 공간 전환", + "focused-color-description": "포커스된 작업 공간의 배경색을 설정합니다.", + "focused-color-label": "포커스된 작업 공간 색상", + "follow-focused-screen-description": "바가 위치한 화면 대신 현재 포커스된 화면의 작업 공간을 표시합니다.", + "follow-focused-screen-label": "포커스된 화면 따르기", + "font-weight-description": "작업 공간 내 텍스트의 시각적 가중치를 설정합니다.", + "font-weight-label": "글꼴 두께", + "grouped-border-opacity-description": "작업 공간 컨테이너 테두리의 불투명도 수준을 설정합니다.", + "grouped-border-opacity-label": "테두리 불투명도", + "hide-unoccupied-description": "창이 없는 작업 공간을 표시하지 않습니다.", + "hide-unoccupied-label": "비어 있으면 숨기기", + "label-mode-description": "작업 공간 레이블 표시 방식을 선택합니다.", + "label-mode-label": "레이블 모드", + "occupied-color-description": "사용 중인 작업 공간의 배경색을 설정합니다.", + "occupied-color-label": "사용 중인 작업 공간 색상", + "pill-size-description": "작업 공간 알약의 크기를 조절합니다 (50%-100%).", + "pill-size-label": "필 크기", + "reverse-scrolling-description": "스크롤할 때 작업 공간 전환 방향을 반대로 합니다.", + "reverse-scrolling-label": "스크롤 반전", + "show-applications-description": "각 작업 공간 내부에 애플리케이션 아이콘을 표시합니다.", + "show-applications-hover-description": "작업 공간에 마우스를 올렸을 때 각 작업 공간 내에 애플리케이션 아이콘을 표시합니다.", + "show-applications-hover-label": "마우스 오버 시 애플리케이션 표시", + "show-applications-label": "애플리케이션 표시", + "show-badge-description": "그룹화 모드에서 작업 공간 번호 배지를 표시합니다.", + "show-badge-label": "작업 공간 배지 표시", + "show-labels-only-when-occupied-description": "창이 있을 때만 작업 공간 레이블을 표시합니다.", + "show-labels-only-when-occupied-label": "사용 중일 때만 레이블 표시", + "unfocused-icons-opacity-description": "포커스되지 않은 앱 아이콘의 불투명도 수준을 설정합니다.", + "unfocused-icons-opacity-label": "포커스되지 않은 아이콘 불투명도" + } + }, + "battery": { + "all-batteries": "모든 배터리 (결합됨)", + "battery-health": "배터리 수명", + "battery-level": "배터리 잔량", + "capacity-level": "용량: {level}", + "charging-rate": "충전 속도: {rate} W", + "discharging-rate": "방전 속도: {rate} W", + "health": "수명: {percent}%", + "inhibit-idle-description": "시스템을 깨어 있는 상태로 유지합니다.", + "no-battery-detected": "배터리가 감지되지 않음", + "plugged-in": "전원 연결됨", + "power-profile": "전원 프로필", + "time-left": "남은 시간: {time}", + "time-until-full": "완충까지 남은 시간: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "사용 가능한 장치", + "blocked": "차단됨", + "connected-devices": "연결된 장치", + "device-address": "장치 주소", + "disabled": "블루투스가 비활성화됨", + "discoverable": "검색 가능", + "enable-message": "사용 가능한 장치를 보려면 블루투스를 활성화하세요.", + "known-devices": "알려진 장치", + "no-devices": "사용 가능한 장치 없음", + "paired-devices": "페어링된 장치", + "pairing-mode": "장치가 페어링 모드인지 확인하세요.", + "scanning": "장치 검색 중...", + "signal-text-excellent": "신호: 매우 좋음", + "signal-text-fair": "신호: 보통", + "signal-text-good": "신호: 좋음", + "signal-text-poor": "신호: 나쁨", + "signal-text-unknown": "신호: 알 수 없음", + "signal-text-very-poor": "신호: 매우 나쁨" + } + }, + "changelog": { + "error": { + "fetch-failed": "변경 로그 데이터를 불러올 수 없습니다. 나중에 다시 시도해 주세요.", + "rate-limit": "GitHub 속도 제한을 초과했습니다. 몇 분 후에 다시 시도해 주세요." + }, + "panel": { + "buttons-discord": "Discord 참여하기", + "buttons-dismiss": "확인", + "buttons-feedback": "피드백 보내기", + "empty": "릴리스 노트는 아직 볼 수 없습니다.", + "highlight-title": "주요 변경 사항", + "released": "출시일: {date}", + "subtitle-fresh": "Noctalia를 설치해 주셔서 감사합니다! 이번 빌드에 포함된 내용은 다음과 같습니다.", + "subtitle-updated": "{previousVersion}에서 업데이트됨", + "title": "{version}의 새로운 기능", + "version": "버전 {version}", + "version-new-user": "새로 설치됨" + } + }, + "common": { + "actions": "작업", + "active": "활성", + "add": "추가", + "appearance": "모양", + "apply": "적용", + "auto-connect": "자동 연결", + "automation": "자동화", + "available": "사용 가능", + "back": "뒤로", + "battery": "배터리", + "behavior": "동작", + "bluetooth": "블루투스", + "brightness": "밝기", + "browse": "찾아보기", + "calendar": "달력", + "calendar-panel": "달력 패널", + "cancel": "취소", + "cards": "카드", + "charging": "충전 중", + "clear": "지우기", + "clipboard": "클립보드", + "close": "닫기", + "color-muted": "차분하게", + "colors": "색상", + "command": "명령", + "confirm": "확인", + "connect": "연결", + "connected": "연결됨", + "connecting": "연결 중...", + "continue": "계속", + "contributors": "기여자", + "copied-to-clipboard": "클립보드에 복사됨", + "countdown": "카운트다운", + "customize": "사용자 지정", + "date": "날짜", + "default": "기본값", + "delete": "삭제", + "devices": "장치", + "disabled": "비활성화됨", + "discharging": "방전 중", + "disconnect": "연결 해제", + "disconnected": "연결 끊김", + "disconnecting": "연결 해제 중...", + "display-mode": "디스플레이 모드", + "documentation": "문서", + "download": "다운로드", + "duration": "지속 시간", + "dysfunctional": "기능 장애", + "edit": "편집", + "enabled": "활성화됨", + "error": "오류", + "ethernet": "이더넷", + "events": "일정", + "execute": "실행", + "faithful": "충실하게", + "focus": "포커스", + "font-weight-bold": "굵게", + "font-weight-light": "라이트", + "font-weight-medium": "중간", + "font-weight-regular": "보통", + "font-weight-semibold": "세미볼드", + "frequency": "빈도", + "gateway": "게이트웨이", + "general": "일반", + "height": "높이", + "hibernate": "최대 절전", + "history": "기록", + "icon": "아이콘", + "idle": "유휴", + "info": "정보", + "input": "입력", + "install": "설치", + "installed": "설치됨", + "interface": "인터페이스", + "internet": "인터넷", + "keybind": "키 바인딩", + "language": "언어", + "loading": "로딩 중...", + "local": "로컬", + "location": "위치", + "lock": "잠금", + "logout": "로그아웃", + "look": "외관", + "margins": "여백", + "media": "미디어", + "media-player": "미디어 재생기", + "memory": "메모리", + "monitors": "모니터", + "move-down": "아래로 이동", + "move-up": "위로 이동", + "network": "네트워크", + "next": "다음", + "night-light": "야간 조명", + "no": "아니요", + "no-results": "결과 없음", + "no-summary": "요약 없음", + "none": "없음", + "not-found": "찾을 수 없음", + "notifications": "알림", + "official": "공식", + "on-surface": "표면적", + "output": "출력", + "pair": "페어링", + "paired": "페어링됨", + "pairing": "페어링 중...", + "panels": "패널", + "password": "비밀번호", + "pause": "일시 정지", + "performance": "성능", + "pin": "고정", + "play": "재생", + "polling": "폴링", + "position": "위치", + "previous": "이전", + "primary": "첫번째", + "random": "랜덤", + "reboot": "재부팅", + "reboot-to-uefi": "UEFI로 재부팅", + "record": "녹화 시작", + "refresh": "새로고침", + "required": "(필수)", + "reset": "초기화", + "reset-to-default": "기본값으로 초기화", + "result-count": "{count}개 결과", + "result-count-plural": "{count}개 결과", + "resume": "재개", + "save": "저장", + "scanning": "검색 중...", + "screen-corners": "화면 모서리", + "search": "검색", + "second": "{count} 초", + "second-plural": "{count} 초", + "secondary": "두번째", + "security": "보안", + "select": "선택", + "select-color": "색상 선택", + "select-color-description": "강조를 위해 테마 색상을 적용합니다.", + "select-icon-color": "아이콘 색상 선택", + "settings": "설정", + "shortcuts": "단축키", + "shutdown": "시스템 종료", + "signal": "신호", + "sound": "소리", + "sources": "소스", + "start": "시작", + "stop": "중지", + "supporters": "후원자", + "suspend": "대기 모드", + "templates": "템플릿", + "tertiary": "세번째", + "test": "테스트", + "thresholds": "임계값", + "title": "제목", + "toast": "토스트", + "trusted": "신뢰함", + "uninstall": "제거", + "unknown": "알 수 없음", + "unpair": "페어링 해제", + "unpin": "고정 해제", + "update": "업데이트", + "upload": "업로드", + "username": "사용자 이름", + "userspace-reboot": "사용자 공간 재시작", + "version": "버전", + "vibrant": "선명하게", + "visualizer": "비주얼라이저", + "volume": "볼륨", + "volumes": "볼륨", + "wallpaper": "배경화면", + "weather": "날씨", + "weather-loading": "날씨 로딩 중...", + "weather-no-location": "날씨를 보려면 설정에서 위치를 설정하세요", + "week": "주", + "widgets": "위젯", + "width": "너비", + "wifi": "Wi-Fi", + "windows": "창", + "yes": "예" + }, + "control-center": { + "power-profile": { + "tooltip-action": "전원 프로필", + "tooltip-disabled": "전원 프로필을 사용하려면 power-profiles-daemon을 설치하세요" + }, + "wifi": { + "label-disconnected": "Wi-Fi 연결 끊김", + "label-ethernet": "이더넷" + } + }, + "display-modes": { + "always-hide": "항상 숨김", + "always-show": "항상 표시", + "force-open": "강제 열기", + "on-hover": "마우스 오버 시" + }, + "hide-modes": { + "auto-hide": "자동 숨기기", + "hidden": "비어 있을 때 숨기기", + "idle": "유휴 상태일 때 숨기기", + "non-exclusive": "비독점", + "transparent": "비어 있을 때 투명하게", + "visible": "항상 표시" + }, + "launcher": { + "categories": { + "all": "전체", + "audiovideo": "오디오 및 비디오", + "chat": "채팅", + "development": "개발", + "education": "교육", + "emoji-activity": "활동", + "emoji-animals": "동물", + "emoji-flags": "국기", + "emoji-food": "음식 및 음료", + "emoji-nature": "자연", + "emoji-objects": "사물", + "emoji-people": "사람 및 신체", + "emoji-recent": "최근", + "emoji-symbols": "기호", + "emoji-travel": "여행 및 장소", + "game": "게임", + "graphics": "그래픽", + "misc": "기타", + "office": "오피스", + "pinned": "고정됨", + "system": "시스템", + "webbrowser": "웹 브라우저" + }, + "providers": { + "applications": "애플리케이션", + "calculator": "계산기", + "calculator-deprecated": ">calc 명령은 더 이상 사용되지 않으며 곧 제거될 예정입니다. 대신 검색창에 수식을 직접 입력하세요.", + "calculator-description": "계산기 — 수식 계산", + "calculator-enter-expression": "수식 입력", + "calculator-press-enter-to-copy": "결과를 복사하려면 엔터 키를 누르세요", + "clipboard": "클립보드 기록", + "clipboard-clear-description": "모든 클립보드 기록 지우기", + "clipboard-clear-description-full": "클립보드 기록에서 모든 항목 제거", + "clipboard-clear-history": "클립보드 기록 지우기", + "clipboard-delete": "클립보드 항목 삭제", + "clipboard-history-disabled": "클립보드 기록이 비활성화됨", + "clipboard-history-disabled-description": "설정에서 클립보드 기록을 활성화하거나 cliphist를 설치하세요", + "clipboard-loading": "클립보드 기록 로딩 중...", + "clipboard-search-description": "클립보드 기록 검색", + "command-description": "셸 명령 실행", + "emoji": "이모티콘 선택기", + "emoji-loading": "이모티콘 로딩 중...", + "emoji-loading-description": "잠시만 기다려 주세요", + "emoji-no-recent": "최근 이모티콘이 없습니다", + "emoji-search-description": "이모티콘 검색 및 복사", + "settings-search-description": "설정 검색 및 이동", + "windows-search-description": "열려 있는 창 검색 및 포커스" + } + }, + "lock-screen": { + "authenticating": "인증 중...", + "password": "비밀번호를 입력하세요...", + "restart": "다시 시작", + "shut-down": "시스템 종료" + }, + "notifications": { + "panel": { + "click-to-collapse": "클릭하여 접기", + "click-to-expand": "클릭하여 펼치기", + "description": "알림이 도착하면 여기에 표시됩니다.", + "no-notifications": "알림 없음" + }, + "range": { + "earlier": "이전", + "today": "오늘", + "yesterday": "어제" + }, + "time": { + "diff-d": "하루 전", + "diff-dd": "{diff}일 전", + "diff-h": "1시간 전", + "diff-hh": "{diff}시간 전", + "diff-m": "1분 전", + "diff-mm": "{diff}분 전", + "now": "방금" + } + }, + "options": { + "bar": { + "density-comfortable": "편안하게", + "density-compact": "컴팩트", + "density-default": "기본", + "density-mini": "미니", + "density-spacious": "넓게", + "display-mode-always-visible": "항상 표시", + "type-floating": "플로팅", + "type-framed": "프레임", + "type-simple": "심플" + }, + "control-center": { + "quick-settings-style-classic": "클래식", + "quick-settings-style-modern": "모던" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "편안한", + "compact": "컴팩트", + "default": "기본값" + }, + "launcher-view-mode": { + "grid": "그리드", + "list": "목록" + }, + "notification-density": { + "compact": "컴팩트", + "default": "기본값" + }, + "scrolling-modes": { + "always": "항상 스크롤", + "hover": "마우스 오버 시 스크롤", + "never": "스크롤 안 함" + }, + "session-menu-grid-layout": { + "grid": "그리드", + "single-row": "단일 행" + }, + "settings-panel-mode": { + "attached": "바에 부착된 패널", + "centered": "중앙 패널", + "window": "별도 창" + }, + "visualizer-types": { + "linear": "선형", + "mirrored": "미러링", + "wave": "웨이브" + }, + "visualizer-visibility": { + "with-background": "배경과 함께 표시될 때만" + }, + "workspace-labels": { + "index": "인덱스", + "index-and-name": "인덱스 및 이름", + "name": "이름" + } + }, + "panels": { + "about": { + "become-supporter": "후원자 되기", + "changelog": "변경 로그 보기", + "changelog-on-startup": "업데이트 시 변경 로그 표시", + "changelog-on-startup-desc": "Noctalia 업데이트 시 변경 로그를 자동으로 표시합니다.", + "contributors-description": "{count}명의 멋진 기여자에게 감사를 전합니다!", + "contributors-description-plural": "{count}명의 멋진 기여자들에게 감사를 전합니다!", + "copy-info": "정보 복사", + "debug-disabled": "디버그 모드 비활성화됨", + "debug-enabled": "디버그 모드 활성화됨", + "info-copied": "정보가 클립보드에 복사됨", + "noctalia-available": "사용 가능:", + "noctalia-desc": "Wayland를 위해 세심하게 제작된 매끄럽고 미니멀한 데스크톱 셸로, Quickshell로 빌드되었습니다.", + "noctalia-git-commit": "Git 커밋:", + "noctalia-installed-version": "설치된 버전:", + "noctalia-latest-version": "최신 버전:", + "noctalia-qs-version": "Noctalia QS 버전:", + "noctalia-title": "Noctalia 셸", + "privacy-policy": "개인정보 처리방침", + "support": "후원하기", + "supporter-badge": "후원자", + "supporters-desc": "멋진 후원자분께 진심으로 감사드립니다!", + "supporters-desc-plural": "저희의 {count}명의 멋진 후원자분들께 진심으로 감사드립니다!", + "supporters-loading": "후원자 불러오는 중...", + "system-board": "메인보드:", + "system-cpu": "CPU:", + "system-disk": "디스크:", + "system-gpu": "GPU:", + "system-host": "호스트:", + "system-install-hint": "시스템 정보를 보려면 fastfetch를 설치하세요", + "system-kernel": "커널:", + "system-loading": "시스템 정보 로딩 중...", + "system-memory": "메모리:", + "system-monitor": "모니터:", + "system-not-installed": "fastfetch가 설치되어 있지 않습니다", + "system-os": "OS:", + "system-packages": "패키지:", + "system-product": "제품:", + "system-title": "시스템 정보", + "system-uptime": "가동 시간:", + "system-wm": "WM:", + "telemetry-data-copied": "원격 분석 데이터가 클립보드에 복사됨", + "telemetry-desc": "익명 시스템 정보(화면 해상도, 컴포지터, 배포판)를 공유하여 Noctalia 개선을 도와주세요. 시작 시 한 번 전송되며 추적하지 않고 데이터는 30일 후 자동 삭제됩니다.", + "telemetry-enabled": "익명 시스템 정보 전송", + "telemetry-show-data": "데이터 보기", + "telemetry-title": "개인정보 보호", + "title": "정보", + "up-to-date": "최신 상태입니다!", + "update-available": "업데이트 가능", + "view-commit": "GitHub에서 커밋 보기" + }, + "audio": { + "devices-desc": "사용 가능한 오디오 입력 및 출력 장치를 구성합니다.", + "devices-input-device-description": "원하는 오디오 입력 장치를 선택하세요.", + "devices-input-device-label": "입력 장치", + "devices-output-device-description": "원하는 오디오 출력 장치를 선택하세요.", + "devices-output-device-label": "출력 장치", + "devices-title": "오디오 장치", + "external-mixer-description": "외부 오디오 믹서 기능을 활성화할 때 실행할 명령 또는 애플리케이션 경로를 입력하세요.", + "external-mixer-label": "외부 오디오 믹서 명령", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "선호하는 미디어 애플리케이션과 무시할 애플리케이션을 설정합니다.", + "media-excluded-player-description": "시스템에서 무시할 재생기의 키워드를 추가하세요. 각 키워드는 새 줄에 입력해야 합니다.", + "media-excluded-player-label": "제외된 재생기", + "media-excluded-player-placeholder": "부분 문자열 입력 후 + 누르기", + "media-frame-rate-description": "높은 프레임 레이트는 부드럽지만 리소스를 더 많이 사용합니다.", + "media-frame-rate-label": "프레임 레이트", + "media-primary-player-description": "메인 재생기를 식별할 키워드를 입력하세요.", + "media-primary-player-label": "메인 재생기", + "media-primary-player-placeholder": "예: spotify, vlc, mpv", + "media-scrolling-speed-description": "제목이 처음부터 끝까지 스크롤되는 시간(초)입니다.", + "media-scrolling-speed-label": "스크롤 속도", + "media-scrolling-title-description": "긴 미디어 제목에 대해 연속 스크롤을 활성화합니다.", + "media-scrolling-title-label": "제목 스크롤", + "media-title": "미디어 재생기", + "on-middle-clicked-description": "버튼을 가운데 클릭했을 때 실행할 명령입니다.", + "panel-applications-empty": "현재 오디오를 재생 중인 애플리케이션이 없습니다.", + "spectrum-mirrored-description": "스펙트럼을 미러링하여 저주파수가 중앙에 오도록 합니다. 비활성화 시 저주파수는 왼쪽에, 고주파수는 오른쪽에 있습니다.", + "spectrum-mirrored-label": "스펙트럼 미러", + "title": "오디오", + "visualizer-type-description": "미디어 재생 시 사용할 비주얼라이저 유형을 선택하세요.", + "visualizer-type-label": "비주얼라이저 유형", + "volumes-desc": "볼륨 제어 및 오디오 레벨을 조정합니다.", + "volumes-feedback-sound-file-description": "볼륨 변경 시 재생되는 사운드 파일 경로.", + "volumes-feedback-sound-file-label": "볼륨 피드백 사운드 파일", + "volumes-feedback-sound-file-select-title": "볼륨 피드백 사운드 파일 선택", + "volumes-input-volume-description": "마이크 입력 볼륨 레벨입니다.", + "volumes-mute-input-description": "기본 오디오 입력(마이크)을 음소거합니다.", + "volumes-mute-input-label": "오디오 입력 음소거", + "volumes-mute-output-description": "시스템의 메인 오디오 출력을 음소거합니다.", + "volumes-mute-output-label": "오디오 출력 음소거", + "volumes-output-volume-description": "시스템 전체 볼륨 레벨입니다.", + "volumes-step-size-description": "볼륨 변경(스크롤 휠, 키보드 단축키)의 단계 크기를 조정합니다.", + "volumes-step-size-label": "볼륨 단계 크기", + "volumes-volume-feedback-description": "볼륨 조절 시 피드백 소리를 재생합니다.", + "volumes-volume-feedback-label": "볼륨 피드백 소리 재생", + "volumes-volume-overdrive-description": "볼륨을 100% 이상으로 높일 수 있도록 허용합니다. 모든 하드웨어에서 지원되지 않을 수 있습니다.", + "volumes-volume-overdrive-label": "볼륨 오버드라이브 허용" + }, + "bar": { + "appearance-auto-hide-delay-description": "마우스가 Bar를 벗어난 후 Bar가 숨겨지기까지의 시간", + "appearance-auto-hide-delay-label": "숨기기 지연", + "appearance-auto-hide-exclusive-note": "참고: 자동 숨기기가 활성화되면 전용 영역이 자동으로 비활성화됩니다.", + "appearance-auto-show-delay-description": "마우스가 가장자리에 진입할 때 바가 표시되기까지의 시간.", + "appearance-auto-show-delay-label": "표시 지연", + "appearance-background-opacity-description": "바에 대해서만 별도로 배경 불투명도를 설정합니다.", + "appearance-background-opacity-label": "바 배경 불투명도", + "appearance-capsule-color-description": "바 캡슐의 색상을 선택하거나, 기본 표면 색상에는 색상을 사용하지 마세요.", + "appearance-capsule-color-label": "캡슐 색상", + "appearance-capsule-opacity-description": "캡슐이 표시될 때 위젯 배경의 불투명도 수준을 설정합니다.", + "appearance-capsule-opacity-label": "캡슐 불투명도", + "appearance-content-padding-description": "바 가장자리와 위젯 사이의 패딩을 조정합니다.", + "appearance-content-padding-label": "콘텐츠 패딩", + "appearance-density-description": "컴팩트하거나 넓은 모양을 위해 바의 패딩을 조정합니다.", + "appearance-density-label": "바 밀도", + "appearance-desc": "바의 모양과 위치를 사용자 지정합니다.", + "appearance-display-mode-description": "바가 언제 표시될지 선택하세요.", + "appearance-enable-exclusion-zone-inset-description": "제외 영역을 1 물리 픽셀만큼 줄여 바 가장자리 아래로 창이 완벽하게 겹치도록 합니다.", + "appearance-enable-exclusion-zone-inset-label": "인셋 제외 영역", + "appearance-floating-description": "바를 플로팅 '알약' 형태로 표시합니다.", + "appearance-floating-label": "플로팅 바", + "appearance-font-scale-description": "바에 표시되는 텍스트의 글꼴 크기 배율을 조정합니다.", + "appearance-font-scale-label": "글꼴 배율", + "appearance-frame-radius": "내부 반경", + "appearance-frame-settings-description": "프레임 두께 및 내부 모서리 반경 조정", + "appearance-frame-settings-label": "프레임 설정", + "appearance-frame-thickness": "두께", + "appearance-hide-on-overview-description": "컴포지터 개요(Overview)가 활성 상태일 때 바를 숨기고 패널을 닫습니다.", + "appearance-hide-on-overview-label": "개요에서 바 숨기기", + "appearance-margins-description": "플로팅 바 주위의 여백을 조정합니다.", + "appearance-margins-horizontal": "수평 여백", + "appearance-margins-vertical": "수직 여백", + "appearance-outer-corners-description": "바에 바깥쪽으로 굽은 모서리를 표시합니다.", + "appearance-outer-corners-label": "외부 모서리", + "appearance-position-description": "화면에서 바를 배치할 위치를 선택하세요.", + "appearance-position-label": "바 위치", + "appearance-show-capsule-description": "위젯 배경을 표시합니다.", + "appearance-show-capsule-label": "캡슐 표시", + "appearance-show-on-workspace-switch-description": "워크스페이스가 변경될 때 Bar를 자동으로 잠시 보여줍니다.", + "appearance-show-on-workspace-switch-label": "작업 공간 전환 시 바 표시", + "appearance-show-outline-description": "모든 위젯 주위에 보이는 테두리를 표시합니다.", + "appearance-show-outline-label": "위젯 윤곽선 표시", + "appearance-type-description": "바의 스타일을 선택하세요: 심플, 플로팅, 프레임.", + "appearance-type-label": "바 유형", + "appearance-use-separate-opacity-description": "바 배경에 대해 별도의 불투명도 값을 사용하려면 활성화하세요.", + "appearance-use-separate-opacity-label": "별도 바 불투명도 사용", + "appearance-widget-spacing-description": "바의 각 위젯 사이 간격을 조정합니다.", + "appearance-widget-spacing-label": "위젯 간격", + "behavior-middle-click-command-description": "가운데 클릭 시 실행할 명령.", + "behavior-middle-click-command-label": "가운데 클릭 명령", + "behavior-middle-click-command-placeholder": "niri msg action 개요 전환", + "behavior-middle-click-description": "바의 빈 영역에서 가운데 클릭 시 수행할 작업을 선택합니다.", + "behavior-middle-click-follow-mouse-description": "선택한 가운데 클릭 패널을 커서 위치에 엽니다.", + "behavior-middle-click-follow-mouse-label": "가운데 클릭 시 마우스 따라가기", + "behavior-middle-click-label": "바 가운데 클릭 동작", + "behavior-right-click-command-description": "마우스 오른쪽 클릭 시 실행할 명령.", + "behavior-right-click-command-label": "오른쪽 클릭 명령", + "behavior-right-click-command-placeholder": "notify-send \"오른쪽 클릭\"", + "behavior-right-click-description": "바의 빈 영역에서 마우스 오른쪽 버튼 클릭 시 수행할 작업을 선택하세요.", + "behavior-right-click-follow-mouse-description": "선택한 오른쪽 클릭 패널을 커서 위치에 엽니다.", + "behavior-right-click-follow-mouse-label": "마우스 오른쪽 클릭 시 마우스 따라가기", + "behavior-right-click-label": "바 오른쪽 클릭 동작", + "behavior-wheel-wrap-description": "활성화되면 스크롤이 마지막 항목에서 첫 번째 항목으로 계속됩니다.", + "behavior-wheel-wrap-label": "순환", + "behavior-workspace-scroll-description": "바의 비어 있는 영역에서 마우스 휠이 무엇을 할지 선택하세요.", + "behavior-workspace-scroll-label": "바 마우스 휠 동작", + "behavior-workspace-scroll-option-content": "콘텐츠", + "behavior-workspace-scroll-option-workspace": "작업 공간", + "monitor-configure-widgets": "위젯 구성", + "monitor-override-settings": "전역 설정 재정의", + "monitor-override-settings-description": "이 모니터에 사용자 지정 설정을 사용합니다.", + "monitor-reset-all": "모두 초기화", + "monitor-widgets-title": "위젯 구성 - {monitor}", + "monitors-desc": "특정 모니터에 바를 표시합니다. 선택하지 않으면 모든 모니터에 표시됩니다.", + "monitors-desc-new": "바를 표시할 모니터를 구성하고 모니터별로 설정을 사용자 지정합니다.", + "monitors-title": "모니터 표시", + "title": "바", + "tray-blacklist-description": "트레이 제외 규칙을 추가합니다. 와일드카드(*)를 지원합니다.", + "tray-blacklist-label": "블랙리스트", + "tray-blacklist-placeholder": "예: nm-applet, Fcitx*", + "tray-pin-application": "애플리케이션 고정", + "tray-unpin-application": "애플리케이션 고정 해제", + "use-global-widgets": "전역 위젯 사용", + "widgets-desc": "위젯을 드래그하여 순서를 변경하세요. 오른쪽 클릭 메뉴를 사용하여 섹션 간에 위젯을 이동하거나 제거할 수 있습니다.", + "widgets-title": "위젯 배치" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "배경화면에서 색상 구성을 생성합니다. 자동으로 색상을 추출하여 조화로운 테마를 만듭니다.", + "color-source-use-wallpaper-colors-label": "배경화면 색상 사용", + "dark-mode-mode-description": "라이트 모드와 다크 모드 간의 자동 전환을 활성화합니다.", + "dark-mode-mode-label": "다크 모드 일정", + "dark-mode-mode-manual": "수동", + "dark-mode-mode-off": "끄기", + "dark-mode-switch-description": "야간에 더 편안하게 볼 수 있도록 어두운 테마로 전환합니다.", + "delete-error-description": "{scheme} 삭제 실패", + "delete-error-title": "삭제 실패", + "delete-success-description": "{scheme} 삭제 성공", + "delete-success-title": "색상 구성 삭제됨", + "download-button": "더 다운로드하기", + "download-downloading": "다운로드 중...", + "download-empty": "사용 가능한 색상 구성 없음", + "download-error-api-error": "API 오류: {status}", + "download-error-description": "{scheme} 다운로드 실패", + "download-error-download-failed": "종료 코드 {code}로 다운로드 실패", + "download-error-invalid-response": "유효하지 않은 API 응답 형식", + "download-error-no-files": "구성에 대한 파일을 찾을 수 없음", + "download-error-parse-failed": "API 응답 파싱 실패: {error}", + "download-error-rate-limit": "GitHub API 속도 제한 초과", + "download-error-title": "다운로드 실패", + "download-fetching": "사용 가능한 색상 구성 가져오는 중...", + "download-success-description": "{scheme} 다운로드 성공", + "download-success-title": "색상 구성 다운로드됨", + "download-title": "색상 구성 다운로드", + "method-description": { + "content": "소스 콘텐츠의 실제 색상을 매우 유사하게 재현하는 고품질 색상 추출을 사용하는 머티리얼 디자인 구성입니다.", + "dysfunctional": "Faithful과 비슷하지만, 두 번째로 지배적인 색상 계열을 기본으로 선택합니다.", + "faithful": "소스 색상에 가깝게 유지하면서 조화로운 팔레트 생성을 시도합니다.", + "fruit-salad": "다양하고 다채로운 색조로 유쾌하고 생동감 넘치는 팔레트를 생성하는 머티리얼 디자인 구성입니다.", + "monochrome": "채도가 최소화된 단일 색조 그레이스케일을 사용하는 머티리얼 디자인 구성입니다.", + "muted": "차분하고 절제된 외관을 위해 부드럽고 채도가 낮은 톤을 생성합니다.", + "rainbow": "최대한의 색상 다양성을 위해 전체 스펙트럼에 걸쳐 색상을 생성하는 머티리얼 디자인 구성입니다.", + "tonal-spot": "머티리얼 디자인 기본 알고리즘입니다. 소스 색상의 톤 변화를 사용하여 균형 잡힌 팔레트를 만듭니다.", + "vibrant": "선명한 톤으로 대담하고 시선을 사로잡는 팔레트를 위해 색상 채도를 극대화합니다." + }, + "predefined-desc": "미리 정의된 색상 구성 모음에서 선택하세요.", + "predefined-generate-templates-label": "미리 정의된 구성에 대한 템플릿 생성", + "predefined-title": "미리 정의된 색상 구성", + "sync-gsettings-description": "시스템 테마를 활성 밝거나 어두운 변형에 맞춥니다.", + "sync-gsettings-label": "시스템 테마 동기화", + "templates-desc": "외부 애플리케이션에 색상을 적용합니다.", + "templates-filter-description": "특정 카테고리의 템플릿을 표시합니다.", + "templates-filter-label": "카테고리로 필터링", + "templates-misc-description": "나만의 템플릿을 만드세요.", + "templates-misc-label": "고급", + "templates-misc-user-templates-description": "무엇을 하는지 알고 있는 경우에만 활성화하세요 — 온라인 문서를 참조하세요.", + "templates-misc-user-templates-label": "사용자 템플릿 활성화", + "templates-none-detected": "감지된 항목 없음", + "templates-write-path": "쓰기: {filepath}", + "title": "색상 구성", + "wallpaper-method-description": "선호하는 팔레트 생성 방법을 선택하세요.", + "wallpaper-method-label": "팔레트 생성 방법", + "wallpaper-monitor-source-description": "배경화면 색상을 추출할 모니터를 선택하세요.", + "wallpaper-monitor-source-label": "색상 생성 소스" + }, + "connections": { + "authentication-required": "인증 필요", + "bluetooth-auto-connect-description": "Bluetooth가 활성화되면 신뢰할 수 있는 페어링된 장치에 자동으로 연결합니다.", + "bluetooth-auto-connect-label": "페어링된 기기 자동 연결", + "bluetooth-devices-unnamed": "이름 없는 장치는 표시되지 않습니다.", + "bluetooth-discoverable": "이 장치는 이 설정 탭이 열려 있는 동안 {hostName}으로 검색될 수 있습니다.", + "bluetooth-rssi-polling-description": "bluetoothctl을 통해 연결된 장치의 RSSI를 주기적으로 샘플링합니다. 모든 장치에서 사용 가능하지 않을 수 있으며, 활성화 시 최소한의 리소스를 사용합니다.", + "bluetooth-rssi-polling-interval-description": "연결된 장치의 신호 강도를 얼마나 자주 업데이트할지 구성합니다.", + "bluetooth-rssi-polling-interval-label": "폴링 간격", + "bluetooth-rssi-polling-label": "블루투스 신호 폴링", + "disable-discoverability-description": "주변 블루투스 장치에서 장치를 숨깁니다.", + "disable-discoverability-label": "장치 가시성 비활성화", + "hide-unnamed-devices-description": "블루투스 주소로만 표시되는 장치를 숨깁니다.", + "hide-unnamed-devices-label": "이름 없는 장치 숨기기", + "pin-instructions": "기기에 표시된 PIN 코드를 입력해주세요.", + "title": "연결", + "wifi-header-text": "사용 가능한 네트워크를 확인하고 인터넷에 연결하려면 Wi-Fi를 사용하세요." + }, + "control-center": { + "cards-desc": "제어 센터에 표시될 컨트롤과 순서를 사용자 지정합니다.", + "desc": "제어 센터 패널의 위치와 동작을 구성합니다.", + "open-at-mouse-description": "바의 빈 공간을 오른쪽 클릭하면 커서 근처에 제어 센터를 엽니다. 비활성화된 경우, 대신 위의 위치 설정이 사용됩니다.", + "open-at-mouse-label": "마우스 오른쪽 클릭 시 커서 위치에 열기", + "position-description": "제어 센터 패널이 열릴 때 나타날 위치를 선택하세요.", + "shortcuts-custom-button-command-description": "버튼을 클릭했을 때 실행할 명령입니다.", + "shortcuts-custom-button-enable-on-state-logic-description": "검사 명령에 따라 두 번째 아이콘과 '켜짐(hot)' 상태를 활성화합니다.", + "shortcuts-custom-button-enable-on-state-logic-label": "켜짐 상태 로직 활성화", + "shortcuts-custom-button-icon-description": "라이브러리에서 아이콘을 선택하세요.", + "shortcuts-custom-button-on-clicked-label": "왼쪽 클릭 명령", + "shortcuts-custom-button-on-middle-clicked-description": "버튼을 가운데 클릭했을 때 실행할 명령.", + "shortcuts-custom-button-on-state-command-description": "버튼이 '켜짐' 상태여야 하는지 확인하기 위해 실행할 명령입니다. 켜짐일 경우 0, 꺼짐일 경우 0이 아닌 값을 반환합니다.", + "shortcuts-custom-button-on-state-command-label": "켜짐 상태 확인 명령", + "shortcuts-custom-button-on-state-icon-description": "버튼이 '켜짐' 상태일 때의 아이콘입니다.", + "shortcuts-custom-button-on-state-icon-label": "켜짐 상태 아이콘", + "shortcuts-custom-button-state-checks-add": "상태 확인 추가", + "shortcuts-custom-button-state-checks-command": "이 상태 확인을 위해 실행할 명령", + "shortcuts-custom-button-state-checks-label": "상태 확인", + "shortcuts-custom-button-state-checks-remove": "제거", + "shortcuts-custom-button-tooltip-description": "버튼 위에 마우스를 올렸을 때 표시할 툴팁입니다.", + "shortcuts-custom-button-tooltip-label": "툴팁", + "shortcuts-title": "단축키 위젯", + "system-monitor-disk-path-description": "제어 센터의 시스템 모니터 카드가 모니터링할 디스크 마운트 지점을 선택하세요.", + "system-monitor-disk-path-label": "시스템 모니터 디스크 경로", + "title": "제어 센터" + }, + "desktop-widgets": { + "clock-enabled-description": "바탕 화면에 시계 위젯을 표시합니다.", + "clock-enabled-label": "시계 위젯 활성화", + "clock-format-label": "형식", + "clock-height-description": "시계 위젯의 높이(픽셀 단위).", + "clock-height-label": "높이", + "clock-minimal-mode-description": "시간과 날짜만 있는 미니멀한 시계 표시를 사용합니다.", + "clock-minimal-mode-label": "미니멀 모드", + "clock-rounded-corners-description": "위젯 배경에 둥근 모서리를 사용합니다.", + "clock-rounded-corners-label": "둥근 모서리", + "clock-show-background-description": "시계 위젯의 배경 컨테이너를 표시합니다.", + "clock-show-background-label": "배경 표시", + "clock-show-date-description": "시간 아래에 현재 날짜를 표시합니다.", + "clock-show-date-label": "날짜 표시", + "clock-show-seconds-description": "시간에 초를 표시합니다.", + "clock-show-seconds-label": "초 표시", + "clock-style-analog": "아날로그", + "clock-style-binary": "바이너리", + "clock-style-description": "시계 표시 스타일을 선택하세요.", + "clock-style-digital": "디지털", + "clock-style-label": "시계 스타일", + "clock-style-minimal": "미니멀", + "clock-width-description": "시계 위젯의 너비(픽셀 단위).", + "cpu-intensive-note": "! 표시된 위젯은 더 많은 CPU를 사용하므로 신중하게 활성화해야 합니다.", + "edit-mode-button-label": "편집 모드 진입", + "edit-mode-controls-explanation": "왼쪽 클릭 및 드래그: 위젯 이동 또는 크기 조정.\n오른쪽 클릭: 컨텍스트 메뉴 옵션 열기.", + "edit-mode-description": "바탕 화면 위젯을 이동하고 재배치하려면 편집 모드를 활성화하세요. 활성화되면 위젯에 드래그 윤곽선이 표시되며 재배치할 수 있습니다.", + "edit-mode-exit-button": "편집 모드 종료", + "edit-mode-grid-snap-label": "그리드 스냅", + "edit-mode-grid-snap-scale-label": "그리드 스냅 배율", + "edit-mode-label": "편집 모드", + "enabled-description": "바탕 화면 위젯을 완전히 활성화하거나 비활성화합니다.", + "enabled-label": "바탕 화면 위젯 활성화", + "general-desc": "바탕 화면에 나타날 위젯을 구성합니다.", + "general-title": "바탕 화면 위젯", + "media-player-enabled-description": "바탕 화면에 미디어 재생기 위젯을 표시합니다.", + "media-player-enabled-label": "미디어 재생기 위젯 활성화", + "media-player-rounded-corners-description": "위젯 가장자리에 둥근 모서리를 활성화합니다.", + "media-player-show-album-art-description": "앨범 아트와 트랙 정보(제목 및 아티스트)를 표시합니다.", + "media-player-show-album-art-label": "앨범 아트 및 제목 표시", + "media-player-show-background-description": "배경 컨테이너를 표시합니다.", + "media-player-show-buttons-description": "미디어 제어 버튼(재생/일시 정지, 이전, 다음)을 표시합니다.", + "media-player-show-buttons-label": "버튼 표시", + "media-player-show-visualizer-description": "오디오 비주얼라이저 오버레이를 표시합니다.", + "media-player-visualizer-type-description": "비주얼라이저 유형을 선택하세요.", + "overview-enabled-description": "컴포지터 개요가 열려 있는 동안 데스크톱 위젯을 표시합니다.", + "overview-enabled-label": "개요에 표시", + "system-stat-layout-bottom": "아래", + "system-stat-layout-description": "범례가 그래프에 상대적으로 어떻게 표시되는지 선택합니다.", + "system-stat-layout-label": "레이아웃", + "system-stat-layout-side": "측면", + "system-stat-rounded-corners-description": "위젯 배경에 둥근 모서리를 사용합니다.", + "system-stat-rounded-corners-label": "둥근 모서리", + "system-stat-show-background-description": "시스템 통계 위젯의 배경 컨테이너를 표시합니다.", + "system-stat-show-background-label": "배경 표시", + "system-stat-stat-type-description": "표시할 시스템 통계를 선택하세요.", + "system-stat-stat-type-label": "통계 유형", + "title": "바탕 화면 위젯", + "weather-enabled-description": "바탕 화면에 날씨 위젯을 표시합니다.", + "weather-enabled-label": "날씨 위젯 활성화", + "weather-show-background-description": "날씨 위젯의 배경 컨테이너를 표시합니다." + }, + "display": { + "monitors-backlight-device-auto-option": "기본값", + "monitors-backlight-device-description": "이 출력에 대한 백라이트 장치를 선택하세요.", + "monitors-backlight-device-label": "백라이트 장치", + "monitors-brightness-step-description": "밝기 변경(스크롤 휠 및 키보드 단축키)의 단계 크기를 조정합니다.", + "monitors-brightness-step-label": "밝기 단계 크기", + "monitors-brightness-unavailable-ddc-disabled": "밝기 제어를 사용할 수 없습니다. 이 디스플레이의 밝기를 제어하려면 \"외부 밝기 지원\"을 활성화하세요.", + "monitors-brightness-unavailable-generic": "이 디스플레이에서는 밝기 제어를 사용할 수 없습니다.", + "monitors-desc": "각 디스플레이의 밝기를 조정합니다.", + "monitors-enforce-minimum-description": "일부 디스플레이에서 밝기 0% 시 백라이트가 완전히 꺼지는 문제를 해결합니다.", + "monitors-enforce-minimum-label": "최소 밝기 강제 (1%)", + "monitors-external-brightness-description": "DDC/CI 프로토콜을 통해 외부 디스플레이의 밝기를 제어하기 위한 DDCUtil 지원을 활성화합니다.", + "monitors-external-brightness-label": "외부 밝기 지원", + "monitors-global-brightness-description": "모든 모니터의 밝기를 한 번에 조정합니다.", + "monitors-global-brightness-label": "모든 모니터", + "monitors-title": "모니터별 설정", + "night-light-auto-schedule-description": "{location}의 일몰 및 일출 시간을 기준으로 함 — 권장됨.", + "night-light-auto-schedule-label": "자동 일정", + "night-light-desc": "청색광(블루 라이트) 방출을 줄여 수면을 돕고 눈의 피로를 줄입니다.", + "night-light-enable-description": "청색광(블루 라이트) 방출을 줄이기 위해 따뜻한 색상 필터를 적용합니다.", + "night-light-enable-label": "야간 조명 활성화", + "night-light-force-activation-description": "일정을 무시하고 야간 필터를 즉시 적용합니다.", + "night-light-force-activation-label": "강제 활성화", + "night-light-manual-schedule-description": "일출 및 일몰 시간을 사용자 지정으로 설정합니다.", + "night-light-manual-schedule-label": "수동 일정", + "night-light-manual-schedule-select-start": "시작 시간 선택", + "night-light-manual-schedule-select-stop": "종료 시간 선택", + "night-light-manual-schedule-sunrise": "일출 시간", + "night-light-manual-schedule-sunset": "일몰 시간", + "night-light-temperature-day": "주간", + "night-light-temperature-day-description": "주간의 색온도를 제어합니다.", + "night-light-temperature-description": "야간 및 주간의 색온도(따뜻함)를 설정합니다.", + "night-light-temperature-label": "색온도", + "night-light-temperature-night": "야간", + "night-light-temperature-night-description": "야간의 색온도를 제어합니다.", + "title": "디스플레이" + }, + "dock": { + "appearance-background-opacity-description": "독의 배경 불투명도를 조정합니다.", + "appearance-border-radius-description": "독의 테두리 반경을 조정합니다.", + "appearance-border-radius-label": "테두리 반경", + "appearance-colorize-icons-description": "독 앱 아이콘에 테마 색상을 적용합니다 (포커스되지 않은 앱만).", + "appearance-colorize-icons-label": "아이콘 색상화", + "appearance-dead-opacity-description": "실행 중이지 않은 앱 아이콘의 불투명도를 조정합니다.", + "appearance-dead-opacity-label": "비활성 불투명도", + "appearance-desc": "독의 동작과 모양을 사용자 지정합니다.", + "appearance-display-auto-hide": "자동 숨기기", + "appearance-display-description": "독의 동작 방식을 선택하세요.", + "appearance-display-exclusive": "독점", + "appearance-dock-indicator-description": "독이 숨겨져 있을 때 작은 표시기를 표시합니다.", + "appearance-dock-indicator-label": "독 표시기", + "appearance-floating-distance-description": "독과 화면 가장자리 사이의 거리를 설정합니다.", + "appearance-floating-distance-label": "독 플로팅 거리", + "appearance-group-apps-description": "동일한 앱의 여러 창을 하나의 Dock 항목으로 그룹화합니다.", + "appearance-group-apps-label": "동일한 앱 그룹화", + "appearance-group-click-action-cycle": "창 전환", + "appearance-group-click-action-description": "그룹화된 앱에 대해 왼쪽 클릭 시 어떤 동작을 할지 선택하세요.", + "appearance-group-click-action-label": "그룹화된 앱 클릭 동작", + "appearance-group-click-action-list": "창 목록 열기", + "appearance-group-context-menu-mode-description": "그룹화된 앱의 상황에 맞는 메뉴가 어떻게 동작할지 선택하세요.", + "appearance-group-context-menu-mode-extended": "확장됨", + "appearance-group-context-menu-mode-label": "그룹화된 앱 메뉴 모드", + "appearance-group-context-menu-mode-list": "창 목록", + "appearance-group-indicator-style-description": "그룹화된 실행 중인 표시기가 포커스된 창 상태를 표시하는 방식을 선택합니다.", + "appearance-group-indicator-style-dots": "점", + "appearance-group-indicator-style-label": "그룹화된 표시기 스타일", + "appearance-group-indicator-style-number": "숫자", + "appearance-hide-show-speed-description": "독 숨기기/보이기 애니메이션 속도를 조정합니다.", + "appearance-hide-show-speed-label": "숨기기/보이기 속도", + "appearance-icon-size-description": "독의 전체 크기를 조정합니다.", + "appearance-icon-size-label": "독 크기", + "appearance-inactive-indicators-description": "현재 활성 앱뿐만 아니라 모든 앱에 대해 표시기 알약을 표시합니다.", + "appearance-inactive-indicators-label": "실행 중 표시기", + "appearance-indicator-color-description": "숨겨진 독 인디케이터의 색상을 선택합니다.", + "appearance-indicator-color-label": "인디케이터 색상", + "appearance-indicator-opacity-description": "숨겨진 독 인디케이터의 불투명도를 조정합니다.", + "appearance-indicator-opacity-label": "인디케이터 불투명도", + "appearance-indicator-thickness-description": "숨겨진 독 인디케이터를 더 두껍게 표시합니다 (3px 대신 6px).", + "appearance-indicator-thickness-label": "더 두꺼운 인디케이터", + "appearance-launcher-icon-description": "Dock Launcher에 사용될 아이콘을 선택하세요.", + "appearance-launcher-icon-label": "런처 아이콘", + "appearance-launcher-position-description": "독에서 런처 아이콘이 나타날 위치를 선택하세요.", + "appearance-launcher-position-end": "끝", + "appearance-launcher-position-label": "런처 위치", + "appearance-launcher-position-start": "시작", + "appearance-launcher-use-distro-logo-description": "배포판 로고를 Dock Launcher 아이콘으로 사용하세요.", + "appearance-launcher-use-distro-logo-label": "아이콘 대신 배포판 로고 사용", + "appearance-pinned-static-description": "고정된 앱 아이콘을 항상 왼쪽에 정적인 순서로 배치합니다.", + "appearance-pinned-static-label": "정적 고정 앱", + "appearance-position-description": "화면에서 독이 나타날 위치를 선택하세요.", + "appearance-position-label": "위치", + "appearance-show-launcher-icon-description": "독에 애플리케이션 런처 아이콘을 표시합니다.", + "appearance-show-launcher-icon-label": "앱 런처 표시", + "appearance-sit-on-frame-description": "독을 프레임 테두리 내부에 정렬하고, 위에 놓이지 않도록 합니다.", + "appearance-sit-on-frame-label": "독은 프레임에 위치", + "appearance-type-attached": "고정됨", + "appearance-type-description": "플로팅 필 형태 또는 가장자리에 고정된 고정 바 중에서 선택하세요.", + "appearance-type-floating": "플로팅", + "appearance-type-label": "독 스타일", + "enabled-description": "독을 완전히 표시하거나 숨깁니다.", + "enabled-label": "독 활성화", + "monitors-desc": "특정 모니터에 독을 표시합니다. 선택하지 않으면 모든 모니터에 표시됩니다.", + "monitors-only-same-monitor-description": "독이 위치한 모니터의 앱만 표시합니다.", + "monitors-only-same-monitor-label": "동일 모니터의 앱만", + "monitors-title": "모니터 표시", + "title": "독" + }, + "general": { + "copy-settings": "설정 복사", + "fonts-default-description": "인터페이스 전반에 사용되는 기본 글꼴입니다.", + "fonts-default-label": "기본 글꼴", + "fonts-default-placeholder": "기본 글꼴 선택...", + "fonts-default-scale-description": "표준 텍스트의 크기를 늘리거나 줄입니다.", + "fonts-default-scale-label": "기본 글꼴 크기", + "fonts-default-search-placeholder": "글꼴 검색...", + "fonts-desc": "인터페이스 전반에 사용될 글꼴을 선택하세요.", + "fonts-monospace-description": "숫자 및 통계 표시에 사용되는 고정폭 글꼴입니다.", + "fonts-monospace-label": "고정폭 글꼴", + "fonts-monospace-placeholder": "고정폭 글꼴 선택...", + "fonts-monospace-scale-description": "고정폭 텍스트의 크기를 늘리거나 줄입니다.", + "fonts-monospace-scale-label": "고정폭 글꼴 크기", + "fonts-monospace-search-placeholder": "고정폭 글꼴 검색...", + "fonts-title": "글꼴", + "keybinds-conflict-description": "이 키 조합은 이미 {action}에 할당되어 있습니다.", + "keybinds-conflict-title": "키 바인딩 충돌", + "keybinds-description": "패널 및 런처에 대한 전역 탐색 키를 구성합니다.", + "keybinds-down": "아래로 이동", + "keybinds-enter": "확인 / 동작", + "keybinds-escape": "닫기 / 뒤로", + "keybinds-left": "왼쪽으로 이동", + "keybinds-modifier-description": "단축키는 보조키(Ctrl 또는 Alt)를 포함해야 합니다.", + "keybinds-modifier-title": "수정자 키 필요", + "keybinds-remove": "삭제", + "keybinds-right": "오른쪽으로 이동", + "keybinds-title": "탐색 키 바인딩", + "keybinds-up": "위로 이동", + "language-desc": "애플리케이션의 선호 언어를 선택하세요.", + "language-select-auto-detect": "자동", + "language-select-description": "애플리케이션 인터페이스에 사용될 언어를 선택합니다.", + "language-select-label": "애플리케이션 언어", + "launch-setup-wizard": "설정 마법사", + "profile-desc": "사용자 세부 정보와 아바타를 편집합니다.", + "profile-picture-description": "인터페이스 전반에 나타나는 프로필 사진입니다.", + "profile-picture-label": "프로필 사진", + "profile-select-avatar": "아바타 이미지 선택", + "profile-title": "프로필", + "profile-tooltip": "프로필 사진", + "reverse-scrolling-description": "해석된 스크롤 방향 반전.", + "reverse-scrolling-label": "스크롤 반전", + "screen-corners-desc": "화면 모서리 라운딩 및 시각 효과를 사용자 지정합니다.", + "screen-corners-radius-description": "화면의 둥근 모서리를 조정합니다.", + "screen-corners-radius-label": "화면 모서리 반경", + "screen-corners-radius-reset": "화면 모서리 반경 초기화", + "screen-corners-show-corners-description": "화면 가장자리에 둥근 모서리를 표시합니다.", + "screen-corners-show-corners-label": "화면 모서리 표시", + "screen-corners-solid-black-description": "바 배경색 대신 완전한 검은색을 사용합니다.", + "screen-corners-solid-black-label": "검은색 모서리", + "screen-corners-title": "화면 모서리", + "settings-copied": "설정이 클립보드에 복사됨", + "smooth-scrolling-description": "목록 스크롤 애니메이션을 적용하여 더 부드러운 휠 경험을 제공합니다.", + "smooth-scrolling-label": "부드러운 스크롤", + "tab-basics": "기본", + "tab-keybinds": "키 바인딩" + }, + "hooks": { + "color-generation-description": "색상 생성이 완료될 때 실행될 명령 (예: 배경화면 기반 테마 적용 후).", + "color-generation-label": "색상 생성됨", + "color-generation-placeholder": "예: notify-send \"색상\" \"생성됨\"", + "info-command-info-description": "• 명령은 셸(sh -lc)을 통해 실행됩니다.
• 명령은 백그라운드(detached)에서 실행됩니다.
• 테스트 버튼은 현재 값으로 실행됩니다.", + "info-parameters-description": "• Wallpaper hook: $1 = 배경화면 경로, $2 = 화면 이름, $3 = 테마 (어둡게/밝게)
• Theme toggle hook: $1 = true/false (다크 모드 상태)
• Screen lock/unlock hooks: $1 = lock/unlock (화면 잠금 상태)
• Performance mode hooks: 매개변수 없음
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "사용 가능한 매개변수", + "noctalia-started-description": "Noctalia 로딩이 완료되었을 때 실행할 명령입니다.", + "noctalia-started-label": "Noctalia 시작됨", + "noctalia-started-placeholder": "예: notify-send 'Noctalia 로드됨'", + "performance-mode-disabled-description": "Noctalia 성능 모드가 비활성화될 때 실행할 명령입니다.", + "performance-mode-disabled-label": "성능 모드 비활성화됨", + "performance-mode-disabled-placeholder": "예: notify-send \"성능\" \"모드 비활성화됨\"", + "performance-mode-enabled-description": "Noctalia 성능 모드가 활성화될 때 실행할 명령입니다.", + "performance-mode-enabled-label": "성능 모드 활성화됨", + "performance-mode-enabled-placeholder": "예: notify-send \"성능\" \"모드 활성화됨\"", + "screen-lock-description": "화면이 잠길 때 실행할 명령입니다.", + "screen-lock-label": "화면 잠김", + "screen-lock-placeholder": "예: notify-send \"화면\" \"잠김\"", + "screen-unlock-description": "잠금 화면이 해제될 때 실행할 명령입니다.", + "screen-unlock-label": "화면 잠금 해제됨", + "screen-unlock-placeholder": "예: notify-send \"화면\" \"잠금 해제됨\"", + "session-description": "시스템 종료 또는 재부팅 전에 실행할 명령입니다. 작업 유형을 $1로 받습니다 (shutdown/reboot).", + "session-label": "세션 종료", + "session-placeholder": "예: notify-send \"세션\" \"$1\"", + "system-hooks-desc": "시스템 이벤트 발생 시 실행할 명령을 구성합니다.", + "system-hooks-enable-description": "모든 훅 명령을 활성화하거나 비활성화합니다.", + "system-hooks-enable-label": "훅 활성화", + "system-hooks-title": "시스템 훅", + "theme-changed-description": "테마가 다크 모드와 라이트 모드 간에 전환될 때 실행할 명령입니다.", + "theme-changed-label": "테마 변경됨", + "theme-changed-placeholder": "예: notify-send \"테마\" \"전환됨\"", + "title": "훅", + "wallpaper-changed-description": "배경화면이 변경될 때 실행할 명령입니다.", + "wallpaper-changed-label": "배경화면 변경됨", + "wallpaper-changed-placeholder": "예: notify-send \"배경화면\" \"변경됨\"" + }, + "idle": { + "custom-add": "명령 추가", + "custom-description": "일정 시간 비활성 상태 후 셸 명령을 실행합니다.", + "custom-entry-command": "명령", + "custom-entry-delete": "삭제", + "custom-entry-edit": "사용자 지정 명령 편집", + "custom-entry-name": "이름", + "custom-entry-name-placeholder": "예: 조명 끄기", + "custom-entry-new": "새 사용자 지정 명령", + "custom-entry-timeout": "비활성 시간", + "custom-entry-timeout-format": "{count} 초", + "custom-entry-timeout-format-plural": "{count} 초", + "custom-entry-unnamed": "이름 없는 명령어", + "custom-label": "사용자 지정 유휴 명령", + "enable-description": "일정 시간 비활성 상태 후 자동으로 화면을 끄거나, 잠그거나, 절전 모드로 전환합니다.", + "enable-label": "유휴 관리 활성화", + "fade-duration-description": "각 동작이 실행되기 전 화면이 검게 페이드되는 애니메이션 시간(초). 마우스 움직임이 있으면 페이드가 취소됩니다.", + "fade-duration-label": "페이드 지속 시간", + "lock-description": "잠금 화면이 활성화되기 전 비활성 시간(초).", + "lock-label": "화면 잠금", + "resume-command-label": "명령 재개", + "screen-off-description": "모니터가 꺼지기 전 비활성 시간(초).", + "screen-off-label": "화면 끄기", + "status-description": "컴포지터가 보고한 유휴 시간.", + "status-label": "유휴 시간", + "suspend-description": "시스템이 일시 중단되기 전 비활성 시간(초).", + "tab-behavior": "동작", + "tab-custom": "사용자 지정", + "timeouts-description": "단계를 비활성화하려면 0으로 설정하세요. 깨어 있기 가 활성화된 동안 시간 초과는 일시 중지됩니다.", + "timeouts-label": "시간 초과", + "title": "유휴", + "unavailable": "이 Compositor에서는 네이티브 유휴 모니터링을 사용할 수 없습니다." + }, + "indicator": { + "default-value": "기본값: {value}", + "system-default": "시스템 기본값" + }, + "launcher": { + "clipboard-desc": "런처에서 클립보드 기록에 액세스하고 관리합니다.", + "execute-desc": "애플리케이션 실행 방식을 구성합니다.", + "execute-title": "실행", + "settings-annotation-tool-description": "클립보드 기록에서 주석 버튼을 클릭할 때 실행할 명령입니다. 이미지가 이 명령으로 파이프됩니다.", + "settings-annotation-tool-label": "주석 도구", + "settings-annotation-tool-placeholder": "예: 'gradia', 'satty -f -'", + "settings-auto-paste-description": "선택한 클립보드 항목을 자동으로 붙여넣습니다. wtype이 필요합니다.", + "settings-auto-paste-label": "자동 붙여넣기", + "settings-clip-chips-description": "탭 바를 표시하여 클립보드 기록을 유형별(이미지, 링크, 파일, 코드 등)로 필터링합니다.", + "settings-clip-chips-label": "카테고리 칩 활성화", + "settings-clip-preview-description": ">clip 명령 사용 시 클립보드 내용의 미리보기를 표시합니다.", + "settings-clip-preview-label": "클립 미리보기 활성화", + "settings-clip-smart-icons-description": "일반적인 클립보드 아이콘 대신 링크, 파일, 색상, 코드에 대한 특정 아이콘을 표시합니다.", + "settings-clip-smart-icons-label": "스마트 아이콘 활성화", + "settings-clip-wrap-text-description": "클립보드 목록에서 텍스트를 자르지 않고 줄바꿈합니다.", + "settings-clip-wrap-text-label": "클립보드 텍스트 줄바꿈", + "settings-clipboard-history-description": "이전에 복사한 항목에 런처에서 액세스합니다.", + "settings-clipboard-history-label": "클립보드 기록 활성화", + "settings-clipboard-watch-image-description": "이미지 변경을 위해 wl-paste에 전달되는 전체 명령 문자열 (재시작 필요).", + "settings-clipboard-watch-image-label": "이미지 감시 명령", + "settings-clipboard-watch-text-description": "텍스트 변경을 위해 wl-paste에 전달되는 전체 명령 문자열 (재시작 필요).", + "settings-clipboard-watch-text-label": "텍스트 감시 명령", + "settings-custom-launch-prefix-description": "기본 런처 대신 사용자 지정 접두사를 사용하여 명령을 실행합니다 (예: systemd 통합을 위한 'runapp').", + "settings-custom-launch-prefix-enabled-description": "기본 방식 대신 사용자 지정 접두사를 사용하여 애플리케이션을 실행합니다.", + "settings-custom-launch-prefix-enabled-label": "사용자 지정 실행 접두사 활성화", + "settings-custom-launch-prefix-label": "사용자 지정 실행 접두사", + "settings-density-description": "애플리케이션 아이콘 크기와 Launcher 밀도를 조정합니다.", + "settings-density-label": "밀도", + "settings-desc": "런처의 동작과 모양을 사용자 지정합니다.", + "settings-enable-session-search-description": "런처에서 검색할 때 세션 작업(잠금, 종료, 재시작 등)을 표시합니다.", + "settings-enable-session-search-label": "세션 검색 활성화", + "settings-enable-settings-search-description": "런처에서 검색할 때 설정 결과를 표시합니다.", + "settings-enable-settings-search-label": "설정 검색 활성화", + "settings-enable-windows-search-description": "활성 창을 검색하고 포커스합니다.", + "settings-enable-windows-search-label": "창 검색 활성화", + "settings-grid-view-description": "항목을 목록 대신 그리드 레이아웃으로 표시합니다.", + "settings-icon-mode-description": "Tabler 아이콘 대신 기본 시스템 아이콘을 사용합니다.", + "settings-icon-mode-label": "기본 아이콘 사용", + "settings-ignore-mouse-input-description": "런처에서 마우스 상호작용 및 스크롤 휠을 비활성화합니다.", + "settings-ignore-mouse-input-label": "마우스 입력 무시", + "settings-overlay-layer-description": "런처를 전체 화면 창 위에 있는 오버레이 레이어에 표시합니다. 활성화되면 런처는 바에 연결되지 않습니다.", + "settings-overlay-layer-label": "전체 화면 위에 표시", + "settings-position-description": "런처 패널이 나타날 위치를 선택하세요.", + "settings-show-categories-description": "애플리케이션 필터링을 위한 카테고리 탭을 표시합니다.", + "settings-show-categories-label": "카테고리 표시", + "settings-show-icon-background-description": "아이콘 뒤에 둥근 사각형 배경을 표시합니다.", + "settings-show-icon-background-label": "아이콘 배경 표시", + "settings-sort-by-usage-description": "활성화하면 자주 실행하는 앱이 목록의 처음에 나타납니다.", + "settings-sort-by-usage-label": "사용 빈도순 정렬", + "settings-terminal-command-description": "터미널을 실행할 명령입니다. 예: 'kitty -e' 또는 'gnome-terminal --'.", + "settings-terminal-command-label": "터미널 명령", + "settings-use-app2unit-description": "앱 프로세스를 더 잘 관리하고 문제를 방지하기 위해 대체 실행 방법을 사용합니다.", + "settings-use-app2unit-label": "애플리케이션 실행에 App2Unit 사용", + "settings-view-mode-description": "런처 항목의 레이아웃을 선택하세요.", + "settings-view-mode-label": "보기 모드", + "title": "런처" + }, + "location": { + "auto-locate-description": "IP 주소를 사용하여 주기적으로 위치를 자동으로 감지합니다.", + "auto-locate-label": "자동 찾기", + "calendar-cards-desc": "달력 패널에서 카드를 정리하고 활성화/비활성화합니다.", + "calendar-cards-title": "달력 카드", + "calendar-header-label": "달력 헤더", + "calendar-month-label": "달력 월", + "date-time-12hour-format-description": "잠금 화면과 달력에서 시간을 12시간 형식으로 표시합니다. 바 시계에는 자체 설정이 있습니다.", + "date-time-12hour-format-label": "12시간 형식 사용", + "date-time-desc": "날짜와 시간이 표시되는 방식을 사용자 지정합니다.", + "date-time-first-day-of-week-automatic": "자동 (시스템 로케일 사용)", + "date-time-first-day-of-week-description": "달력에서 한 주가 시작되는 요일을 선택하세요.", + "date-time-first-day-of-week-label": "주의 시작 요일", + "date-time-show-events-description": "달력 패널에 일정을 표시합니다.", + "date-time-show-events-label": "달력 일정 표시", + "date-time-title": "날짜 및 시간", + "date-time-use-analog-description": "달력 창에 아날로그 스타일 시계를 표시합니다.", + "date-time-use-analog-label": "아날로그 스타일 시계 사용", + "date-time-week-numbers-description": "달력에 해당 연도의 주차(예: 38주차)를 표시합니다.", + "date-time-week-numbers-label": "주 번호 표시", + "geolocate-now-button": "지금 찾기", + "location-desc": "위치를 설정하여 정확한 날씨 및 야간 조명 일정을 가져오세요.", + "location-search-description": "예: 종로구, 해운대, 강릉", + "location-search-label": "위치 검색", + "location-search-placeholder": "위치 이름 입력", + "location-title": "내 위치", + "weather-desc": "선호하는 온도 단위를 선택하세요.", + "weather-enabled-description": "인터페이스 전반에 날씨 정보를 표시하고 날씨 데이터를 가져옵니다. 비활성화하면 모든 날씨 요소가 숨겨지며 네트워크 요청이 발생하지 않습니다.", + "weather-enabled-label": "날씨 활성화", + "weather-fahrenheit-description": "온도를 섭씨(°C) 대신 화씨(°F)로 표시합니다.", + "weather-fahrenheit-label": "화씨(°F)로 온도 표시", + "weather-hide-city-description": "인터페이스 전반의 날씨 표시에서 도시 이름을 숨깁니다.", + "weather-hide-city-label": "도시 이름 숨기기", + "weather-hide-timezone-description": "인터페이스 전반의 날씨 표시에서 시간대 약어를 숨깁니다.", + "weather-hide-timezone-label": "시간대 숨기기", + "weather-show-effects-description": "날씨 카드에 추가 시각 효과(비, 눈, 번개 등)를 표시합니다.", + "weather-show-effects-label": "날씨 효과 표시", + "weather-show-in-calendar-description": "달력 뷰에 일일 일기 예보를 직접 표시합니다.", + "weather-show-in-calendar-label": "달력에 날씨 표시", + "weather-talia-mascot-always-description": "tabler icons 대신 항상 Talia emojis를 사용하세요.", + "weather-talia-mascot-always-label": "항상 Talia 날씨 이모티콘 표시" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "fprintd(지문 인증)가 활성화된 경우, 이 옵션을 사용하면 지문 대신 비밀번호로 로그인할 수 있습니다.", + "allow-password-with-fprintd-label": "fprintd로 암호 로그인 허용", + "auto-start-auth-description": "키 입력이나 버튼 클릭 없이 지문 인증을 자동으로 시작합니다.", + "auto-start-auth-label": "인증 자동 시작", + "clock-format-description": "날짜/시간 구문 토큰을 사용하여 시계 형식을 사용자 지정합니다.", + "clock-format-label": "시계 형식", + "clock-style-analog": "아날로그", + "clock-style-custom": "사용자 지정", + "clock-style-description": "잠금 화면 시계의 시각적 스타일을 선택하세요.", + "clock-style-digital": "디지털", + "clock-style-label": "시계 스타일", + "compact-lockscreen-description": "로그인 입력과 시스템 제어만 표시하고 날씨 및 미디어 위젯을 숨깁니다.", + "compact-lockscreen-label": "컴팩트 잠금 화면", + "enable-lockscreen-media-controls-description": "잠금 화면에 대화형 미디어 재생 제어를 표시합니다.", + "enable-lockscreen-media-controls-label": "잠금 화면 미디어 제어", + "lock-on-suspend-description": "시스템 대기 모드 진입 시 자동으로 화면을 잠급니다.", + "lock-on-suspend-label": "대기 모드 시 잠금", + "lock-screen-animations-description": "잠금 화면 애니메이션 활성화 또는 비활성화.", + "lock-screen-animations-label": "잠금 화면 애니메이션", + "lock-screen-blur-strength-description": "잠금 화면 배경화면에 흐림 효과를 적용합니다.", + "lock-screen-blur-strength-label": "잠금 화면 흐림 강도", + "lock-screen-tint-strength-description": "잠금 화면 배경화면에 색조 오버레이를 적용합니다.", + "lock-screen-tint-strength-label": "잠금 화면 틴트 강도", + "monitors-desc": "특정 모니터에 잠금 화면을 표시합니다. 아무것도 선택하지 않으면 기본적으로 모두에 표시됩니다.", + "password-chars-description": "비밀번호를 숨기는 데 사용되는 귀여운 아이콘.", + "password-chars-label": "무작위 비밀번호 아이콘", + "show-hibernate-description": "전원 제어에 '최대 절전' 옵션을 표시합니다.", + "show-hibernate-label": "최대 절전 표시", + "show-session-buttons-description": "잠금 화면에서 전원 설정에 대한 액세스를 허용합니다.", + "show-session-buttons-label": "전원 제어", + "title": "잠금 화면" + }, + "network": { + "bluetooth-description": "블루투스 관리를 활성화합니다.", + "bluetooth-rssi-polling-description": "bluetoothctl을 통해 연결된 장치의 RSSI를 주기적으로 샘플링합니다. 모든 장치에서 사용 가능하지 않을 수 있으며 활성화 시 최소한의 리소스를 사용합니다.", + "bluetooth-rssi-polling-label": "블루투스 신호 폴링", + "desc": "Wi-Fi 및 블루투스 연결을 관리합니다.", + "wifi-description": "무선 네트워크를 관리합니다 (NetworkManager 필요)." + }, + "notifications": { + "duration-critical-urgency-description": "긴급 레벨의 알림이 표시되는 시간입니다.", + "duration-critical-urgency-label": "중요도 긴급", + "duration-desc": "중요도에 따라 알림이 표시되는 시간을 구성합니다.", + "duration-low-urgency-description": "낮은 레벨의 알림이 표시되는 시간입니다.", + "duration-low-urgency-label": "중요도 낮음", + "duration-normal-urgency-description": "보통 레벨의 알림이 표시되는 시간입니다.", + "duration-normal-urgency-label": "중요도 보통", + "duration-respect-expire-description": "알림에 설정된 만료 시간을 사용합니다.", + "duration-respect-expire-label": "만료 시간 준수", + "duration-title": "알림 지속 시간", + "history-clear-dismiss-description": "알림을 닫을 때 기록에서 지웁니다.", + "history-clear-dismiss-label": "해제 시 지우기", + "history-critical-urgency-description": "긴급 레벨의 알림을 기록에 저장합니다.", + "history-critical-urgency-label": "중요도 긴급 기록 저장", + "history-desc": "레벨 수준에 따라 기록에 저장할 알림을 제어합니다.", + "history-low-urgency-description": "낮은 레벨의 알림을 기록에 저장합니다.", + "history-low-urgency-label": "중요도 낮음 기록 저장", + "history-normal-urgency-description": "보통 레벨의 알림을 기록에 저장합니다.", + "history-normal-urgency-label": "중요도 보통 기록 저장", + "monitors-desc": "특정 모니터에 알림을 표시합니다. 선택하지 않으면 모든 모니터에 표시됩니다.", + "rules-action-block": "블록", + "rules-action-block-desc": "완전히 건너뜁니다.", + "rules-action-hide": "숨기기", + "rules-action-hide-desc": "팝업 없음, 소리 없음, 기록에 추가됩니다.", + "rules-action-label": "액션", + "rules-action-mute": "음소거", + "rules-action-mute-desc": "소리 없음, 팝업은 계속 표시되며 기록에 남음.", + "rules-add": "규칙 추가", + "rules-delete": "규칙 삭제", + "rules-description": "앱 이름 또는 내용과 일치시킵니다. 규칙은 순서대로 확인되며, 첫 번째 일치하는 항목이 적용됩니다.", + "rules-edit": "규칙 편집", + "rules-label": "필터 규칙", + "rules-pattern-label": "패턴", + "rules-pattern-placeholder": "firefox, discord, 또는 /regex/", + "rules-tab": "규칙", + "settings-always-on-top-description": "전체 화면 창 및 다른 레이어 위에 알림을 표시합니다.", + "settings-background-opacity-description": "알림 배경의 불투명도를 조정합니다.", + "settings-density-description": "알림 카드 밀도를 선택하세요.", + "settings-density-label": "밀도", + "settings-desc": "알림 모양과 동작을 구성합니다.", + "settings-do-not-disturb-description": "활성화하면 모든 알림 팝업을 비활성화합니다.", + "settings-enabled-description": "알림 데몬을 활성화하거나 비활성화합니다. Noctalia 셸을 다시 시작해야 합니다.", + "settings-enabled-label": "알림 활성화", + "settings-location-description": "화면상에 알림이 나타나는 위치입니다.", + "settings-markdown-description": "Markdown 서식을 사용하여 알림 내용을 렌더링합니다.", + "settings-markdown-label": "Markdown 활성화", + "sounds-desc": "알림 소리 효과 및 볼륨을 구성합니다.", + "sounds-enabled-description": "수신 알림에 대한 소리 효과를 활성화합니다.", + "sounds-enabled-label": "알림 소리 활성화", + "sounds-excluded-apps-description": "자체 내장 소리가 있는 특정 애플리케이션에 대해 구성된 알림 소리 재생을 건너뜁니다.", + "sounds-excluded-apps-label": "제외된 애플리케이션", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "긴급 레벨의 알림에 재생할 소리 파일 경로입니다.", + "sounds-files-critical-label": "긴급 레벨의 알림 소리", + "sounds-files-critical-select-title": "긴급 레벨의 알림 소리 파일 선택", + "sounds-files-desc": "다양한 알림 수준에 대해 사용자 지정 소리 파일을 구성합니다.", + "sounds-files-low-description": "낮은 레벨의 알림에 재생할 소리 파일 경로입니다.", + "sounds-files-low-label": "낮은 레벨의 알림 소리", + "sounds-files-low-select-title": "낮은 레벨의 알림 소리 파일 선택", + "sounds-files-normal-description": "보통 레벨의 알림에 재생할 소리 파일 경로입니다.", + "sounds-files-normal-label": "보통 레벨의 알림 소리", + "sounds-files-normal-select-title": "보통 레벨의 알림 소리 파일 선택", + "sounds-files-placeholder": "소리 파일 경로 입력", + "sounds-files-select-file": "소리 파일 선택", + "sounds-files-title": "소리 파일", + "sounds-files-unified-description": "알림에 재생할 소리 파일 경로입니다.", + "sounds-files-unified-label": "알림 소리", + "sounds-files-unified-select-title": "알림 소리 파일 선택", + "sounds-separate-description": "중요도 낮음, 보통, 긴급의 알림에 대해 서로 다른 소리 파일을 사용합니다.", + "sounds-separate-label": "중요도 별로 다른 소리를 사용", + "sounds-title": "소리 설정", + "sounds-unavailable-description": "알림 소리를 활성화하려면 Qt6 Multimedia를 설치하세요.", + "sounds-unavailable-label": "알림 소리 사용 불가", + "sounds-volume-description": "알림 소리의 볼륨 레벨을 조정합니다.", + "sounds-volume-label": "소리 볼륨", + "toast-battery-description": "배터리 잔량이 이 비율 아래로 떨어지면 경고를 표시합니다.", + "toast-battery-label": "배터리 경고", + "toast-desc": "토스트 모양과 동작을 구성합니다.", + "toast-keyboard-description": "키보드 레이아웃 변경 시 토스트를 표시합니다.", + "toast-keyboard-label": "키보드 레이아웃", + "toast-media-description": "미디어 재생 상태가 변경될 때 토스트를 표시합니다.", + "toast-media-label": "미디어" + }, + "osd": { + "always-on-top-description": "전체 화면 창 및 다른 레이어 위에 OSD를 표시합니다.", + "always-on-top-label": "항상 위에 표시", + "background-opacity-description": "OSD 배경의 투명도를 제어합니다.", + "background-opacity-label": "배경 불투명도", + "description": "볼륨 및 밝기 오버레이와 같은 화면 표시기를 구성합니다.", + "duration-auto-hide-description": "OSD가 사라지기 전의 시간을 조정합니다.", + "duration-auto-hide-label": "자동 숨기기 시간", + "duration-desc": "OSD가 자동으로 숨겨지기 전까지 표시되는 시간입니다.", + "duration-title": "자동 숨기기 시간 제한", + "enabled-description": "볼륨 및 밝기 변경 사항을 실시간으로 표시합니다.", + "enabled-label": "OSD (화면 표시) 활성화", + "events-desc": "OSD를 트리거할 이벤트를 선택하세요.", + "general-desc": "OSD의 가시성과 동작을 구성합니다.", + "location-description": "화면 표시가 나타나는 위치입니다.", + "monitors-desc": "특정 모니터에 OSD를 표시합니다. 선택하지 않으면 모든 모니터에 표시됩니다.", + "title": "OSD (화면 표시)", + "types-brightness-description": "화면 밝기 변경 시 OSD를 표시합니다.", + "types-brightness-label": "밝기", + "types-custom-text-description": "IPC의 사용자 지정 텍스트 메시지에 대해 OSD를 표시합니다.", + "types-custom-text-label": "사용자 지정 텍스트", + "types-desc": "OSD를 트리거할 이벤트를 선택하세요.", + "types-input-volume-description": "마이크 볼륨 변경 시 OSD를 표시합니다.", + "types-input-volume-label": "입력 볼륨", + "types-lockkey-description": "Caps Lock, Num Lock, Scroll Lock 전환 시 OSD를 표시합니다.", + "types-lockkey-label": "잠금 키", + "types-media-description": "미디어 재생 상태 변경(재생, 일시 정지, 건너뛰기) 시 OSD를 표시합니다.", + "types-media-label": "미디어 재생", + "types-title": "OSD 트리거 이벤트", + "types-volume-description": "오디오 출력 볼륨 변경 시 OSD를 표시합니다.", + "types-volume-label": "출력 볼륨" + }, + "plugins": { + "auto-update": "플러그인 자동 업데이트", + "auto-update-description": "셸 시작 시 모든 플러그인을 자동으로 업데이트합니다.", + "available-description": "구성된 소스에서 플러그인을 찾아보고 설치합니다.", + "available-label": "사용 가능한 플러그인", + "available-no-plugins-description": "플러그인 소스를 확인하거나 목록을 새로고침하세요.", + "available-no-plugins-label": "사용 가능한 플러그인 없음", + "check-for-updates": "업데이트 확인", + "checking-for-updates": "업데이트 확인 중...", + "collision-already-installed": "이 플러그인은 이미 설치되어 있습니다", + "collision-custom-version-exists": "\"{source}\"의 사용자 지정 버전이 이미 설치되어 있습니다", + "collision-official-version-exists": "이 플러그인의 공식 버전이 이미 설치되어 있습니다", + "development-disable": "이 플러그인의 개발 모드를 비활성화", + "development-enable": "이 플러그인에 대한 개발 모드를 활성화합니다.", + "filter-downloaded": "다운로드됨", + "filter-not-downloaded": "다운로드되지 않음", + "filter-tags-description": "카테고리 또는 다운로드 상태별로 플러그인을 필터링합니다.", + "filter-tags-label": "태그", + "hot-reload-description": "파일이 변경될 때 자동으로 플러그인을 다시 로드합니다. 플러그인 개발에 유용합니다.", + "hot-reload-label": "핫 리로드 (개발 모드)", + "hot-reloaded": "플러그인 다시 로드됨: {name}", + "install-error": "설치 실패: {error}", + "install-incompatible": "{plugin}은(는) Noctalia v{version} 이상이 필요합니다", + "install-success": "{plugin} 설치 성공", + "installed-description": "로컬에 설치된 모든 플러그인을 관리하고 구성합니다.", + "installed-label": "설치된 플러그인", + "installed-no-plugins-description": "\"사용 가능\" 섹션에서 플러그인을 설치하세요.", + "installed-no-plugins-label": "설치된 플러그인 없음", + "installing": "{plugin} 설치 중...", + "notify-updates": "플러그인 업데이트 알림", + "notify-updates-description": "새로운 플러그인 업데이트가 있을 때 알림 팝업을 표시합니다.", + "open-plugin-page": "플러그인 페이지 열기", + "open-plugins-tab": "플러그인 설정 열기", + "plugin-settings-title": "{plugin} 설정", + "refresh-refreshing": "플러그인 목록 새로고침 중...", + "refresh-tooltip": "사용 가능한 플러그인 새로고침", + "settings-error-not-loaded": "플러그인 로드되지 않음", + "settings-saved": "플러그인 설정 저장됨", + "settings-tooltip": "플러그인 설정", + "source-custom": "사용자 지정 소스", + "sources-add-custom": "사용자 지정 저장소 추가", + "sources-add-dialog-description": "GitHub 저장소를 플러그인 소스로 추가합니다.", + "sources-add-dialog-error": "플러그인 소스 추가 실패", + "sources-add-dialog-name": "저장소 이름", + "sources-add-dialog-name-placeholder": "내 사용자 지정 플러그인", + "sources-add-dialog-success": "플러그인 소스 추가 성공", + "sources-add-dialog-title": "플러그인 소스 추가", + "sources-add-dialog-url": "저장소 URL", + "sources-description": "플러그인 저장소를 관리합니다.", + "sources-placeholder": "내 멋진 저장소", + "sources-remove-tooltip": "플러그인 소스 제거", + "title": "플러그인", + "translations-reloaded": "번역 다시 로드됨: {name}", + "uninstall-dialog-description": "정말 {plugin}을(를) 제거하시겠습니까? 모든 플러그인 데이터가 삭제됩니다.", + "uninstall-dialog-title": "플러그인 제거", + "uninstall-error": "제거 실패: {error}", + "uninstall-success": "{plugin} 제거 성공", + "uninstalling": "{plugin} 제거 중...", + "update-all": "모두 업데이트 ({count})", + "update-all-success": "모든 플러그인 업데이트 성공", + "update-available": "새 플러그인 업데이트 사용 가능", + "update-available-plural": "플러그인 업데이트 사용 가능 ({count}개)", + "update-error": "플러그인 업데이트 실패: {plugin}: {error}", + "update-pending": "v{current} → v{new} (Noctalia v{required} 필요)", + "update-success": "{plugin}을(를) v{version}으로 업데이트함", + "update-version": "v{current} → v{new}", + "updating": "업데이트 중..." + }, + "region": { + "title": "지역" + }, + "session-menu": { + "countdown-duration-description": "전원 작업을 실행하기 전 카운트다운 타이머의 지속 시간을 설정합니다.", + "countdown-duration-label": "카운트다운 지속 시간", + "enable-countdown-description": "전원 작업을 실행하기 전에 카운트다운 타이머를 표시합니다.", + "enable-countdown-label": "카운트다운 타이머 활성화", + "entries-desc": "세션 메뉴에 나타날 전원 작업과 순서를 사용자 지정합니다.", + "entries-title": "전원 작업", + "entry-settings-command-description": "이 작업에 대해 실행할 사용자 지정 명령입니다. 시스템 기본 명령을 사용하려면 비워 두세요.", + "entry-settings-command-placeholder": "예: systemctl poweroff", + "entry-settings-default-command-lock": "내부 잠금 화면 (명령 없음)", + "entry-settings-default-command-logout": "내부 로그아웃 (명령 없음)", + "entry-settings-default-info-description": "사용자 지정 명령이 지정되지 않은 경우 시스템 기본 명령이 사용됩니다.", + "entry-settings-default-info-label": "기본 명령", + "entry-settings-keybind-description": "세션 메뉴가 열려 있을 때 이 작업을 트리거할 키 조합을 할당합니다.", + "entry-settings-keybind-placeholder": "키를 할당하려면 기록을 누르세요...", + "entry-settings-keybind-recording": "녹음 중... 아무 키 조합이나 누르세요", + "entry-settings-title": "{entry} 구성", + "entry-settings-tooltip": "명령 구성", + "general-desc": "세션 메뉴 패널의 동작과 모양을 구성합니다.", + "large-buttons-layout-description": "세션 메뉴 버튼의 표시 방식을 선택하세요.", + "large-buttons-layout-label": "큰 버튼 레이아웃", + "large-buttons-style-description": "세션 메뉴를 그리드 레이아웃의 큰 버튼으로 표시합니다.", + "large-buttons-style-label": "큰 버튼 스타일", + "position-description": "세션 메뉴 패널이 열릴 때 나타날 위치를 선택하세요.", + "show-header-description": "세션 메뉴 상단에 제목과 닫기 버튼을 표시합니다.", + "show-header-label": "헤더 표시", + "show-keybinds-description": "세션 옵션에 키 바인딩 힌트를 표시합니다.", + "show-keybinds-label": "단축키 표시" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Noctalia 성능 모드가 활성화된 동안 데스크톱 위젯을 계속 표시합니다.", + "noctalia-performance-disable-desktop-widgets-label": "성능 모드에서 데스크톱 위젯 활성화", + "noctalia-performance-disable-wallpaper-description": "Noctalia 성능 모드가 활성화된 동안 데스크톱, 개요, 잠금 화면 배경화면을 계속 표시합니다.", + "noctalia-performance-disable-wallpaper-label": "성능 모드에서 배경화면 렌더링 활성화", + "title": "시스템" + }, + "system-monitor": { + "critical-color-label": "위험 색상", + "custom-highlight-colors-title-label": "사용자 지정 강조 색상", + "disk-available-label": "사용 가능한 디스크 공간", + "disk-section-label": "디스크 사용량", + "enable-dgpu-monitoring-description": "경고: 이 기능은 외장 GPU(NVIDIA/AMD)를 깨워 하이브리드 그래픽 노트북의 배터리 수명에 상당한 영향을 줄 수 있습니다.", + "enable-dgpu-monitoring-label": "외장 GPU 모니터링 활성화", + "external-monitor-description": "외부 시스템 모니터 애플리케이션 기능을 활성화할 때 실행할 명령 또는 애플리케이션 경로를 입력하세요.", + "external-monitor-label": "외부 시스템 모니터 명령", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "시스템 모니터의 동작과 모양을 구성합니다.", + "gpu-section-label": "GPU 온도", + "highlight-colors-section-label": "강조 색상", + "polling-interval-label": "폴링 간격", + "polling-section-description": "각 시스템 값을 갱신하는 빈도를 정합니다.", + "polling-section-label": "폴링 간격", + "threshold-critical": "심각", + "threshold-warning": "경고", + "thresholds-section-description": "각 시스템 값에 대한 경고/위험 임계값을 조정합니다.", + "use-custom-highlight-colors-description": "비활성화하면 테마 기본 강조 색상이 사용됩니다.", + "use-custom-highlight-colors-label": "사용자 지정 강조 색상 사용", + "warning-color-label": "경고 색상" + }, + "user-interface": { + "allow-panels-without-bar-description": "활성화하면 바가 없는 화면에서도 패널을 열 수 있습니다. 비활성화하면 바가 있는 화면에서만 패널이 열리므로 메모리 사용량을 줄일 수 있습니다.", + "allow-panels-without-bar-label": "바가 없는 화면에서 패널 허용", + "animation-disable-description": "더 빠르고 반응이 좋은 경험을 위해 모든 애니메이션을 비활성화합니다.", + "animation-disable-label": "UI 애니메이션 비활성화", + "animation-speed-description": "전역 애니메이션 속도를 조정합니다.", + "animation-speed-label": "애니메이션 속도", + "animation-speed-reset": "애니메이션 속도 초기화", + "appearance-desc": "툴팁, 테두리, 그림자와 같은 시각적 요소를 사용자 지정합니다.", + "blur-behind-description": "컴포지터 블러 프로토콜을 사용하여 바와 패널 뒤 영역을 흐리게 처리합니다.", + "blur-behind-label": "배경 흐림", + "box-border-description": "콘텐츠 영역 주위에 윤곽선을 표시합니다.", + "box-border-label": "컨테이너 윤곽선", + "box-border-radius-description": "사이드바, 카드, 콘텐츠 패널과 같은 주요 레이아웃 섹션의 모서리 둥글기를 조정합니다.", + "box-border-radius-label": "컨테이너 반경", + "control-border-radius-description": "버튼, 토글, 텍스트 필드를 포함한 대화형 요소의 굴곡을 제어합니다.", + "control-border-radius-label": "입력 반경", + "desc": "인터페이스의 모양, 느낌 및 동작을 사용자 지정합니다.", + "dim-desktop-description": "패널이나 메뉴가 열려 있을 때 바탕 화면을 어둡게 합니다.", + "dim-desktop-label": "바탕 화면 어둡게 하기", + "dimmer-opacity-description": "바탕 화면 어둡게 하기의 불투명도 수준을 설정합니다.", + "dimmer-opacity-label": "어두운 바탕 화면 불투명도", + "panel-background-opacity-description": "모든 패널(바, 런처, 설정 등...)의 배경 불투명도를 설정합니다.", + "panel-background-opacity-label": "패널 배경 불투명도", + "panels-attached-to-bar-description": "패널이 바와 화면 가장자리에 고정되어 세련된 반전 모서리로 매끄러운 모양을 만듭니다.", + "panels-attached-to-bar-label": "패널을 가장자리에 스냅", + "panels-overlay-description": "전체 화면 애플리케이션 위에서도 패널과 바가 계속 보이도록 보장합니다.", + "panels-overlay-label": "패널과 바를 항상 위에 유지", + "scaling-description": "바를 제외한 일반 사용자 인터페이스의 크기를 변경합니다.", + "scaling-label": "인터페이스 배율", + "scrollbar-always-visible-description": "콘텐츠를 스크롤할 수 있을 때 스크롤바를 항상 표시하고, 마우스 오버 시에만 표시하지 않습니다.", + "scrollbar-always-visible-label": "항상 스크롤바 표시", + "settings-panel-header": "설정 패널", + "settings-panel-mode-description": "설정 레이아웃을 선택하세요 (다시 열어야 할 수 있음).", + "settings-panel-mode-label": "설정 패널 모드", + "settings-panel-sidebar-card-style-description": "설정 사이드바를 둥근 모서리가 있는 채워진 배경으로 감쌉니다.", + "settings-panel-sidebar-card-style-label": "사이드바 카드 스타일", + "shadows-description": "바와 패널 아래에 그림자를 활성화합니다.", + "shadows-direction-description": "그림자가 드리워질 위치를 선택하세요.", + "shadows-direction-label": "그림자 방향", + "shadows-label": "그림자", + "title": "사용자 인터페이스", + "tooltips-description": "인터페이스 전반에 툴팁을 활성화하거나 비활성화합니다.", + "tooltips-label": "툴팁 표시", + "translucent-widgets-description": "패널 내의 버튼, 탭 및 기타 위젯을 반투명하게 만듭니다.", + "translucent-widgets-label": "반투명 위젯" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "알파벳순", + "automation-change-mode-description": "자동으로 변경할 때 배경화면 선택 방식을 선택하세요.", + "automation-change-mode-label": "변경 모드", + "automation-custom-interval-description": "시간을 HH:MM으로 입력하세요 (예: 01:30).", + "automation-custom-interval-label": "사용자 지정 간격", + "automation-interval-description": "배경화면을 자동으로 변경할 빈도입니다.", + "automation-interval-label": "배경화면 간격", + "automation-random-wallpaper-description": "정기적인 간격으로 랜덤 배경화면 변경을 예약합니다.", + "automation-scheduled-change-description": "정기적인 간격으로 배경화면을 자동으로 변경합니다.", + "automation-scheduled-change-label": "자동 변경", + "look-feel-edge-smoothness-description": "전환 가장자리에 부드러운 페더링 효과를 적용합니다.", + "look-feel-edge-smoothness-label": "전환 가장자리 부드럽게", + "look-feel-fill-color-description": "배경화면 뒤에 나타날 수 있는 채우기 색상을 선택하세요.", + "look-feel-fill-mode-description": "이미지가 모니터 해상도에 맞춰 스케일링되는 방식을 선택하세요.", + "look-feel-fill-mode-label": "채우기 모드", + "look-feel-skip-startup-transition-description": "쉘 시작 시 배경화면 애니메이션을 건너뛰기.", + "look-feel-skip-startup-transition-label": "시작 전환 건너뛰기", + "look-feel-title": "모양 및 느낌", + "look-feel-transition-duration-description": "전환 애니메이션의 지속 시간(초)입니다.", + "look-feel-transition-duration-label": "전환 지속 시간", + "look-feel-transition-type-description": "배경화면 전환 시의 애니메이션 유형입니다.", + "look-feel-transition-type-label": "전환 유형", + "settings-clear-cache-button": "캐시 지우기", + "settings-clear-cache-description": "캐시된 크기 조정된 배경화면을 지워 디스크 공간을 확보합니다.", + "settings-clear-cache-label": "배경화면 캐시", + "settings-clear-cache-toast": "배경화면 캐시가 지워졌습니다", + "settings-desc": "배경화면 관리 및 표시 방식을 제어합니다.", + "settings-enable-management-description": "Noctalia로 배경화면을 관리합니다. 다른 애플리케이션을 사용하려면 체크 해제하세요.", + "settings-enable-management-label": "배경화면 관리 활성화", + "settings-enable-overview-description": "개요 화면에 흐리고 어두운 배경화면을 적용합니다.", + "settings-enable-overview-label": "개요 배경화면 활성화", + "settings-folder-description": "메인 배경화면 폴더의 경로입니다.", + "settings-folder-label": "배경화면 폴더", + "settings-hide-wallpaper-filenames-tooltip-hide": "파일명 숨기기", + "settings-hide-wallpaper-filenames-tooltip-show": "파일명 표시", + "settings-monitor-specific-description": "각 모니터에 대해 다른 배경화면 폴더를 설정합니다.", + "settings-monitor-specific-label": "모니터별 디렉토리", + "settings-monitor-specific-tooltip": "모니터 배경화면 폴더", + "settings-overview-blur-strength-description": "개요에 흐림 강도를 적용합니다.", + "settings-overview-blur-strength-label": "개요 흐림 강도", + "settings-overview-tint-description": "개요에 색조 강도를 적용합니다.", + "settings-overview-tint-label": "개요 색조 강도", + "settings-recursive-search-description": "배경화면 디렉토리의 하위 폴더에서도 배경화면을 검색합니다.", + "settings-recursive-search-label": "하위 폴더 검색", + "settings-select-monitor-folder": "모니터 배경화면 폴더 선택", + "settings-selector-description": "배경화면을 선택하세요.", + "settings-selector-position-description": "배경화면 선택 패널이 나타날 위치를 선택하세요.", + "settings-show-hidden-files-tooltip-hide": "숨김 파일 숨기기", + "settings-show-hidden-files-tooltip-show": "숨김 파일 표시", + "settings-title": "배경화면 설정", + "settings-use-original-images-description": "표시 전에 배경화면 크기 조정을 건너뜁니다. 디스크 공간을 절약하고 배경화면 변경 시 CPU 사용량을 줄이지만, 매우 큰 이미지의 경우 더 많은 메모리를 사용할 수 있습니다.", + "settings-use-original-images-label": "원본 이미지 사용", + "settings-view-mode-description": "디렉토리의 배경화면 표시 방식을 선택하세요.", + "settings-view-mode-label": "보기 모드", + "view-mode-browse": "디렉토리 찾아보기", + "view-mode-cycle-tooltip": "보기 모드: {mode} (클릭하여 변경)", + "view-mode-recursive": "하위 디렉토리 평탄화", + "view-mode-single": "루트 디렉토리" + } + }, + "placeholders": { + "add-new-keybind": "새 키 바인딩 추가", + "command-example": "echo \"Hello World\"", + "enter-command": "실행할 명령 입력 (앱 또는 사용자 지정 스크립트)", + "enter-ipc-identifier": "IPC 명령을 위한 고유 식별자 입력", + "enter-path": "경로 입력...", + "enter-text-to-collapse": "예: '재생 중인 항목 없음'. 패턴에는 /regex/를 사용하세요.", + "enter-tooltip": "툴팁 입력", + "enter-width-pixels": "너비 입력 (픽셀 단위)", + "keybind-recording": "키 바인딩 기록 중...", + "search": "검색...", + "search-icons": "예: noctalia, niri, battery, cloud", + "search-launcher": "항목 검색... 또는 명령을 위해 > 사용", + "search-wallhaven": "Wallhaven에서 검색...", + "search-wallpapers": "배경화면 필터링을 위해 입력..." + }, + "positions": { + "bottom": "하단", + "bottom-center": "하단 중앙", + "bottom-left": "하단 왼쪽", + "bottom-right": "하단 오른쪽", + "center": "중앙", + "center-left": "왼쪽 중앙", + "center-right": "오른쪽 중앙", + "close-to-bar": "바 버튼 근처", + "follow-bar": "바 따르기", + "left": "왼쪽", + "right": "오른쪽", + "top": "상단", + "top-center": "상단 중앙", + "top-left": "상단 왼쪽", + "top-right": "상단 오른쪽" + }, + "session-menu": { + "action-in-seconds": "{seconds}초 후 {action}...", + "cancel-timer": "타이머 취소", + "title": "세션 메뉴" + }, + "setup": { + "all-done": "완료되었습니다!", + "appearance": { + "subheader": "다크 모드와 색상 소스(배경화면 또는 사전 정의)를 선택하세요." + }, + "customize": { + "header": "경험 사용자 지정", + "subheader": "바 위치, 밀도, 배율 등을 조정합니다." + }, + "skip-setup": "설정 건너뛰기", + "telemetry-wizard-done": "알겠습니다!", + "telemetry-wizard-note": "언제든지 설정에서 활성화하거나 비활성화할 수 있습니다", + "telemetry-wizard-subtitle": "Noctalia 개선을 돕기 위해 익명 분석을 추가했습니다", + "telemetry-wizard-title": "개인정보 보호 업데이트", + "wallpaper": { + "choose-dir": "배경화면 이미지가 포함된 디렉토리를 선택하세요", + "dir-browse": "배경화면 폴더 찾아보기", + "dir-description": "배경화면이 포함된 폴더를 선택하세요.", + "dir-label": "배경화면 디렉토리", + "dir-select-title": "배경화면 폴더 선택", + "header": "배경화면 선택", + "no-dir": "선택된 배경화면 디렉토리 없음", + "no-valid": "다음 위치에서 유효한 이미지 파일을 찾을 수 없음: {dir}", + "none-in-dir": "디렉토리에서 배경화면을 찾을 수 없음", + "preview-error": "이미지 로드 실패", + "select-prompt": "아래에서 배경화면을 선택하세요", + "subheader": "아름다운 배경으로 분위기를 설정하세요." + }, + "welcome": "환영합니다", + "welcome-note": "시작하기 위한 몇 가지 기본 사항입니다 — 전체 옵션은 설정에 있습니다", + "welcome-subtitle": "당신만의 독특한 데스크톱을 만들어 보세요", + "welcome-title": "Noctalia에 오신 것을 환영합니다!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "감지된 미디어 재생기 없음", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "알 수 없는 앱", + "uptime": "가동 시간: {uptime}", + "user-requested": "사용자 요청됨", + "welcome-back": "환영합니다,", + "widget-settings-title": "{widget} 설정" + }, + "system-monitor": { + "core-usage": "코어 {id} 사용량", + "cpu-temp": "CPU 온도", + "cpu-usage": "CPU 사용량", + "disk": "디스크", + "download-speed": "다운로드 속도", + "gpu-temp": "GPU 온도", + "load-average": "로드 평균", + "title": "시스템 모니터", + "upload-speed": "업로드 속도" + }, + "toast": { + "airplane-mode": { + "description": "모든 무선 통신을 비활성화합니다.", + "title": "비행기 모드" + }, + "battery": { + "critical": "배터리 부족", + "critical-desc": "배터리 {percent}% — 즉시 충전기를 연결해주세요", + "low": "배터리 부족", + "low-desc": "배터리가 {percent}%입니다 — 충전기를 연결해주세요" + }, + "bluetooth": { + "address-copied": "주소가 클립보드에 복사됨", + "auto-connect-disabled": "자동 연결 비활성화됨", + "auto-connect-enabled": "자동 연결 활성화됨", + "auto-connecting": "{count}개의 장치에 연결 중...", + "confirm-code": "다른 장치에서 코드 {value}를 확인하세요.", + "connect-failed": "장치 연결 실패", + "disconnect-failed": "장치 연결 해제 실패", + "discoverable-change-failed": "검색 가능 상태 변경 실패", + "discoverable-disabled": "검색 가능 비활성화됨", + "discoverable-enabled": "검색 가능 활성화됨", + "display-code": "다른 장치에 코드 {value}를 입력하세요.", + "forget-failed": "장치 지우기 실패", + "pair-failed": "장치 페어링 실패", + "passkey-required": "장치에서 패스키가 필요합니다.", + "pincode-required": "장치에서 PIN 코드가 필요합니다.", + "state-change-failed": "블루투스 상태 변경 실패" + }, + "clipboard": { + "long-text": "긴 텍스트", + "unavailable": "클립보드 기록 사용 불가", + "unavailable-desc": "'cliphist' 애플리케이션이 설치되어 있지 않습니다. 클립보드 기록 기능을 사용하려면 설치하세요" + }, + "custom-command-failed": { + "description": "명령 실패: {command}\\n종료 코드: {code}", + "title": "사용자 지정 명령 실패" + }, + "do-not-disturb": { + "disabled": "방해 금지 모드 비활성화됨", + "disabled-desc": "모든 알림 표시 중", + "enabled": "방해 금지 모드 활성화됨", + "enabled-desc": "알림은 기록에서 확인할 수 있습니다" + }, + "donation-opened": "브라우저에서 후원 페이지가 열림", + "internet-limited": "인터넷 없이 연결됨", + "keyboard-layout": { + "changed": "키보드 레이아웃이 {layout}(으)로 변경됨", + "title": "키보드" + }, + "missing-control-center": { + "description": "제어 센터 위젯이 바에서 제거되었습니다. 바에서 다시 액세스하려면 위젯을 다시 추가해야 합니다. 바를 오른쪽 클릭하여 열 수도 있습니다.", + "label": "마지막 제어 센터 위젯 제거됨" + }, + "night-light": { + "forced": "강제 활성화", + "normal": "일반 모드", + "not-installed": "wlsunset이 설치되지 않음" + }, + "noctalia-performance": { + "disabled": "성능 모드 비활성화됨", + "enabled": "성능 모드 활성화됨", + "label": "Noctalia 성능" + }, + "power-profile": { + "changed": "전원 프로필 변경됨", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "템플릿 처리 중 오류가 발생했습니다", + "title": "템플릿 처리 실패" + }, + "vpn": { + "connected": "'{name}'에 연결됨", + "disconnected": "'{name}' 연결 끊김" + }, + "wallpaper-colors": { + "disabled": "배경화면 색상 비활성화됨", + "enabled": "배경화면 색상 활성화됨", + "label": "배경화면 색상" + }, + "wifi": { + "connected": "'{ssid}'에 연결됨", + "connection-failed": "연결 실패", + "connection-timeout": "연결 시간 초과", + "disconnected": "'{ssid}' 연결 끊김", + "incorrect-password": "잘못된 비밀번호", + "network-not-found": "네트워크를 찾을 수 없음" + } + }, + "tooltips": { + "add-widget": "위젯 추가", + "auto-connect": "이 장치의 자동 연결 토글", + "bluetooth-auto-connect-off": "자동 연결 비활성화됨", + "bluetooth-auto-connect-on": "자동 연결 활성화됨", + "bluetooth-devices": "블루투스 장치", + "brightness-at": "밝기: {brightness}%", + "click-to-start-recording": "화면 녹화 (녹화 시작)", + "click-to-stop-recording": "화면 녹화 (녹화 중지)", + "collapse": "사이드바 접기", + "copy-address": "주소 복사", + "delete-notification": "알림 삭제", + "dismiss-notification": "알림 닫기", + "do-not-disturb-enabled": "방해 금지 모드", + "expand": "사이드바 펼치기", + "forget-network": "네트워크 지우기", + "grid-view": "그리드 보기", + "hidden-files-hide": "숨김 파일", + "hide-unnamed-devices": "이름 없는 장치 숨기기", + "home": "홈 디렉토리", + "input-muted": "입력 음소거 전환", + "keep-awake": "깨어 있기", + "keyboard-layout": "{layout} 키보드 레이아웃", + "list-view": "목록 보기", + "manage-vpn": "VPN 연결", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "최대 위젯 도달", + "microphone-volume-at": "마이크 볼륨: {volume}%", + "move-to-section": "{section}(으)로 이동", + "mute": "음소거", + "next-media": "다음 트랙", + "next-month": "다음 달", + "night-light-not-installed": "야간 조명 (사용 불가)", + "noctalia-performance-enabled": "Noctalia 성능 모드", + "open-annotation-tool": "주석 도구로 열기", + "open-control-center": "제어 센터", + "open-notification-history-enable-dnd": "알림 기록", + "open-settings": "설정", + "open-tray-dropdown": "시스템 트레이", + "output-muted": "출력 음소거 전환", + "power-profile": "{profile} 전원 프로필", + "previous-media": "이전 트랙", + "previous-month": "이전 달", + "refresh-devices": "장치 새로고침", + "refresh-wallhaven": "Wallhaven 결과 새로고침", + "refresh-wallpaper-list": "배경화면 목록 새로고침", + "remove-widget": "위젯 제거", + "search-close": "검색 닫기", + "session-menu": "세션 메뉴", + "show-all-devices": "모든 장치 표시", + "switch-to-dark-mode": "다크 모드", + "switch-to-light-mode": "라이트 모드", + "unmute": "음소거 해제", + "up": "상위 디렉토리", + "volume-at": "출력 볼륨: {volume}%", + "wallpaper-selector": "배경화면 선택기" + }, + "wallpaper": { + "browse": { + "empty-directory": "이 디렉토리는 비어 있습니다.", + "go-root": "배경화면 루트로 이동", + "go-up": "상위 폴더로 이동", + "go-up-hint": "뒤로 버튼을 사용하여 상위로 이동하세요." + }, + "configure-directory": "이미지가 있는 배경화면 디렉토리를 구성하세요.", + "fill-modes": { + "crop": "자르기 (채움)", + "fit": "맞춤 (포함)", + "repeat": "반복 (타일)", + "stretch": "늘리기" + }, + "no-match": "일치하는 항목이 없습니다.", + "no-wallpaper": "배경화면을 찾을 수 없습니다.", + "panel": { + "apikey-help": "NSFW 콘텐츠에 액세스하려면 API 키가 필요합니다.", + "apikey-label": "API 키", + "apikey-managed-by-env": "NOCTALIA_WALLHAVEN_API_KEY 환경 변수를 통해 관리됩니다.", + "apikey-placeholder": "Wallhaven API 키 입력", + "appearance-dark-tab": "어둡게", + "appearance-light-tab": "라이트", + "categories-anime": "애니메이션", + "categories-label": "카테고리", + "categories-people": "인물", + "color-extraction-disabled": "배경화면 색상 추출 사용", + "color-extraction-enabled": "사전 정의된 색상 구성표 사용", + "header-devices-apply-all-tooltip": "모니터별 배경화면 선택", + "header-devices-per-monitor-tooltip": "모든 모니터에 적용", + "header-link-light-dark-tooltip": "테마 변경 시 배경화면 유지", + "header-separate-light-dark-tooltip": "테마에 따라 배경화면 자동 전환", + "header-sun-linked-tooltip": "밝고 어두운 배경화면 분리", + "header-sun-separate-tooltip": "밝고 어두운 모드에 동일한 배경화면", + "order-asc": "오름차순", + "order-desc": "내림차순", + "order-label": "순서", + "purity-label": "콘텐츠 필터", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Sketchy", + "ratios-all-portrait": "모든 세로", + "ratios-all-wide": "모든 가로", + "ratios-any": "모두", + "ratios-label": "화면비", + "resolution-atleast": "최소", + "resolution-exact": "정확히", + "resolution-label": "해상도", + "resolution-mode-label": "모드", + "solid-color-tooltip": "단색 배경", + "sort-date-asc": "오래된 순으로 정렬", + "sort-date-desc": "최신 순으로 정렬", + "sort-name-asc": "이름으로 정렬 (A-Z)", + "sort-name-desc": "이름으로 정렬 (Z-A)", + "sort-random": "무작위로 정렬", + "sorting-date-added": "추가된 날짜", + "sorting-favorites": "즐겨찾기", + "sorting-label": "정렬 기준", + "sorting-relevance": "관련성", + "sorting-toplist": "인기 목록", + "sorting-views": "조회수", + "source-label": "소스", + "source-wallhaven": "Wallhaven", + "title": "배경화면 선택기", + "wallhaven-settings-title": "Wallhaven 설정" + }, + "transitions": { + "disc": "디스크", + "fade": "페이드", + "honeycomb": "벌집", + "pixelate": "픽셀화", + "stripes": "스트라이프", + "wipe": "와이프" + }, + "try-different-search": "다른 검색어를 시도해 보세요.", + "wallhaven": { + "loading": "배경화면 로딩 중...", + "no-results": "배경화면을 찾을 수 없습니다. 다른 검색어를 시도해 보세요.", + "page": "{total} 중 {current}", + "page-prefix": "페이지", + "page-suffix": "{total}개 중" + } + }, + "weather": { + "clear-sky": "맑음", + "drizzle": "이슬비", + "fog": "안개", + "mainly-clear": "대체로 맑음", + "overcast": "흐림", + "partly-cloudy": "구름 조금", + "rain-showers": "소나기", + "snow": "눈", + "thunderstorm": "뇌우" + }, + "widgets": { + "color-picker": { + "palette-description": "다양한 미리 정의된 색상 중에서 선택하세요.", + "palette-label": "팔레트", + "palette-theme-colors": "테마 색상 팔레트에 빠르게 액세스합니다.", + "title": "색상 선택기" + }, + "datetime-tokens": { + "ampm-lowercase": "소문자 오전/오후 (am/pm)", + "ampm-uppercase": "대문자 오전/오후 (AM/PM)", + "common-12hour-time-minutes": "12시간제 시간 및 분", + "common-24hour-time-minutes": "24시간제 시간 및 분", + "common-24hour-time-seconds": "24시간제 시간 및 초", + "common-european-date": "유럽식 날짜 형식", + "common-iso-date": "ISO 날짜 형식", + "common-us-date": "미국식 날짜 형식", + "common-weekday-date": "요일 및 날짜", + "common-weekday-month-day": "요일, 월 및 일", + "day-abbreviated": "축약된 요일 이름", + "day-full": "전체 요일 이름", + "day-leading-zero": "앞에 0이 있는 일 (01-31)", + "day-no-leading-zero": "앞에 0이 없는 일 (1-31)", + "hour-leading-zero": "앞에 0이 있는 시 (00-23) — 24시간 형식", + "hour-no-leading-zero": "앞에 0이 없는 시 (0-23) — 24시간 형식", + "minute-leading-zero": "앞에 0이 있는 분 (00-59)", + "minute-no-leading-zero": "앞에 0이 없는 분 (0-59)", + "month-abbreviated": "축약된 월 이름", + "month-full": "전체 월 이름", + "month-number-leading-zero": "앞에 0이 있는 월 번호 (01-12)", + "month-number-no-zero": "앞에 0이 없는 월 번호 (1-12)", + "second-leading-zero": "앞에 0이 있는 초 (00-59)", + "second-no-leading-zero": "앞에 0이 없는 초 (0-59)", + "timezone-abbreviation": "시간대 약어", + "year-four-digit": "4자리 연도", + "year-two-digit": "2자리 연도 (00-99)" + }, + "file-picker": { + "item": "개 항목", + "items": "개 항목", + "search-placeholder": "파일 및 폴더 검색...", + "select-current": "현재 항목 선택", + "select-file": "파일 선택", + "select-folder": "폴더 선택", + "selected": "선택됨:", + "title": "파일 선택기" + }, + "file-picker-title": "파일 선택", + "icon-picker": { + "title": "아이콘 선택기" + }, + "tooltip-placeholder": "플레이스홀더" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "익명 ID", + "ca-cert": "CA 인증서", + "eap-method": "EAP 방식", + "password": "사용자 비밀번호", + "phase2-auth": "2단계 인증", + "username": "ID / 사용자 이름" + }, + "panel": { + "action-required": "조치 필요", + "add-hidden-network": "숨겨진 네트워크 추가", + "add-network": "네트워크 추가", + "available-interfaces": "사용 가능한 인터페이스", + "available-networks": "사용 가능한 네트워크", + "disabled": "Wi‑Fi가 비활성화됨", + "disconnecting": "연결 해제 중...", + "dns": "DNS", + "enable-message": "사용 가능한 네트워크를 보려면 Wi‑Fi를 활성화하세요.", + "enter-password": "Wi‑Fi 비밀번호 입력...", + "forget": "지우기", + "forget-network": "이 네트워크 지우기", + "forgetting": "지우는 중...", + "hidden-network": "숨겨진 네트워크", + "interface": "네트워크 인터페이스", + "internet-connected": "인터넷 연결됨", + "internet-limited": "인터넷 없음", + "internet-status": "인터넷 상태", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "알려진 네트워크", + "link-speed": "링크 속도", + "network-name-ssid": "네트워크 이름 (SSID)", + "no-ethernet-devices": "이더넷 장치 감지되지 않음", + "no-networks": "Wi‑Fi 네트워크를 찾을 수 없음", + "saved": "저장됨", + "scan-again": "다시 검색", + "searching": "네트워크 검색 중...", + "security-open": "열기", + "security-wep": "WEP", + "security-wpa": "WPA/WPA2/WPA3 개인", + "security-wpa-ent": "WPA 엔터프라이즈", + "security-wpa2-ent": "WPA2 엔터프라이즈", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 엔터프라이즈", + "show-password": "비밀번호 표시", + "title": "Wi‑Fi" + }, + "signal": { + "excellent": "매우 좋음", + "fair": "보통", + "good": "좋음", + "poor": "불량", + "weak": "약함" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/ku.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/ku.json new file mode 100644 index 0000000..b0c4901 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/ku.json @@ -0,0 +1,1861 @@ +{ + "actions": { + "clear-history": "Dîrokê pak bike", + "close-app": "{app} bigire", + "connect-vpn": "Bi {name} ve girê bide", + "cycle-visualizer": "Dîmenkerê çerxê", + "disable-bluetooth": "Bluetooth çalak neke", + "disable-dnd": "'Dengê dernexîne' çalak neke", + "disable-wifi": "Wi-Fi çalak neke", + "disconnect-vpn": "Girêdanê bi {name} re qut bike", + "enable-bluetooth": "Bluetooth çalak bike", + "enable-dnd": "'Dengê dernexîne' çalak bike", + "enable-wifi": "Wi-Fi çalak bike", + "launcher-settings": "Sazkariyên destpêkarê", + "lower-to-bottom": "Bo jêr daxîne", + "open-calendar": "Salnameyê veke", + "open-display-settings": "Sazkariyên dîmenderê", + "open-launcher": "Destpêkarê veke", + "open-settings": "Sazkariyan veke", + "raise-to-top": "Bo jor bilind bike", + "random-wallpaper": "Wêneyê dîwêrê rastehatî", + "run-custom-command": "Fermana kesane bixebtîne", + "show-active-only": "Tenê yên çalak nîşan bide", + "show-all": "Hemûyan nîşan bide", + "toggle-mute": "Guhertîna bêdengkirinê", + "widget-settings": "Sazkariyên sepanokê" + }, + "authentication": { + "error": "Şaşetiya rastkirinê", + "failed": "Rastkirin têk çû" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Rengên rûkarê li ser îkona çarçoveya çalak bisepîne.", + "hide-mode-description": "Dema ku çarçove neçalak be, kontrol dike ka sepanok çawa tev digere.", + "scrolling-mode-description": "Dema ku şemitandina nivîsê ji bo sernavên çarçoveyên ên dirêj çalak e, kontrol bike.", + "show-app-icon-description": "Îkona sepanê li kêleka sernavê çarçoveyê nîşan bide.", + "show-app-icon-label": "Îkona sepanê nîşan bide" + }, + "audio-visualizer": { + "color-name-description": "Rengê ji bo dîtbarîner hilbijêre.", + "color-name-label": "Rengê tijîkirinê", + "hide-when-idle-description": "Dema ku çalak be, dîtbarîner veşartî ye heya ku lîstikvanek bi awayekî çalak were lîstin.", + "hide-when-idle-label": "Dema ku medyaya lê nalîze veşêre", + "width-description": "Ferehiya pêkhateya kesane." + }, + "battery": { + "device-default": "Berdest (Amûra dîmenderê)", + "device-description": "Ka kîjan amûrê betarî hilbijêre ku were nîşandan.", + "device-label": "Amûrê betarî", + "display-mode-description": "Hilbijêre ka bila betarî di darikê de were xuyakirin", + "display-mode-graphic": "Betariyê grafîkî", + "display-mode-graphic-clean": "Betariyê grafîkî (bê %)", + "display-mode-icon-always": "Îkon - Her dem nîşan bide %", + "display-mode-icon-hover": "Îkon - Li ser şemitandinê nîşan bide", + "display-mode-icon-only": "Tenê îkon", + "hide-if-idle-description": "Dema ku betarî neyê tijîkirin an valakirin, sepanokê veşêre.", + "hide-if-idle-label": "Dema de bêkar veşêre", + "hide-if-not-detected-description": "Dema betarî li ser pergalê neyê dîtin, sepanokê veşêre.", + "hide-if-not-detected-label": "Dema ku neyê dîtin veşêre", + "low-battery-threshold-label": "Asta hişyariya betarî ya kêm", + "show-noctalia-performance-description": "Guhêrbarê awaya performansê ya Noctalia di hundirê destgeha betariyê de nîşan bide.
Sî û anîmasyonan di Noctalia de neçalak dike da ku bikaranîna çavkaniyan kêm bike.", + "show-noctalia-performance-label": "Guhêrbarê Performansa Noctalia nîşan bide", + "show-power-profile-description": "Hilbijartina profîla hêzê di hundirê destgeha betariyê de nîşan bide.", + "show-power-profile-label": "Kontrolên profîla hêzê nîşan bide" + }, + "clock": { + "clock-display-description": "Nîşandana demjimêra xwe bi riya tevlîkirina nîşanekan ji lîsteya jêrîn kesane bike. Ji bo ku tu formata 12 demjimêran bi kar bînî, divê tu nîşana 'AP' têxînî nav.", + "clock-display-label": "Nîşandana demjimêrê", + "custom-font-description": "Ji bo nîşanadana demjimêrê cûrenivîseke kesane hilbijêre.", + "custom-font-label": "Cûrenivîsa kesane", + "custom-font-placeholder": "Cûrenivîsa kesane hilbijêre...", + "custom-font-search-placeholder": "Cûrenivîsan bigere......", + "horizontal-bar-description": "Serişte: Ji bo afirandina şikandina rêzê, \\n bi kar bîne.", + "horizontal-bar-label": "Darika asoyî", + "preview": "Pêşdîtin", + "tooltip-format-description": "Derdê rêzika ji bo sernavê ku dema mirov li ser demjimêrê disekine tê nîşandan. Ji bo bikaranîna sernavê xwerû vala bihêle.", + "tooltip-format-label": "Formata sernavê", + "use-custom-font-description": "Hilbijartina cûrenivîsa berdest bi tîpeke kesane ji bo demjimêrê derbas bike.", + "use-custom-font-label": "Cûrenivîsa kesane bi kar bîne", + "use-monospaced-font-description": "Dema ku çalak be, demjimêr wê cûrenivîsa monospace bi kar bîne.", + "use-monospaced-font-label": "Cûrenivîsa monospace bi kar bîne", + "vertical-bar-description": "Bi kar tînin valahiyekê ji hev veqetînin her beşek li ser rêzeke nû.", + "vertical-bar-label": "Darê vertical" + }, + "control-center": { + "browse-file": "Pelê bigere", + "browse-library": "Geroka pirtûkxaneyê", + "color-selection-description": "Rengên rûkarkê bo îkonan bisepîne.", + "enable-colorization-description": "Çalak bike rengkirina îkona navenda kontrolê, rengên temayê bi kar bîne.", + "icon-description": "Îkonekê ji pirtûkxaneyê an jî dosyeyeke xwerû hilbijêre.", + "select-custom-icon": "Îkoneke kesane hilbijêre", + "use-distro-logo-description": "Li şûna îkoneke kesane, logoya belavkirina xwe bi kar bîne.", + "use-distro-logo-label": "Li şûna îkonê logoya belavkirina bi kar bîne" + }, + "custom-button": { + "collapse-condition-description": "Ger nivîsa derketî bi vê nirxê re têkildar be, dê bişkok hilweşe.", + "collapse-condition-label": "Rewşa hilweşînê", + "color-selection-description": "Rengên mûqeyê li îkon û nivîsê bi kar bîne.", + "display-command-output-description": "Fermanekê binivîse ku di navberên rêkûpêk de bixebite. Rêza yekem a derketina wê dê wekî nivîs were xuyang kirin.", + "display-command-output-label": "Nîşandana derketina fermanê", + "display-command-output-stream-description": "Fermanekê binivîse ku bi berdewamî bixebite.", + "dynamic-text": "Nivîsa dînamîkî", + "enable-colorization-description": "Rengînkirinê ji bo îkona bişkoka xwerû û nivîsê çalak bike, rengên temayê bi kar bîne.", + "enable-colorization-label": "Rengînkirinê çalak bike", + "hide-mode-always-expanded": "Her dem berferehkirî", + "hide-mode-description": "Dema ku derana fermanê tune be, dîtina sepanokê kontrol dike.", + "hide-mode-expand-with-output": "Berfireh bike dema ku deran hebe", + "hide-mode-label": "Awaya veşartinê", + "hide-mode-max-transparent": "Pir berfirehkirî lê zelal", + "icon-description": "Îkonekê ji pirtûkxaneyê hilbijêre.", + "ipc-identifier-description": "Nasnameya yekta ji bo fermanên IPC. Vê nasnameyê bi 'qs -c noctalia-shell ipc call cb [çalakî] [nasname]' bikar bînin da ku vê bişkokê bi rêya IPC kontrol bikin.", + "ipc-identifier-label": "Nasnameya IPC", + "left-click-description": "Fermana ku dema bişkok li ser çepê tê klîkkirin tê meşandin.", + "left-click-label": "Klîka çepê", + "left-click-update-text": "Nûvekirina nivîsa ku li ser klîka çepê tê nîşandan", + "max-text-length-horizontal-description": "Jimara herî pir ya tîpan ku di darika asoyî de tên nîşandan (0 ji bo veşartina nivîsê).", + "max-text-length-horizontal-label": "Dirêjahiya herî zêde ya nivîsê (horîzontal)", + "max-text-length-vertical-description": "Jimara herî pir ya tîpan ku di darika stûnî de were nîşandan (0 ji bo veşartina nivîsê).", + "max-text-length-vertical-label": "Dirêjahiya herî pir ya nivîsê (stûnî)", + "middle-click-description": "Fermana ku dema bişkok bi mişkê navîn tê tikandin bê meşandin.", + "middle-click-label": "Klîka navîn", + "middle-click-update-text": "Nûvekirina nivîsa ku bi klîka navîn tê nîşandan", + "parse-json-description": "Parskirina derketina fermanê wekî objeyek JSON da ku nivîs û îkonê bi awayekî dînamîkî were danîn.", + "parse-json-label": "Deranê wekî JSON analîz bike", + "refresh-interval-description": "Navbera bi milîsaniyeyan.", + "refresh-interval-label": "Navbera nûkirinê", + "right-click-description": "Fermana ku dema bişkok bi aliyê rastê ve hat tikandin, tê bicihkirin.", + "right-click-label": "Rast bikirtînin", + "right-click-update-text": "Nûkirina nivîsa ku li ser rast-klîkê tê nîşandan", + "show-icon-description": "Guherîna xuya û ne xuya ya îkona widgetê.", + "show-icon-label": "Nîşanê nîşan bide", + "text-stream-description": "Rêzikên ji fermanê hatine weşandin dê wekî nivîs li ser bişkokê werin xuyandin.", + "text-stream-label": "Rêz", + "wheel-description": "Fermana ku dema çerxa mişkê tê bikaranîn were meşandin.
Ji bo delta çerxa mişkê di fermanê de $delta bikar bîne.", + "wheel-down-description": "Fermana ku dema çerxa mişkê ber bi jêr ve tê gerandin, were meşandin.", + "wheel-down-label": "Fermana daketina çerxê", + "wheel-label": "Tekerê gerandinê", + "wheel-mode-separate-description": "Fermana cuda ji bo teker ber bi jor û xwarê çalak bike.", + "wheel-mode-separate-label": "Fermanên çerxê yên cuda", + "wheel-up-description": "Fermana ku dema çerxa mişkê ber bi jor ve tê gerandin, tê meşandin.", + "wheel-up-label": "Fermana hilkişîna çerxê", + "wheel-update-text": "Nûkirina nivîsa ku li ser gerandinê tê nîşandan" + }, + "keyboard-layout": { + "show-icon-description": "Nîşana şêweya klavyeyê nîşan bide." + }, + "lock-keys": { + "hide-when-off-description": "Eger nexwan çalak nabe nîşanek veşêre.", + "hide-when-off-label": "Veşêre gava ku ne çalak e", + "show-caps-lock-description": "Rewşa Caps Lock nîşan bide.", + "show-caps-lock-label": "Girtina hûrfên mezîn", + "show-num-lock-description": "Rewşa Num Lock nîşan bide.", + "show-num-lock-label": "Girtina jimareyan", + "show-scroll-lock-description": "Rewşa Scroll Lock nîşan bide.", + "show-scroll-lock-label": "Girtina rêçêkirinê" + }, + "media-mini": { + "compact-mode-description": "Çalak bike sêwirandinek ku cih digire ji bo panelê lîstikvanê medyayê.", + "compact-mode-label": "Awaya kompakt", + "hide-mode-description": "Kontrola ku widget çawa tevdigere dema ku tu medya nayê lîstin.", + "max-width-description": "Mezinahiya herî mezin a asoyî ya sepanokê saz dike. Sepanok wê bi naveroka kurt re were biçûkkirin.", + "no-active-player": "Bê lîstikvanê çalak", + "panel-section-description": "Xuyakirina xuyabûn û reftarên panelê lîstikvanê medyayê.", + "panel-section-label": "Panelê lîstikvanê medyayê", + "scrolling-mode-description": "Kontrola dema ku geroka nivîsê ji bo sernavên rêyên dirêj tê çalak kirin.", + "scrolling-mode-label": "Moda Gerandinê", + "show-album-art-description": "Wêneyê albûmê ya strana ku niha lê tê guhdarîkirin nîşan bide.", + "show-album-art-label": "Wêneyê albûmê nîşan bide", + "show-artist-first-description": "Hunermend - sernav li şûna sernav - hunermend nîşan bide.", + "show-artist-first-label": "Pêşî hunermend nîşan bide", + "show-progress-ring-description": "Pêşandanek pêşketina dorhêlî ya ku pêşketina şopê nîşan dide.", + "show-progress-ring-label": "Nîşanek xeleka pêşketinê bide", + "show-visualizer-description": "Dîmenek dîtbarî ya dengî dema muzîk lêdixe nîşan bide.", + "show-visualizer-label": "Nîşana dîtbarîkerê bide", + "use-fixed-width-description": "Dema ku were çalak kirin, widget dê her dem firehiya herî zêde bikar bîne li şûna ku bi dînamîkî li gorî naverokê were sererast kirin.", + "use-fixed-width-label": "Bi berfirehî bikar bîne", + "visualizer-type-description": "Şêwaza dîtbarîzera deng hilbijêre ku were nîşandan.", + "visualizer-type-label": "Cureyê dîtbarîker" + }, + "notification-history": { + "hide-widget-when-zero-description": "Dema ku ti agahdarî tune be, îkona agahdarîyê veşêre.", + "hide-widget-when-zero-label": "Îkonê bê agahdarî veşêre", + "hide-widget-when-zero-unread-description": "Dema ku ti agahdarîya nexwendî tune be, îkona agahdarîyê veşêre.", + "hide-widget-when-zero-unread-label": "Îkonê bê nexwendî veşêre", + "show-unread-badge-description": "Nîşaneyekê nîşan bide ku hejmara agahiyên nehatine xwendin nîşan dide.", + "show-unread-badge-label": "Nîşana ne xwendî nîşan bide", + "unread-badge-color-description": "Rengê nîşana agahdariya nexwendî hilbijêre.", + "unread-badge-color-label": "Rengê nîşana nexwendî" + }, + "section-editor": { + "placeholder": "Sepanokekê hilbijêre ku lê tevlî bikî...", + "search-placeholder": "Sepanokê bigere..." + }, + "spacer": { + "width-description": "Firehiya firehiyê bi pîkselan." + }, + "system-monitor": { + "compact-mode-description": "Statîstîkan wekî şîroveya barên piçûk nîşan bide li şûna nirxên nivîskî. Pêşî li guhertina layoutê digire.", + "compact-mode-label": "Awaya kompakt", + "cpu-frequency-description": "Leza demjimêra CPU ya heyî di GHz de nîşan bide.", + "cpu-temperature-description": "Germahiya YKN ku hebe nîşan bide.", + "cpu-temperature-label": "Germahiya YKN", + "cpu-usage-description": "Rêjeya bikaranîna CPU ya niha nîşan bide.", + "cpu-usage-label": "Bikaranîna YKN", + "disk-path-description": "Hilbijêre kîjan xala çiyayê dîskê were şopandin.", + "disk-path-label": "Rêya dîskê", + "gpu-temperature-description": "Germahiya YKG ku hebe, nîşan bide.", + "load-average-description": "Barkirina navîn a pergalê nîşan bide.", + "load-average-label": "Barkirina navîn", + "memory-percentage-description": "Bîranînê wekî rêjeyekê nîşan bide, ne wekî nirxên mutleq.", + "memory-percentage-label": "Bîr wekî sedî", + "memory-usage-description": "Agahiyên bikaranîna RAM'a niha nîşan bide.", + "memory-usage-label": "Bikaranîna bîrê", + "network-traffic-description": "Leza leza barkirina torê û dakêşana torê nîşan bide.", + "network-traffic-label": "Trafîka torê", + "storage-as-percentage-label": "Dîsk wekî rêje", + "storage-usage-description": "Agahiyên bikaranîna cîhê dîskê nîşan bide.", + "storage-usage-label": "Bikaranîna hilanînê", + "swap-usage-description": "Bikaranîna bîra swapê nîşan bide.", + "swap-usage-label": "Bikaranîna Swapê", + "use-monospace-font-description": "Bi kar tîne fonta monospace ji bo firehiya karakterê ya domdar.", + "use-monospace-font-label": "Tîpa yek-firehî" + }, + "taskbar": { + "colorize-icons-description": "Rengên temayê li îkonên taskbarê bicîh bike.", + "hide-mode-description": "Dema ku çarçoveyên lihevhatî tune bin, sepanok çawa tev digere kontrol dike", + "hide-mode-label": "Moda veşartinê", + "icon-scale-description": "Pîvana îkonên taskbarê destnîşan dike.", + "icon-scale-label": "Pîvandina îkonan", + "max-width-description": "Herî zêde firehiya taskbarê wekî rêjeya firehiya ekranê.", + "max-width-label": "Ferehiya herî pir", + "only-active-workspaces-description": "Tenê sepanên ji cihên kar ên çalak nîşan bide.", + "only-active-workspaces-label": "Tenê ji cihên kar ên çalak", + "only-same-monitor-description": "Tenê sepanên ji çavdêriya ku bar lê ye nîşan bide.", + "only-same-monitor-label": "Tenê ji heman monitorê", + "show-pinned-apps-description": "Nîşandana sepanên pêçandî yên ji dockê di taskbarê de.", + "show-pinned-apps-label": "Nîşandan sepanên pêçandî", + "show-title-description": "Sernavên pencereyê di taskbarê de nîşan bide.", + "show-title-description-disabled": "Darika erkê ya stûnî nîşandana sernavan piştgirî nake.", + "show-title-label": "Navê pêşandanê", + "smart-width-description": "Bi awayekî otomatîkî firehiya têketinê li gorî hejmara têketinan eyar bike.", + "smart-width-label": "Firehiya berz", + "title-width-description": "Firehiya firehiya sernavên paceyan di taskbarê de (bi pîksel).", + "title-width-label": "Firehiya sernavê", + "title-width-reset-tooltip": "Firehiya firehiya sernavê" + }, + "tray": { + "chevron-color-label": "Rengê chevron", + "colorize-icons-description": "Rengên rûkarê li ser îkonên devera agahiyê bisepîne.", + "colorize-icons-label": "Îkonan rengîn bike", + "drawer-enabled-description": "Dema ku çalak be, hêmanên trayê yên nehatine pin kirin di panelek dolabê de têne xuyang kirin.
Dema ku neçalak be, hemî hêmanên trayê inline têne xuyang kirin.", + "drawer-enabled-label": "Çalak bike dolabê", + "hide-passive-description": "Dema ku hat aktîfkirin, tiştên trayê yên bi statûya 'Pasîf' dê werin veşartin.", + "hide-passive-label": "Veşartina tiştên pasîf" + }, + "volume": { + "display-mode-description": "Tu dixwazî ev nirx çawa xuya bike hilbijêre." + }, + "workspace": { + "character-count-description": "Hejmara tîpên ku ji navên qada xebatê bên nîşandan (1-10).", + "character-count-label": "Hejmara tîpan", + "empty-color-description": "Rengê paşxaneya ji bo Workspaceyên val destnîşan bike.", + "empty-color-label": "Rengê cîhê kar ê vala", + "enable-scrollwheel-description": "Bi tekerê mişkê di navbera cîhên xebatê de biguhere.", + "enable-scrollwheel-label": "Bişkoka gerokê bizivirîne da ku cîhên kar biguherî", + "focused-color-description": "Rengê paşperdeyê ji bo cîhê karê ku hatiye fokus kirin destnîşan bike.", + "focused-color-label": "Rengê cîhê kar ê fokusdayî", + "follow-focused-screen-description": "Dîmên xebatê ji dîmendera niha ya ku balê dikişîne, ne ji dîmendera ku bar lê ye, nîşan bide.", + "follow-focused-screen-label": "Şopandina ekrana fokusê", + "grouped-border-opacity-description": "Ayarê asta zelalbûna sînorên konteynerê cîhê kar.", + "grouped-border-opacity-label": "Rûdana sînor", + "hide-unoccupied-description": "Cihên xebatê yên bê pencere nîşan nede.", + "hide-unoccupied-label": "Veşartî ne dagirkirî", + "label-mode-description": "Çawa nîşaneyên cîhê kar têne nîşandan hilbijêre.", + "label-mode-label": "Moda labelê", + "occupied-color-description": "Rengê paşxaneyê ji bo Workspaceên dagirkirî destnîşan bike.", + "occupied-color-label": "Rengê qada xebatê ya dagirkirî", + "reverse-scrolling-description": "Dema ku hûn digerînin, rêgeha guhertina cîhê kar berevajî bike.", + "reverse-scrolling-label": "Şemitandinê berevajî bike", + "show-applications-description": "Nîşaneyên sepanan di hundirê her cîhê xebatê de nîşan bide.", + "show-applications-label": "Nîşan sepanan bide", + "show-badge-description": "Di moda komkirî de nîşana jimareya cîhê kar nîşan bide.", + "show-badge-label": "Nîşana qada xebatê nîşan bide", + "show-labels-only-when-occupied-description": "Tenê etîketên cîhê xebatê nîşan bide dema ku ew pencereyan dihewînin.", + "show-labels-only-when-occupied-label": "Tenê dema dagirkirî etîketan nîşan bide", + "unfocused-icons-opacity-description": "Ayarê asta zelalbûna îkonên sepanên ne di fokusê de bike.", + "unfocused-icons-opacity-label": "Şefafiya îkonên ne baldar" + } + }, + "battery": { + "battery-health": "Rewşa betariyê", + "battery-level": "Asta bataryayê", + "capacity-level": "Kapasîte: {level}", + "charging-rate": "Rêjeya barkirinê: {rate} W", + "discharging-rate": "Rêjeya valabûna betariyê: {rate} W", + "health": "Tendûrûstî: {percent}%", + "inhibit-idle-description": "Pergalê şiyar dihêle.", + "no-battery-detected": "nehat dîtin", + "plugged-in": "Hat girêdan", + "power-profile": "Profîla hêzê", + "time-left": "Demê mayî: {time}", + "time-until-full": "Dem mayî bo tijîbûnê: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Amûrên heyî", + "blocked": "Astengkirî", + "connected-devices": "Amûrên girêdayî", + "device-address": "Navnîşana amûrê", + "disabled": "Bluetooth neçalak e", + "discoverable": "Dîtbar", + "enable-message": "Bo dîtina amûrên heyî Bluetooth çalak bike.", + "known-devices": "Amûrên naskirî", + "no-devices": "Amûr tune ne", + "paired-devices": "Amûrên hevgirtî", + "pairing-mode": "Piştrast bike ku cîhaza te di moda cotbûnê de ye.", + "scanning": "Li amûran digere...", + "signal-text-excellent": "Nîşan: Baş e gelek", + "signal-text-fair": "Nîşan: Bihêz e", + "signal-text-good": "Nîşan: Baş e", + "signal-text-poor": "Nîşan: Lawaz e", + "signal-text-unknown": "Nîşan: Nenas", + "signal-text-very-poor": "Nîşan: Gelekî lawaz e" + } + }, + "changelog": { + "error": { + "fetch-failed": "Nikare daneyên têketinê guhertinê bar bike. Paşê dîsa biceribîne.", + "rate-limit": "Rêjeya GitHub derbas bû. Di nav çend xulekanan de dîsa biceribîne." + }, + "panel": { + "buttons-discord": "Têkeve Discorda me", + "buttons-dismiss": "Baş e", + "buttons-feedback": "Nerînan bide", + "empty": "Nîşeyên weşanê hîn ne pêkan in.", + "highlight-title": "Berbiroj", + "released": "Di {date} de hat weşandin", + "subtitle-fresh": "Spas ji bo sazkirina Noctalia! Ev tiştên ku di vê çêkirinê de hene.", + "subtitle-updated": "Nûkirin ji {previousVersion}", + "title": "Çi nû di {versiyon} de heye", + "version": "Goherto {version}", + "version-new-user": "Sazkirina nû" + } + }, + "common": { + "actions": "Çalakî", + "add": "Tevlî bike", + "appearance": "Xuyang", + "apply": "Bisepîne", + "automation": "Xweberî", + "available": "Heyî", + "back": "Paş", + "battery": "Betarî", + "bluetooth": "Bluetooth", + "brightness": "Ronahî", + "browse": "Bigere", + "calendar": "Salname", + "calendar-panel": "Destgeha salnameyê", + "cancel": "Têk bibe", + "cards": "Kert", + "charging": "Barkirin", + "clear": "Pak bike", + "clipboard": "Klîpbir", + "close": "Bigire", + "color-muted": "Bêdengkirî", + "colors": "Rengan", + "command": "Ferman", + "connect": "Girêdan", + "connected": "Girêdayî", + "connecting": "Tê girêdan...", + "continue": "Berdewam", + "contributors": "Beşdar", + "copied-to-clipboard": "Hate bîra miqeyd kirin", + "countdown": "Jimartin", + "date": "Dîrok", + "default": "Xwerû", + "delete": "Jê bibe", + "devices": "Amûr", + "disabled": "Nabe kar", + "discharging": "Deravkirin", + "disconnect": "Qutbûn", + "disconnected": "Berdîtin qut bûye", + "disconnecting": "Derdibirîne...", + "display-mode": "Awaya nîşandanê", + "download": "Daxistin", + "duration": "Dirêjahî", + "edit": "Biguherîne", + "enabled": "Çalak", + "error": "Şaşetî", + "events": "Bûyer", + "execute": "Bicîh bîne", + "faithful": "Dilsoz", + "focus": "Balê bide", + "frequency": "Frekans", + "gateway": "Dergeh", + "general": "Giştî", + "height": "Bilindî", + "hibernate": "Bicemdîne", + "history": "Dîrok", + "icon": "Îkon", + "idle": "Betal", + "info": "Zanyarî", + "input": "Ketan", + "install": "Sazkirin", + "installed": "Sazkirî", + "interface": "Navrû", + "internet": "Înternet", + "language": "Ziman", + "loading": "Tê barkirin...", + "local": "Herêmî", + "location": "Cih", + "lock": "Kilît bike", + "logout": "Derkeve", + "look": "Xuyang", + "margins": "Qirax", + "media": "Medya", + "media-player": "Lîstikvanê medyayê", + "memory": "Bîr", + "monitors": "Dîmender", + "network": "Tor", + "next": "Pêş", + "night-light": "Ronahiya şevê", + "no": "Na", + "no-results": "Ti encam tune", + "no-summary": "Kurte tune ye", + "none": "Tune", + "not-found": "Nehat dîtin", + "notifications": "Agahdarî", + "official": "Fermî", + "on-surface": "Li ser rûyê erdê", + "output": "Deran", + "pair": "Hevpar", + "paired": "Hevpar bûyî", + "pairing": "Hevberkirin...", + "panels": "Panelan", + "password": "Şîfre", + "pause": "Rawestîne", + "pin": "Bikêşîne", + "play": "Bilîzîne", + "polling": "Lêpirsîn", + "position": "Cih", + "previous": "Berê", + "primary": "Serekî", + "random": "Bêserûber", + "reboot": "Ji nû dest pê bike", + "record": "tomarkirinê dest pê bike", + "refresh": "Nûkirin", + "required": "(pêwîst)", + "reset": "Vegerandin", + "reset-to-default": "Ji bo rewşa berdest ji nû ve saz bike", + "result-count": "{count} encam", + "result-count-plural": "{count} encaman", + "resume": "Berdewam kirin", + "save": "Tomar bike", + "scanning": "Digere...", + "screen-corners": "Kişekanên dîmenderê", + "search": "Bigere", + "secondary": "Duyemîn", + "security": "Ewlehî", + "select": "Hilbijêre", + "select-color": "Rengê hilbijêre", + "select-icon-color": "Rengê îkonê hilbijêre", + "shortcuts": "Kurterê", + "shutdown": "Vemrîne", + "signal": "Sînyal", + "sound": "Deng", + "sources": "Çavkanî", + "start": "Destpêkirin", + "stop": "Raweste", + "supporters": "Piştgir", + "suspend": "Rawestîne", + "templates": "Şablonan", + "tertiary": "Sêyemîn", + "test": "Test", + "thresholds": "Asteyan", + "title": "Sernav", + "toast": "Toast", + "trusted": "Bawerîpêkirî", + "uninstall": "Rakirin", + "unknown": "Nenas", + "unpair": "Hevparekê berdan", + "unpin": "Bi dar nexe", + "update": "Nûkirin", + "upload": "Barkirin", + "version": "Guherto", + "vibrant": "Geş", + "visualizer": "Dîmender", + "volume": "Hêjmar", + "volumes": "Deng", + "wallpaper": "Wêneyê dîwêr", + "weather": "Hewa", + "weather-loading": "Rewşa bayê tê barkirin...", + "week": "Hefte", + "widgets": "Sepanok", + "width": "Firehî", + "windows": "Çarçove", + "yes": "Erê" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Power profile", + "tooltip-disabled": "Daemonê power-profiles-daemon saz bike da ku profîlên hêzê bikar bîne" + }, + "wifi": { + "label-disconnected": "Wi-Fi qut bû", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Her tim veşêre", + "always-show": "Her tim nîşan bide", + "force-open": "Bişkîne veke", + "on-hover": "Li ser mişkê" + }, + "hide-modes": { + "auto-hide": "Xweber veşêre", + "hidden": "Veşêre dema vala be", + "idle": "Dema bêkar veşêre", + "transparent": "Dema vala zelal e", + "visible": "Her tim xuya dike" + }, + "launcher": { + "categories": { + "all": "Hemû", + "audiovideo": "Deng & vîdyo", + "chat": "Axaftin", + "development": "Pêşveçûn", + "education": "Perwerde", + "emoji-activity": "Çalakî", + "emoji-animals": "Ajal", + "emoji-flags": "Ala", + "emoji-food": "Xwarin & vexwarin", + "emoji-nature": "Xweza", + "emoji-objects": "Tişt", + "emoji-people": "Mirov & beden", + "emoji-recent": "Dawî", + "emoji-symbols": "Sembolan", + "emoji-travel": "Gerr û cih", + "game": "Lîstik", + "graphics": "Grafîk", + "misc": "Ên din", + "office": "Ofîs", + "pinned": "Bidarxistî", + "system": "Pergal", + "webbrowser": "Geroka tevnê" + }, + "providers": { + "applications": "Sepan", + "calculator": "Jimarkar", + "calculator-deprecated": "Fermana >calc hatiye betalkirin û dê di demeke nêz de were rakirin. Ji kerema xwe rasterast di lêgerînê de îfadeyên matematîkê binivîse.", + "calculator-description": "Hesabker — nirxandina îfadeyên matematîkî", + "calculator-enter-expression": "Têketineke matematîkî binivîse", + "calculator-press-enter-to-copy": "Enter bitikîne kopî bike", + "clipboard": "Dîroka clipboardê", + "clipboard-clear-description": "Hemû dîroka clipboardê paqij bike", + "clipboard-clear-description-full": "Hemû tiştan ji dîroka clipboardê rake", + "clipboard-clear-history": "Dîroka clipboardê paqij bike", + "clipboard-delete": "Jêbirina navnîşa clipboardê", + "clipboard-history-disabled": "Dîroka clipboardê neçalak e", + "clipboard-history-disabled-description": "Dîroka clipboardê di mîhengan de çalak bike an cliphist saz bike", + "clipboard-loading": "Dîroka clipboardê tê barkirin...", + "clipboard-search-description": "Dîroka jêgirtindankê bigere", + "command-description": "Fermanên shell bixebitîne", + "emoji": "Hilbijêrê îmojî", + "emoji-loading": "Îmojî tên barkriin...", + "emoji-loading-description": "Tika raweste", + "emoji-no-recent": "Hîna emojiyên dawî tune ne", + "emoji-search-description": "Emojiyan bigere û jê bigire", + "settings-search-description": "Li sazkariyan bigere", + "windows-search-description": "Pencereyên vekirî bigere û bîne pêş" + } + }, + "lock-screen": { + "authenticating": "Rastkirin...", + "password": "Borînpeyva xwe têxîne..", + "restart": "Ji nû ve dest pê bike", + "shut-down": "Vemrîne" + }, + "notifications": { + "panel": { + "click-to-expand": "Bo dirêjkirinê bitikîne", + "description": "Agahdariyên te dema ku werin ew ê li vir xuya bibin.", + "no-notifications": "Agahdarî tune ne" + }, + "range": { + "earlier": "Berê", + "today": "Îro", + "yesterday": "Duh" + }, + "time": { + "diff-d": "1 roj berê", + "diff-dd": "{diff} roj berê", + "diff-h": "1 demjimêr berê", + "diff-hh": "{diff} demjimêr berê", + "diff-m": "1 xulek berê", + "diff-mm": "{diff} xulek berê", + "now": "niha" + } + }, + "options": { + "bar": { + "density-comfortable": "Rêhet", + "density-compact": "Kompakt", + "density-default": "Berdest", + "density-mini": "Piçûk", + "density-spacious": "Berfireh", + "display-mode-always-visible": "Her dem dîtbar", + "type-floating": "Herikbar", + "type-framed": "Çarçovekirî", + "type-simple": "Sade" + }, + "control-center": { + "quick-settings-style-classic": "Klasîk", + "quick-settings-style-modern": "Nûjen" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "default": "Berdest" + }, + "launcher-view-mode": { + "grid": "Tor", + "list": "Lîste" + }, + "scrolling-modes": { + "always": "Her tim bigere", + "hover": "Li ser gerandinê bigere", + "never": "Qet negere" + }, + "session-menu-grid-layout": { + "grid": "Tor", + "single-row": "Yek rêz" + }, + "settings-panel-mode": { + "attached": "Panel bi bar ve girêdayî ye", + "centered": "Panela navendîkirî", + "window": "Pencereya cuda" + }, + "visualizer-types": { + "linear": "Xêzikî", + "mirrored": "Neynikîkirî", + "wave": "Pêl" + }, + "visualizer-visibility": { + "with-background": "Tenê bi paşxaneyê" + }, + "workspace-labels": { + "index": "Naverok", + "index-and-name": "Naverok û nav", + "name": "Nav" + } + }, + "panels": { + "about": { + "changelog": "Dîroka guhertinan bibîne", + "contributors-description": "Silav ji {count} beşkdarên me yên gewre re!", + "contributors-description-plural": "Spas ji {count} beşdarên me yên ecêb!", + "copy-info": "Zanyariyan jê bigire", + "info-copied": "Agahî li clipboardê hat kopîkirin", + "noctalia-desc": "Şêlekek sermaseyê ya spehî û mînîmal ku bi baldarî ji bo Waylandê hatiye çêkirin, bi Quickshellê hatiye avakirin.", + "noctalia-git-commit": "Pêşkêşkirina Git:", + "noctalia-installed-version": "Guhertoya sazkirî:", + "noctalia-latest-version": "Guhertoya herî dawî:", + "noctalia-title": "Noctalia qalik", + "privacy-policy": "Siyaseta nepenîtiyê", + "support": "Piştgiriya me bike", + "supporter-badge": "Piştgir", + "system-cpu": "YKN:", + "system-disk": "Dîsk:", + "system-gpu": "YKG:", + "system-host": "Host:", + "system-install-hint": "Fastfetch saz bike da ku agahiyên pergalê bibîne", + "system-kernel": "Kernel:", + "system-loading": "Agahiyên pergalê bar dikin...", + "system-memory": "Bîr:", + "system-monitor": "Dîmender:", + "system-not-installed": "fastfetch nehatiye sazkirin", + "system-os": "PX:", + "system-packages": "Pakêt:", + "system-product": "Berhem:", + "system-title": "Zanyariyên pergalê", + "system-uptime": "Dema xebitandinê:", + "system-wm": "RÇ:", + "telemetry-data-copied": "Daneyên Telemetry hatin jê girtin bo jêgirtindankê", + "telemetry-desc": "Bi parvekirina agahdariya sîstema anonîm (çareseriya ekranê, berhevkar, belavkirin) alîkariya pêşxistina Noctalia bikin. Carekê di destpêkê de tê şandin, şopandin tune, dane piştî 30 rojan bixweber têne jêbirin.", + "telemetry-enabled": "Agahiyên pergalê yên nenas bişîne", + "telemetry-show-data": "Dîtina daneyan", + "telemetry-title": "Taybetî", + "title": "Derbar", + "up-to-date": "Tu rojane ye!", + "update-available": "Rojanekirin heye", + "view-commit": "Li ser GitHubê guhertinê bibîne" + }, + "audio": { + "devices-desc": "Amûrên têketin û derketina deng ên berdest mîheng bike.", + "devices-input-device-description": "Amûra têketina deng a xwestî hilbijêre.", + "devices-input-device-label": "Amûra ketanê", + "devices-output-device-description": "Amûra derketina deng a xwestî hilbijêre.", + "devices-output-device-label": "Amûra deranê", + "devices-title": "Amûrên deng", + "external-mixer-description": "Navnîşana ferman an rêya sepanê binivîse ku dema çalakkirina taybetmendiya mîksera dengî ya derveyî were destpêkirin.", + "external-mixer-label": "Fermana mîksera dengî ya derveyî", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Serlêdanên xwe yên medyayê yên bijartî û yên ku te paşguh kirine destnîşan bike.", + "media-excluded-player-description": "Peyvên bingehîn ên lîstikvanên ku hûn dixwazin sîstem wan paşguh bike zêde bikin. Divê her peyvek bingehîn li ser rêzeke nû be.", + "media-excluded-player-label": "Lîstikvanê derxistî", + "media-excluded-player-placeholder": "binavê substring binivîse û + bitikîne", + "media-frame-rate-description": "Rêjeyên bilindtir nermtir in lê bêtir çavkaniyan bikar tînin.", + "media-frame-rate-label": "Rêjeya çarçoveyê", + "media-primary-player-description": "Têgehek binivîse da ku lîstikvanê xwe yê sereke nas bike.", + "media-primary-player-label": "Lîstikvanê sereke", + "media-primary-player-placeholder": "mînakî spotify, vlc, mpv", + "media-scrolling-speed-description": "Demjimêra bi saniyeyan ku sernav ji destpêkê heta dawiyê bigere.", + "media-scrolling-speed-label": "Leza gerandinê", + "media-scrolling-title-description": "Çalak bike geroka domdar ji bo sernavên medyayê yên dirêj.", + "media-scrolling-title-label": "Navnîşana ku digere", + "media-title": "Lîstikvanên medyayê", + "on-middle-clicked-description": "Fermana ku dema bişkok bi mişkê navîn tê tikandin, bê rêvebirin.", + "panel-applications-empty": "Ni sepanên ku niha deng lêdixin tune ne", + "title": "Deng", + "visualizer-type-description": "Cureyeke cureyek dîtbarîkirinê ji bo lêdana medyayê.", + "visualizer-type-label": "Curey vîzyonê", + "volumes-desc": "Kontrolên deng û astên bihîstwerê sererast bikin.", + "volumes-input-volume-description": "Asta asta dengê mîkrofonê.", + "volumes-mute-input-description": "Dengê têketina dengî ya xwerû (mîkrofon) bêdeng bike.", + "volumes-mute-input-label": "Dengê têketinê bêdeng bike", + "volumes-mute-output-description": "Dengê sereke yê pergalê bêdeng bike.", + "volumes-mute-output-label": "Dengê derketinê bêdeng bike", + "volumes-output-volume-description": "Asta rêjeya dengê ya tevahiya pergalê.", + "volumes-step-size-description": "Guhertina pîvana gavê ji bo guhertinên deng (tekerê mişkê, kurteriyên klavyeyê).", + "volumes-step-size-label": "Mezinahiya gava dengdanê", + "volumes-volume-feedback-description": "Dema ku deng tê sererastkirin dengekî bersivdanê lêxe.", + "volumes-volume-feedback-label": "Dengê berteka qebareyê lêxe", + "volumes-volume-overdrive-description": "Destûr dide bilindkirina deng ji sedî 100î zêdetir. Dibe ku ji hêla hemû hardware ve neyê piştgirîkirin.", + "volumes-volume-overdrive-label": "Destûra zêdekirina deng" + }, + "bar": { + "appearance-auto-hide-delay-description": "Demê ku berî darik were veşartin piştî mişîk diçe", + "appearance-auto-hide-delay-label": "Derengbûnê veşêre", + "appearance-auto-hide-exclusive-note": "Nîşe: Dema ku Veşartina xweber çalak be qada taybet tê neçalakkirin", + "appearance-auto-show-delay-description": "Demê berî ku darik tê nîşandan dema mîşk dikeve qiraxê", + "appearance-auto-show-delay-label": "Derengbûnê nîşan bide", + "appearance-background-opacity-description": "Şeffafiya paşxanê bi taybetî ji bo barî destnîşan bike.", + "appearance-background-opacity-label": "Zelaliya paşrûyê darikê", + "appearance-capsule-opacity-description": "Dema kapsul xuya dibe, asta zelalbûna paşrûyên sepanokê saz dike.", + "appearance-capsule-opacity-label": "Zelaliya kapsulê", + "appearance-density-description": "Pêdivî ye ku pêlava barê were sererast kirin ji bo xuyangek berbiçav an fireh.", + "appearance-density-label": "Çalyî bar", + "appearance-desc": "Xuyakirina xuyabûn û pozîsyona bar.", + "appearance-display-mode-description": "Hilbijêre ka kengî bila darik were xuyakirin.", + "appearance-floating-description": "Bariş wekî 'heb'eke herikbar nîşan bide.", + "appearance-floating-label": "Darika herîkbar", + "appearance-frame-radius": "Nîvçapa Hundirîn", + "appearance-frame-settings-description": "Stûrahiya çarçoveyê û nîvçapa goşeyê hundirîn sererast bike", + "appearance-frame-settings-label": "Sazkariyên Çarçoveyê", + "appearance-frame-thickness": "Qalindî", + "appearance-hide-on-overview-description": "Dema ku pêşdîtina berhevker çalak be, bar veşêre û panelan bigire.", + "appearance-hide-on-overview-label": "Li ser dîtinê bar veşêre", + "appearance-margins-description": "Marginên dora bara herikbar eyar bike.", + "appearance-margins-horizontal": "Asoyî", + "appearance-margins-vertical": "Stûnî", + "appearance-outer-corners-description": "Goşeyên derveyî yên çemandî li ser barî nîşan bide.", + "appearance-outer-corners-label": "Goşeyên derve", + "appearance-position-description": "Cihê ku bar li ser ekranê deyne hilbijêre.", + "appearance-position-label": "Cihê darikê", + "appearance-show-capsule-description": "paşrûyên sepanokê nîşan bide.", + "appearance-show-capsule-label": "Pêşkêşkirina kapsulê", + "appearance-show-outline-description": "Dîwarê xuya li dora her widgetê nîşan dide.", + "appearance-show-outline-label": "Xêzên sepanokê nîşan bide", + "appearance-type-description": "Stîla darikê hilbijêre: Sade, Herikbar an Çarçovekirî.", + "appearance-type-label": "Cûreya darikê", + "appearance-use-separate-opacity-description": "Destûrê bide ku ji bo paşxaneya bar nirxek opakbûna cuda were bikaranîn.", + "appearance-use-separate-opacity-label": "Şefafiya bara cuda bikar bîne", + "monitor-configure-widgets": "Sepanokan saz bike", + "monitor-override-settings": "Mîhengên gerdûnî betal bike", + "monitor-override-settings-description": "Ji bo vî monitorê mîhengên xwerû bikar bîne.", + "monitor-reset-all": "Hemûyan vegerîne", + "monitor-widgets-title": "Rêkxistina sepanokê- {monitor}", + "monitors-desc": "Nîşan bide bar li ser monitorên taybet. Eger tu kes neyê hilbijartin, ew ê bi awayekî xwerû hemûyan nîşan bide.", + "monitors-desc-new": "Mîheng bike ka kîjan çavdêr Barê nîşan didin û mîhengên ji bo her çavdêrekê xwerû bike.", + "monitors-title": "Dîmender", + "title": "Darik", + "tray-blacklist-description": "Zêdekirina rêgezên derxistina trayê, piştgiriya wildcardan (*) dike.", + "tray-blacklist-label": "Reşkirin", + "tray-blacklist-placeholder": "mînakî nm-applet, Fcitx*", + "tray-pin-application": "Sepanê bi dar bixe", + "tray-unpin-application": "Sepanê bi dar nexe", + "use-global-widgets": "Sepanokên gerdûnî bi kar bîne", + "widgets-desc": "sepanokan kaş bike da ku rêza wan biguherînî. Ji bo livandinê sepanokan di navbera beşan de yan rakirina wan, menuya rast-tikandin bi kar bîne.", + "widgets-title": "Bicihkirina sepanokan" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Ji dîwarê xwe şemayên rengan çêbike. Rengan bixweber derdixe da ku temayek hevgirtî biafirîne.", + "color-source-use-wallpaper-colors-label": "Rengên dîwêr bikar bîne", + "dark-mode-mode-description": "Veguhestina otomatîkî di navbera moda ronahî û tarî de çalak dike.", + "dark-mode-mode-label": "Bernameya moda tarî", + "dark-mode-mode-manual": "Bi destan", + "dark-mode-mode-off": "Vemirî", + "dark-mode-switch-description": "Veguherîne bo temayek tarîtir ji bo dîtina hêsantir bi şev.", + "delete-error-description": "Xwe ji birina {scheme} bi ser neket", + "delete-error-title": "Serbirin nemû ye", + "delete-success-description": "Bi serkeftî {scheme} hat jêbirin", + "delete-success-title": "Şêweya rengan hate jêbirin", + "download-button": "Bêtir daxîne", + "download-downloading": "Tê daxistin...", + "download-empty": "Rengên rengan tune ne", + "download-error-api-error": "Çewtiya API: {status}", + "download-error-description": "Daxistina {scheme} bi ser neket", + "download-error-download-failed": "Daxistin bi koda derketinê ya: {code} têk çû", + "download-error-invalid-response": "Formata bersiva API ne derbasdar e", + "download-error-no-files": "Tu dosye ji bo şemayê nehatin dîtin", + "download-error-parse-failed": "Şirovekirina bersiva API-yê bi ser neket: {error}", + "download-error-rate-limit": "Rêjeya daxwazên API ya GitHubê derbas bû", + "download-error-title": "Daxistin bi ser neket", + "download-fetching": "Digerandina şemayên rengan ên berdest...", + "download-success-description": "{scheme} bi serkeftî hate daxistin", + "download-success-title": "Şêweya rengê hate daxistin", + "download-title": "Dakêşkirina rengên rengîn", + "method-description": { + "content": "Şemaya Sêwirana Materyalê bi derxistina rengê ya bi kalîte ku bi rengên rastî yên naveroka çavkaniyê re li hev tê.", + "faithful": "Dema hewl dide ku nêzî rengê bingehîn bimîne, di heman demê de paletek hevaheng çêbike.", + "fruit-salad": "Şemaya Materyal Dîzaynê ku paletek dilîzîk, geş bi rengên cihêreng û pirrengî çêdike.", + "monochrome": "Şemaya Materyal Dîzaynê ya ku bi rengê yekane ya gewr û naveroka kromatîk a hindik bikar tîne.", + "muted": "Ji bo xuyangek aram û neberçav, rengên nermtir û bêrengtir çêdike.", + "rainbow": "Şemaya Materyal Dîzaynê ku ji bo pirrengiya herî zêde ya rengan, rengan li seranserê spektrumê çêdike.", + "tonal-spot": "Algorîtmaya default a Material Design. Bi bikaranîna guhertoyên tonal ên rengê çavkaniyê, paletên hevseng diafirîne.", + "vibrant": "Ji bo paletên berbiçav û balkêş ên bi rengên geş, têrbûna rengan zêde dike." + }, + "predefined-desc": "Ji berhevokek şemayên rengan ên pêşdiyarkirî hilbijêrin.", + "predefined-generate-templates-label": "Şablonan ji bo şemayên pêşdiyarkirî çêbike", + "predefined-title": "Şemayên rengan ên pêşdiyarkirî", + "templates-desc": "Rengan li ser sepanên derveyî bisepîne.", + "templates-filter-description": "Şablonên ji kategoriyek taybet nîşan bide.", + "templates-filter-label": "Li gorî kategoriyê parzûn bike", + "templates-misc-description": "Şablonên xwe biafirîne.", + "templates-misc-label": "Pêşketî", + "templates-misc-user-templates-description": "Tenê eger tu dizanî çi dikî çalak bike — serî li belgekirina me ya serhêl bide.", + "templates-misc-user-templates-label": "Şablonên bikarhêner çalak bike", + "templates-none-detected": "Tiştek nehat dîtin", + "templates-write-path": "Dinivîse: {filepath}", + "title": "Şêweya rengan", + "wallpaper-method-description": "Rêbaza xweya bijarte ya hilberîna paletê hilbijêre.", + "wallpaper-method-label": "Rêbaza çêkirina paletê", + "wallpaper-monitor-source-description": "Ji bo derxistina rengên dîwêrê, kîjan monitorê bikar bîne hilbijêre.", + "wallpaper-monitor-source-label": "Çavkaniya hilberîna rengan" + }, + "control-center": { + "cards-desc": "Xwerû bike ka kîjan kontrol di navenda kontrolê de û bi çi rêzê de xuya dibin.", + "desc": "Vesazkirina cih û reftarên panelê navenda kontrolê.", + "position-description": "Dema ku destgeha navenda kontrolê vebe, hilbijêre ka li ku derê bila xuya bibe.", + "shortcuts-custom-button-command-description": "Fermana ku dema ku bişkok tê tikandin were meşandin.", + "shortcuts-custom-button-enable-on-state-logic-description": "Çalak bike îkonek duyem û rewşa 'germ' li ser bingeha fermana kontrolê.", + "shortcuts-custom-button-enable-on-state-logic-label": "Mantiqa rewşa-li-ser çalak bike", + "shortcuts-custom-button-on-clicked-label": "Fermana klîka çepê", + "shortcuts-custom-button-on-state-command-description": "Fermana ku bê meşandin ji bo kontrolkirina ka divê bişkok di rewşa 'vekirî' de be. Ji bo vekirî 0, ji bo girtî ne-sifir vedigerîne.", + "shortcuts-custom-button-on-state-command-label": "Fermana kontrola rewşa li ser", + "shortcuts-custom-button-on-state-icon-description": "Îkona ji bo bişkokê dema ku di rewşa 'vekirî' de ye.", + "shortcuts-custom-button-on-state-icon-label": "Îkona rewşa çalak", + "shortcuts-custom-button-state-checks-add": "Rewşa dewletê zêde bike", + "shortcuts-custom-button-state-checks-command": "Fermana ji bo vê kontrola rewşê were meşandin", + "shortcuts-custom-button-state-checks-label": "Kontrolên dewletê", + "shortcuts-custom-button-state-checks-remove": "Rake", + "shortcuts-custom-button-tooltip-description": "Serişteya ku dema mişk li ser bişkokê be tê nîşandan.", + "shortcuts-custom-button-tooltip-label": "Serpêhatî", + "shortcuts-title": "Kurteriyên sepanokan", + "system-monitor-disk-path-description": "Hilbijêre kîjan xala çiyayê dîskê karta çavdêriya pergalê ya di navenda kontrolê de divê çavdêriyê bike.", + "system-monitor-disk-path-label": "Rêya dîska çavdêriya pergalê", + "title": "Navenda kontrolê" + }, + "desktop-widgets": { + "clock-enabled-description": "Sepanokeke demjimêrê li ser sermaseyê nîşan bide.", + "clock-enabled-label": "Sepanoka demjimêrê çalak bike", + "clock-format-label": "Format", + "clock-height-description": "Bilindahiya sepanokê demjimêrê bi pixelan.", + "clock-height-label": "Bilindî", + "clock-minimal-mode-description": "Dîmendereke demjimêrê ya herî kêm bi dem û dîrokê bi kar bîne.", + "clock-minimal-mode-label": "Moda herî kêm", + "clock-rounded-corners-description": "Quncikên gilover ji bo paşrûyê sepanokê bi kar bîne.", + "clock-rounded-corners-label": "Quncikên gilover", + "clock-show-background-description": "Paşrûya konteynera sepanoka demjimêrê nîşan bide.", + "clock-show-background-label": "Paşrûyê nîşan bide", + "clock-show-date-description": "Dîroka îro li jêr demjimêrê nîşan bide.", + "clock-show-date-label": "Dîrokê nîşan bide", + "clock-show-seconds-description": "Çirkeyan di demê de nîşan bide.", + "clock-show-seconds-label": "Çirkeyan nîşan bide", + "clock-style-analog": "Analog", + "clock-style-binary": "Dûalî", + "clock-style-description": "Şêwaza nîşandana demjimêrê hilbijêre.", + "clock-style-digital": "Jimarî", + "clock-style-label": "Şêwaza demjimêrê", + "clock-style-minimal": "Kêmtirîn", + "clock-width-description": "Ferehiya sepanoka demjimêrê bi pîkselan.", + "edit-mode-button-label": "Têkeve awaya guhertinê", + "edit-mode-controls-explanation": "Çep-bitikîne & kaş bike: Sepanokê bilivîne yan mezinahiya wê biguherîne. Rast-bitikîne: Vebijêrkên menuya naverokê veke.", + "edit-mode-description": "Awaya guhertinê çalak bike da ku sepanokên sermaseyê bilivînî û ji nû ve bicih bikî . Dema ku çalak be, sepanok xêzek kaşkirinê nîşan dide û dikare ji nû ve were bicihkirin.", + "edit-mode-exit-button": "Ji awaya guhertinê derkeve", + "edit-mode-grid-snap-label": "Girêdana torê", + "edit-mode-label": "Awaya guhertinê", + "enabled-description": "Sepanokên sermaseyê bi tevahî çalak an neçalak bike.", + "enabled-label": "Sepanokên sermaseyê çalak bike", + "general-desc": "Sepanokên ku li ser sermaseya te xuya dibin birêkxistin bike.", + "general-title": "Sepanokên sermaseyê", + "media-player-enabled-description": "Sepanokê lîstikvanê medyayê li ser sermaseyê nîşan bide.", + "media-player-enabled-label": "Sepanokê lîstikvanê medyayê çalak bike", + "media-player-rounded-corners-description": "Quncikê giolver li ser qiraxên sepanokê çalak bike.", + "media-player-show-album-art-description": "Wêneyê bergê elbumê û zanyariyên stranê (nav û hunermend) nîşan bide.", + "media-player-show-album-art-label": "Wêneyê bergê û sernavê nîşan bide", + "media-player-show-background-description": "Konteynerê paşrûyê nîşan bide.", + "media-player-show-buttons-description": "Bişkokên kontrola medyayê (lîstin/rawestandin, pêş, paş) nîşan bide.", + "media-player-show-buttons-label": "Bişkokan nîşan bide", + "media-player-show-visualizer-description": "overlayê dîtbarîkarê dengî nîşan bide.", + "media-player-visualizer-type-description": "Cûreyeke dîtbariyê hilbijêre.", + "system-stat-layout-bottom": "Bişkok", + "system-stat-layout-description": "Choose how the legend is displayed relative to the graph.", + "system-stat-layout-label": "Şêwaz", + "system-stat-layout-side": "Kêlek", + "system-stat-rounded-corners-description": "Quncikên girover ji bo paşrûya sepanokê bi kar bîne.", + "system-stat-rounded-corners-label": "Quncikên gilover", + "system-stat-show-background-description": "Ji bo sepanokê amara pergalê konteynerê paşrûyê nîşan bide.", + "system-stat-show-background-label": "Paşrûyê nîşan bide", + "system-stat-stat-type-description": "Hilbijêre kîjan amara pergalê bila were nîşandan.", + "system-stat-stat-type-label": "Cûreya amarî", + "title": "Sepanokên sermaseyê", + "weather-enabled-description": "Sepanokê rewşa bayê li ser sermaseyê nîşan bide.", + "weather-enabled-label": "Sepanokê rewşa bayê çalak bike", + "weather-show-background-description": "Konteynera paşrûyê ya sepanokê rewşa bayê nîşan bide." + }, + "display": { + "monitors-brightness-step-description": "Pîvana gava guhertinên ronahiyê (tekerê mişkê û kurteriyên klavyeyê) eyar bike.", + "monitors-brightness-step-label": "Mezinahiya gava ronahiyê", + "monitors-brightness-unavailable-ddc-disabled": "Kontrola ronahiyê peyda nabe. Ji bo kontrolkirina ronahiya vê dîmenderê, \"Piştgiriya ronahiya derveyî\" çalak bike.", + "monitors-brightness-unavailable-generic": "Kontrola ronahiyê ji bo vê dîmêrê berdest nîne.", + "monitors-desc": "Guherandinên ronahiya mîhengên ji bo her dîmenderê.", + "monitors-enforce-minimum-description": "Çareseriya pirsgirêka ku ronahiya paş di hin dîmendêran de di ronahiya %0 de bi temamî dimire.", + "monitors-enforce-minimum-label": "Bi zorê ronahiyê (1%) bide sepandin", + "monitors-external-brightness-description": "Piştgiriya DDCUtil ji bo kontrolkirina ronîbûnê li ser dîmenderên derveyî bi riya protokola DDC/CI çalak bike.", + "monitors-external-brightness-label": "Piştgiriya ronahiya derve", + "monitors-title": "Sazkariyê bo her dîmender", + "night-light-auto-schedule-description": "Li gorî dema rojavabûn û rohilatbûna rojê li {cih} — pêşniyarkirî.", + "night-light-auto-schedule-label": "Bernamekirina otomatîk", + "night-light-desc": "Kêmkirina belavbûna ronahiya şîn ji bo ku hûn çêtir razên û zexta çavan kêm bikin.", + "night-light-enable-description": "Fîltreyeke rengê germ bikar bîne da ku belavbûna ronahiya şîn kêm bike.", + "night-light-enable-label": "Ronahiya şevê çalak bike", + "night-light-force-activation-description": "Bernameyê paşguh dike û fîltra şevê yekser tetbîq dike.", + "night-light-force-activation-label": "Çalakkirina bide zorkirin", + "night-light-manual-schedule-description": "Demjimêrên kesane ji bo rojhilatbûn û rojavabûnê Saz bike.", + "night-light-manual-schedule-label": "Bernamekirina destî", + "night-light-manual-schedule-select-start": "Dema destpêkirinê hilbijêre", + "night-light-manual-schedule-select-stop": "Dema dawîbûnê hilbijêre", + "night-light-manual-schedule-sunrise": "Dema rojhilatbûnê", + "night-light-manual-schedule-sunset": "Dema rojavabûnê", + "night-light-temperature-day": "Roj", + "night-light-temperature-day-description": "Kontrola germa di dema rojê de dike.", + "night-light-temperature-description": "Germahiya rengê ji bo şev û rojê destnîşan bike.", + "night-light-temperature-label": "Germa rengan", + "night-light-temperature-night": "Şev", + "night-light-temperature-night-description": "Germahiya di dema şevê de kontrol dike.", + "title": "Dîmender" + }, + "dock": { + "appearance-background-opacity-description": "Zelalbûna paşrûyê benderê biguherîne.", + "appearance-border-radius-description": "Radyusa sînorê benderê biguherîne.", + "appearance-border-radius-label": "Radyusa sînorê", + "appearance-colorize-icons-description": "Rengên rûkarê li ser îkonên sepanê bisepîne (tenê sepanên bal-nekişandî).", + "appearance-colorize-icons-label": "Îkonan bi reng bike", + "appearance-dead-opacity-description": "Zelalbûna îkonên sepanên ku naxebitin biguherîne.", + "appearance-dead-opacity-label": "Zelalbûna mirî", + "appearance-desc": "Xweşikahî û xuyabûna dockê xweş bike.", + "appearance-display-auto-hide": "Xweber veşêre", + "appearance-display-description": "Hilbijêre ka bender çawa tev digere.", + "appearance-display-exclusive": "Taybet", + "appearance-floating-distance-description": "Dûrahiya di navbera benderê û qiraxa dîmenderê de saz bike.", + "appearance-floating-distance-label": "Dûrahiya herikînê ya benderê", + "appearance-hide-show-speed-description": "Lezbûna anîmasyona veşartin/nîşandan a benderê biguherîne.", + "appearance-hide-show-speed-label": "Lezê nîşan bide/veşêre", + "appearance-icon-size-description": "Mezinahiya giştî ya dockê sererast bike.", + "appearance-icon-size-label": "Mezinahiya benderê", + "appearance-inactive-indicators-description": "Pîlûlên nîşanderê ji bo hemû sepanan, ne tenê ya niha çalak, nîşan bide.", + "appearance-inactive-indicators-label": "Nîşaneyên xebatê", + "appearance-pinned-static-description": "Her gav îkonên sepanên pêvekirî bi rêza statîk ber bi çepê ve bikişîne.", + "appearance-pinned-static-label": "Serîlêdanên sabîtkirî yên statîk", + "appearance-position-description": "Cihê ku bender li ser dîmenderêê xuya dibe hilbijêre.", + "appearance-position-label": "Cih", + "enabled-description": "Nîşan bide an jî bi tevahî dokê veşêre.", + "enabled-label": "Dock çalak bike", + "monitors-desc": "Nîşan bide dock li ser çavdêrên taybet. Eger tu neyê hilbijartin, ew bi awayekî xwerû li ser hemûyan e.", + "monitors-only-same-monitor-description": "Tenê sepanên ji çavdêriya ku dock lê ye nîşan bide.", + "monitors-only-same-monitor-label": "Tenê sepanên ji heman monitorê", + "monitors-title": "Pêşangeha çavdêriyê", + "title": "Bender" + }, + "general": { + "copy-settings": "Sazkariyan jê bigire", + "fonts-default-description": "Cûrenivîsa sereke ku li seranserê navrûyê tê bikaranîn.", + "fonts-default-label": "Cûrenivîsa berdest", + "fonts-default-placeholder": "Cûrenivîsa berdest hilbijêre...", + "fonts-default-scale-description": "Mezinahiya nivîsa standard mezin an biçûk bike.", + "fonts-default-scale-label": "Mezinahiya cûrenivîsê", + "fonts-default-search-placeholder": "Cûrenivîsê bigere...", + "fonts-desc": "Cûrenivîsa ku di seranserê navrûyê de tên bikaranîn hilbijêre.", + "fonts-monospace-description": "Cûrenivîsa monospace ji bo nîşandana jimar û amaran tê bikaranîn.", + "fonts-monospace-label": "Cûrenivîsa monospace", + "fonts-monospace-placeholder": "Cûrenivîsa monospace hilbijêre...", + "fonts-monospace-scale-description": "Mezinahiya nivîsa monospace mezin an biçûk bike.", + "fonts-monospace-scale-label": "Mezinahiya cûernivîsa monospace", + "fonts-monospace-search-placeholder": "Lêgerîn li fonta monospace...", + "fonts-title": "Cûrenivîs", + "keybinds-down": "Bilivîne jêr", + "keybinds-left": "Bilivîne çep", + "keybinds-right": "Bilivîne rast", + "language-desc": "Zimanê xwe yê daxwazkirî ji bo sepanê hilbijêre.", + "language-select-auto-detect": "Xweber", + "language-select-description": "Zimanê ku di navrûya sepanê de tê bikaranîn hilbijêre.", + "language-select-label": "Zimanê sepanê", + "launch-setup-wizard": "Sêrbazê sazkirinê", + "profile-desc": "Hûrgilyên bikarhêner û avatarê xwe biguherîne.", + "profile-picture-description": "Wêneyê profîla we ku li seranserê navrûyê xuya dibe.", + "profile-picture-label": "Wêneyê profîlê", + "profile-select-avatar": "Wêneyê avatarê hilbijêre", + "profile-title": "Profîl", + "profile-tooltip": "Wêneyê profîlê", + "screen-corners-desc": "Xêzkirina goşeyên ekranê û bandorên dîtbarî xweş bike.", + "screen-corners-radius-description": "Goşeyên giloverkirî yên ekranê eyar bike.", + "screen-corners-radius-label": "Radyusa quncikên dîmenderê", + "screen-corners-radius-reset": "Radiusê goşeyên ekranê ji nû ve saz bike", + "screen-corners-show-corners-description": "Goşeyên giloverkirî li qiraxa ekranê nîşan bide.", + "screen-corners-show-corners-label": "Nîşandan quncikên ekranê", + "screen-corners-solid-black-description": "Reşekî zexm bikar bîne li şûna rengê paşnavê bar.", + "screen-corners-solid-black-label": "Goşeyên reş ên zexm", + "screen-corners-title": "Quncikên dîmenderê", + "settings-copied": "Sazkarî hatin jê girtin bo jêgirtindankê", + "tab-basics": "Bingehîn" + }, + "hooks": { + "info-command-info-description": "• Ferman bi rêya shellê (sh -lc) tên meşandin
• Ferman di paşperdeyê de dimeşin (veqetandî)
• Bişkokên ceribandinê bi nirxên heyî dimeşin", + "info-parameters-description": "• Hooka dîwêr: $1 = rêya dîwêr, $2 = navê ekranê
• Hooka guherîna mijarê: $1 = rast/derew (rewşa moda tarî)
• Hookên kilîtkirin/vekirina ekranê: Parametre tune
• Hookên moda performansê: Parametre tune
• Hooka danişînê: $1 = çalakî (girtin/ji nû ve destpêkirin)", + "info-parameters-label": "Parametreyên berdest", + "noctalia-started-description": "Fermana ku dema Noctalia barkirina xwe qedand pêk were.", + "noctalia-started-label": "Noctalia hêla destpêkirin", + "noctalia-started-placeholder": "mînakî notify-send 'Noctalia hate barkirin'", + "performance-mode-disabled-description": "Fermana ku dema moda performansê ya Noctalia bêbandor dibe, tê meşandin.", + "performance-mode-disabled-label": "Rewaya kar rawestayî ye", + "performance-mode-disabled-placeholder": "mînakî notify-send \"Performans\" \"Moda neçalak\"", + "performance-mode-enabled-description": "Fermana ku dema moda performansê ya Noctalia were çalak kirin, were meşandin.", + "performance-mode-enabled-label": "Hêza xebatê aktîf bû", + "performance-mode-enabled-placeholder": "mînakî notify-send \"Performans\" \"Moda çalak\"", + "screen-lock-description": "Fermana ku dema ekran tê kilîtkirin dê were meşandin.", + "screen-lock-label": "Ekrana kilîtkirî ye", + "screen-lock-placeholder": "mînakî notify-send \"Dîmender\" \"Kilîtkirî\"", + "screen-unlock-description": "Fermana ku dema ekrana kilîtkirî vebe, bê tetbîqkirin.", + "screen-unlock-label": "Ekrana vekirî ye", + "screen-unlock-placeholder": "mînakî notify-send \"Screen\" \"Unlocked\"", + "session-description": "Fermana ku berî girtin an ji nû ve destpêkirinê were darve kirin. Cureya çalakiyê wekî $1 (girtin/ji nû ve destpêkirin) distîne.", + "session-label": "Dawiya rûniştinê", + "session-placeholder": "mînakî notify-send \"Session\" \"$1\"", + "system-hooks-desc": "Fermanan saz bike ku dema bûyerên pergalê çêdibin werin darvekirin.", + "system-hooks-enable-description": "Hemû fermanên hook çalak bike an jî neçalak bike.", + "system-hooks-enable-label": "Hooks çalak bike", + "system-hooks-title": "Sîstem hooks", + "theme-changed-description": "Fermana ku dema mijar di navbera Moda Tarî û Ronahî de diguhere, were meşandin.", + "theme-changed-label": "Şêwe hate guhertin", + "theme-changed-placeholder": "mînakî notify-send \"Tema\" \"Guherandin\"", + "title": "Çengel", + "wallpaper-changed-description": "Fermana ku dema dîwêr diguhere bê cîbicîkirin.", + "wallpaper-changed-label": "Dîwar hate guhertin", + "wallpaper-changed-placeholder": "mînakî notify-send \"Dîwarê dîwarê\" \"Guherandin\"" + }, + "indicator": { + "default-value": "Xwerû: {value}", + "system-default": "Sîstema xwerû" + }, + "launcher": { + "clipboard-desc": "Gihîştin û dîroka clipboardê xwe ji destpêkerê birêve bibin.", + "execute-desc": "Vê vesaz bike ka sepan çawa tên destpêkirin.", + "execute-title": "Cîbicîkirin", + "settings-annotation-tool-description": "Fermana ku dema tikandina bişkoka şîrovekirinê de di dîroka clipboardê de were xebitandin. Wêne dê bi rêya pipe’ê were şandin nav vê fermanê.", + "settings-annotation-tool-label": "Amûra şîrovekirinê", + "settings-annotation-tool-placeholder": "mînakî 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Bi otomatîkî tişta clipboardê ya hilbijartî bişîne. Wtype hewce dike.", + "settings-auto-paste-label": "Otomatîkî bixwe ve zeliqandin", + "settings-clip-preview-description": "Dîmenek pêşdîtinê ya naveroka clipboardê nîşan bide dema ku emrê >clip tê bikaranîn.", + "settings-clip-preview-label": "Pêşdîtina klîpê çalak bike", + "settings-clip-wrap-text-description": "Tekstê di lîsteya clipboardê de bipêçe, li şûna ku were kurtkirin.", + "settings-clip-wrap-text-label": "Pêça nivîsa clipboardê", + "settings-clipboard-history-description": "Gihîştina tiştên ku berê hatine kopîkirin ji destpêkerê.", + "settings-clipboard-history-label": "Dîroka clipboardê çalak bike", + "settings-clipboard-watch-image-description": "Ji bo guhertinên wêneyê, rêza fermanê ya tevahî ji wl-paste re hat şandin (ji nû ve destpêkirinê pêwist dike).", + "settings-clipboard-watch-image-label": "Fermana temaşekirina wêneyê", + "settings-clipboard-watch-text-description": "Ji bo guhertinên wêneyê, rêza fermanê ya tevahî ji wl-paste re hat şandin (ji nû ve destpêkirinê pêwist dike).", + "settings-clipboard-watch-text-label": "Fermana çavdêriya nivîsê", + "settings-custom-launch-prefix-description": "Fermanên pêşgiran bi destpêkerek xwerû pêşda bikin (mînakî 'runapp' ji bo entegrasyona systemd).", + "settings-custom-launch-prefix-enabled-description": "Bi awayekî xwerû destpêkê ji bo destpêkirina sepanan bikar bîne li şûna rêbaza standard.", + "settings-custom-launch-prefix-enabled-label": "Pêşgira destpêkirinê ya xwerû çalak bike", + "settings-custom-launch-prefix-label": "Pêşgira destpêkirinê ya kesane", + "settings-desc": "Xweşikbûn û xuyabûna destpêkerê xweş bike.", + "settings-enable-settings-search-description": "Dema ku di nav destpêker de digerî, encamên mîhengan nîşan bide.", + "settings-enable-settings-search-label": "Lêgerîna mîhengan çalak bike", + "settings-enable-windows-search-description": "Li çarçoveyên çalak bigere û balê bide ser.", + "settings-enable-windows-search-label": "Lêgerînê çarçoveyê çalak bike", + "settings-grid-view-description": "Tiştan di şêweya grîdê de nîşan bide, ne di lîsteyê de.", + "settings-icon-mode-description": "Îkonên pergalê yên xwemalî li şûna îkonên Tabler bikar bîne.", + "settings-icon-mode-label": "Îkonên xwecihî bikar bîne", + "settings-ignore-mouse-input-description": "Di nav destpêkerê de, danûstandina mişkê û çerxa gerokê neçalak bike.", + "settings-ignore-mouse-input-label": "Guherandina mişkê paşguh bike", + "settings-position-description": "Cihê ku panelê destpêkerê lê xuya dibe hilbijêre.", + "settings-show-categories-description": "Tabên kategoriyê nîşan bide ji bo parzûnkirina sepanan.", + "settings-show-categories-label": "Kategoriyan nîşan bide", + "settings-show-icon-background-description": "Li paş îkonan paşxaneyeke çargoşe ya gilover nîşan bide.", + "settings-show-icon-background-label": "Paşxaneya îkonê nîşan bide", + "settings-sort-by-usage-description": "Dema ku çalak be, sepanên ku pir caran têne destpêkirin pêşî di lîsteyê de xuya dibin.", + "settings-sort-by-usage-label": "Li gorî ya herî pir tê bikaranîn rêz bike", + "settings-terminal-command-description": "Fermana destpêkirina termînalê. Mînakî 'kitty -e' an 'gnome-terminal --'.", + "settings-terminal-command-label": "Fermana termînalê", + "settings-use-app2unit-description": "Rêbazeke destpêkirinê ya alternatîf bikar tîne da ku pêvajoyên sepanê baştir birêve bibe û pêşî li pirsgirêkan bigire.", + "settings-use-app2unit-label": "App2Unit bi kar bîne da ku sepanan bide destpêkirin", + "settings-view-mode-label": "Awaya nîşandanê", + "title": "Destpêkar" + }, + "location": { + "calendar-cards-desc": "kertan rêkxistin bike di destgeha salnameyê de çalak bike/neke.", + "calendar-cards-title": "Kartên salnameyê", + "calendar-header-label": "Sernavê salnameyê", + "calendar-month-label": "Meha salnameyê", + "date-time-12hour-format-description": "Demê bi formata 12-demjimêr li ser dîmendera kilîtkirinê û salnameyê nîşan bide. Darika demjimêrê sazkariyên xwe yên taybet hene.", + "date-time-12hour-format-label": "Formata demê ya 12-demjimêr bi kar bîne", + "date-time-desc": "Çawa dîrok û dem xuya dibin kesane bike.", + "date-time-first-day-of-week-automatic": "Xweber (a pergalê bi kar bîne)", + "date-time-first-day-of-week-description": "Roja kîjanê di salnameyê de dest bi hefteyê dike hilbijêre.", + "date-time-first-day-of-week-label": "Roja yekem a hefteyê", + "date-time-show-events-description": "Bûyeran di destgeha salnameyê de nîşan bide.", + "date-time-show-events-label": "Bûyerên salnameyê nîşan bide", + "date-time-title": "Dîrok & Dem", + "date-time-use-analog-description": "Demjimêreke bi şêwaza analog li ser çarçoveya salnameyê û dîmendera kilîtkirinê nîşan bide.", + "date-time-use-analog-label": "Demjimêrê bi şêwaza analog bi kar bîne", + "date-time-week-numbers-description": "Hefteya salê (mînakî hefteya 38) di salnameyê de nîşan bide.", + "date-time-week-numbers-label": "Jimarên hefteyê nîşan bide", + "location-desc": "Ji bo wergirtina rewşa bayê û bernamekirina ronahiya şevê ya rast, cihê xwe saz bike.", + "location-search-description": "mînakî Toronto, ON", + "location-search-label": "Ji bo cihekî bigere", + "location-search-placeholder": "Navê cihê têxîne", + "location-title": "Cihê te", + "weather-desc": "Yekîneya germahiya xwe ya bijarte hilbijêre.", + "weather-enabled-description": "Agahiyên hewayê li seranserê navbeynê nîşan bide û daneyên hewayê bîne. Dema ku neçalak be, dê hemû hêmanên hewayê werin veşartin û dê tu daxwazên torê neyên kirin.", + "weather-enabled-label": "Çalak bike hewayê", + "weather-fahrenheit-description": "Germahiya bi Fahrenheit nîşan bide, ne bi Celsius.", + "weather-fahrenheit-label": "Pîvana germahiyê bi Fahrenheit (°F) nîşan bide", + "weather-hide-city-description": "Navê bajarê ji dîmenên hewayê yên li seranserê navbeynê veşêre.", + "weather-hide-city-label": "Veşêre nameyê sûke", + "weather-hide-timezone-description": "Demjim kurtkirina herêma demjimêrê ji pêşandanên hewayê li seranserê navbeynê.", + "weather-hide-timezone-label": "Demjimêrê veşêre", + "weather-show-effects-description": "Bandora bandorên dîtbarî yên din (wek baran, berf, an birûsk) li ser karta hewayê nîşan bide.", + "weather-show-effects-label": "Nîşandan bandorên hewayê", + "weather-show-in-calendar-description": "Pêşbîniya hewayê ya rojane rasterast di dîmena salnameya xwe de nîşan bide.", + "weather-show-in-calendar-label": "Nîşandanîna hewayê di salnameyê de" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "Dema ku fprintd (nasnameya tiliyan) çalak e, ev vebijêrk dihêle hûn bi şîfreya xwe têkevin, ne bi tiliya xwe.", + "allow-password-with-fprintd-label": "Rêdan bi têketina şîfreyê bi fprintd", + "auto-start-auth-description": "Otomatîkî dest bi rastkirina tiliyê dike bêyî ku pêdivî bi pêlkeyek an klîkek hebe.", + "auto-start-auth-label": "Destpêkirina otomatîkî ya rastkirinê", + "clock-format-label": "Formata demjimêrê", + "clock-style-analog": "Analog", + "clock-style-custom": "Kesane bike", + "clock-style-digital": "Jimarî", + "compact-lockscreen-description": "Tenê têketina ketanê û kontrolên pergalê nîşan bide, sepanokên rewşa bayê û medyayê veşêre.", + "compact-lockscreen-label": "Kilîta ekrana berhevkirî", + "lock-on-suspend-description": "Dema ku pergal tê rawestandin, ekranê bixweber kilît bike.", + "lock-on-suspend-label": "Li ser rawestandinê kilît bike", + "show-hibernate-description": "Vebijêrka 'Xewnê' di kontrolên hêzê de nîşan bide.", + "show-hibernate-label": "Nîşan bide xewnê", + "show-session-buttons-description": "Destûra gihîştina mîhengên hêzê ji ekrana kilîtkirî re bide.", + "show-session-buttons-label": "Kontrolên hêzê", + "title": "Dîmendera kilîtkirinê" + }, + "network": { + "bluetooth-description": "Rêveberiya Bluetooth çalak bike.", + "bluetooth-rssi-polling-description": "Demjimêr RSSI ji bo cîhazên girêdayî bi rêya bluetoothctl nimûne bike. Dibe ku ji bo hemû cîhazên berdest nebe; dema ku çalak be çavkaniyên hindik bikar tîne.", + "bluetooth-rssi-polling-label": "Pirsgirêka sînyala Bluetoothê", + "desc": "Girêdanên Wi-Fi û Bluetooth bi rê ve bibe.", + "wifi-description": "Torên bêtêl bi rê ve bibe (pêdivî bi NetworkManager heye)." + }, + "notifications": { + "duration-critical-urgency-description": "Çend demjimêr notên girîng ên pêşîn xuya dimînin.", + "duration-critical-urgency-label": "Lezgînîya krîtîk", + "duration-desc": "Dem bi awayê lezgîniya wan notîs çiqas dirêj xuya dimînin mîheng bike.", + "duration-low-urgency-description": "Çiqas notîfîkasyonên pêşîniya kêm xuya dimînin.", + "duration-low-urgency-label": "Lezgînî kêm", + "duration-normal-urgency-description": "Notîfîkasyonên pêşîniya normal çiqas dirêj xuya dimînin.", + "duration-normal-urgency-label": "Lezgînahiya normal", + "duration-respect-expire-description": "Dem wextê qedandinê yê ku di agahdariyê de hatiye danîn bikar bîne.", + "duration-respect-expire-label": "Dem bihurmet demdirêj qedandin", + "duration-title": "Dirêjahiya agahdariyê", + "history-critical-urgency-description": "Agahiyên girîng ên pêşîn li dîrokê tomar bike.", + "history-critical-urgency-label": "Dema awarte ya krîtîk tomar bike", + "history-desc": "Kontrol bike kaîdî notîfîkasyonên ku li gorî asta lezgîniya wan di dîrokê de têne hilanîn.", + "history-low-urgency-description": "Agahdariyên bi girîngiya nizm di dîrokê de tomar bike.", + "history-low-urgency-label": "Dîroka lezgîniya kêm tomar bike", + "history-normal-urgency-description": "Agahdariyên pêşîniya normal di dîrokê de tomar bike.", + "history-normal-urgency-label": "Dîroka lezgîniya normal tomar bike", + "monitors-desc": "Nîşandana agahdarkirinê li ser çavdêrên taybet. Heke neyên hilbijartin, bi awayekî xwerû hemûyan nîşan dide.", + "settings-always-on-top-description": "Nîşandanên li ser pencereyên tev-ekran û qatên din nîşan bide.", + "settings-background-opacity-description": "Guhertina zelaliya paşxaneyên agahdariyan.", + "settings-desc": "Xuyakir û reftarê agahdariyan saz bike.", + "settings-do-not-disturb-description": "Dema ku hat çalak kirin, hemû pop-upên agahdarkirinê neçalak bike.", + "settings-enabled-description": "Aktîv bike an jî deaktîv bike daemona ragihandinê, ji bo vê yekê ji nû ve destpêkirina şêla Noctalia hewce dike.", + "settings-enabled-label": "Agahdariyan çalak bike", + "settings-location-description": "Agahî li ser ekranê li ku derê xuya dibin.", + "sounds-desc": "Dengê bandorên hişyariyê û qebareyê saz bike.", + "sounds-enabled-description": "Çalak bike bandorên dengî ji bo agahiyên hatin.", + "sounds-enabled-label": "Dengên agahdariyê çalak bike", + "sounds-excluded-apps-description": "Berbîna lêdana dengê agahdarkirinê ya ku hatî vesazkirin ji bo sepanên taybet ên ku dengên wan ên çêkirî hene.", + "sounds-excluded-apps-label": "Serlêdanên hatine derxistin", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Rêya pelê dengî ya ku ji bo agahiyên pêşîniya krîtîk tê lîstin.", + "sounds-files-critical-label": "Dengek lezgîniya krîtîk", + "sounds-files-critical-select-title": "Pelê dengî yê lezgîniya krîtîk hilbijêre", + "sounds-files-desc": "Pelên dengî yên xwerû ji bo astên lezgîniya agahdarkirinê yên cuda mîheng bike.", + "sounds-files-low-description": "Rêya pelê dengî ya ku ji bo agahiyên pêşîniya kêm tê lîstin.", + "sounds-files-low-label": "Dengekî lezî kêm", + "sounds-files-low-select-title": "Pelê dengî yê bilezînek kêm hilbijêre", + "sounds-files-normal-description": "Rêya pelê dengî ya ku ji bo agahiyên pêşîniya normal tê lîstin.", + "sounds-files-normal-label": "Dengê lezgîniya normal", + "sounds-files-normal-select-title": "Pelê dengî yê bileziya asayî hilbijêre", + "sounds-files-placeholder": "Rêya pelê dengî binivîse", + "sounds-files-select-file": "Pelê deng hilbijêre", + "sounds-files-title": "Pelên deng", + "sounds-files-unified-description": "Rêya pelê dengî ya ku ji bo agahdariyan tê lîstin.", + "sounds-files-unified-label": "Dengê agahdarkirinê", + "sounds-files-unified-select-title": "Pelê dengê agahdarkirinê hilbijêre", + "sounds-separate-description": "Ji bo agahiyên girîngiyên kêm, normal û krîtîk pelên dengî yên cuda bikar bîne.", + "sounds-separate-label": "Dengê cuda li gorî pêşîniyê bikar bîne", + "sounds-title": "Mîhengên deng", + "sounds-unavailable-description": "Ji bo ku dengên agahdarkirinê çalak bibin, Qt6 Multimedia saz bikin.", + "sounds-unavailable-label": "Dengên agahdarkirinê peyda nabin", + "sounds-volume-description": "Ayarê asta deng ji bo dengên agahdarkirinê rast bike.", + "sounds-volume-label": "Jimara dengê", + "toast-battery-label": "Hişyariya betariyê", + "toast-desc": "Xuyabûn û reftarên toastê saz bike.", + "toast-keyboard-description": "Dema ku şewaza kilîtdankê were guhertin, hişyarî nîşan bide.", + "toast-keyboard-label": "Şêwaza kilîtdankê", + "toast-media-description": "Dema rewşa lêdana medyayê biguhere, toastekê nîşan bide.", + "toast-media-label": "Medya" + }, + "osd": { + "always-on-top-description": "OSD li ser pencereyên tev-ekran û qatên din nîşan bide.", + "always-on-top-label": "Her tim li ser", + "background-opacity-description": "Kontrola zelalbûna paşxaneya OSD.", + "background-opacity-label": "Zelaliya paşrûya destgehê", + "description": "Mîhengên nîşanderên ser-ekranê yên wekî pêşandanên deng û ronahiyê saz bike.", + "duration-auto-hide-description": "Dema berî windabûna OSD biguherîne.", + "duration-auto-hide-label": "Piştî otomatîk veşêre", + "duration-desc": "Çiqas dirêj OSD berî ku bixweber veşêre, xuya dimîne.", + "duration-title": "Demjimêra veşartina otomatîk", + "enabled-description": "Guhertinên deng û ronahiyê di dema rast de nîşan bide.", + "enabled-label": "Pêşandana ser ekranê (OSD) çalak bike", + "events-desc": "Bûyerên ku pêşandana ser ekranê çalak dikin hilbijêre.", + "general-desc": "Vebijêrkên dîtin û tevgera OSD'ê saz bike.", + "location-description": "Li ku derê pêşandanên ser ekranê xuya dibin.", + "monitors-desc": "OSD li ser çavdêrên taybet nîşan bide. Ger tu neyên hilbijartin, bi awayekî xwerû li ser hemûyan nîşan dide.", + "title": "Pêşandana ser ekranê", + "types-brightness-description": "Dîmendera OSD nîşan bide dema ronahiya ekranê diguhere.", + "types-brightness-label": "Ronahî", + "types-custom-text-description": "OSD ya nivîsandeyên xwerû yên ji IPC'yê nîşan bide.", + "types-custom-text-label": "Teksta xweser", + "types-desc": "Bûyerên ku OSD'ê çalak dikin hilbijêre.", + "types-input-volume-description": "Dîmenê OSD nîşan bide dema ku qebareya mîkrofonê diguhere.", + "types-input-volume-label": "Dengê ketanê", + "types-lockkey-description": "Dema Caps Lock, Num Lock, an Scroll Lock hatin guhertin, OSD nîşan bide.", + "types-lockkey-label": "Kilîtên qeflê", + "types-media-description": "Dema rewşa lêdana medyayê biguhere OSD nîşan bide (lêdan, rawestandin, derbasbûn).", + "types-media-label": "Lîstina medyayê", + "types-title": "Bûyerên destpêker ên OSD", + "types-volume-description": "Dema ku asta dengê derketina bihîstwerê diguhere, OSD nîşan bide.", + "types-volume-label": "Jimara deranê" + }, + "plugins": { + "available-description": "Pêvekên ji çavkaniyên rêkxistî bigere û saz bike", + "available-label": "Pêvekên berdest", + "available-no-plugins-description": "Çavkaniyên pêvekê xwe kontrol bikin an lîsteyê nû bikin.", + "available-no-plugins-label": "Tiştên pêvek tune ne", + "check-for-updates": "Ji bo rojanekirinê kontrol bike", + "checking-for-updates": "Ji bo rojanekirinê kontrol dike...", + "collision-already-installed": "Ev pêvek berê hatiye sazkirin", + "collision-custom-version-exists": "Guhertoyeke taybet a ji \"{source}\" berê hatiye sazkirin", + "collision-official-version-exists": "Guhertoya fermî ya vê pêvekê berê hatiye sazkirin", + "filter-downloaded": "Daxistî", + "filter-not-downloaded": "Nehatiye daxistin", + "filter-tags-description": "Parzûna pêvekên bi kategoriya an rewşa daxistinê.", + "filter-tags-label": "Tagan", + "hot-reload-description": "Bi guhertina pelên wan, bixweber pêvekên xwe ji nû ve bar bike. Ji bo pêşxistina pêvekan bikêr e.", + "hot-reload-label": "Nûbarkirina germ (moda pêşvebirinê)", + "hot-reloaded": "Plugin ji nû ve hate barkirin: {name}", + "install-error": "Sazkirin bi ser neket: {error}", + "install-incompatible": "{plugin} pêdivî bi Noctalia v{version} an bilindtir heye", + "install-success": "Bi serkeftî {plugin} hate sazkirin", + "installed-description": "Birêvebir û hemû pêvekên ku herêmî hatine sazkirin mîheng bike.", + "installed-label": "Pêvekên sazkirî", + "installed-no-plugins-description": "Pêvekên ji beşa \"Berdest\" a saz bike.", + "installed-no-plugins-label": "Tu pêvek nehatine sazkirin", + "installing": "Sazkirina {plugin}...", + "open-plugin-page": "Rûpela pêvekê veke", + "open-plugins-tab": "Vekirina mîhengên pêvekê", + "plugin-settings-title": "Sazkairyên {plugin}", + "refresh-refreshing": "Lîsteya pêvekên nûvekirinê...", + "refresh-tooltip": "Nûkirina pêvekên berdest", + "settings-error-not-loaded": "Pêvek nehatiye barkirin", + "settings-saved": "Ayarlarê pluginê hatin tomarkirin", + "settings-tooltip": "Mîhengên pêvekê", + "source-custom": "Çavkaniya xwerû", + "sources-add-custom": "Lê zêde bike embarê xwerû", + "sources-add-dialog-description": "Depoyek GitHub wekî çavkaniyek plugin zêde bike.", + "sources-add-dialog-error": "Çavkaniya pêvekê nehat zêdekirin", + "sources-add-dialog-name": "Navê embarê", + "sources-add-dialog-name-placeholder": "Pêvekên min ên xweser", + "sources-add-dialog-success": "Çavkaniya pêvekê bi serkeftî hate zêdekirin", + "sources-add-dialog-title": "Çavkaniya pêvekê zêde bike", + "sources-add-dialog-url": "Girêdanê depoyê", + "sources-description": "Depoyên pêvekan bi rê ve bibe.", + "sources-placeholder": "Depoya min a bedew", + "sources-remove-tooltip": "Çavkaniya pêvekê jê bibe", + "title": "Pêvek", + "uninstall-dialog-description": "Tu dixwazî {plugin} jê bibî? Ev ê hemû daneyên pêvekê jê bibe.", + "uninstall-dialog-title": "Rakêşana pêvekê", + "uninstall-error": "Nekarî were rakirin: {error}", + "uninstall-success": "Bi serkeftî {plugin} hate rakirin", + "uninstalling": "Rakirina {plugin}...", + "update-all": "Hemûyan nû bike ({count})", + "update-all-success": "Hemû pêvek bi serkeftî hatin nûkirin", + "update-available": "Rojanekirina pêveka nû heye", + "update-available-plural": "Rojanekirinên pêvekên nû hene ({count})", + "update-error": "Nekarî plugîn nûve bikin: {plugin}: {error}", + "update-pending": "v{current} → v{new} (hewce dike Noctalia v{required})", + "update-success": "{plugin} hate nûkirin bo v{version}", + "update-version": "v{current} → v{new}", + "updating": "Tê rojanekirin..." + }, + "region": { + "title": "Herêm" + }, + "session-menu": { + "countdown-duration-description": "Demjimêra heta pêkanîna kiryarên hêzê destnîşan bike.", + "countdown-duration-label": "Dirêjahiya jimartinê berjêr", + "enable-countdown-description": "Demjimêrek paşvekirinê nîşan bide berî pêkanîna kiryarên hêzê.", + "enable-countdown-label": "Demjimêra paşvejimêr çalak bike", + "entries-desc": "Çalakiyên hêzê yên ku di menuya danişînê de û bi kîjan rêzê de xuya dibin, xweş bikin.", + "entries-title": "Kiryarên hêzê", + "entry-settings-command-description": "Fermana xwerû ya ku ji bo vê çalakiyê were meşandin. Ji bo bikaranîna fermana pergalê ya xwerû vala bihêle.", + "entry-settings-command-placeholder": "mînakî systemctl poweroff", + "entry-settings-default-command-lock": "Kilîta hundirîn a ekranê (bê ferman)", + "entry-settings-default-command-logout": "Derketina navxweyî (bê ferman)", + "entry-settings-default-info-description": "Heke fermana xwerû neyê diyarkirin, fermana pergalê ya xwerû dê were bikar anîn.", + "entry-settings-default-info-label": "Fermana berdest", + "entry-settings-title": "Vexîne {entry}", + "entry-settings-tooltip": "Fermanê vesazkirinê", + "general-desc": "Maneja reftar û xuyabûna panelê menûya rûniştinê.", + "large-buttons-layout-description": "Çawa bişkokên menuya danişînê bên nîşandan hilbijêre.", + "large-buttons-layout-label": "Rêzika bişkokên mezin", + "large-buttons-style-description": "Pêşeka danişînê bi bişkokên mezin di şêweya grîd de nîşan bide.", + "large-buttons-style-label": "Şêwaza bişkokên mezin", + "position-description": "Dema ku panelê menuya danişînê vebe, hilbijêre ku li ku derê xuya bibe.", + "show-header-description": "Sernavê û bişkoka girtinê li jorê menuyê rûniştinê nîşan bide.", + "show-header-label": "Serpê nîşan bide" + }, + "system-monitor": { + "critical-color-label": "Rengê krîtîk", + "custom-highlight-colors-title-label": "Rengên ronîkirina xwerû", + "disk-section-label": "Bikaranîna dîskê", + "enable-dgpu-monitoring-description": "Hişyarî: Ev dê YKG a te ya veqetandî (NVIDIA/AMD) hişyar bike, ku dibe ku bandorek girîng li ser jiyana pîlê li ser laptopên bi grafîkên hîbrîd bike.", + "enable-dgpu-monitoring-label": "Çavdêriya YKG veqetandî çalak bike", + "external-monitor-description": "Fermanî an rêya sepanê binivîse ku dema sepana monitora pergala derveyî çalak dike were xebitandin.", + "external-monitor-label": "Fermana monitora pergala derveyî", + "external-monitor-placeholder": "çavkanî || navendamisyonê || jdsîstemonîtor || statîstîkênbingehîn || navenda-çavdêriya-sîstemê || gnome-sîstem-monitor || plasma-sîstemmonitor || mate-sîstem-monitor || ukui-sîstem-monitor || deepin-sîstem-monitor || pantheon-sîstem-monitor", + "general-desc": "Sazkirina reftar û xuyabûna çavdêriya pergalê.", + "gpu-section-label": "Germahiya YKG", + "highlight-colors-section-label": "Rengên ronîkirinê", + "polling-interval-label": "Navbera anketê", + "polling-section-description": "Mîheng bike ku her pîvana pergalê çiqas caran were nûkirin.", + "polling-section-label": "Navberên anketê", + "threshold-critical": "Astana krîtîk", + "threshold-warning": "Asteya hişyariyê", + "thresholds-section-description": "Guherîn sînorên hişyariyê/krîtîk û navberên anketê ji bo her metrîkên pergalê.", + "use-custom-highlight-colors-description": "Dema ku neçalak be, rengên ronîkirina xwerû yên temayê têne bikar anîn.", + "use-custom-highlight-colors-label": "Rengên ronîkirina xwerû bikar bîne", + "warning-color-label": "Rengê hişyariyê" + }, + "user-interface": { + "allow-panels-without-bar-description": "Dema ku çalak be, panel dikarin li ser her ekranê vebin. Dema ku neçalak be, panel tenê dê li ser ekranên ku barekî wan heye vebin, ku dikare bikaranîna bîrê kêm bike.", + "allow-panels-without-bar-label": "Destûrê bide panelan li ser ekranan bêyî barekî", + "animation-disable-description": "Hemû anîmasyonan neçalak bike ji bo ezmûneke bileztir û bersivdartir.", + "animation-disable-label": "Anîmasyonên NB çalak neke", + "animation-speed-description": "Lezaaniya lezgîniya gerdûnî sererast bike.", + "animation-speed-label": "Leza anîmasyonê", + "animation-speed-reset": "Lezaaniya bilezîneka nû bike", + "appearance-desc": "Xwerûkirina hêmanên dîtbarî yên wekî serişteyên amûran, sînor û siya.", + "box-border-description": "Xêzekê li dora deverên naverokê nîşan dide.", + "box-border-label": "Çarçoweya konteynerê", + "box-border-radius-description": "Goşeya dorfirehiya beşên sereke yên layoutê, wekî barên kêlekê, kart, û panelên naverokê, eyar dike.", + "box-border-radius-label": "Radiusa konteynirê", + "control-border-radius-description": "Kontrola çemandina hêmanên înteraktîv, di nav de bişkok, guhêrbarok û qadên nivîsê.", + "control-border-radius-label": "Tîrêjê têketinê", + "desc": "Xuyakirina xuyabûn, hest, û reftarên navrûyê.", + "dim-desktop-description": "Dema panel an menü vebin, sermaseyê tarî bike.", + "dim-desktop-label": "Dim sermaseyê", + "dimmer-opacity-description": "Ayarê asta zelalbûna ji bo tarîkirina sermaseyê bike.", + "dimmer-opacity-label": "Şefafiya sermaseyê sermaseyê kêmkirî", + "panel-background-opacity-description": "Zelalbûna paşrûyê ji bo hemû destgehan (darik, destpêker, sazkarî, hwd...) saz bike.", + "panel-background-opacity-label": "Zelaliya paşrûyê destgehê", + "panels-attached-to-bar-description": "Panelên bi qiraxên bar û ekranê ve têne kilît kirin, bi goşeyên berevajîkirî yên şêwekar xuyangek bêkêmasî diafirînin.", + "panels-attached-to-bar-label": "Panêlan bi kêlekê ve bişkînin", + "panels-overlay-description": "Pişkin dike ku panel û bar, heta ser sepanên tev-ekran jî, xuya bimînin.", + "panels-overlay-label": "Destgeh & darikê li jor bihêle", + "scaling-description": "Guhertina mezinahiya navrûya giştî ya bikarhêner, ji bilî bar.", + "scaling-label": "Pîvana navrûyê", + "settings-panel-mode-description": "Hilbijêre şêweyê mîhengan (dibe ku ji nû ve vekirinê hewce bike).", + "settings-panel-mode-label": "Awaya destgeha sazkariyan", + "shadows-description": "Dîsa siya di bin bar û panelan de çalak dike.", + "shadows-direction-description": "Hilbijêre ku siya ji ku tê avêtin.", + "shadows-direction-label": "Arasteya siya", + "shadows-label": "Siyeyên ketî", + "title": "Navrûya bikarhêner", + "tooltips-description": "Aktîf bike an neçalak bike serişteyên amûrê li seranserê navbeynê.", + "tooltips-label": "Nîşandanê sernavên amûran" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Alfabetîk", + "automation-change-mode-description": "Dema ku dîwar otomatîk diguhere, hilbijêre ka dîwar çawa têne hilbijartin.", + "automation-change-mode-label": "Guherandin moda", + "automation-custom-interval-description": "Demê wekî HH:MM (mînakî 01:30) têxîne.", + "automation-custom-interval-label": "Navbera xwerû", + "automation-interval-description": "Çend caran divê dîwarên dîwêr bixweber werin guhertin.", + "automation-interval-label": "Navbera dîwêr", + "automation-random-wallpaper-description": "Bername guhertina dîwarê rasthatî di navberên rêkûpêk de.", + "automation-scheduled-change-description": "Bi awayekî otomatîkî di navberên rêkûpêk de dîwêran biguherîne.", + "automation-scheduled-change-label": "Guherîna plansazkirî", + "look-feel-edge-smoothness-description": "Bandora bo bandorê perîşan û nerm li keviya veguherînan dike.", + "look-feel-edge-smoothness-label": "Nermkirina keviya derbasbûnê", + "look-feel-fill-color-description": "Rengê dagirtinê hilbijêre ku dibe ku li pişt wêneyê dîwêr xuya bibe", + "look-feel-fill-mode-description": "Hilbijêre ka divê wêne çawa were pîvandin da ku bi çareseriya çavdêriya te re têkildar be.", + "look-feel-fill-mode-label": "Moda dagirtinê", + "look-feel-title": "Binêre û hîs bike", + "look-feel-transition-duration-description": "Dirêjahiya anîmasyonên derbasbûnê bi çirkeyan.", + "look-feel-transition-duration-label": "Dirêjahiya derbasbûnê", + "look-feel-transition-type-description": "Cûreyê anîmasyonê dema ku di navbera wêneyên dîwêr de diguhere.", + "look-feel-transition-type-label": "Curey guherînê", + "settings-desc": "Kontrolkirina çawaniya birêvebirin û nîşandana dîwêrên dîmenderê.", + "settings-enable-management-description": "Bi Noctalia dîwaran birêve bibin. Ger hûn dixwazin serlêdanek din bikar bînin, nîşana wê rake.", + "settings-enable-management-label": "Aktîvkirina birêvebirina dîwarê", + "settings-enable-overview-description": "Dîwarekî dîm û nezelal li ser dîmendera giştî ferz dike.", + "settings-enable-overview-label": "Aktîfkirina dîwarê giştî", + "settings-folder-description": "Rêya peldanka te ya sereke ya dîwêr.", + "settings-folder-label": "Peldanka wêneyê dîwêr", + "settings-hide-wallpaper-filenames-tooltip-hide": "Navên pelan veşêre", + "settings-hide-wallpaper-filenames-tooltip-show": "Navên pelan nîşan bide", + "settings-monitor-specific-description": "Peldankekî dîwarê cuda ji bo her monitorekî destnîşan bike.", + "settings-monitor-specific-label": "Navnîşanên taybet bi çavdêran", + "settings-monitor-specific-tooltip": "Peldanka dîwarê çavdêriyê", + "settings-recursive-search-description": "Herwiha li peldankên binê peldanka dîwêr jî li dîwêran bigere.", + "settings-recursive-search-label": "Lêgerîna binyadan", + "settings-select-monitor-folder": "Hilbijêre peldanka dîwarê dîmenderê", + "settings-selector-description": "Wêneyê dîwarê xwe hilbijêre.", + "settings-selector-position-description": "Cihê ku panelê hilbijêra dîwêr lê xuya dike hilbijêre.", + "settings-show-hidden-files-tooltip-hide": "Pelên veşartî veşêre", + "settings-show-hidden-files-tooltip-show": "Pelên veşartî nîşan bide", + "settings-title": "Mîhengên dîwêr", + "settings-view-mode-description": "Ji bo dîmendana dîwêrên ji pelrêça xwe rêbazek hilbijêre.", + "settings-view-mode-label": "Awayê dîtinê", + "view-mode-browse": "Navîgasyon di peldankan de", + "view-mode-cycle-tooltip": "Awayê dîtinê: {mode} (bitikîne da biguherî)", + "view-mode-recursive": "Binkêşkên jêrîn ên rûxandî", + "view-mode-single": "Peldaîna bingehîn" + } + }, + "placeholders": { + "command-example": "echo \"Silav cihan\"", + "enter-command": "Fermana ku bê tetbîqkirin binivîse (sepan an nivîsara taybet)", + "enter-ipc-identifier": "Ji bo fermanên IPC nasnameyek yekta binivîse", + "enter-path": "Rêya binivîse...", + "enter-text-to-collapse": "mînakî 'tiştek nayê lîstin'. Use /regex/ for patterns.", + "enter-tooltip": "Têketina sernavê amûrê", + "enter-width-pixels": "Firehiya bi pîxel binivîse", + "search": "Bigere...", + "search-icons": "mînakî noctalia, niri, betarî, ewr", + "search-launcher": "Li navan bigere... an ji bo fermanan > bi kar bîne", + "search-wallhaven": "Lêgerîn li Wallhaven...", + "search-wallpapers": "Binivîse da ku dîwêran parzûn bikî..." + }, + "positions": { + "bottom": "Jêr", + "bottom-center": "Jêr navend", + "bottom-left": "Jêr çepê", + "bottom-right": "Jêr rastê", + "center": "Navend", + "center-left": "Navend çep", + "center-right": "Navend rast", + "close-to-bar": "Nêzîkî bişkoka darikê", + "follow-bar": "Darikê bişopîne", + "left": "Çep", + "right": "Rast", + "top": "Jor", + "top-center": "Jor navend", + "top-left": "Jorî çep", + "top-right": "Rastê jor" + }, + "session-menu": { + "action-in-seconds": "di {seconds} çirke de {action} ...", + "cancel-timer": "Demjimêr betal bike", + "title": "Menuya danişînê" + }, + "setup": { + "all-done": "Qediya!", + "appearance": { + "subheader": "Moda tarî û çavkaniyên rengan hilbijêre (wallpaper an pêşdiyarkirî)." + }, + "customize": { + "header": "Xweserkirina ezmûna xwe bike", + "subheader": "Rewşa bar, dendikbûn, pîvan û hwd. eyar bike." + }, + "skip-setup": "Sazkirinê Derbas Bike", + "telemetry-wizard-done": "Gihîşt!", + "telemetry-wizard-note": "Hûn di kontrolê de ne — hûn dikarin vê yekê di mîhengan de her dem çalak an neçalak bikin", + "telemetry-wizard-subtitle": "Me analîtîkên anonîm zêde kirin da ku alîkariya başkirina Noctalia bikin", + "telemetry-wizard-title": "Nûkirina Nepenîtiyê", + "wallpaper": { + "choose-dir": "Dîrekteriyek ku wêneyên dîwarê te tê de hilbijêre", + "dir-browse": "Geroka peldanka dîwêr", + "dir-description": "Peldêra ku dîwarên te dihewîne hilbijêre.", + "dir-label": "Navnîşana dîwarê", + "dir-select-title": "Pelê dîwarê hilbijêre", + "header": "Wêneyê dîwarê xwe hilbijêre", + "no-dir": "Tuwalxaneyek dîwêr nehatiye hilbijartin", + "no-valid": "Di {dir} de pelên wêneyan ên derbasdar nehatin dîtin", + "none-in-dir": "Di wê peldankê de tu dîwarên dîwaran nehatin dîtin", + "preview-error": "Wêne barkirin têk çû", + "select-prompt": "Wêneyekî dîwarê jêrîn hilbijêre", + "subheader": "Bi dîmenek xweşik hestê saz bike." + }, + "welcome-note": "Tenê çend bingehîn ji bo ku hûn dest pê bikin — vebijarkên tevahî di mîhengan de ne", + "welcome-subtitle": "Werin em sermaseya we bêhempa bikin", + "welcome-title": "Bi xêr hatî Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "%{percent}", + "location-display": "{nav} ({koordinat})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "Tu lîstikvanen medyayê nehatin dîtin", + "scaling-percentage": "%{percentage}", + "signal-strength": "%{signal}", + "unknown-app": "Sepanek nenas", + "uptime": "Dema xebitandinê: {uptime}", + "user-requested": "Daxwaza bikarhêner", + "welcome-back": "Silav,", + "widget-settings-title": "Sazkariyên {widget}" + }, + "system-monitor": { + "cpu-temp": "Germahiya YKN", + "cpu-usage": "Bikaranîna YKN", + "disk": "Dîsk", + "download-speed": "Leza daxistinê", + "gpu-temp": "Germahiya YKG", + "load-average": "Barkirina navîn", + "title": "Çavdêriya pergalê", + "upload-speed": "Leza barkirinê" + }, + "toast": { + "airplane-mode": { + "title": "Awaya balafirê" + }, + "battery": { + "low": "Pîl kêm e", + "low-desc": "Betarî li ser %{percent} e. Tijîkarê girê bide" + }, + "bluetooth": { + "address-copied": "Navnîşan li clipboardê hate kopîkirin", + "confirm-code": "Koda {value} li ser amûra din piştrast bike.", + "connect-failed": "Têk çû girêdan bi cîhazê re", + "disconnect-failed": "Têkçûn di qutkirina ji cîhazê de", + "discoverable-change-failed": "Guhertina rewşa dîtşbar têk çû", + "discoverable-disabled": "Dîtbarî hat neçalakkirin", + "discoverable-enabled": "Dîtbarî hat neçalakkirin", + "display-code": "Koda {value} li ser amûra din binivîse.", + "forget-failed": "Têk çû ku amûr were jibîrkirin", + "pair-failed": "Cîhaz bi hev ve nehat girêdan", + "passkey-required": "Pêdivî bi borînpeyvê amûrê heye.", + "pincode-required": "Koda PIN ji hêla cîhazê ve tê xwestin.", + "state-change-failed": "Guhertina rewşa Bluetooth têk çû" + }, + "clipboard": { + "long-text": "Teksta dirêj", + "unavailable": "Dîroka clipboardê tune ye", + "unavailable-desc": "Serlêdana 'cliphist' nehatiye sazkirin. Ji bo bikaranîna taybetmendiyên dîroka clipboardê, ji kerema xwe wê saz bikin" + }, + "do-not-disturb": { + "disabled": "Dengê dernexîne neçalak e", + "disabled-desc": "Hemû agahdarî tên nîşandan", + "enabled": "Dengê dernexîne çalak e", + "enabled-desc": "Tu yê van agahdariyan di dîroka xwe de bibînî" + }, + "donation-opened": "Rûpela bexşînê di geroka we de vebû", + "internet-limited": "Pêwendîdar bê înternetê", + "keyboard-layout": { + "changed": "Şêwaza kilîtdankê bo {layout} hat guhertin", + "title": "Kilîtdank" + }, + "missing-control-center": { + "description": "Kurteya kontrolê ji barê hatiye rakirin. Ji bo ku hûn dîsa ji barê bigihîjinê, hûn ê hewce ne ku kurteyê ji nû ve zêde bikin. Her weha hûn dikarin bi rast-klîkkirina li ser barê jî vekin", + "label": "Amûra dawî ya navenda kontrolê hate rakirin" + }, + "night-light": { + "forced": "Çalakdarkirina bi zorê", + "normal": "Moda normal", + "not-installed": "wlsunset nehatiye sazkirin" + }, + "noctalia-performance": { + "disabled": "Rewşa performansê hat betalkirin", + "enabled": "Rewşa performansê çalak bû", + "label": "Performansa Noctalia" + }, + "power-profile": { + "changed": "Profîla hêzê hate guhertin", + "profile-name": "{profîl}" + }, + "theming-processor-failed": { + "desc-generic": "Şaşetiyek di dema pêvajoykirina şablonan de rû da", + "title": "Pêvajoya şablonê têk çû" + }, + "vpn": { + "connected": "Hat girêdan bi '{name}' re", + "disconnected": "Ji '{name}' qut bû" + }, + "wallpaper-colors": { + "disabled": "Rengên dîwêr hatine bêbandorkirin", + "enabled": "Rengên dîwêr hatine çalak kirin", + "label": "Afirandina rengên dîwêr" + }, + "wifi": { + "connected": "Bi '{ssid}' ve hat girêdan", + "connection-failed": "Girêdan têk çû", + "connection-timeout": "Dema girêdanê qediya", + "disconnected": "Ji '{ssid}' qut bû", + "incorrect-password": "Borînpeyva şaş e", + "network-not-found": "Tor nehat dîtin" + } + }, + "tooltips": { + "add-widget": "Sepanokê tevlî bike", + "bluetooth-devices": "Amûrên Bluetooth", + "brightness-at": "Ronahî: {brightness}%", + "click-to-start-recording": "Tomarkarê dîmenderê (dest bi tomarkirinê bike)", + "click-to-stop-recording": "Tomarkarê dîmenderê (tomarkirinê rawestîne)", + "collapse": "Darika kêlelê veşêre", + "copy-address": "Navnîşana kopî bike", + "delete-notification": "Agahdariyê jê bibe", + "do-not-disturb-enabled": "Dengê dernexîne", + "expand": "Darika kêlekê fereh bike", + "forget-network": "Torê ji bîr bike", + "grid-view": "Dîtina torî", + "hidden-files-hide": "Pelên veşartî", + "hide-unnamed-devices": "Veşêre cîhazên bê nav", + "home": "Dîrektoriya malê", + "input-muted": "Guherandina bêdengkirina têketinê", + "keep-awake": "Hişyar bimîne", + "keyboard-layout": "Şêwaza kilîtdankê {layout}", + "list-view": "Dîtina lîsteyê", + "manage-vpn": "Girêdanên VPN", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "Jimara herî pir a sepanokan hat gihîştin", + "microphone-volume-at": "Dengê mîkrofonê: {volume}%", + "move-to-section": "Here'ke {section} here", + "mute": "Bêdeng bike", + "next-media": "Strana pêş", + "next-month": "Meha bê", + "night-light-not-installed": "Ronahiya şevê (tune)", + "noctalia-performance-enabled": "Noctalia moda performansê", + "open-annotation-tool": "Bi amûra şîrovekirinê veke", + "open-control-center": "Navenda kontrolê", + "open-notification-history-enable-dnd": "Dîroka agahdariyê", + "open-settings": "Sazkarî", + "open-tray-dropdown": "Trayê sîstemê", + "output-muted": "Deng vedengê vemirîne/veke", + "power-profile": "Profîla hêzê ya {profile}", + "previous-media": "Pêşiya şopê", + "previous-month": "Meha borî", + "refresh-devices": "Amûran rojane bike", + "refresh-wallhaven": "Nûkirina encamên Wallhavenê", + "refresh-wallpaper-list": "Nûkirina lîsteya dîwêr", + "remove-widget": "Rakire amûrekê", + "search-close": "Lêgerînê bigire", + "session-menu": "Menuya danişînê", + "show-all-devices": "Hemû amûran nîşan bide", + "switch-to-dark-mode": "Moda tarî", + "switch-to-light-mode": "Awaya ronahî", + "unmute": "Bêdeng neke", + "up": "Rêgeha resen", + "volume-at": "Dengê deranê: {volume}%", + "wallpaper-selector": "Hilbijêrê wêneyê dîwêr" + }, + "wallpaper": { + "browse": { + "empty-directory": "Ev peldank vala ye.", + "go-root": "Biçe peldanka sereke ya dîwêr", + "go-up": "Biçe peldanka dê û bav", + "go-up-hint": "Ji bo ku hûn ber bi jor ve biçin, bişkoka paş bikar bînin." + }, + "configure-directory": "Rêgeha wêneyê dîwêrê xwe bi wêneyan re bi rêkxistin bike.", + "fill-modes": { + "crop": "Birîn (Dagirtin)", + "fit": "Guncav (Tê de)", + "repeat": "Dubarekirin (Tile)", + "stretch": "Dirêjkirin" + }, + "no-match": "Lihevhatin nehat dîtin.", + "no-wallpaper": "Wêneyê dîwêr nehat dîtin.", + "panel": { + "apikey-help": "Pêdivî bi kilîta API heye ji bo gihîştina naveroka NSFW.", + "apikey-label": "Kilîta API", + "apikey-managed-by-env": "Bi rêya guherbara hawîrdorê ya NOCTALIA_WALLHAVEN_API_KEY tê birêvebirin.", + "apikey-placeholder": "API Key-a Wallhavenê xwe binivîse", + "categories-anime": "Anîme", + "categories-label": "Beş", + "categories-people": "Mirov", + "order-asc": "Berjor", + "order-desc": "Berjêr", + "order-label": "Ferman", + "purity-label": "Parzûna naverokê", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Sketchy", + "ratios-all-portrait": "Hemû portre", + "ratios-all-wide": "Hemû fereh", + "ratios-any": "Her", + "ratios-label": "Rêjeya aliyê", + "resolution-atleast": "Herî kêm", + "resolution-exact": "Rast", + "resolution-label": "Çareserî", + "resolution-mode-label": "Awa", + "solid-color-tooltip": "Zemînek bi rengê zexm", + "sorting-date-added": "Dîroka tevlîkirinê", + "sorting-favorites": "Bijarte", + "sorting-label": "Rêz bike bi ya", + "sorting-relevance": "Girîngî", + "sorting-toplist": "Lîsteya sereke", + "sorting-views": "Dîtin", + "source-label": "Çavkanî", + "source-wallhaven": "Wallhaven", + "title": "Hilbijêrê wêneya dîwêr", + "wallhaven-settings-title": "Sazkariyên Wallhaven" + }, + "transitions": { + "disc": "Dîsk", + "fade": "Wendabûn", + "stripes": "xêz", + "wipe": "Rake" + }, + "try-different-search": "Lêgerîneke cûda bi kar bîne.", + "wallhaven": { + "loading": "Wêneyên dîwaran tên barkirin...", + "no-results": "Wêneyên dîwaran nehatin dîtin. Lêgerîneke cûda bi kar bîne.", + "page": "{current} ji {total}", + "page-prefix": "Rûpel", + "page-suffix": "ji {total}" + } + }, + "weather": { + "clear-sky": "Ezmanê zelal", + "drizzle": "\nBaran", + "fog": "Mij", + "mainly-clear": "Bi piranî zelal", + "overcast": "Ewrîn", + "partly-cloudy": "Nîv ewrîn", + "rain-showers": "Barîna baranê", + "snow": "Berf", + "thunderstorm": "Bahoza bruskî" + }, + "widgets": { + "color-picker": { + "palette-description": "Ji rêzek fereh ji rengên pêşdanasînkirî hilbijêre.", + "palette-label": "Destgeh", + "palette-theme-colors": "Gihîştina bilez bo destgeha rengên rûkara te.", + "title": "Hilbijêrê rengê" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm bi tîpên biçûk", + "ampm-uppercase": "AM/PM bi tîpên mezin", + "common-12hour-time-minutes": "12-demjimêr bi xulekan", + "common-24hour-time-minutes": "24-demjimêr bi xulekan", + "common-24hour-time-seconds": "24-demjimêr bi çirkeyan", + "common-european-date": "Awaya demê ya Ewropî", + "common-iso-date": "Awaya demê ya ISO", + "common-us-date": "Awaya demê ya DYE", + "common-weekday-date": "Roja hefteyê bi demê", + "common-weekday-month-day": "Rojên hefteyê, meh û roj", + "day-abbreviated": "Navê rojê ya kurtkirî", + "day-full": "Navê rojê ya tevahî", + "day-leading-zero": "Roj bi sifira destpêkê (01-31)", + "day-no-leading-zero": "Roj bê sifira destpêkê (1-31)", + "hour-leading-zero": "Demjimêr bi sifira destpêkê (00-23) — 24-awaya demjimêrê", + "hour-no-leading-zero": "Demjimêr bê sifira destpêkê (0-23) — 24-awaya demjimêrê", + "minute-leading-zero": "Xulek bi sifira destpêkê (00-59)", + "minute-no-leading-zero": "Xulek bê sifira destpêkê (0-59)", + "month-abbreviated": "Navê meha kurtkirî", + "month-full": "Navê mehê yê tevahî", + "month-number-leading-zero": "Meh wekî jimarek bi sifira destpêkê (01-12)", + "month-number-no-zero": "Meh wekî jimarek bê sifira destpêkê (1-12)", + "second-leading-zero": "Çirke bi sifira destpêkê (00-59)", + "second-no-leading-zero": "Çirke bê sifira destpêkê (0-59)", + "timezone-abbreviation": "Kurteriya herêma demê", + "year-four-digit": "Sal wekî çar-jimar", + "year-two-digit": "Sal wekî du-jimar (00-99)" + }, + "file-picker": { + "item": "hêman", + "items": "hêman", + "search-placeholder": "Li pel û peldankan bigere...", + "select-current": "A heyî hilbijêre", + "select-file": "Pelê hilbijêre", + "select-folder": "Peldankê hilbijêre", + "selected": "Hilbijartî:", + "title": "Hilbijêrê pelê" + }, + "file-picker-title": "Pelekê hilbijêre", + "icon-picker": { + "title": "Hilbijêrê îkonê" + }, + "tooltip-placeholder": "Cihgir" + }, + "wifi": { + "panel": { + "action-required": "Çalakî pêwîst e", + "available-interfaces": "Navrûyên heyî", + "available-networks": "Torên heyî", + "disabled": "Wi‑Fi neçalak e", + "disconnecting": "Tê qutkirin...", + "dns": "DNS", + "enable-message": "Wi‑Fi çalak bike da ku torên heyî bibînî.", + "enter-password": "Borînpeyva Wi‑Fi têxîne...", + "forget": "Ji bîr bike", + "forget-network": "Vê torê ji bîr bike", + "forgetting": "Tê jibîrkirin...", + "interface": "Navrûya torê", + "internet-connected": "Înternet hat girêdan", + "internet-limited": "Înternet tune ye", + "internet-status": "Rewşa înternetê", + "ipv4": "IPv4", + "known-networks": "Torên nas", + "link-speed": "Lêza girêdanê", + "no-ethernet-devices": "Amûrên Ethernetê nehatin dîtin", + "no-networks": "Torên Wi‑Fi nehatin dîtin", + "saved": "Hat tomarkirin", + "scan-again": "Dîsa bigere", + "searching": "Ji bo toran bigere...", + "title": "Wi-Fi" + }, + "signal": { + "excellent": "Bihêz e", + "fair": "Navîn e", + "good": "Baş e", + "poor": "Lawaz e" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/nl.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/nl.json new file mode 100644 index 0000000..8cb2029 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/nl.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "Geschiedenis wissen", + "close-app": "Sluit {app}", + "connect-vpn": "Verbinding maken met {name}", + "cycle-visualizer": "Cyclusvisualisatie", + "disable-bluetooth": "Bluetooth uitschakelen", + "disable-dnd": "'Niet storen' uitschakelen", + "disable-wifi": "Wi-Fi uitschakelen", + "disconnect-vpn": "Verbinding met {name} verbreken", + "dock-settings": "Dock-instellingen", + "enable-bluetooth": "Bluetooth inschakelen", + "enable-dnd": "'Niet storen' inschakelen", + "enable-wifi": "Wi-Fi inschakelen", + "launcher-settings": "Launcher instellingen", + "lower-to-bottom": "Laagste naar beneden", + "open-calendar": "Open agenda", + "open-display-settings": "Beeldscherminstellingen", + "open-launcher": "Launcher openen", + "open-settings": "Instellingen openen", + "raise-to-top": "Naar boven halen", + "random-wallpaper": "Willekeurige achtergrond", + "run-custom-command": "Aangepast commando uitvoeren", + "show-active-only": "Alleen actieve weergeven", + "show-all": "Alles tonen", + "toggle-mute": "Dempen aan/uit", + "widget-settings": "Widgetinstellingen" + }, + "authentication": { + "error": "Authenticatiefout", + "failed": "Authenticatie mislukt" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Pas themakleuren toe op het pictogram van het actieve venster.", + "hide-mode-description": "Bepaalt hoe de widget zich gedraagt wanneer er geen actief venster is.", + "scrolling-mode-description": "Bepaalt wanneer tekstscrollen wordt ingeschakeld voor lange venstertitels.", + "show-app-icon-description": "Toon het applicatiepictogram naast de venstertitel.", + "show-app-icon-label": "App-pictogram tonen", + "show-app-text-description": "Toon de titel van het actieve venster.", + "show-app-text-label": "Venstertitel weergeven" + }, + "audio-visualizer": { + "color-name-description": "Selecteer de kleur voor de visualizer.", + "color-name-label": "Vulkleur", + "height-description": "Aangepaste componentbreedte.", + "hide-when-idle-description": "Indien ingeschakeld wordt de visualizer verborgen, tenzij een speler actief afspeelt.", + "hide-when-idle-label": "Verbergen wanneer geen media speelt", + "width-description": "Aangepaste componentbreedte." + }, + "battery": { + "device-default": "Standaard (weergaveapparaat)", + "device-description": "Selecteer welk accuapparaat u wilt weergeven.", + "device-label": "Accuapparaat", + "display-mode-description": "Kies hoe de batterij wordt weergegeven in de balk.", + "display-mode-graphic": "Grafische accu", + "display-mode-graphic-clean": "Grafische accu (geen %)", + "display-mode-icon-always": "Pictogram - Altijd % tonen", + "display-mode-icon-hover": "Icoon - Tonen bij hover", + "display-mode-icon-only": "Alleen pictogram", + "hide-if-idle-description": "Verberg de widget wanneer de accu niet wordt opgeladen of ontladen.", + "hide-if-idle-label": "Verbergen indien inactief", + "hide-if-not-detected-description": "Verberg de widget als er geen accu op het systeem wordt gedetecteerd.", + "hide-if-not-detected-label": "Verbergen indien niet gedetecteerd", + "low-battery-threshold-label": "Waarschuwingsdrempel voor lage accu", + "show-noctalia-performance-description": "Toon de Noctalia-prestatiemodus-schakelaar in het accupaneel.
Schakelt schaduwen en animaties in Noctalia uit om het resourcegebruik te verminderen.", + "show-noctalia-performance-label": "Noctalia-Performance-schakelaar tonen", + "show-power-profile-description": "Toon de keuze van het stroomprofiel in het accupaneel.", + "show-power-profile-label": "Stroomprofielbediening tonen" + }, + "brightness": { + "apply-all-description": "Helderheid voor alle monitoren wijzigen bij gebruik van het scrollwiel.", + "apply-all-label": "Schuifwijzigingen toepassen op alle monitoren" + }, + "clock": { + "clock-display-description": "Pas de weergave van je klok aan door tokens uit onderstaande lijst toe te voegen. Om de 12-uursnotatie te gebruiken, moet je de 'AP'-token opnemen.", + "clock-display-label": "Klokweergave", + "custom-font-description": "Selecteer een aangepast lettertype voor de klokweergave.", + "custom-font-label": "Aangepast lettertype", + "custom-font-placeholder": "Selecteer aangepast lettertype...", + "custom-font-search-placeholder": "Lettertypen zoeken...", + "horizontal-bar-description": "Tip: gebruik \\n om een regeleinde te maken.", + "horizontal-bar-label": "Horizontale balk", + "preview": "Voorbeeld", + "tooltip-format-description": "Opmaakreeks voor de tooltip die wordt weergegeven bij het zweven over de klok. Laat leeg om de standaard tooltip te gebruiken.", + "tooltip-format-label": "Tooltip-opmaak", + "use-custom-font-description": "Overschrijf het standaardlettertype met een aangepast lettertype voor de klok.", + "use-custom-font-label": "Aangepast lettertype gebruiken", + "use-monospaced-font-description": "Indien ingeschakeld gebruikt de klok het monospace-lettertype.", + "use-monospaced-font-label": "Monospace-lettertype gebruiken", + "vertical-bar-description": "Gebruik een spatie om elk deel op een nieuwe regel te plaatsen.", + "vertical-bar-label": "Verticale balk" + }, + "control-center": { + "browse-file": "Bestand kiezen", + "browse-library": "Bibliotheek doorbladeren", + "color-selection-description": "Themakleuren toepassen op pictogrammen.", + "enable-colorization-description": "Schakel kleurtoepassing voor pictogram in, themakleuren toepassend.", + "icon-description": "Selecteer een pictogram uit de bibliotheek of een aangepast bestand.", + "select-custom-icon": "Aangepast pictogram selecteren", + "use-distro-logo-description": "Gebruik het logo van je distributie in plaats van een aangepast pictogram.", + "use-distro-logo-label": "Distributielogo gebruiken in plaats van pictogram" + }, + "custom-button": { + "collapse-condition-description": "Als de uitvoer overeenkomt met deze waarde, wordt de knop samengevouwen.", + "collapse-condition-label": "Inklapvoorwaarde", + "color-selection-description": "Pas themakleuren toe op pictogram en tekst.", + "default-tooltip": "Aangepaste knop, configureren in instellingen", + "display-command-output-description": "Voer een commando in dat regelmatig wordt uitgevoerd. De eerste regel van de uitvoer wordt als tekst weergegeven.", + "display-command-output-label": "Commando-uitvoer weergeven", + "display-command-output-stream-description": "Voer een commando in dat continu wordt uitgevoerd.", + "dynamic-text": "Dynamische tekst", + "enable-colorization-description": "Kleurtoepassing inschakelen voor het aangepaste knoppictogram en de tekst, waarbij themakleuren worden toegepast.", + "enable-colorization-label": "Kleurtoepassing inschakelen", + "general-tooltip-text-description": "Aangepaste tekst om weer te geven in de tooltip van de knop.", + "general-tooltip-text-label": "Aangepaste tooltiptekst", + "hide-mode-always-expanded": "Altijd uitgeklapt", + "hide-mode-description": "Bepaalt de zichtbaarheid van de widget wanneer het commando geen uitvoer heeft.", + "hide-mode-expand-with-output": "Uitklappen wanneer er uitvoer is", + "hide-mode-label": "Verbergmodus", + "hide-mode-max-transparent": "Maximaal uitgeklapt maar transparant", + "icon-description": "Selecteer een pictogram uit de bibliotheek.", + "ipc-identifier-description": "Unieke identificatie voor IPC-opdrachten. Gebruik deze identificatie met 'qs -c noctalia-shell ipc call cb [actie] [identificatie]' om deze knop via IPC te bedienen.", + "ipc-identifier-label": "IPC-identificatie", + "left-click-description": "Commando dat wordt uitgevoerd wanneer met de linkermuisknop op de knop wordt geklikt.", + "left-click-label": "Linkermuisklik", + "left-click-update-text": "Tekst bijwerken bij linksklik", + "max-text-length-horizontal-description": "Maximaal aantal tekens dat moet worden weergegeven in horizontale balk (0 om tekst te verbergen).", + "max-text-length-horizontal-label": "Max. tekstlengte (horizontaal)", + "max-text-length-vertical-description": "Maximaal aantal tekens dat moet worden weergegeven in verticale balk (0 om tekst te verbergen).", + "max-text-length-vertical-label": "Max. tekstlengte (verticaal)", + "middle-click-description": "Commando dat wordt uitgevoerd wanneer met de middelste muisknop op de knop wordt geklikt.", + "middle-click-label": "Middelste muisklik", + "middle-click-update-text": "Tekst bijwerken bij middelste muisklik", + "parse-json-description": "Interpreteer de commando-uitvoer als JSON-object om tekst en pictogram dynamisch in te stellen.", + "parse-json-label": "Uitvoer als JSON parseren", + "refresh-interval-description": "Interval in milliseconden.", + "refresh-interval-label": "Verversingsinterval", + "right-click-description": "Commando dat wordt uitgevoerd wanneer met de rechtermuisknop op de knop wordt geklikt.", + "right-click-label": "Rechtermuisklik", + "right-click-update-text": "Tekst bijwerken die wordt weergegeven bij rechtsklikken", + "show-exec-tooltip-description": "Tooltips met commando details weergeven (linker-/rechter-/middenklik, scrollwiel).", + "show-exec-tooltip-label": "Opdracht-tooltips weergeven", + "show-icon-description": "Schakelt de zichtbaarheid van het widgetpictogram.", + "show-icon-label": "Pictogram weergeven", + "show-text-tooltip-description": "Toon tooltips met de uitvoer van het tekstcommando.", + "show-text-tooltip-label": "Dynamische teksttooltips weergeven", + "text-stream-description": "Gestreamde regels uit het commando worden als tekst op de knop weergegeven.", + "text-stream-label": "Stream", + "wheel-description": "Commando om uit te voeren wanneer het scrollwiel wordt gebruikt.
Gebruik $delta voor de scrollwiel-delta in het commando.", + "wheel-down": "Scrollwiel omlaag", + "wheel-down-description": "Commando om uit te voeren wanneer het scrollwiel omlaag wordt bewogen.", + "wheel-down-label": "Scrollwiel omlaag commando", + "wheel-label": "Scrollwiel", + "wheel-mode-separate-description": "Afzonderlijke commando's inschakelen voor scrollwiel omhoog en omlaag.", + "wheel-mode-separate-label": "Afzonderlijke scrollwielcommando's", + "wheel-up": "Scrollwiel omhoog", + "wheel-up-description": "Commando om uit te voeren wanneer het scrollwiel omhoog wordt bewogen.", + "wheel-up-label": "Scrollwiel omhoog commando", + "wheel-update-text": "Weergegeven tekst bij scrollen bijwerken" + }, + "keyboard-layout": { + "show-icon-description": "Toon het toetsenbordindeling-icoon." + }, + "lock-keys": { + "hide-when-off-description": "Verberg de indicator wanneer de toets niet actief is.", + "hide-when-off-label": "Verbergen wanneer uitgeschakeld", + "show-caps-lock-description": "Toon de status van caps lock.", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "Toon de status van num lock.", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "Toon de status van scroll lock.", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "Schakel een ruimtebesparende lay-out in voor het mediaplayerpaneel.", + "compact-mode-label": "Compacte modus", + "hide-mode-description": "Bepaalt hoe de widget zich gedraagt wanneer er geen media wordt afgespeeld.", + "max-width-description": "Stelt de maximale horizontale grootte van de widget in. De widget krimpt mee met kortere inhoud.", + "no-active-player": "Geen actieve speler", + "panel-section-description": "Configureer het uiterlijk en gedrag van het mediaplayerpaneel.", + "panel-section-label": "Mediaplayerpaneel", + "scrolling-mode-description": "Bepaalt wanneer tekstscrollen wordt ingeschakeld voor lange tracktitels.", + "scrolling-mode-label": "Scrollmodus", + "show-album-art-description": "Toon de albumhoes van het huidige nummer.", + "show-album-art-label": "Albumhoes tonen", + "show-artist-first-description": "Toon artiest - titel in plaats van titel - artiest.", + "show-artist-first-label": "Toon eerst de artiest", + "show-progress-ring-description": "Toon een circulaire voortgangsindicator die het bestandsspoor voortgang toont.", + "show-progress-ring-label": "Voortgangscirkel tonen", + "show-visualizer-description": "Toon een audiovisualizer wanneer muziek wordt afgespeeld.", + "show-visualizer-label": "Visualizer tonen", + "use-fixed-width-description": "Indien ingeschakeld gebruikt de widget altijd de maximale breedte in plaats van zich aan te passen aan de inhoud.", + "use-fixed-width-label": "Vaste breedte gebruiken", + "visualizer-type-description": "Kies de stijl van de audiovisualizer.", + "visualizer-type-label": "Type visualizer" + }, + "notification-history": { + "hide-widget-when-zero-description": "Het meldingspictogram verbergen wanneer er geen meldingen zijn.", + "hide-widget-when-zero-label": "Pictogram verbergen zonder meldingen", + "hide-widget-when-zero-unread-description": "Het meldingspictogram verbergen wanneer er geen ongelezen meldingen zijn.", + "hide-widget-when-zero-unread-label": "Pictogram verbergen zonder ongelezen meldingen", + "show-unread-badge-description": "Toon een badge met het aantal ongelezen meldingen.", + "show-unread-badge-label": "Ongelezen-badge tonen", + "unread-badge-color-description": "Selecteer de kleur voor de badge van ongelezen meldingen.", + "unread-badge-color-label": "Kleur van de ongelezen badge" + }, + "section-editor": { + "placeholder": "Selecteer een widget om toe te voegen...", + "search-placeholder": "Widget zoeken..." + }, + "spacer": { + "width-description": "Breedte van de ruimte in pixels." + }, + "system-monitor": { + "compact-mode-description": "Statistieken weergeven als mini-staafdiagrammen in plaats van tekstwaarden. Voorkomt lay-outverschuiving.", + "compact-mode-label": "Compacte modus", + "cpu-cores-description": "Het gebruik van CPU-kernen individueel weergeven.", + "cpu-cores-label": "CPU-kernen", + "cpu-frequency-description": "De huidige CPU-kloksnelheid in GHz weergeven.", + "cpu-frequency-label": "CPU-frequentie weergeven", + "cpu-temperature-description": "Toon CPU-temperatuur indien beschikbaar.", + "cpu-temperature-label": "CPU-temperatuur", + "cpu-usage-description": "Toon het huidige CPU-gebruik in procent.", + "cpu-usage-label": "CPU-gebruik", + "disk-path-description": "Selecteer welk schijfkoppelpunt u wilt monitoren.", + "disk-path-label": "Schijfpad", + "gpu-temperature-description": "Toon GPU-temperatuurmetingen indien beschikbaar.", + "load-average-description": "Systeembelasting gemiddelde weergeven.", + "load-average-label": "Gemiddelde belasting", + "memory-percentage-description": "Toon geheugengebruik als percentage in plaats van absolute waarden.", + "memory-percentage-label": "Geheugen als percentage", + "memory-usage-description": "Toon informatie over huidig RAM-gebruik.", + "memory-usage-label": "Geheugengebruik", + "network-traffic-description": "Toon uploadsnelheid en downloadsnelheid van het netwerk.", + "network-traffic-label": "Netwerkverkeer", + "storage-as-percentage-description": "Toon schijfruimte als percentage in plaats van absolute waarden.", + "storage-as-percentage-label": "Schijf als percentage", + "storage-available-description": "Toont hoeveel schijfruimte beschikbaar is in plaats van hoeveel er wordt gebruikt.", + "storage-available-label": "Beschikbare schijfruimte", + "storage-usage-description": "Toon informatie over schijfruimtegebruik.", + "storage-usage-label": "Opslaggebruik", + "swap-usage-description": "Toon swap geheugengebruik.", + "swap-usage-label": "Swapgebruik", + "use-monospace-font-description": "Gebruik een monospace lettertype voor een consistente tekenbreedte.", + "use-monospace-font-label": "Lettertype met vaste breedte", + "use-padding-description": "Vult de tekstwaarden aan met voorloopspaties om lay-outverschuiving te voorkomen.", + "use-padding-description-disabled-monospace-font": "Een monospace lettertype is nodig voor deze functie.", + "use-padding-description-disabled-vertical": "De verticale taakbalk ondersteunt geen tekstvulling.", + "use-padding-label": "Tekstopvulling" + }, + "taskbar": { + "colorize-icons-description": "Pas themakleuren toe op taakbalkpictogrammen.", + "hide-mode-description": "Bepaalt hoe de widget zich gedraagt wanneer er geen overeenkomende vensters zijn.", + "hide-mode-label": "Verbergmodus", + "icon-scale-description": "Stelt de schaalfactor in voor taakbalkpictogrammen.", + "icon-scale-label": "Icoon schaling", + "max-width-description": "Maximale breedte van de taakbalk als percentage van de schermbreedte.", + "max-width-label": "Maximale breedte", + "only-active-workspaces-description": "Toon alleen apps van actieve werkruimten.", + "only-active-workspaces-label": "Alleen van actieve werkruimten", + "only-same-monitor-description": "Toon alleen apps van de monitor waar de balk zich bevindt.", + "only-same-monitor-label": "Alleen vanaf dezelfde monitor", + "show-pinned-apps-description": "Toon vastgemaakte apps van het dock in de taakbalk.", + "show-pinned-apps-label": "Toon vastgemaakte apps", + "show-title-description": "Venster titels weergeven in de taakbalk.", + "show-title-description-disabled": "Verticale taakbalk ondersteunt geen titelopties.", + "show-title-label": "Titel weergeven", + "smart-width-description": "Pas automatisch de breedte van items aan op basis van het aantal items.", + "smart-width-label": "Slimme breedte", + "title-width-description": "Stel de breedte van venstertitels in de taakbalk in (in pixels).", + "title-width-label": "Titelbreedte", + "title-width-reset-tooltip": "Titelbreedte resetten" + }, + "tray": { + "chevron-color-description": "Themakleuren toepassen op het pijl-icoon van de lade.", + "chevron-color-label": "Pijlkleur", + "colorize-icons-description": "Pas themakleuren toe op systeemvakpictogrammen.", + "colorize-icons-label": "Pictogrammen inkleuren", + "drawer-enabled-description": "Indien ingeschakeld worden niet-vastgemaakte systeemvakitems in een lade weergegeven.
Indien uitgeschakeld worden alle items inline getoond.", + "drawer-enabled-label": "Lade inschakelen", + "hide-passive-description": "Indien ingeschakeld, worden tray-items met de status 'Passief' verborgen.", + "hide-passive-label": "Verberg passieve items" + }, + "volume": { + "display-mode-description": "Kies hoe je wilt dat deze waarde wordt weergegeven." + }, + "workspace": { + "character-count-description": "Aantal tekens dat wordt weergegeven van werkruimtenamen (1-10).", + "character-count-label": "Aantal tekens", + "empty-color-description": "Stel de achtergrondkleur in voor lege Workspaces.", + "empty-color-label": "Kleur van lege Workspace", + "enable-scrollwheel-description": "Schakel tussen werkruimtes met het muiswiel.", + "enable-scrollwheel-label": "Scroll om tussen werkruimtes te schakelen", + "focused-color-description": "Stel de achtergrondkleur in voor de gefocuste Workspace.", + "focused-color-label": "Kleur van de gefocuste Workspace", + "follow-focused-screen-description": "Werkruimten weergeven van het momenteel gefocuste scherm, in plaats van het scherm waar de balk zich bevindt.", + "follow-focused-screen-label": "Gefocust scherm volgen", + "font-weight-description": "Stel het visuele gewicht in voor tekst binnen de werkruimte.", + "font-weight-label": "Lettertypegewicht", + "grouped-border-opacity-description": "Stel het dekkingsniveau in voor de randen van de werkruimtecontainer.", + "grouped-border-opacity-label": "Randdekking", + "hide-unoccupied-description": "Werkruimten zonder vensters niet weergeven.", + "hide-unoccupied-label": "Ongebruikte verbergen", + "label-mode-description": "Kies hoe labels van werkruimten worden weergegeven.", + "label-mode-label": "Labelmodus", + "occupied-color-description": "Stel de achtergrondkleur in voor bezette Workspaces.", + "occupied-color-label": "Kleur van bezette workspace", + "pill-size-description": "Pas de grootte van de werkruimte-pillen aan (50%-100%).", + "pill-size-label": "Pilgrootte", + "reverse-scrolling-description": "Keer de richting van het wisselen van werkruimtes om bij het scrollen.", + "reverse-scrolling-label": "Omgekeerd scrollen", + "show-applications-description": "Toon applicatiepictogrammen in elke werkruimte.", + "show-applications-hover-description": "Toon applicatiepictogrammen in elke werkruimte wanneer de werkruimte wordt aangewezen.", + "show-applications-hover-label": "Toon applicaties bij hoveren", + "show-applications-label": "Toon applicaties", + "show-badge-description": "Toon de werkomgevingnummerbadge in gegroepeerde modus.", + "show-badge-label": "Werkruimtebadge weergeven", + "show-labels-only-when-occupied-description": "Toon alleen werkruimtelabels wanneer ze vensters bevatten.", + "show-labels-only-when-occupied-label": "Toon labels alleen wanneer bezet", + "unfocused-icons-opacity-description": "Stel het dekkingsniveau in voor app-pictogrammen die niet in focus zijn.", + "unfocused-icons-opacity-label": "Ondoorzichtige, niet-focusseerbare pictogrammen" + } + }, + "battery": { + "all-batteries": "Alle accu's (gecombineerd)", + "battery-health": "Accuconditie", + "battery-level": "Accuniveau", + "capacity-level": "Capaciteit: {level}", + "charging-rate": "Laadsnelheid: {rate} W", + "discharging-rate": "Ontlaadsnelheid: {rate} W", + "health": "Gezondheid: {percent}%", + "inhibit-idle-description": "Houdt het systeem wakker.", + "no-battery-detected": "Geen accu gedetecteerd", + "plugged-in": "Op netstroom", + "power-profile": "Energieprofiel", + "time-left": "Resterende tijd: {time}", + "time-until-full": "Tijd tot vol: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Beschikbare apparaten", + "blocked": "Geblokkeerd", + "connected-devices": "Verbonden apparaten", + "device-address": "Apparaatadres", + "disabled": "Bluetooth is uitgeschakeld", + "discoverable": "Zichtbaar", + "enable-message": "Schakel Bluetooth in om beschikbare apparaten te zien.", + "known-devices": "Bekende apparaten", + "no-devices": "Geen apparaten beschikbaar", + "paired-devices": "Gekoppelde apparaten", + "pairing-mode": "Zorg ervoor dat je apparaat in koppelmodus staat.", + "scanning": "Zoeken naar apparaten...", + "signal-text-excellent": "Signaal: Uitstekend", + "signal-text-fair": "Signaal: Redelijk", + "signal-text-good": "Signaal: Goed", + "signal-text-poor": "Signaal: Zwak", + "signal-text-unknown": "Signaal: Onbekend", + "signal-text-very-poor": "Signaal: Zeer slecht" + } + }, + "changelog": { + "error": { + "fetch-failed": "Kan changeloggegevens niet laden. Probeer het later opnieuw.", + "rate-limit": "GitHub-limiet bereikt. Probeer het over enkele minuten opnieuw." + }, + "panel": { + "buttons-discord": "Word lid van onze Discord", + "buttons-dismiss": "OK", + "buttons-feedback": "Geef feedback", + "empty": "Er zijn nog geen release-opmerkingen beschikbaar.", + "highlight-title": "Hoogtepunten", + "released": "Uitgebracht op {date}", + "subtitle-fresh": "Bedankt voor het installeren van Noctalia! Dit zit er in deze build.", + "subtitle-updated": "Bijgewerkt vanaf {previousVersion}", + "title": "Wat is er nieuw in {version}", + "version": "Versie {version}", + "version-new-user": "Nieuwe installatie" + } + }, + "common": { + "actions": "Acties", + "active": "Actief", + "add": "Toevoegen", + "appearance": "Uiterlijk", + "apply": "Toepassen", + "auto-connect": "Automatisch verbinden", + "automation": "Automatisering", + "available": "Beschikbaar", + "back": "Terug", + "battery": "Accu", + "behavior": "Gedrag", + "bluetooth": "Bluetooth", + "brightness": "Helderheid", + "browse": "Bladeren", + "calendar": "Kalender", + "calendar-panel": "Kalenderpaneel", + "cancel": "Annuleren", + "cards": "Kaarten", + "charging": "Opladen", + "clear": "Wissen", + "clipboard": "Klembord", + "close": "Sluiten", + "color-muted": "Gedempt", + "colors": "Kleuren", + "command": "Commando", + "confirm": "Bevestigen", + "connect": "Verbinden", + "connected": "Verbonden", + "connecting": "Verbinden...", + "continue": "Doorgaan", + "contributors": "Medewerkers", + "copied-to-clipboard": "Gekopieerd naar klembord", + "countdown": "Aftellen", + "customize": "Aanpassen", + "date": "Datum", + "default": "Standaard", + "delete": "Verwijderen", + "devices": "Apparaten", + "disabled": "Uitgeschakeld", + "discharging": "Afvoeren", + "disconnect": "Verbreek de verbinding", + "disconnected": "Verbinding verbroken", + "disconnecting": "Verbinding verbroken...", + "display-mode": "Weergavemodus", + "documentation": "Documentatie", + "download": "Downloaden", + "duration": "Duur", + "dysfunctional": "Dysfunctioneel", + "edit": "Bewerken", + "enabled": "Ingeschakeld", + "error": "Fout", + "ethernet": "Ethernet", + "events": "Evenementen", + "execute": "Uitvoeren", + "faithful": "Getrouw", + "focus": "Focus", + "font-weight-bold": "Vet", + "font-weight-light": "Licht", + "font-weight-medium": "Medium", + "font-weight-regular": "Normaal", + "font-weight-semibold": "Semi-vet", + "frequency": "Frequentie", + "gateway": "Poort", + "general": "Algemeen", + "height": "Hoogte", + "hibernate": "Sluimeren", + "history": "Geschiedenis", + "icon": "Icoon", + "idle": "Inactief", + "info": "Info", + "input": "Invoer", + "install": "Installeren", + "installed": "Geïnstalleerd", + "interface": "Interface", + "internet": "Internet", + "keybind": "Toetsencombinatie", + "language": "Taal", + "loading": "Laden...", + "local": "Lokaal", + "location": "Locatie", + "lock": "Vergrendelen", + "logout": "Uitloggen", + "look": "Uiterlijk", + "margins": "Marges", + "media": "Media", + "media-player": "Mediaplayer", + "memory": "Geheugen", + "monitors": "Monitoren", + "move-down": "Naar beneden verplaatsen", + "move-up": "Omhoog verplaatsen", + "network": "Netwerk", + "next": "Volgende", + "night-light": "Nachtlicht", + "no": "Nee", + "no-results": "Geen resultaten", + "no-summary": "Geen samenvatting", + "none": "Geen", + "not-found": "Niet gevonden", + "notifications": "Meldingen", + "official": "Officieel", + "on-surface": "Op oppervlak", + "output": "Uitvoer", + "pair": "Koppelen", + "paired": "Gekoppeld", + "pairing": "Koppelen...", + "panels": "Panelen", + "password": "Wachtwoord", + "pause": "Pauze", + "performance": "Prestaties", + "pin": "Vastmaken", + "play": "Afspelen", + "polling": "Polling", + "position": "Positie", + "previous": "Vorige", + "primary": "Primair", + "random": "Willekeurig", + "reboot": "Herstarten", + "reboot-to-uefi": "Herstarten naar UEFI", + "record": "Opname starten", + "refresh": "Vernieuwen", + "required": "(vereist)", + "reset": "Resetten", + "reset-to-default": "Herstellen naar Standaard", + "result-count": "{count} resultaat", + "result-count-plural": "{count} resultaten", + "resume": "Hervatten", + "save": "Opslaan", + "scanning": "Scannen...", + "screen-corners": "Schermhoeken", + "search": "Zoeken", + "second": "{count} seconde", + "second-plural": "{count} seconden", + "secondary": "Secundair", + "security": "Beveiliging", + "select": "Selecteer", + "select-color": "Kleur selecteren", + "select-color-description": "Themakleuren toepassen voor nadruk.", + "select-icon-color": "Icoonkleur selecteren", + "settings": "Instellingen", + "shortcuts": "Snelkoppelingen", + "shutdown": "Afsluiten", + "signal": "Signaal", + "sound": "Geluid", + "sources": "Bronnen", + "start": "Begin", + "stop": "Stop", + "supporters": "Ondersteuners", + "suspend": "Onderbreken", + "templates": "Sjablonen", + "tertiary": "Tertiair", + "test": "Test", + "thresholds": "Drempels", + "title": "Titel", + "toast": "Toast", + "trusted": "Vertrouwd", + "uninstall": "Verwijderen", + "unknown": "Onbekend", + "unpair": "Ontkoppelen", + "unpin": "Losmaken", + "update": "Update", + "upload": "Uploaden", + "username": "Gebruikersnaam", + "userspace-reboot": "Gebruikersruimte herstarten", + "version": "Versie", + "vibrant": "Levendig", + "visualizer": "Visualiseerder", + "volume": "Volume", + "volumes": "Volumes", + "wallpaper": "Achtergrond", + "weather": "Weer", + "weather-loading": "Weer laden...", + "weather-no-location": "Stel een locatie in de Instellingen in om het weer te zien", + "week": "Week", + "widgets": "Widgets", + "width": "Breedte", + "wifi": "Wi-Fi", + "windows": "Vensters", + "yes": "Ja" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Energieprofiel", + "tooltip-disabled": "Installeer power-profiles-daemon om energieprofielen te gebruiken" + }, + "wifi": { + "label-disconnected": "Wi-Fi niet verbonden", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Altijd verbergen", + "always-show": "Altijd weergeven", + "force-open": "Openbreken", + "on-hover": "Bij hover" + }, + "hide-modes": { + "auto-hide": "Automatisch verbergen", + "hidden": "Verbergen indien leeg", + "idle": "Verbergen indien inactief", + "non-exclusive": "Niet-exclusief", + "transparent": "Transparant wanneer leeg", + "visible": "Altijd zichtbaar" + }, + "launcher": { + "categories": { + "all": "Alle", + "audiovideo": "Audio & video", + "chat": "Chat", + "development": "Ontwikkeling", + "education": "Onderwijs", + "emoji-activity": "Activiteit", + "emoji-animals": "Dieren", + "emoji-flags": "Vlaggen", + "emoji-food": "Eten en drinken", + "emoji-nature": "Natuur", + "emoji-objects": "Objecten", + "emoji-people": "Mensen en lichaam", + "emoji-recent": "Recent", + "emoji-symbols": "Symbolen", + "emoji-travel": "Reizen en plaatsen", + "game": "Spellen", + "graphics": "Grafische weergave", + "misc": "Diversen", + "office": "Kantoor", + "pinned": "Vastgemaakt", + "system": "Systeem", + "webbrowser": "Webbrowser" + }, + "providers": { + "applications": "Applicaties", + "calculator": "Rekenmachine", + "calculator-deprecated": "De >calc opdracht is verouderd en zal binnenkort verwijderd worden. Typ wiskundige uitdrukkingen in plaats daarvan direct in de zoekopdracht.", + "calculator-description": "Rekenmachine — bereken wiskundige expressies", + "calculator-enter-expression": "Voer een wiskundige expressie in", + "calculator-press-enter-to-copy": "Druk op Enter om het resultaat te kopiëren", + "clipboard": "Klembordgeschiedenis", + "clipboard-clear-description": "Klembordgeschiedenis wissen", + "clipboard-clear-description-full": "Alle items uit de klembordgeschiedenis verwijderen", + "clipboard-clear-history": "Klembordgeschiedenis wissen", + "clipboard-delete": "Klemborditem verwijderen", + "clipboard-history-disabled": "Klembordgeschiedenis uitgeschakeld", + "clipboard-history-disabled-description": "Schakel klembordgeschiedenis in de instellingen in of installeer cliphist", + "clipboard-loading": "Klembordgeschiedenis laden...", + "clipboard-search-description": "Zoek in klembordgeschiedenis", + "command-description": "Shell-opdrachten uitvoeren", + "emoji": "Emoji-kiezer", + "emoji-loading": "Emoji's laden...", + "emoji-loading-description": "Even geduld", + "emoji-no-recent": "Nog geen recente emoji's", + "emoji-search-description": "Zoek en kopieer emoji's", + "settings-search-description": "Zoeken en navigeren naar instellingen", + "windows-search-description": "Zoek en focus open vensters" + } + }, + "lock-screen": { + "authenticating": "Bezig met verifiëren...", + "password": "Voer je wachtwoord in...", + "restart": "Opnieuw opstarten", + "shut-down": "Uitschakelen" + }, + "notifications": { + "panel": { + "click-to-collapse": "Klikken om in te klappen", + "click-to-expand": "Klik om uit te vouwen", + "description": "Je meldingen verschijnen hier zodra ze binnenkomen.", + "no-notifications": "Geen meldingen" + }, + "range": { + "earlier": "Eerder", + "today": "Vandaag", + "yesterday": "Gisteren" + }, + "time": { + "diff-d": "1 dag geleden", + "diff-dd": "{diff} dagen geleden", + "diff-h": "1 uur geleden", + "diff-hh": "{diff} uur geleden", + "diff-m": "1 minuut geleden", + "diff-mm": "{diff} minuten geleden", + "now": "nu" + } + }, + "options": { + "bar": { + "density-comfortable": "Comfortabel", + "density-compact": "Compact", + "density-default": "Standaard", + "density-mini": "Mini", + "density-spacious": "Ruim", + "display-mode-always-visible": "Altijd zichtbaar", + "type-floating": "Zwevend", + "type-framed": "Omkaderd", + "type-simple": "Eenvoudig" + }, + "control-center": { + "quick-settings-style-classic": "Klassiek", + "quick-settings-style-modern": "Modern" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "Comfortabel", + "compact": "Compact", + "default": "Standaard" + }, + "launcher-view-mode": { + "grid": "Raster", + "list": "Lijst" + }, + "notification-density": { + "compact": "Compact", + "default": "Standaard" + }, + "scrolling-modes": { + "always": "Altijd scrollen", + "hover": "Scrollen bij hover", + "never": "Nooit scrollen" + }, + "session-menu-grid-layout": { + "grid": "Raster", + "single-row": "Enkele rij" + }, + "settings-panel-mode": { + "attached": "Paneel bevestigd aan de balk", + "centered": "Gecentreerd paneel", + "window": "Apart venster" + }, + "visualizer-types": { + "linear": "Lineair", + "mirrored": "Gespiegeld", + "wave": "Golf" + }, + "visualizer-visibility": { + "with-background": "Alleen met achtergrond" + }, + "workspace-labels": { + "index": "Index", + "index-and-name": "Index en naam", + "name": "Naam" + } + }, + "panels": { + "about": { + "become-supporter": "Word supporter", + "changelog": "Bekijk wijzigingslogboek", + "changelog-on-startup": "Toon wijzigingslogboek bij update", + "changelog-on-startup-desc": "Toon automatisch de changelog wanneer Noctalia wordt bijgewerkt.", + "contributors-description": "Een shout-out naar onze {count} geweldige bijdrager!", + "contributors-description-plural": "Een shout-out naar onze {count} geweldige bijdragers!", + "copy-info": "Kopieer info", + "debug-disabled": "Foutopsporingsmodus uitgeschakeld", + "debug-enabled": "Foutopsporingsmodus ingeschakeld", + "info-copied": "Info gekopieerd naar klembord", + "noctalia-available": "Beschikbaar:", + "noctalia-desc": "Een strakke en minimale desktopshell, met zorg gemaakt voor Wayland en gebouwd met Quickshell.", + "noctalia-git-commit": "Git commit:", + "noctalia-installed-version": "Geïnstalleerde versie:", + "noctalia-latest-version": "Nieuwste versie:", + "noctalia-qs-version": "Noctalia QS versie:", + "noctalia-title": "Noctalia-shell", + "privacy-policy": "Privacybeleid", + "support": "Steun ons", + "supporter-badge": "Supporter", + "supporters-desc": "Een enorme dank aan onze geweldige supporter!", + "supporters-desc-plural": "Enorm bedankt aan onze {count} geweldige supporters!", + "supporters-loading": "Donateurs laden...", + "system-board": "Moederbord:", + "system-cpu": "CPU:", + "system-disk": "Schijf:", + "system-gpu": "GPU:", + "system-host": "Host:", + "system-install-hint": "Installeer fastfetch om systeeminformatie te bekijken", + "system-kernel": "Kernel:", + "system-loading": "Systeeminformatie laden...", + "system-memory": "Geheugen:", + "system-monitor": "Monitor:", + "system-not-installed": "fastfetch is niet geïnstalleerd", + "system-os": "Besturingssysteem:", + "system-packages": "Pakketten:", + "system-product": "Product:", + "system-title": "Systeeminformatie", + "system-uptime": "Uptime:", + "system-wm": "WM:", + "telemetry-data-copied": "Telemetriegegevens gekopieerd naar klembord", + "telemetry-desc": "Help Noctalia te verbeteren door anonieme systeeminformatie (schermresolutie, compositor, distributie) te delen. Wordt eenmalig bij het opstarten verzonden, geen tracking, data wordt na 30 dagen automatisch verwijderd.", + "telemetry-enabled": "Anonieme systeeminformatie verzenden", + "telemetry-show-data": "Gegevens bekijken", + "telemetry-title": "Privacy", + "title": "Over", + "up-to-date": "Je bent up-to-date!", + "update-available": "Update beschikbaar", + "view-commit": "Bekijk commit op GitHub" + }, + "audio": { + "devices-desc": "Configureer beschikbare audio-invoer- en -uitvoerapparaten.", + "devices-input-device-description": "Selecteer het gewenste audio-invoerapparaat.", + "devices-input-device-label": "Invoerapparaat", + "devices-output-device-description": "Selecteer het gewenste audio-uitvoerapparaat.", + "devices-output-device-label": "Uitvoerapparaat", + "devices-title": "Audio-apparaten", + "external-mixer-description": "Voer het commando of het applicatiepad in om te starten bij het activeren van de externe audiomixerfunctie.", + "external-mixer-label": "Extern audio mixer commando", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Stel je favoriete en genegeerde media-applicaties in.", + "media-excluded-player-description": "Voeg trefwoorden toe voor spelers die het systeem moet negeren. Elk trefwoord moet op een nieuwe regel staan.", + "media-excluded-player-label": "Uitgesloten spelers", + "media-excluded-player-placeholder": "typ een substring en druk op +", + "media-frame-rate-description": "Hogere waarden zijn vloeiender maar gebruiken meer resources.", + "media-frame-rate-label": "Framesnelheid", + "media-primary-player-description": "Voer een trefwoord in om je hoofdspeler te herkennen.", + "media-primary-player-label": "Primaire speler", + "media-primary-player-placeholder": "bijv. spotify, vlc, mpv", + "media-scrolling-speed-description": "Tijd in seconden waarin de titel van begin tot eind scrolt.", + "media-scrolling-speed-label": "Scrollsnelheid", + "media-scrolling-title-description": "Schakel continu scrollen in voor lange mediatitels.", + "media-scrolling-title-label": "Scrollende titel", + "media-title": "Mediaplayers", + "on-middle-clicked-description": "Commando dat moet worden uitgevoerd wanneer er met de middelste muisknop op de knop wordt geklikt.", + "panel-applications-empty": "Er spelen momenteel geen applicaties audio af", + "spectrum-mirrored-description": "Spiegel het spectrum zodat lage frequenties in het midden zijn. Indien uitgeschakeld, zijn lage frequenties links en hoge rechts.", + "spectrum-mirrored-label": "Spiegelspectrum", + "title": "Audio", + "visualizer-type-description": "Kies een visualisatietype voor mediaweergave.", + "visualizer-type-label": "Visualisatietype", + "volumes-desc": "Pas volumeregelaars en audioniveaus aan.", + "volumes-feedback-sound-file-description": "Pad naar het geluidsbestand dat wordt afgespeeld bij het wijzigen van het volume.", + "volumes-feedback-sound-file-label": "Geluidsbestand voor volumefeedback", + "volumes-feedback-sound-file-select-title": "Selecteer volumefeedback-geluidsbestand", + "volumes-input-volume-description": "Volumeniveau van microfooninvoer.", + "volumes-mute-input-description": "Dempt de standaard audio-invoer (microfoon).", + "volumes-mute-input-label": "Audio-invoer dempen", + "volumes-mute-output-description": "Dempt de hoofd audio-uitvoer van het systeem.", + "volumes-mute-output-label": "Audio-uitvoer dempen", + "volumes-output-volume-description": "Systeembreed volumeniveau.", + "volumes-step-size-description": "Pas de stapgrootte voor volumewijzigingen aan (scrollwiel, sneltoetsen).", + "volumes-step-size-label": "Volumestapgrootte", + "volumes-volume-feedback-description": "Een feedbackgeluid afspelen bij het aanpassen van het volume.", + "volumes-volume-feedback-label": "Volume feedback geluid afspelen", + "volumes-volume-overdrive-description": "Sta toe dat het volume boven 100% wordt verhoogd. Mogelijk niet door alle hardware ondersteund.", + "volumes-volume-overdrive-label": "Volume-overdrive toestaan" + }, + "bar": { + "appearance-auto-hide-delay-description": "Tijd voordat de Bar verdwijnt nadat de muis deze verlaat.", + "appearance-auto-hide-delay-label": "Verbergvertraging", + "appearance-auto-hide-exclusive-note": "Opmerking: De exclusieve zone wordt automatisch uitgeschakeld wanneer automatisch verbergen actief is.", + "appearance-auto-show-delay-description": "Tijd voordat de balk verschijnt wanneer de muis de rand bereikt.", + "appearance-auto-show-delay-label": "Toonvertraging", + "appearance-background-opacity-description": "Stel de achtergronddoorzichtigheid specifiek in voor de balk.", + "appearance-background-opacity-label": "Balk achtergronddoorzichtigheid", + "appearance-capsule-color-description": "Kies een kleur voor balkcapsules, of gebruik geen kleur voor de standaard oppervlaktekleur.", + "appearance-capsule-color-label": "Capsulekleur", + "appearance-capsule-opacity-description": "Stel het doorzichtigheidsniveau in voor widget-achtergronden wanneer capsule wordt getoond.", + "appearance-capsule-opacity-label": "Capsule-doorzichtigheid", + "appearance-content-padding-description": "Pas de opvulling tussen de baranden en widgets aan.", + "appearance-content-padding-label": "Inhoudopvulling", + "appearance-density-description": "Pas de opvulling van de balk aan voor een compacte of ruime uitstraling.", + "appearance-density-label": "Balkdichtheid", + "appearance-desc": "Pas de uitstraling en positie van de balk aan.", + "appearance-display-mode-description": "Kies wanneer de balk zichtbaar is.", + "appearance-enable-exclusion-zone-inset-description": "Verklein de uitsluitingszone met 1 fysieke pixel, zodat gelijkgelegen vensters perfect onder de rand van de balk overlappen.", + "appearance-enable-exclusion-zone-inset-label": "Binnenmarge van de uitsluitingszone", + "appearance-floating-description": "Toon de balk weer als een zwevende 'pil'.", + "appearance-floating-label": "Zwevende balk", + "appearance-font-scale-description": "De schaal van de lettergrootte aanpassen voor tekst die in de balk wordt weergegeven.", + "appearance-font-scale-label": "Lettertypeschaal", + "appearance-frame-radius": "Binnenradius", + "appearance-frame-settings-description": "Pas de kaderdikte en binnenste hoekradius aan", + "appearance-frame-settings-label": "Kaderinstellingen", + "appearance-frame-thickness": "Dikte", + "appearance-hide-on-overview-description": "Verberg de balk en sluit panelen wanneer het compositoroverzicht actief is.", + "appearance-hide-on-overview-label": "Balk verbergen in overzicht", + "appearance-margins-description": "Pas de marges rond de zwevende balk aan.", + "appearance-margins-horizontal": "Horizontale marge", + "appearance-margins-vertical": "Verticale marge", + "appearance-outer-corners-description": "Toon naar buiten afgeronde hoeken op de balk weer.", + "appearance-outer-corners-label": "Buitenhoeken", + "appearance-position-description": "Kies waar je de balk op het scherm wilt plaatsen.", + "appearance-position-label": "Positie van de balk", + "appearance-show-capsule-description": "Widget-achtergronden tonen.", + "appearance-show-capsule-label": "Capsules tonen", + "appearance-show-on-workspace-switch-description": "De Bar automatisch kort tonen wanneer de Workspace verandert.", + "appearance-show-on-workspace-switch-label": "Balk tonen bij werkruimtewissel", + "appearance-show-outline-description": "Toon een zichtbare rand rondom elke widget.", + "appearance-show-outline-label": "Toon widget-omtrekken", + "appearance-type-description": "Kies de stijl van de balk: Eenvoudig, Zwevend of Omkaderd.", + "appearance-type-label": "Balktype", + "appearance-use-separate-opacity-description": "Maak het mogelijk om een aparte dekkingswaarde te gebruiken voor de achtergrond van de balk.", + "appearance-use-separate-opacity-label": "Gebruik afzonderlijke balkdekking", + "appearance-widget-spacing-description": "Pas de afstand tussen elke widget in de balk aan.", + "appearance-widget-spacing-label": "Widgetafstand", + "behavior-middle-click-command-description": "Commando om uit te voeren bij middelklik.", + "behavior-middle-click-command-label": "Middelklik Commando", + "behavior-middle-click-command-placeholder": "niri msg action overzicht schakelen", + "behavior-middle-click-description": "Kies wat een middenklik doet op lege gebieden van de Balk.", + "behavior-middle-click-follow-mouse-description": "Open het geselecteerde middenklik-paneel op de cursorpositie.", + "behavior-middle-click-follow-mouse-label": "Middenklik volgt muis", + "behavior-middle-click-label": "Balk middenklik actie", + "behavior-right-click-command-description": "Commando om uit te voeren bij rechtermuisklik.", + "behavior-right-click-command-label": "Rechtermuisklikopdracht", + "behavior-right-click-command-placeholder": "notify-send \"Rechtermuisklik\"", + "behavior-right-click-description": "Kies wat een rechtermuisklik doet op lege gebieden van de balk.", + "behavior-right-click-follow-mouse-description": "Open het geselecteerde rechtermuisknop-paneel op de cursorpositie.", + "behavior-right-click-follow-mouse-label": "Rechtermuisklik volgt muis", + "behavior-right-click-label": "Actie bij rechtermuisklik op de Balk", + "behavior-wheel-wrap-description": "Indien ingeschakeld, gaat het scrollen door van het laatste naar het eerste item.", + "behavior-wheel-wrap-label": "Doorlopen", + "behavior-workspace-scroll-description": "Kies wat het muiswiel doet op lege gebieden van de balk.", + "behavior-workspace-scroll-label": "Muiswielactie van de Balk", + "behavior-workspace-scroll-option-content": "Inhoud", + "behavior-workspace-scroll-option-workspace": "Werkruimte", + "monitor-configure-widgets": "Widgets configureren", + "monitor-override-settings": "Globale instellingen overschrijven", + "monitor-override-settings-description": "Gebruik aangepaste instellingen voor deze monitor.", + "monitor-reset-all": "Alles resetten", + "monitor-widgets-title": "Widgetconfiguratie - {monitor}", + "monitors-desc": "Balk op specifieke monitoren weergeven. Standaard op alle als er geen zijn gekozen.", + "monitors-desc-new": "Configureer welke monitoren de Bar weergeven en pas de instellingen per monitor aan.", + "monitors-title": "Monitorweergave", + "title": "Balk", + "tray-blacklist-description": "Voeg uitsluitingsregels voor het systeemvak toe, jokertekens (*) worden ondersteund.", + "tray-blacklist-label": "Zwarte lijst", + "tray-blacklist-placeholder": "bijv. nm-applet, Fcitx*", + "tray-pin-application": "Toepassing vastmaken", + "tray-unpin-application": "Toepassing losmaken", + "use-global-widgets": "Globale widgets gebruiken", + "widgets-desc": "Sleep widgets om hun volgorde te wijzigen. Gebruik het rechtermuisknopmenu om widgets tussen secties te verplaatsen of te verwijderen.", + "widgets-title": "Widgetpositionering" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Genereer kleurenschema's van je achtergrond. Extraheert automatisch kleuren om een samenhangend thema te creëren.", + "color-source-use-wallpaper-colors-label": "Achtergrondkleuren gebruiken", + "dark-mode-mode-description": "Schakelt automatisch tussen lichte en donkere modus.", + "dark-mode-mode-label": "Schema voor donkere modus", + "dark-mode-mode-manual": "Handmatig", + "dark-mode-mode-off": "Uit", + "dark-mode-switch-description": "Schakelt over naar een donkerder thema voor comfortabeler kijken 's nachts.", + "delete-error-description": "Verwijderen van {scheme} mislukt", + "delete-error-title": "Verwijderen mislukt", + "delete-success-description": "{scheme} succesvol verwijderd", + "delete-success-title": "Kleurenschema verwijderd", + "download-button": "Meer downloaden", + "download-downloading": "Downloaden...", + "download-empty": "Geen kleurenschema's beschikbaar", + "download-error-api-error": "API-fout: {status}", + "download-error-description": "Download van {scheme} mislukt", + "download-error-download-failed": "Download mislukt met afsluitcode: {code}", + "download-error-invalid-response": "Ongeldig API-antwoordformaat", + "download-error-no-files": "Geen bestanden gevonden voor schema", + "download-error-parse-failed": "Fout bij parseren van API-antwoord: {error}", + "download-error-rate-limit": "GitHub API-snelheidslimiet overschreden", + "download-error-title": "Download mislukt", + "download-fetching": "Beschikbare kleurenschema's ophalen...", + "download-success-description": "{scheme} succesvol gedownload", + "download-success-title": "Kleurenschema gedownload", + "download-title": "Kleurenschema's downloaden", + "method-description": { + "content": "Material Design-schema met high-fidelity kleurextractie die nauw aansluit bij de werkelijke kleuren van de broninhoud.", + "dysfunctional": "Zoals Faithful, maar kiest de op één na meest dominante kleurenfamilie als primair.", + "faithful": "Probeert dicht bij de bronkleur te blijven en tegelijkertijd een harmonieus palet te genereren.", + "fruit-salad": "Material Design-schema dat een speels, levendig palet produceert met diverse en gevarieerde tinten.", + "monochrome": "Material Design-schema met een grijswaarde met één tint en minimale chromatische inhoud.", + "muted": "Genereert zachtere, minder verzadigde tinten voor een kalme en ingetogen uitstraling.", + "rainbow": "Material Design-schema dat kleuren over het volledige spectrum genereert voor maximale kleurdiversiteit.", + "tonal-spot": "Standaard Material Design algoritme. Creëert gebalanceerde paletten met behulp van tonale variaties van de bronkleur.", + "vibrant": "Maximaliseert de kleurverzadiging voor gedurfde, opvallende paletten met levendige tinten." + }, + "predefined-desc": "Kies uit een verzameling vooraf gedefinieerde kleurenschema's.", + "predefined-generate-templates-label": "Sjablonen genereren voor vooraf gedefinieerde schema's", + "predefined-title": "Vooraf gedefinieerde kleurenschema's", + "sync-gsettings-description": "Pas het systeemthema aan de actieve lichte of donkere variant aan.", + "sync-gsettings-label": "Systeemthema synchroniseren", + "templates-desc": "Pas kleuren toe op externe applicaties.", + "templates-filter-description": "Sjablonen uit een specifieke categorie weergeven.", + "templates-filter-label": "Filteren op categorie", + "templates-misc-description": "Maak uw eigen sjablonen.", + "templates-misc-label": "Geavanceerd", + "templates-misc-user-templates-description": "Alleen inschakelen als u weet wat u doet — raadpleeg onze online documentatie.", + "templates-misc-user-templates-label": "Gebruikerssjablonen inschakelen", + "templates-none-detected": "Geen gedetecteerd", + "templates-write-path": "Schrijft: {filepath}", + "title": "Kleurschema", + "wallpaper-method-description": "Kies je favoriete methode voor het genereren van paletten.", + "wallpaper-method-label": "Paletgeneratiemethode", + "wallpaper-monitor-source-description": "Selecteer welke monitor te gebruiken voor het extraheren van wallpaperkleuren.", + "wallpaper-monitor-source-label": "Bron van kleurgeneratie" + }, + "connections": { + "authentication-required": "Authenticatie vereist", + "bluetooth-auto-connect-description": "Automatisch verbinding maken met vertrouwde gekoppelde apparaten wanneer Bluetooth is ingeschakeld.", + "bluetooth-auto-connect-label": "Gekoppelde apparaten automatisch verbinden", + "bluetooth-devices-unnamed": "Naamloze apparaten worden niet getoond.", + "bluetooth-discoverable": "Dit apparaat is vindbaar als {hostName} zolang dit instellingen-tabblad geopend is.", + "bluetooth-rssi-polling-description": "Periodiek RSSI van verbonden apparaten via bluetoothctl samplen. Mogelijk niet beschikbaar voor alle apparaten; gebruikt minimale middelen wanneer ingeschakeld.", + "bluetooth-rssi-polling-interval-description": "Configureer hoe vaak de signaalsterkte voor verbonden apparaten moet worden bijgewerkt.", + "bluetooth-rssi-polling-interval-label": "Polling-interval", + "bluetooth-rssi-polling-label": "Bluetooth-signaalpeiling", + "disable-discoverability-description": "Verberg je apparaat voor Bluetooth-apparaten in de buurt.", + "disable-discoverability-label": "Zichtbaarheid van apparaat uitschakelen", + "hide-unnamed-devices-description": "Verberg apparaten die alleen als Bluetooth-adressen verschijnen.", + "hide-unnamed-devices-label": "Onbenoemde apparaten verbergen", + "pin-instructions": "Voer de PIN-code in die op uw apparaat wordt weergegeven.", + "title": "Verbindingen", + "wifi-header-text": "Gebruik Wi-Fi om beschikbare netwerken te zien en verbinding te maken met het internet." + }, + "control-center": { + "cards-desc": "Pas aan welke bedieningselementen in het bedieningscentrum verschijnen en in welke volgorde.", + "desc": "Configureer de positie en het gedrag van het bedieningscentrumpaneel.", + "open-at-mouse-description": "Bij een rechtermuisklik op een leeg gedeelte van de Bar, open het Controlecentrum dicht bij de cursor. Indien uitgeschakeld, wordt de bovenstaande positie-instelling gebruikt.", + "open-at-mouse-label": "Openen bij cursor met rechtermuisklik", + "position-description": "Kies waar het bedieningscentrumpaneel verschijnt wanneer het wordt geopend.", + "shortcuts-custom-button-command-description": "Het commando dat wordt uitgevoerd wanneer op de knop wordt geklikt.", + "shortcuts-custom-button-enable-on-state-logic-description": "Schakel een tweede pictogram en aan-status in op basis van een controlecommando.", + "shortcuts-custom-button-enable-on-state-logic-label": "Logica voor aan-status inschakelen", + "shortcuts-custom-button-icon-description": "Selecteer een pictogram uit de bibliotheek.", + "shortcuts-custom-button-on-clicked-label": "Commando bij linkermuisklik", + "shortcuts-custom-button-on-middle-clicked-description": "Commando dat wordt uitgevoerd wanneer de knop met de middelste muisknop wordt geklikt.", + "shortcuts-custom-button-on-state-command-description": "Commando dat wordt uitgevoerd om te controleren of de knop in de aan-status moet staan. Geeft 0 terug voor aan, een andere waarde voor uit.", + "shortcuts-custom-button-on-state-command-label": "Controlecommando voor aan-status", + "shortcuts-custom-button-on-state-icon-description": "Het pictogram voor de knop wanneer deze in de aan-status staat.", + "shortcuts-custom-button-on-state-icon-label": "Pictogram voor aan-status", + "shortcuts-custom-button-state-checks-add": "Statuscontrole toevoegen", + "shortcuts-custom-button-state-checks-command": "Commando dat wordt uitgevoerd voor deze statuscontrole", + "shortcuts-custom-button-state-checks-label": "Statuscontroles", + "shortcuts-custom-button-state-checks-remove": "Verwijderen", + "shortcuts-custom-button-tooltip-description": "De tooltip die wordt getoond wanneer je over de knop zweeft.", + "shortcuts-custom-button-tooltip-label": "Tooltip", + "shortcuts-title": "Snelkoppelingwidgets", + "system-monitor-disk-path-description": "Selecteer welk schijf-mountpunt de systeemmonitor-kaart in het bedieningscentrum moet bewaken.", + "system-monitor-disk-path-label": "Systeemmonitor schijfpad", + "title": "Bedieningscentrum" + }, + "desktop-widgets": { + "clock-enabled-description": "Toon een klokwidget op het bureaublad.", + "clock-enabled-label": "Klokwidget inschakelen", + "clock-format-label": "Formaat", + "clock-height-description": "Hoogte van de klokwidget in pixels.", + "clock-height-label": "Hoogte", + "clock-minimal-mode-description": "Gebruik een minimale klokweergave met tijd en datum.", + "clock-minimal-mode-label": "Minimale modus", + "clock-rounded-corners-description": "Gebruik afgeronde hoeken voor de widgetachtergrond.", + "clock-rounded-corners-label": "Afgeronde hoeken", + "clock-show-background-description": "Toon de achtergrondcontainer voor de klokwidget.", + "clock-show-background-label": "Achtergrond weergeven", + "clock-show-date-description": "Toon de huidige datum onder de tijd.", + "clock-show-date-label": "Toon datum", + "clock-show-seconds-description": "Toon seconden in de tijd.", + "clock-show-seconds-label": "Toon seconden", + "clock-style-analog": "Analoog", + "clock-style-binary": "Binair", + "clock-style-description": "Kies de weergavestijl van de klok.", + "clock-style-digital": "Digitaal", + "clock-style-label": "Klokstijl", + "clock-style-minimal": "Minimaal", + "clock-width-description": "Breedte van de klokwidget in pixels.", + "cpu-intensive-note": "Widgets gemarkeerd met ! gebruiken meer CPU en moeten bewust ingeschakeld worden.", + "edit-mode-button-label": "Ga naar de bewerkingsmodus", + "edit-mode-controls-explanation": "Linksklikken en slepen: Widget verplaatsen of het formaat wijzigen.\nRechtermuisknop: Het contextmenu openen.", + "edit-mode-description": "Schakel de bewerkingsmodus in om desktopwidgets te verplaatsen en te herpositioneren. In de ingeschakelde modus tonen widgets een sleepomtrek en kunnen ze worden verplaatst.", + "edit-mode-exit-button": "Bewerkingsmodus verlaten", + "edit-mode-grid-snap-label": "Raster uitlijnen", + "edit-mode-grid-snap-scale-label": "Rasteruitlijningsschaal", + "edit-mode-label": "Bewerkingsmodus", + "enabled-description": "Desktopwidgets volledig in- of uitschakelen.", + "enabled-label": "Desktopwidgets inschakelen", + "general-desc": "Configureer widgets die op je bureaublad verschijnen.", + "general-title": "Desktopwidgets", + "media-player-enabled-description": "Toon een mediaplayerwidget op het bureaublad.", + "media-player-enabled-label": "Mediaplayerwidget inschakelen", + "media-player-rounded-corners-description": "Ronde hoeken inschakelen op de widgetranden.", + "media-player-show-album-art-description": "Toon de albumhoes en trackinformatie (titel en artiest).", + "media-player-show-album-art-label": "Toon albumhoes & titel", + "media-player-show-background-description": "Toon de achtergrondcontainer voor de mediaplayerwidget.", + "media-player-show-buttons-description": "Toon mediabesturingsknoppen (afspelen/pauzeren, vorige, volgende) op de mediaplayerwidget.", + "media-player-show-buttons-label": "Knoppen weergeven", + "media-player-show-visualizer-description": "Toon de audio-visualisatie overlay.", + "media-player-visualizer-type-description": "Kies een visualisatietype voor de achtergrond van de desktop mediaplayer.", + "overview-enabled-description": "Toon desktopwidgets terwijl het compositor-overzicht geopend is.", + "overview-enabled-label": "Tonen in overzicht", + "system-stat-layout-bottom": "Onder", + "system-stat-layout-description": "Kies hoe de legenda wordt weergegeven ten opzichte van de grafiek.", + "system-stat-layout-label": "Indeling", + "system-stat-layout-side": "Zijde", + "system-stat-rounded-corners-description": "Afgeronde hoeken gebruiken voor de widget-achtergrond.", + "system-stat-rounded-corners-label": "Afgeronde Hoeken", + "system-stat-show-background-description": "Toon de achtergrondcontainer voor de systeemstatistieken-widget.", + "system-stat-show-background-label": "Achtergrond weergeven", + "system-stat-stat-type-description": "Kies welke systeemstatistiek weer te geven.", + "system-stat-stat-type-label": "Statistiektype", + "title": "Desktopwidgets", + "weather-enabled-description": "Toon een weerwidget op het bureaublad.", + "weather-enabled-label": "Weerwidget inschakelen", + "weather-show-background-description": "Toon de achtergrondcontainer voor de weerwidget." + }, + "display": { + "monitors-backlight-device-auto-option": "Standaard", + "monitors-backlight-device-description": "Selecteer een achtergrondverlichtingsapparaat voor deze uitvoer.", + "monitors-backlight-device-label": "Achtergrondverlichting Apparaat", + "monitors-brightness-step-description": "Pas de stapgrootte voor helderheidswijzigingen aan (scrollwiel en sneltoetsen).", + "monitors-brightness-step-label": "Stapgrootte helderheid", + "monitors-brightness-unavailable-ddc-disabled": "Helderheidsregeling niet beschikbaar. Schakel \"Ondersteuning voor externe helderheid\" in om de helderheid van dit scherm te regelen.", + "monitors-brightness-unavailable-generic": "Helderheidsregeling is niet beschikbaar voor dit scherm.", + "monitors-desc": "Pas helderheidsinstellingen per scherm aan.", + "monitors-enforce-minimum-description": "Lost het probleem op dat de achtergrondverlichting op sommige schermen volledig uitgaat bij 0% helderheid.", + "monitors-enforce-minimum-label": "Minimale helderheid afdwingen (1%)", + "monitors-external-brightness-description": "Schakel DDCUtil-ondersteuning in om de helderheid op externe schermen te regelen via het DDC/CI-protocol.", + "monitors-external-brightness-label": "Ondersteuning voor externe helderheid", + "monitors-global-brightness-description": "Pas de helderheid van alle monitoren tegelijk aan.", + "monitors-global-brightness-label": "Alle monitoren", + "monitors-title": "Instellingen per monitor", + "night-light-auto-schedule-description": "Gebaseerd op de zonsopgang- en zonsondergangtijd in {location} — aanbevolen.", + "night-light-auto-schedule-label": "Automatische planning", + "night-light-desc": "Verminder blauw licht om beter te slapen en vermoeidheid van de ogen te beperken.", + "night-light-enable-description": "Past een warme kleurfilter toe om de afgifte van blauw licht te verminderen.", + "night-light-enable-label": "Nachtlicht inschakelen", + "night-light-force-activation-description": "Negeert het schema en past het nachtfilter direct toe.", + "night-light-force-activation-label": "Geforceerde activatie", + "night-light-manual-schedule-description": "Stel aangepaste tijden voor zonsopgang en zonsondergang in.", + "night-light-manual-schedule-label": "Handmatige planning", + "night-light-manual-schedule-select-start": "Starttijd selecteren", + "night-light-manual-schedule-select-stop": "Stoptijd selecteren", + "night-light-manual-schedule-sunrise": "Tijd van zonsopgang", + "night-light-manual-schedule-sunset": "Tijd van zonsondergang", + "night-light-temperature-day": "Dag", + "night-light-temperature-day-description": "Regelt de kleurtemperatuur overdag.", + "night-light-temperature-description": "Stel de kleurwarmte voor nacht en dag in.", + "night-light-temperature-label": "Kleurtemperatuur", + "night-light-temperature-night": "Nacht", + "night-light-temperature-night-description": "Regelt de kleurtemperatuur 's nachts.", + "title": "Beeldscherm" + }, + "dock": { + "appearance-background-opacity-description": "Pas de achtergronddekking van de dock aan.", + "appearance-border-radius-description": "Pas de randradius van het dock aan.", + "appearance-border-radius-label": "Randradius", + "appearance-colorize-icons-description": "Pas themakleuren toe op dock-pictogrammen (alleen niet-focuste apps).", + "appearance-colorize-icons-label": "Pictogrammen inkleuren", + "appearance-dead-opacity-description": "Pas de transparantie aan van app-iconen die niet actief zijn.", + "appearance-dead-opacity-label": "Dode opaciteit", + "appearance-desc": "Pas het gedrag en uiterlijk van de dock aan.", + "appearance-display-auto-hide": "Automatisch verbergen", + "appearance-display-description": "Kies hoe de dock zich gedraagt.", + "appearance-display-exclusive": "Exclusief", + "appearance-dock-indicator-description": "Toon een kleine indicator wanneer het dock verborgen is.", + "appearance-dock-indicator-label": "Dock-indicator", + "appearance-floating-distance-description": "Stel de afstand in tussen de dock en de rand van het scherm.", + "appearance-floating-distance-label": "Afstand zwevende dock", + "appearance-group-apps-description": "Groepeer meerdere vensters van dezelfde app in één Dock-item.", + "appearance-group-apps-label": "Dezelfde apps groeperen", + "appearance-group-click-action-cycle": "Vensters wisselen", + "appearance-group-click-action-description": "Kies wat de linkermuisklik doet voor gegroepeerde apps.", + "appearance-group-click-action-label": "Actie bij klik op gegroepeerde app", + "appearance-group-click-action-list": "Vensterlijst openen", + "appearance-group-context-menu-mode-description": "Kies hoe het contextmenu zich gedraagt voor gegroepeerde apps.", + "appearance-group-context-menu-mode-extended": "Uitgebreid", + "appearance-group-context-menu-mode-label": "Gegroepeerde app-menumodus", + "appearance-group-context-menu-mode-list": "Vensterlijst", + "appearance-group-indicator-style-description": "Kies hoe gegroepeerde actieve indicatoren de status van het gefocuste venster weergeven.", + "appearance-group-indicator-style-dots": "Punten", + "appearance-group-indicator-style-label": "Gegroepeerde indicatorstijl", + "appearance-group-indicator-style-number": "Nummer", + "appearance-hide-show-speed-description": "Pas de snelheid van de verberg/weergeef animatie van de dock aan.", + "appearance-hide-show-speed-label": "Verberg/weergeef snelheid", + "appearance-icon-size-description": "Pas de algehele grootte van de dock aan.", + "appearance-icon-size-label": "Grootte van de dock", + "appearance-inactive-indicators-description": "Toon indicatorpillen voor alle apps, niet alleen de actieve.", + "appearance-inactive-indicators-label": "Lopende indicatoren", + "appearance-indicator-color-description": "Kies de kleur van de indicator voor het verborgen dock.", + "appearance-indicator-color-label": "Indicatorkleur", + "appearance-indicator-opacity-description": "Pas de ondoorzichtigheid van de indicator voor het verborgen dock aan.", + "appearance-indicator-opacity-label": "Ondoorzichtigheid van de indicator", + "appearance-indicator-thickness-description": "Schakel een dikkere indicator in voor het verborgen dock (6px in plaats van 3px).", + "appearance-indicator-thickness-label": "Dikkere indicator", + "appearance-launcher-icon-description": "Kies het pictogram dat wordt gebruikt voor de Dock Launcher.", + "appearance-launcher-icon-label": "Launcher-pictogram", + "appearance-launcher-position-description": "Kies waar het starter-icoon in het dock verschijnt.", + "appearance-launcher-position-end": "Einde", + "appearance-launcher-position-label": "Positie van de starter", + "appearance-launcher-position-start": "Start", + "appearance-launcher-use-distro-logo-description": "Gebruik het logo van je distributie als het Dock Launcher-pictogram.", + "appearance-launcher-use-distro-logo-label": "Gebruik distro-logo in plaats van pictogram", + "appearance-pinned-static-description": "Zet vastgemaakte app-pictogrammen altijd in statische volgorde aan de linkerkant.", + "appearance-pinned-static-label": "Statische vastgezette apps", + "appearance-position-description": "Kies waar het dock op het scherm verschijnt.", + "appearance-position-label": "Positie", + "appearance-show-launcher-icon-description": "Toon het pictogram van de applicatiestarter in het dock.", + "appearance-show-launcher-icon-label": "Toon app-starter", + "appearance-sit-on-frame-description": "Lijn het dock uit binnen de kadergrens, in plaats van erbovenop te liggen.", + "appearance-sit-on-frame-label": "Dock zit op het frame", + "appearance-type-attached": "Gedockt", + "appearance-type-description": "Kies tussen een zwevende pilvorm of een statische balk die aan de rand is bevestigd.", + "appearance-type-floating": "Zwevend", + "appearance-type-label": "Dockstijl", + "enabled-description": "Dock geheel tonen of verbergen.", + "enabled-label": "Dock inschakelen", + "monitors-desc": "Dock op specifieke monitoren weergeven. Standaard op alle als er geen zijn gekozen.", + "monitors-only-same-monitor-description": "Toon alleen apps van de monitor waar het dock zich bevindt.", + "monitors-only-same-monitor-label": "Alleen apps van dezelfde monitor", + "monitors-title": "Monitorweergave", + "title": "Dock" + }, + "general": { + "copy-settings": "Instellingen kopiëren", + "fonts-default-description": "Hoofdlettertype dat in de hele interface wordt gebruikt.", + "fonts-default-label": "Standaardlettertype", + "fonts-default-placeholder": "Selecteer standaardlettertype...", + "fonts-default-scale-description": "Vergroot of verklein de grootte van standaardtekst.", + "fonts-default-scale-label": "Grootte standaardlettertype", + "fonts-default-search-placeholder": "Lettertype zoeken...", + "fonts-desc": "Kies de lettertypes die in de interface worden gebruikt.", + "fonts-monospace-description": "Monospace-lettertype dat wordt gebruikt voor cijfers en statische weergaven.", + "fonts-monospace-label": "Monospace-lettertype", + "fonts-monospace-placeholder": "Selecteer monospace-lettertype...", + "fonts-monospace-scale-description": "Vergroot of verklein de grootte van monospace-tekst.", + "fonts-monospace-scale-label": "Grootte monospace-lettertype", + "fonts-monospace-search-placeholder": "Monospace-lettertype zoeken...", + "fonts-title": "Lettertypes", + "keybinds-conflict-description": "De toetscombinatie is al toegewezen aan {action}.", + "keybinds-conflict-title": "Toetsencombinatieconflict", + "keybinds-description": "Globale navigatietoetsen voor panelen en de starter configureren.", + "keybinds-down": "Omlaag", + "keybinds-enter": "Bevestigen / Actie", + "keybinds-escape": "Sluiten / Terug", + "keybinds-left": "Naar links", + "keybinds-modifier-description": "Sneltoetsen moeten een modificatietoets (Ctrl of Alt) bevatten.", + "keybinds-modifier-title": "Modificator vereist", + "keybinds-remove": "Verwijderen", + "keybinds-right": "Naar rechts", + "keybinds-title": "Navigatietoetsencombinaties", + "keybinds-up": "Omhoog", + "language-desc": "Kies je voorkeurstaal voor de applicatie.", + "language-select-auto-detect": "Automatisch", + "language-select-description": "Selecteer de taal die in de interface van de applicatie wordt gebruikt.", + "language-select-label": "Applicatietaal", + "launch-setup-wizard": "Installatiewizard", + "profile-desc": "Bewerk je gebruikersgegevens en avatar.", + "profile-picture-description": "Je profielfoto die overal in de interface verschijnt.", + "profile-picture-label": "Profielfoto", + "profile-select-avatar": "Avatarafbeelding selecteren", + "profile-title": "Profiel", + "profile-tooltip": "Profielfoto", + "reverse-scrolling-description": "Geïnterpreteerde schuifrichting omkeren.", + "reverse-scrolling-label": "Scrollen omkeren", + "screen-corners-desc": "Pas afronding en visuele effecten van schermhoeken aan.", + "screen-corners-radius-description": "Pas de afronding van de schermhoeken aan.", + "screen-corners-radius-label": "Straal schermhoeken", + "screen-corners-radius-reset": "Straal schermhoeken resetten", + "screen-corners-show-corners-description": "Toon afgeronde hoeken aan de rand van het scherm.", + "screen-corners-show-corners-label": "Schermhoeken tonen", + "screen-corners-solid-black-description": "Gebruik effen zwart in plaats van de achtergrondkleur van de balk.", + "screen-corners-solid-black-label": "Effen zwarte hoeken", + "screen-corners-title": "Schermhoeken", + "settings-copied": "Instellingen gekopieerd naar klembord", + "smooth-scrolling-description": "Animeer lijstscrollen voor een vloeiendere scrollervaring.", + "smooth-scrolling-label": "Vloeiend scrollen", + "tab-basics": "Basis", + "tab-keybinds": "Toetsencombinaties" + }, + "hooks": { + "color-generation-description": "Commando dat wordt uitgevoerd wanneer de kleurgeneratie is voltooid (bijv. na thematisering op basis van de achtergrond).", + "color-generation-label": "Kleuren gegenereerd", + "color-generation-placeholder": "bijv. notify-send \"Kleuren\" \"Gegenereerd\"", + "info-command-info-description": "• Commando's worden uitgevoerd via shell (sh -lc)
• Commando's draaien op de achtergrond (losgekoppeld)
• Testknoppen worden uitgevoerd met de huidige waarden", + "info-parameters-description": "• Wallpaper hook: $1 = achtergrondpad, $2 = schermnaam, $3 = thema (donker/licht)
• Theme toggle hook: $1 = true/false (donkere modus-status)
• Screen lock/unlock hooks: $1 = lock/unlock (schermvergrendelingsstatus)
• Performance mode hooks: Geen parameters
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "Beschikbare parameters", + "noctalia-started-description": "Opdracht die moet worden uitgevoerd wanneer Noctalia volledig is geladen.", + "noctalia-started-label": "Noctalia gestart", + "noctalia-started-placeholder": "bijv. notify-send 'Noctalia geladen'", + "performance-mode-disabled-description": "Commando dat wordt uitgevoerd wanneer de Noctalia prestatiemodus wordt uitgeschakeld.", + "performance-mode-disabled-label": "Prestatiemodus uitgeschakeld", + "performance-mode-disabled-placeholder": "bijv. notify-send \"Prestatie\" \"Modus uitgeschakeld\"", + "performance-mode-enabled-description": "Commando dat wordt uitgevoerd wanneer de Noctalia prestatiemodus wordt ingeschakeld.", + "performance-mode-enabled-label": "Prestatiemodus ingeschakeld", + "performance-mode-enabled-placeholder": "bijv. notify-send \"Prestatie\" \"Modus ingeschakeld\"", + "screen-lock-description": "Commando dat wordt uitgevoerd wanneer het scherm wordt vergrendeld.", + "screen-lock-label": "Scherm vergrendeld", + "screen-lock-placeholder": "bijv. notify-send \"Scherm\" \"Vergrendeld\"", + "screen-unlock-description": "Commando dat wordt uitgevoerd wanneer het scherm wordt ontgrendeld.", + "screen-unlock-label": "Scherm ontgrendeld", + "screen-unlock-placeholder": "bijv. notify-send \"Scherm\" \"Ontgrendeld\"", + "session-description": "Commando dat moet worden uitgevoerd voor het afsluiten of opnieuw opstarten. Ontvangt actietype als $1 (afsluiten/opnieuw opstarten).", + "session-label": "Sessie beëindigen", + "session-placeholder": "bijv. notify-send \"Session\" \"$1\"", + "system-hooks-desc": "Configureer commando's die worden uitgevoerd wanneer systeemgebeurtenissen plaatsvinden.", + "system-hooks-enable-description": "Schakel alle hook-commando's in of uit.", + "system-hooks-enable-label": "Hooks inschakelen", + "system-hooks-title": "Systeemhooks", + "theme-changed-description": "Commando dat wordt uitgevoerd wanneer het thema tussen donkere en lichte modus schakelt.", + "theme-changed-label": "Thema gewijzigd", + "theme-changed-placeholder": "bijv. notify-send \"Thema\" \"Gewisseld\"", + "title": "Hooks", + "wallpaper-changed-description": "Commando dat wordt uitgevoerd wanneer de achtergrond verandert.", + "wallpaper-changed-label": "Achtergrond gewijzigd", + "wallpaper-changed-placeholder": "bijv. notify-send \"Achtergrond\" \"Gewijzigd\"" + }, + "idle": { + "custom-add": "Commando toevoegen", + "custom-description": "Voer een shell-commando uit na een periode van inactiviteit.", + "custom-entry-command": "Commando", + "custom-entry-delete": "Verwijderen", + "custom-entry-edit": "Aangepaste Opdracht Bewerken", + "custom-entry-name": "Naam", + "custom-entry-name-placeholder": "bijv. Lichten uitdoen", + "custom-entry-new": "Nieuwe Aangepaste Opdracht", + "custom-entry-timeout": "Inactiviteitstijd", + "custom-entry-timeout-format": "{count} seconde", + "custom-entry-timeout-format-plural": "{count} seconden", + "custom-entry-unnamed": "Naamloos commando", + "custom-label": "Aangepaste inactiviteitscommando's", + "enable-description": "Schakel het scherm automatisch uit, vergrendel of onderbreek na een periode van inactiviteit.", + "enable-label": "Inactiviteitsbeheer inschakelen", + "fade-duration-description": "Seconden voor de fade-to-black animatie voordat elke actie wordt geactiveerd. Elke muisbeweging annuleert de fade.", + "fade-duration-label": "Vervagingsduur", + "lock-description": "Seconden van inactiviteit voordat het vergrendelscherm wordt geactiveerd.", + "lock-label": "Scherm vergrendelen", + "resume-command-label": "Commando hervatten", + "screen-off-description": "Seconden van inactiviteit voordat monitoren worden uitgeschakeld.", + "screen-off-label": "Scherm uitschakelen", + "status-description": "Inactiviteitstijd zoals gerapporteerd door de compositor.", + "status-label": "Inactiviteitstijd", + "suspend-description": "Seconden van inactiviteit voordat het systeem onderbreekt.", + "tab-behavior": "Gedrag", + "tab-custom": "Aangepast", + "timeouts-description": "Stel in op 0 om een fase uit te schakelen. Time-outs worden gepauzeerd zolang 'Wakker houden' actief is.", + "timeouts-label": "Time-outs", + "title": "Inactiviteit", + "unavailable": "Native inactiviteitsmonitoring is niet beschikbaar op deze Compositor." + }, + "indicator": { + "default-value": "Standaard: {value}", + "system-default": "Systeemstandaard" + }, + "launcher": { + "clipboard-desc": "Toegang tot en beheer uw klembordgeschiedenis vanuit de launcher.", + "execute-desc": "Configureer hoe applicaties worden gestart.", + "execute-title": "Uitvoering", + "settings-annotation-tool-description": "Commando dat wordt uitgevoerd bij het klikken op de annotatieknop in de klembordgeschiedenis. De afbeelding wordt naar dit commando gepiped.", + "settings-annotation-tool-label": "Annotatietool", + "settings-annotation-tool-placeholder": "bijv. 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Plak automatisch het geselecteerde klemborditem. Vereist wtype.", + "settings-auto-paste-label": "Automatisch plakken", + "settings-clip-chips-description": "Toon een tabbladbalk om de klembordgeschiedenis op type te filteren (Afbeeldingen, Links, Bestanden, Code, enz.).", + "settings-clip-chips-label": "Categoriechips inschakelen", + "settings-clip-preview-description": "Toon een voorbeeld van de inhoud van het klembord bij gebruik van het >clip-commando.", + "settings-clip-preview-label": "Klembordvoorbeeld inschakelen", + "settings-clip-smart-icons-description": "Toon specifieke pictogrammen voor links, bestanden, kleuren en code in plaats van een generiek klembordpictogram.", + "settings-clip-smart-icons-label": "Slimme pictogrammen inschakelen", + "settings-clip-wrap-text-description": "Tekst in de klembordlijst wrappen in plaats van af te breken.", + "settings-clip-wrap-text-label": "Klembordtekst omwikkelen", + "settings-clipboard-history-description": "Toegang tot eerder gekopieerde items vanuit de launcher.", + "settings-clipboard-history-label": "Klembordgeschiedenis inschakelen", + "settings-clipboard-watch-image-description": "Volledige commandoreeks die aan wl-paste wordt doorgegeven voor afbeeldingswijzigingen (herstart vereist).", + "settings-clipboard-watch-image-label": "Afbeelding Bewakingscommando", + "settings-clipboard-watch-text-description": "Volledige commandoreeks die aan wl-paste wordt doorgegeven voor tekstwijzigingen (vereist herstart).", + "settings-clipboard-watch-text-label": "Tekst volgcommando", + "settings-custom-launch-prefix-description": "Voorzie commando's van een aangepaste launcher-prefix (bijv. 'runapp' voor systemd-integratie).", + "settings-custom-launch-prefix-enabled-description": "Gebruik een aangepaste prefix om applicaties te starten in plaats van de standaardmethode.", + "settings-custom-launch-prefix-enabled-label": "Aangepaste startprefix inschakelen", + "settings-custom-launch-prefix-label": "Aangepaste startprefix", + "settings-density-description": "Pas de grootte van applicatiepictogrammen en de dichtheid van de Launcher aan.", + "settings-density-label": "Dichtheid", + "settings-desc": "Pas het gedrag en uiterlijk van de launcher aan.", + "settings-enable-session-search-description": "Toon sessieacties (vergrendelen, afsluiten, herstarten, etc.) bij het zoeken in de Launcher.", + "settings-enable-session-search-label": "Sessie zoeken inschakelen", + "settings-enable-settings-search-description": "Toon instellingen in de zoekresultaten van de Launcher.", + "settings-enable-settings-search-label": "Instellingen zoeken inschakelen", + "settings-enable-windows-search-description": "Actieve vensters zoeken en focussen.", + "settings-enable-windows-search-label": "Vensterzoekfunctie inschakelen", + "settings-grid-view-description": "Items in een raster weergeven in plaats van een lijst.", + "settings-icon-mode-description": "Gebruik native systeemiconen in plaats van Tabler-iconen.", + "settings-icon-mode-label": "Gebruik native iconen", + "settings-ignore-mouse-input-description": "Schakel muisinteractie en scrollwiel uit in de launcher.", + "settings-ignore-mouse-input-label": "Negeer muisinvoer", + "settings-overlay-layer-description": "Toon de starter op de overlaylaag, boven vensters op volledig scherm. Indien ingeschakeld, wordt de starter niet aan de balk bevestigd.", + "settings-overlay-layer-label": "Boven volledig scherm weergeven", + "settings-position-description": "Kies waar het launcherpaneel verschijnt.", + "settings-show-categories-description": "Categorieën-tabbladen weergeven voor het filteren van applicaties.", + "settings-show-categories-label": "Categorieën weergeven", + "settings-show-icon-background-description": "Toon een afgeronde rechthoek achtergrond achter pictogrammen.", + "settings-show-icon-background-label": "Toon pictogramachtergrond", + "settings-sort-by-usage-description": "Indien ingeschakeld, verschijnen vaak gestarte apps bovenaan in de lijst.", + "settings-sort-by-usage-label": "Sorteren op meest gebruikt", + "settings-terminal-command-description": "Commando om een terminal te starten. Bijv. 'kitty -e' of 'gnome-terminal --'.", + "settings-terminal-command-label": "Terminalcommando", + "settings-use-app2unit-description": "Gebruikt een alternatieve startmethode om app-processen beter te beheren en problemen te voorkomen.", + "settings-use-app2unit-label": "App2Unit gebruiken om applicaties te starten", + "settings-view-mode-description": "Kies de indeling voor de starter-items.", + "settings-view-mode-label": "Weergavemodus", + "title": "Launcher" + }, + "location": { + "auto-locate-description": "Detecteert automatisch uw locatie periodiek met behulp van uw IP-adres.", + "auto-locate-label": "Automatische Lokalisatie", + "calendar-cards-desc": "Kaarten organiseren en in-/uitschakelen in het kalenderpaneel.", + "calendar-cards-title": "Kalenderkaarten", + "calendar-header-label": "Kalenderkop", + "calendar-month-label": "Kalendermaand", + "date-time-12hour-format-description": "Toon tijd in 12-uurs formaat op het vergrendelscherm en in de kalender. De klok op de balk heeft zijn eigen instellingen.", + "date-time-12hour-format-label": "12-uurs tijdnotatie gebruiken", + "date-time-desc": "Pas aan hoe datum en tijd worden weergegeven.", + "date-time-first-day-of-week-automatic": "Automatisch (systeemlocale gebruiken)", + "date-time-first-day-of-week-description": "Kies welke dag de week in de kalender start.", + "date-time-first-day-of-week-label": "Eerste dag van de week", + "date-time-show-events-description": "Toon gebeurtenissen in het kalenderpaneel.", + "date-time-show-events-label": "Kalendergebeurtenissen tonen", + "date-time-title": "Datum & tijd", + "date-time-use-analog-description": "Toon een analoge klok op het kalendervenster.", + "date-time-use-analog-label": "Analoge klok gebruiken", + "date-time-week-numbers-description": "Toon het weeknummer van het jaar (bijv. week 38) in de kalender.", + "date-time-week-numbers-label": "Weeknummers tonen", + "geolocate-now-button": "Nu lokaliseren", + "location-desc": "Krijg nauwkeurige weersinformatie en nachtlichtplanning door je locatie in te stellen.", + "location-search-description": "bijv. Toronto, ON", + "location-search-label": "Zoek een locatie", + "location-search-placeholder": "Voer de locatienaam in", + "location-title": "Je locatie", + "weather-desc": "Kies je voorkeurseenheid voor temperatuur.", + "weather-enabled-description": "Toon weerinformatie in de interface en haal weergegevens op. Wanneer uitgeschakeld, worden alle weerelementen verborgen en worden er geen netwerkverzoeken gedaan.", + "weather-enabled-label": "Weer inschakelen", + "weather-fahrenheit-description": "Temperatuur in Fahrenheit weergeven in plaats van Celsius.", + "weather-fahrenheit-label": "Temperatuur in Fahrenheit (°F) weergeven", + "weather-hide-city-description": "Verberg de stadsnaam in weerweergaven in de hele interface.", + "weather-hide-city-label": "Verberg stadsnaam", + "weather-hide-timezone-description": "Verberg de tijdzone-afkorting in alle weerweergaves in de interface.", + "weather-hide-timezone-label": "Verberg tijdzone", + "weather-show-effects-description": "Toon extra visuele effecten (zoals regen, sneeuw of bliksem) op de weerkaart.", + "weather-show-effects-label": "Weerseffecten weergeven", + "weather-show-in-calendar-description": "Toon de dagelijkse weersverwachting direct in je kalenderview.", + "weather-show-in-calendar-label": "Weer in kalender weergeven", + "weather-talia-mascot-always-description": "Gebruik altijd Talia emojis in plaats van tabler icons.", + "weather-talia-mascot-always-label": "Altijd Talia weer-emoji's tonen" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "Wanneer fprintd (vingerafdrukauthenticatie) actief is, kunt u met deze optie nog steeds inloggen met uw wachtwoord in plaats van een vingerafdruk.", + "allow-password-with-fprintd-label": "Wachtwoordlogin toestaan met fprintd", + "auto-start-auth-description": "Start bijvoorbeeld automatisch de vingerafdrukauthenticatie zonder dat er een toetsaanslag of klik nodig is.", + "auto-start-auth-label": "Automatische authenticatie bij opstarten", + "clock-format-description": "Pas het klokformaat aan met datum-/tijd-syntaxtokens.", + "clock-format-label": "Klokformaat", + "clock-style-analog": "Analoog", + "clock-style-custom": "Aangepast", + "clock-style-description": "Kies de visuele stijl van de klok op het vergrendelscherm.", + "clock-style-digital": "Digitaal", + "clock-style-label": "Klokstijl", + "compact-lockscreen-description": "Toon alleen de logininvoer en systeemknoppen, verberg weer- en mediawidgets.", + "compact-lockscreen-label": "Compact vergrendelscherm", + "enable-lockscreen-media-controls-description": "Toon interactieve mediabediening op het vergrendelscherm.", + "enable-lockscreen-media-controls-label": "Mediabediening op het Vergrendelscherm", + "lock-on-suspend-description": "Vergrendel het scherm automatisch wanneer het systeem wordt onderbroken.", + "lock-on-suspend-label": "Vergrendelen bij onderbreken", + "lock-screen-animations-description": "Vergrendelschermanimaties in- of uitschakelen.", + "lock-screen-animations-label": "Vergrendelschermanimaties", + "lock-screen-blur-strength-description": "Past een vervagingseffect toe op de achtergrond van het vergrendelscherm.", + "lock-screen-blur-strength-label": "Sterkte van de vervaging van het vergrendelscherm", + "lock-screen-tint-strength-description": "Past een getinte overlay toe op de achtergrond van het vergrendelscherm.", + "lock-screen-tint-strength-label": "Sterkte van de vergrendelschermtint", + "monitors-desc": "Vergrendelscherm tonen op specifieke monitoren. Standaard op alle, indien er geen zijn gekozen.", + "password-chars-description": "Leuke pictogrammen om je wachtwoord te verbergen.", + "password-chars-label": "Willekeurige wachtwoordpictogrammen", + "show-hibernate-description": "De optie 'sluimerstand' tonen in de energieopties.", + "show-hibernate-label": "Sluimerstand tonen", + "show-session-buttons-description": "Toegang tot energie-instellingen toestaan vanaf het vergrendelscherm.", + "show-session-buttons-label": "Energieopties", + "title": "Vergrendelscherm" + }, + "network": { + "bluetooth-description": "Bluetooth-beheer activeren.", + "bluetooth-rssi-polling-description": "Periodiek de RSSI bemonsteren voor verbonden apparaten via bluetoothctl. Mogelijk niet beschikbaar voor alle apparaten; gebruikt minimale resources wanneer ingeschakeld.", + "bluetooth-rssi-polling-label": "Bluetooth-signaalpolling", + "desc": "Beheer Wi-Fi- en Bluetooth-verbindingen.", + "wifi-description": "Draadloze netwerken beheren (vereist NetworkManager)." + }, + "notifications": { + "duration-critical-urgency-description": "Hoe lang meldingen met kritieke prioriteit zichtbaar blijven.", + "duration-critical-urgency-label": "Kritieke urgentie", + "duration-desc": "Configureer hoe lang meldingen zichtbaar blijven op basis van hun urgentieniveau.", + "duration-low-urgency-description": "Hoe lang meldingen met lage prioriteit zichtbaar blijven.", + "duration-low-urgency-label": "Lage urgentie", + "duration-normal-urgency-description": "Hoe lang meldingen met normale prioriteit zichtbaar blijven.", + "duration-normal-urgency-label": "Normale urgentie", + "duration-respect-expire-description": "Gebruik de vervaltijd die in de melding is ingesteld.", + "duration-respect-expire-label": "Vervaltijd respecteren", + "duration-title": "Duur van meldingen", + "history-clear-dismiss-description": "Melding uit de geschiedenis wissen wanneer deze wordt gesloten.", + "history-clear-dismiss-label": "Wissen bij sluiten", + "history-critical-urgency-description": "Meldingen met kritieke prioriteit opslaan in de geschiedenis.", + "history-critical-urgency-label": "Kritieke urgentie opslaan in geschiedenis", + "history-desc": "Bepalen welke meldingen worden opgeslagen in de geschiedenis op basis van hun urgentieniveau.", + "history-low-urgency-description": "Meldingen met lage prioriteit opslaan in de geschiedenis.", + "history-low-urgency-label": "Lage urgentie opslaan in geschiedenis", + "history-normal-urgency-description": "Meldingen met normale prioriteit opslaan in de geschiedenis.", + "history-normal-urgency-label": "Normale urgentie opslaan in geschiedenis", + "monitors-desc": "Meldingen op specifieke monitoren tonen. Standaard op alle als er geen zijn gekozen.", + "rules-action-block": "Blok", + "rules-action-block-desc": "Slaat volledig over.", + "rules-action-hide": "Verbergen", + "rules-action-hide-desc": "Geen pop-up, geen geluid, wordt toegevoegd aan de geschiedenis.", + "rules-action-label": "Actie", + "rules-action-mute": "Dempen", + "rules-action-mute-desc": "Geen geluid, toont nog steeds pop-up en in geschiedenis.", + "rules-add": "Regel toevoegen", + "rules-delete": "Regel verwijderen", + "rules-description": "Komt overeen met app-naam of inhoud. Regels worden in volgorde gecontroleerd, en de eerste overeenkomst wordt toegepast.", + "rules-edit": "Regel bewerken", + "rules-label": "Filterregels", + "rules-pattern-label": "Patroon", + "rules-pattern-placeholder": "firefox, discord, of /regex/", + "rules-tab": "Regels", + "settings-always-on-top-description": "Toon meldingen boven fullscreen-vensters en andere lagen.", + "settings-background-opacity-description": "Pas de dekking van meldingsachtergronden aan.", + "settings-density-description": "Kies de dichtheid van de meldingenkaart.", + "settings-density-label": "Dichtheid", + "settings-desc": "Configureer uiterlijk en gedrag van meldingen.", + "settings-do-not-disturb-description": "Schakel alle meldingspop-ups uit wanneer ingeschakeld.", + "settings-enabled-description": "Schakel de meldingsdaemon in of uit, vereist een herstart van Noctalia shell.", + "settings-enabled-label": "Meldingen inschakelen", + "settings-location-description": "Waar meldingen op het scherm verschijnen.", + "settings-markdown-description": "Meldingsinhoud weergeven met Markdown-opmaak.", + "settings-markdown-label": "Markdown inschakelen", + "sounds-desc": "Configureer meldingsgeluidseffecten en volume.", + "sounds-enabled-description": "Schakel geluidseffecten in voor inkomende meldingen.", + "sounds-enabled-label": "Meldingsgeluiden inschakelen", + "sounds-excluded-apps-description": "Sla het afspelen van het geconfigureerde notificatiegeluid over voor specifieke applicaties die hun eigen ingebouwde geluiden hebben.", + "sounds-excluded-apps-label": "Uitgesloten applicaties", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Pad naar het geluidsbestand dat wordt afgespeeld voor meldingen met kritieke prioriteit.", + "sounds-files-critical-label": "Kritieke urgentie geluid", + "sounds-files-critical-select-title": "Selecteer kritieke urgentie geluidsbestand", + "sounds-files-desc": "Configureer aangepaste geluidsbestanden voor verschillende meldingsurgentieniveaus.", + "sounds-files-low-description": "Pad naar het geluidsbestand dat wordt afgespeeld voor meldingen met lage prioriteit.", + "sounds-files-low-label": "Lage urgentie geluid", + "sounds-files-low-select-title": "Selecteer lage urgentie geluidsbestand", + "sounds-files-normal-description": "Pad naar het geluidsbestand dat wordt afgespeeld voor meldingen met normale prioriteit.", + "sounds-files-normal-label": "Normale urgentie geluid", + "sounds-files-normal-select-title": "Selecteer normale urgentie geluidsbestand", + "sounds-files-placeholder": "Voer pad naar geluidsbestand in", + "sounds-files-select-file": "Selecteer geluidsbestand", + "sounds-files-title": "Geluidsbestanden", + "sounds-files-unified-description": "Pad naar het geluidsbestand dat wordt afgespeeld voor meldingen.", + "sounds-files-unified-label": "Meldingsgeluid", + "sounds-files-unified-select-title": "Selecteer meldingsgeluidsbestand", + "sounds-separate-description": "Gebruik verschillende geluidsbestanden voor lage, normale en kritieke prioriteitsmeldingen.", + "sounds-separate-label": "Gebruik verschillende geluiden per prioriteit", + "sounds-title": "Geluidsinstellingen", + "sounds-unavailable-description": "Installeer Qt6 Multimedia om meldingsgeluiden in te schakelen.", + "sounds-unavailable-label": "Meldingsgeluiden niet beschikbaar", + "sounds-volume-description": "Pas het volumeniveau aan voor meldingsgeluiden.", + "sounds-volume-label": "Geluidsvolume", + "toast-battery-description": "Toon een waarschuwing wanneer het accuniveau onder dit percentage zakt.", + "toast-battery-label": "Accuwaarschuwing", + "toast-desc": "Configureer het uiterlijk en gedrag van toasts.", + "toast-keyboard-description": "Toon een toastmelding wanneer de toetsenbordindeling verandert.", + "toast-keyboard-label": "Toetsenbordindeling", + "toast-media-description": "Een toast weergeven wanneer de afspeelstatus van media verandert.", + "toast-media-label": "Media" + }, + "osd": { + "always-on-top-description": "Toon OSD boven fullscreen-vensters en andere lagen.", + "always-on-top-label": "Altijd bovenop", + "background-opacity-description": "Regelt de transparantie van de OSD-achtergrond.", + "background-opacity-label": "Achtergrond dekkendheid", + "description": "Configureer on-screenindicatoren zoals overlays voor volume en helderheid.", + "duration-auto-hide-description": "Stel de tijd in voordat OSD verdwijnt.", + "duration-auto-hide-label": "Automatisch verbergen na", + "duration-desc": "Hoe lang OSD zichtbaar blijft voordat het automatisch verdwijnt.", + "duration-title": "Timeout automatisch verbergen", + "enabled-description": "Toon volumeveranderingen en helderheid in realtime.", + "enabled-label": "On-screenweergave inschakelen", + "events-desc": "Selecteer welke gebeurtenissen de weergave op het scherm activeren.", + "general-desc": "Configureer de zichtbaarheid en het gedrag van OSD.", + "location-description": "Waar on-screenweergaven verschijnen.", + "monitors-desc": "Toon OSD op specifieke monitoren. Standaard op alle als er geen zijn gekozen.", + "title": "On-screenweergave", + "types-brightness-description": "Toon het OSD wanneer de schermhelderheid verandert.", + "types-brightness-label": "Helderheid", + "types-custom-text-description": "OSD weergeven voor aangepaste tekstberichten via IPC.", + "types-custom-text-label": "Aangepaste tekst", + "types-desc": "Selecteer de gebeurtenissen die de OSD activeren.", + "types-input-volume-description": "Toon het OSD wanneer het microfoonvolume verandert.", + "types-input-volume-label": "Invoervolume", + "types-lockkey-description": "Toon het OSD wanneer Caps Lock, Num Lock of Scroll Lock wordt omgeschakeld.", + "types-lockkey-label": "Vergrendeltoetsen", + "types-media-description": "OSD weergeven wanneer de afspeelstatus van media verandert (afspelen, pauzeren, overslaan).", + "types-media-label": "Media afspelen", + "types-title": "OSD triggergebeurtenissen", + "types-volume-description": "Toon het OSD wanneer het uitvoervolume verandert.", + "types-volume-label": "Uitvoervolume" + }, + "plugins": { + "auto-update": "Plugins automatisch bijwerken", + "auto-update-description": "Update automatisch alle plugins wanneer de shell start.", + "available-description": "Blader door en installeer plugins vanuit geconfigureerde bronnen.", + "available-label": "Beschikbare plug-ins", + "available-no-plugins-description": "Controleer je plugin-bronnen of vernieuw de lijst.", + "available-no-plugins-label": "Geen plug-ins beschikbaar", + "check-for-updates": "Controleren op updates", + "checking-for-updates": "Controleren op updates...", + "collision-already-installed": "Deze plugin is al geïnstalleerd", + "collision-custom-version-exists": "Er is al een aangepaste versie van \"{source}\" geïnstalleerd", + "collision-official-version-exists": "De officiële versie van deze plugin is al geïnstalleerd", + "development-disable": "Ontwikkelingsmodus voor deze plug-in uitschakelen", + "development-enable": "Ontwikkelingsmodus inschakelen voor deze plug-in.", + "filter-downloaded": "Gedownload", + "filter-not-downloaded": "Niet gedownload", + "filter-tags-description": "Filter plug-ins op categorie of downloadstatus.", + "filter-tags-label": "Tags", + "hot-reload-description": "Laad plugins automatisch opnieuw wanneer hun bestanden veranderen. Handig voor plugin-ontwikkeling.", + "hot-reload-label": "Hot reload (ontwikkelmodus)", + "hot-reloaded": "Plugin opnieuw geladen: {name}", + "install-error": "Installatie mislukt: {error}", + "install-incompatible": "{plugin} vereist Noctalia v{version} of hoger", + "install-success": "{plugin} succesvol geïnstalleerd", + "installed-description": "Beheer en configureer alle lokaal geïnstalleerde plugins.", + "installed-label": "Geïnstalleerde plug-ins", + "installed-no-plugins-description": "Installeer plugins uit de sectie \"Beschikbaar\".", + "installed-no-plugins-label": "Geen plugins geïnstalleerd", + "installing": "{plugin} installeren...", + "notify-updates": "Plugin-updatemeldingen", + "notify-updates-description": "Toon een melding wanneer nieuwe plug-in-updates beschikbaar zijn.", + "open-plugin-page": "Open plug-in pagina", + "open-plugins-tab": "Open plugininstellingen", + "plugin-settings-title": "{plugin} Instellingen", + "refresh-refreshing": "Pluginlijst vernieuwen...", + "refresh-tooltip": "Beschikbare plugins vernieuwen", + "settings-error-not-loaded": "Plugin niet geladen", + "settings-saved": "Plugininstellingen opgeslagen", + "settings-tooltip": "Plugin instellingen", + "source-custom": "Aangepaste bron", + "sources-add-custom": "Aangepaste repository toevoegen", + "sources-add-dialog-description": "Voeg een GitHub repository toe als een pluginbron.", + "sources-add-dialog-error": "Pluginbron toevoegen mislukt", + "sources-add-dialog-name": "Repositorynaam", + "sources-add-dialog-name-placeholder": "Mijn aangepaste plugins", + "sources-add-dialog-success": "Pluginbron succesvol toegevoegd", + "sources-add-dialog-title": "Pluginbron toevoegen", + "sources-add-dialog-url": "Repository URL", + "sources-description": "Beheer plugin-repositories.", + "sources-placeholder": "Mijn coole repository", + "sources-remove-tooltip": "Pluginbron verwijderen", + "title": "Plugins", + "translations-reloaded": "Vertalingen opnieuw geladen: {name}", + "uninstall-dialog-description": "Weet je zeker dat je {plugin} wilt verwijderen? Hiermee worden alle plugin-gegevens verwijderd.", + "uninstall-dialog-title": "Plugin verwijderen", + "uninstall-error": "Verwijderen mislukt: {error}", + "uninstall-success": "{plugin} is succesvol verwijderd", + "uninstalling": "{plugin} verwijderen...", + "update-all": "Alles bijwerken ({count})", + "update-all-success": "Alle plugins succesvol bijgewerkt", + "update-available": "Nieuwe plugin update beschikbaar", + "update-available-plural": "Nieuwe plugin updates beschikbaar ({count})", + "update-error": "Plugin bijwerken mislukt: {plugin}: {error}", + "update-pending": "v{current} → v{new} (vereist Noctalia v{required})", + "update-success": "{plugin} is bijgewerkt naar v{version}", + "update-version": "v{current} → v{new}", + "updating": "Bezig met updaten..." + }, + "region": { + "title": "Regio" + }, + "session-menu": { + "countdown-duration-description": "Stel in hoe lang de afteltimer duurt voordat energieacties worden uitgevoerd.", + "countdown-duration-label": "Afteltimer duur", + "enable-countdown-description": "Toon een afteltimer voordat energieacties worden uitgevoerd.", + "enable-countdown-label": "Afteltimer inschakelen", + "entries-desc": "Aanpassen welke energieacties in het sessiemenu verschijnen en in welke volgorde.", + "entries-title": "Energieacties", + "entry-settings-command-description": "Aangepast commando om uit te voeren voor deze actie. Laat leeg om het standaard systeemcommando te gebruiken.", + "entry-settings-command-placeholder": "bijv. systemctl poweroff", + "entry-settings-default-command-lock": "Intern vergrendelscherm (geen commando)", + "entry-settings-default-command-logout": "Interne afmelding (geen commando)", + "entry-settings-default-info-description": "Als er geen aangepast commando is opgegeven, wordt het standaard systeemcommando gebruikt.", + "entry-settings-default-info-label": "Standaardopdracht", + "entry-settings-keybind-description": "Wijs een toetscombinatie toe om deze actie te activeren wanneer het sessiemenu geopend is.", + "entry-settings-keybind-placeholder": "Druk op Opnemen om een toets toe te wijzen...", + "entry-settings-keybind-recording": "Opname... Druk op een willekeurige toetscombinatie", + "entry-settings-title": "Configureer {entry}", + "entry-settings-tooltip": "Configureer opdracht", + "general-desc": "Configureer het gedrag en uiterlijk van het sessiemenupaneel.", + "large-buttons-layout-description": "Kies hoe sessiemenuknoppen worden weergegeven.", + "large-buttons-layout-label": "Lay-out met grote knoppen", + "large-buttons-style-description": "Toon het sessiemenu met grote knoppen in een rasterindeling.", + "large-buttons-style-label": "Stijl met grote knoppen", + "position-description": "Kies waar het sessiemenupaneel verschijnt wanneer het wordt geopend.", + "show-header-description": "Toon de titel en sluitknop bovenaan het sessiemenu.", + "show-header-label": "Koptekst tonen", + "show-keybinds-description": "Toon toetscombinatiehints bij sessie-opties.", + "show-keybinds-label": "Toetsencombinaties weergeven" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Houd desktopwidgets zichtbaar terwijl de Noctalia prestatiemodus is ingeschakeld.", + "noctalia-performance-disable-desktop-widgets-label": "Desktopwidgets inschakelen in prestatiemodus", + "noctalia-performance-disable-wallpaper-description": "Houd bureaublad-, overzicht- en vergrendelschermachtergronden zichtbaar terwijl de Noctalia prestatiemodus is ingeschakeld.", + "noctalia-performance-disable-wallpaper-label": "Achtergrondweergave inschakelen in prestatiemodus", + "title": "Systeem" + }, + "system-monitor": { + "critical-color-label": "Kritische kleur", + "custom-highlight-colors-title-label": "Aangepaste markeerkleuren", + "disk-available-label": "Beschikbare schijfruimte", + "disk-section-label": "Schijfgebruik", + "enable-dgpu-monitoring-description": "Waarschuwing: Dit zal uw dedicated GPU (NVIDIA/AMD) activeren, wat een aanzienlijke impact kan hebben op de accuduur van laptops met hybride grafische kaarten.", + "enable-dgpu-monitoring-label": "Dedicated GPU-monitoring inschakelen", + "external-monitor-description": "Voer de opdracht of applicatiepad in om te starten bij het activeren van de externe systeemmonitor applicatie.", + "external-monitor-label": "Externe systeemmonitor opdracht", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "Configureer het gedrag en de weergave van de systeemmonitor.", + "gpu-section-label": "GPU-temperatuur", + "highlight-colors-section-label": "Markeerkleuren", + "polling-interval-label": "Peilingsinterval", + "polling-section-description": "Configureer hoe vaak elke systeemmetriek wordt bijgewerkt.", + "polling-section-label": "Peilingsintervallen", + "threshold-critical": "Kritiek", + "threshold-warning": "Waarschuwing", + "thresholds-section-description": "Pas de waarschuwing/kritieke drempels en de pollingsintervallen voor elke systeemmetriek aan.", + "use-custom-highlight-colors-description": "Indien uitgeschakeld, worden de standaard markeerkleuren van het thema gebruikt.", + "use-custom-highlight-colors-label": "Gebruik aangepaste markeerkleuren", + "warning-color-label": "Waarschuwingskleur" + }, + "user-interface": { + "allow-panels-without-bar-description": "Indien ingeschakeld, kunnen panelen op elk scherm worden geopend. Indien uitgeschakeld, worden panelen alleen geopend op schermen met een balk, wat het geheugengebruik kan verminderen.", + "allow-panels-without-bar-label": "Sta panelen toe op schermen zonder balk", + "animation-disable-description": "Schakel alle animaties uit voor een snellere, responsievere ervaring.", + "animation-disable-label": "UI-animaties uitschakelen", + "animation-speed-description": "Pas de globale animatiesnelheid aan.", + "animation-speed-label": "Animatiesnelheid", + "animation-speed-reset": "Animatiesnelheid resetten", + "appearance-desc": "Pas visuele elementen aan, zoals tooltips, randen en schaduwen.", + "blur-behind-description": "Vervaagt het gebied achter balken en panelen met behulp van het compositor-blurprotocol.", + "blur-behind-label": "Achtergrond vervagen", + "box-border-description": "Toon een omlijning rond inhoudsgebieden.", + "box-border-label": "Containeromtrek", + "box-border-radius-description": "Past de hoekronding aan van belangrijke lay-outsecties, zoals zijbalken, kaarten en inhoudspanelen.", + "box-border-radius-label": "Hoekradius", + "control-border-radius-description": "Regelt de kromming van interactieve elementen, waaronder knoppen, schakelaars en tekstvelden.", + "control-border-radius-label": "Voer straal in", + "desc": "Pas de look, feel en het gedrag van de interface aan.", + "dim-desktop-description": "Dim de desktop wanneer panelen of menu's geopend zijn.", + "dim-desktop-label": "Desktop dimmen", + "dimmer-opacity-description": "Stel het opaciteitsniveau in voor het dimmen van het bureaublad.", + "dimmer-opacity-label": "Doorzichtigheid van gedimde desktop", + "panel-background-opacity-description": "Stel de achtergronddoorzichtigheid in voor alle panelen (balk, launcher, instellingen, enz...).", + "panel-background-opacity-label": "Paneel achtergronddoorzichtigheid", + "panels-attached-to-bar-description": "Panelen vergrendelen aan de balk en schermranden, voor een naadloze look met stijlvolle omgekeerde hoeken.", + "panels-attached-to-bar-label": "Panelen aan randen vastklikken", + "panels-overlay-description": "Zorgt ervoor dat panelen en de balk zichtbaar blijven, zelfs boven fullscreen-applicaties.", + "panels-overlay-label": "Panelen & balk bovenop houden", + "scaling-description": "Wijzigt de grootte van de algemene gebruikersinterface, exclusief de balk.", + "scaling-label": "Interfaceschaling", + "scrollbar-always-visible-description": "Houd schuifbalken altijd zichtbaar wanneer inhoud scrollbaar is, in plaats van ze alleen bij hover te tonen.", + "scrollbar-always-visible-label": "Schuifbalken altijd weergeven", + "settings-panel-header": "Instellingenpaneel", + "settings-panel-mode-description": "Kies lay-out voor instellingen (mogelijk opnieuw openen vereist).", + "settings-panel-mode-label": "Instellingenpaneelmodus", + "settings-panel-sidebar-card-style-description": "Omhult de instellingen zijbalk met een gevulde achtergrond en afgeronde hoeken.", + "settings-panel-sidebar-card-style-label": "Zijbalk kaartstijl", + "shadows-description": "Schakelt slagschaduwen onder balken en panelen in.", + "shadows-direction-description": "Kies waar de schaduw wordt geworpen.", + "shadows-direction-label": "Richting van schaduw", + "shadows-label": "Slagschaduwen", + "title": "Gebruikersinterface", + "tooltips-description": "Schakel tooltips in of uit in de hele interface.", + "tooltips-label": "Tooltips tonen", + "translucent-widgets-description": "Maak knoppen, tabbladen en andere widgets in panelen semi-transparant.", + "translucent-widgets-label": "Doorzichtige widgets" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Alfabetisch", + "automation-change-mode-description": "Kies hoe achtergronden worden geselecteerd bij automatisch wisselen.", + "automation-change-mode-label": "Wisselmodus", + "automation-custom-interval-description": "Voer tijd in als UU:MM (bijv. 01:30).", + "automation-custom-interval-label": "Aangepast interval", + "automation-interval-description": "Hoe vaak achtergronden automatisch worden gewisseld.", + "automation-interval-label": "Achtergrondinterval", + "automation-random-wallpaper-description": "Plan willekeurige achtergrondwissels op vaste intervallen.", + "automation-scheduled-change-description": "Wissel automatisch achtergronden op vaste intervallen.", + "automation-scheduled-change-label": "Geplande wissel", + "look-feel-edge-smoothness-description": "Past een zacht, vervaagd effect toe op de rand van overgangen.", + "look-feel-edge-smoothness-label": "Overgangsranden verzachten", + "look-feel-fill-color-description": "Kies een vulkleur die achter de achtergrond kan verschijnen.", + "look-feel-fill-mode-description": "Selecteer hoe de afbeelding moet schalen om overeen te komen met de resolutie van je monitor.", + "look-feel-fill-mode-label": "Vulmodus", + "look-feel-skip-startup-transition-description": "Sla de achtergrondanimatie over wanneer de shell start.", + "look-feel-skip-startup-transition-label": "Opstartovergang overslaan", + "look-feel-title": "Uiterlijk & gevoel", + "look-feel-transition-duration-description": "Duur van overgangsanimaties in seconden.", + "look-feel-transition-duration-label": "Overgangsduur", + "look-feel-transition-type-description": "Animatietype bij het wisselen tussen achtergronden.", + "look-feel-transition-type-label": "Overgangstype", + "settings-clear-cache-button": "Cache wissen", + "settings-clear-cache-description": "Wis in de cache opgeslagen, verkleinde achtergronden om schijfruimte vrij te maken.", + "settings-clear-cache-label": "Achtergrondcache", + "settings-clear-cache-toast": "Achtergrondcache geleegd", + "settings-desc": "Bepaal hoe achtergronden worden beheerd en weergegeven.", + "settings-enable-management-description": "Beheer achtergronden met Noctalia. Schakel uit als je liever een andere applicatie gebruikt.", + "settings-enable-management-label": "Achtergrondbeheer inschakelen", + "settings-enable-overview-description": "Past een vervaagde en gedimde achtergrond toe op het overzichtsscherm.", + "settings-enable-overview-label": "Achtergrond voor overzicht inschakelen", + "settings-folder-description": "Pad naar je hoofdachtergrondmap.", + "settings-folder-label": "Achtergrondmap", + "settings-hide-wallpaper-filenames-tooltip-hide": "Bestandsnamen verbergen", + "settings-hide-wallpaper-filenames-tooltip-show": "Bestandsnamen weergeven", + "settings-monitor-specific-description": "Stel een andere achtergrondmap in voor elke monitor.", + "settings-monitor-specific-label": "Monitor-specifieke mappen", + "settings-monitor-specific-tooltip": "Monitor-achtergrondmap", + "settings-overview-blur-strength-description": "Past de vervagingssterkte toe op het overzicht.", + "settings-overview-blur-strength-label": "Vervagingssterkte van het overzicht", + "settings-overview-tint-description": "Past de tintsterkte toe op het overzicht.", + "settings-overview-tint-label": "Overzichtstintsterkte", + "settings-recursive-search-description": "Zoek ook naar achtergronden in submappen van de achtergrondmap.", + "settings-recursive-search-label": "Submappen doorzoeken", + "settings-select-monitor-folder": "Achtergrondmap voor monitor selecteren", + "settings-selector-description": "Kies je achtergrond.", + "settings-selector-position-description": "Kies waar het achtergrondselectiepaneel verschijnt.", + "settings-show-hidden-files-tooltip-hide": "Verborgen bestanden verbergen", + "settings-show-hidden-files-tooltip-show": "Verborgen bestanden weergeven", + "settings-title": "Achtergrondinstellingen", + "settings-use-original-images-description": "Sla het formaat wijzigen van achtergronden over voordat ze worden weergegeven. Bespaart schijfruimte en vermindert CPU-gebruik tijdens achtergrondwisselingen, maar kan meer geheugen gebruiken voor zeer grote afbeeldingen.", + "settings-use-original-images-label": "Gebruik originele afbeeldingen", + "settings-view-mode-description": "Kies hoe achtergronden uit uw map worden weergegeven.", + "settings-view-mode-label": "Weergavemodus", + "view-mode-browse": "Blader door mappen", + "view-mode-cycle-tooltip": "Weergavemodus: {mode} (klik om te wijzigen)", + "view-mode-recursive": "Afgevlakte submappen", + "view-mode-single": "Rootdirectory" + } + }, + "placeholders": { + "add-new-keybind": "Nieuwe toetsencombinatie toevoegen", + "command-example": "echo \"Hello World\"", + "enter-command": "Voer het uit te voeren commando in (app of aangepast script)", + "enter-ipc-identifier": "Voer een unieke identificatie in voor IPC-opdrachten", + "enter-path": "Pad invoeren...", + "enter-text-to-collapse": "bijv. 'er wordt niets afgespeeld'. Gebruik /regex/ voor patronen.", + "enter-tooltip": "Voer tooltip in", + "enter-width-pixels": "Voer breedte in pixels in", + "keybind-recording": "Toetsencombinatie opnemen...", + "search": "Zoeken...", + "search-icons": "bijv. noctalia, niri, battery, cloud", + "search-launcher": "Items zoeken... of gebruik > voor commando's", + "search-wallhaven": "Zoek in Wallhaven...", + "search-wallpapers": "Typ om achtergronden te filteren..." + }, + "positions": { + "bottom": "Onder", + "bottom-center": "Middenonder", + "bottom-left": "Linksonder", + "bottom-right": "Rechtsonder", + "center": "Centreren", + "center-left": "Midden links", + "center-right": "Midden rechts", + "close-to-bar": "Dicht bij balkknop", + "follow-bar": "Balk volgen", + "left": "Links", + "right": "Rechts", + "top": "Boven", + "top-center": "Middenboven", + "top-left": "Linksboven", + "top-right": "Rechtsboven" + }, + "session-menu": { + "action-in-seconds": "{action} over {seconds} seconden...", + "cancel-timer": "Timer annuleren", + "title": "Sessie-menu" + }, + "setup": { + "all-done": "Klaar!", + "appearance": { + "subheader": "Kies donkere modus en kleurbronnen (achtergrond of voorgedefinieerd)." + }, + "customize": { + "header": "Pas je ervaring aan", + "subheader": "Pas balkpositie, dichtheid, schaal en meer aan." + }, + "skip-setup": "Setup overslaan", + "telemetry-wizard-done": "Begrepen!", + "telemetry-wizard-note": "Je hebt de controle — schakel dit op elk moment in of uit in de instellingen", + "telemetry-wizard-subtitle": "We hebben anonieme analyses toegevoegd om Noctalia te helpen verbeteren", + "telemetry-wizard-title": "Privacy-update", + "wallpaper": { + "choose-dir": "Kies een map met je achtergrondafbeeldingen", + "dir-browse": "Bladeren naar achtergrondmap", + "dir-description": "Kies de map met je achtergronden.", + "dir-label": "Achtergrondmap", + "dir-select-title": "Achtergrondmap selecteren", + "header": "Kies je achtergrond", + "no-dir": "Geen achtergrondmap geselecteerd", + "no-valid": "Geen geldige afbeeldingsbestanden gevonden in: {dir}", + "none-in-dir": "Geen achtergronden gevonden in map", + "preview-error": "Afbeelding laden mislukt", + "select-prompt": "Selecteer hieronder een achtergrond", + "subheader": "Zet de toon met een mooie achtergrond." + }, + "welcome": "Welkom", + "welcome-note": "Een paar basisinstellingen om je op weg te helpen — alle opties vind je in instellingen", + "welcome-subtitle": "Laten we uw bureaublad uniek maken", + "welcome-title": "Welkom bij Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "Geen mediaplayer gedetecteerd", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "Onbekende app", + "uptime": "Actieve tijd: {uptime}", + "user-requested": "Door gebruiker aangevraagd", + "welcome-back": "Welkom terug,", + "widget-settings-title": "Instellingen voor {widget}" + }, + "system-monitor": { + "core-usage": "Kern {id} gebruik", + "cpu-temp": "CPU-temperatuur", + "cpu-usage": "CPU-gebruik", + "disk": "Schijf", + "download-speed": "Downloadsnelheid", + "gpu-temp": "GPU-temperatuur", + "load-average": "Gem. belasting", + "title": "Systeemmonitor", + "upload-speed": "Uploadsnelheid" + }, + "toast": { + "airplane-mode": { + "description": "Alle draadloze communicatie uitschakelen.", + "title": "Vliegtuigmodus" + }, + "battery": { + "critical": "Kritiek accuniveau", + "critical-desc": "Accu is {percent}% — sluit de oplader onmiddellijk aan", + "low": "Accu bijna leeg", + "low-desc": "Accu is op {percent}% — sluit de oplader aan" + }, + "bluetooth": { + "address-copied": "Adres gekopieerd naar klembord", + "auto-connect-disabled": "Automatisch verbinden uitgeschakeld", + "auto-connect-enabled": "Automatisch verbinden ingeschakeld", + "auto-connecting": "Verbinding maken met {count} appara(a)t(en)...", + "confirm-code": "Bevestig code {value} op het andere apparaat.", + "connect-failed": "Verbinding met apparaat mislukt", + "disconnect-failed": "Verbinding verbreken met apparaat mislukt", + "discoverable-change-failed": "Zichtbaarheidsstatus wijzigen mislukt", + "discoverable-disabled": "Zichtbaar uitgeschakeld", + "discoverable-enabled": "Zichtbaar ingeschakeld", + "display-code": "Voer code {value} in op het andere apparaat.", + "forget-failed": "Apparaat vergeten mislukt", + "pair-failed": "Apparaat koppelen mislukt", + "passkey-required": "Passkey vereist door het apparaat.", + "pincode-required": "PIN-code vereist door het apparaat.", + "state-change-failed": "Bluetooth‑status wijzigen mislukt" + }, + "clipboard": { + "long-text": "Lange tekst", + "unavailable": "Klembordgeschiedenis niet beschikbaar", + "unavailable-desc": "De toepassing 'cliphist' is niet geïnstalleerd. Installeer deze om functies voor klembordgeschiedenis te gebruiken" + }, + "custom-command-failed": { + "description": "Commando mislukt: {command}\\nExitcode: {code}", + "title": "Aangepaste opdracht mislukt" + }, + "do-not-disturb": { + "disabled": "'Niet storen' uitgeschakeld", + "disabled-desc": "Alle meldingen worden weergegeven", + "enabled": "'Niet storen' ingeschakeld", + "enabled-desc": "Je vindt deze meldingen terug in je geschiedenis" + }, + "donation-opened": "Donatiepagina geopend in uw browser", + "internet-limited": "Verbonden zonder internet", + "keyboard-layout": { + "changed": "Toetsenbordindeling gewijzigd naar {layout}", + "title": "Toetsenbord" + }, + "missing-control-center": { + "description": "De bedieningscentrumwidget is uit de balk verwijderd. Om er vanuit de balk weer toegang toe te krijgen, moet je de widget opnieuw toevoegen. Je kunt het ook openen door met de rechtermuisknop op de balk te klikken", + "label": "Laatste bedieningscentrumwidget verwijderd" + }, + "night-light": { + "forced": "Geforceerde activatie", + "normal": "Normale modus", + "not-installed": "wlsunset niet geïnstalleerd" + }, + "noctalia-performance": { + "disabled": "Prestatiemodus uitgeschakeld", + "enabled": "Prestatiemodus ingeschakeld", + "label": "Noctalia Performance" + }, + "power-profile": { + "changed": "Energieprofiel gewijzigd", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "Er is een fout opgetreden tijdens het verwerken van sjablonen", + "title": "Sjabloonverwerking mislukt" + }, + "vpn": { + "connected": "Verbonden met '{name}'", + "disconnected": "Verbinding met '{name}' verbroken" + }, + "wallpaper-colors": { + "disabled": "Achtergrondkleuren uitgeschakeld", + "enabled": "Achtergrondkleuren ingeschakeld", + "label": "Achtergrondkleuren genereren" + }, + "wifi": { + "connected": "Verbonden met '{ssid}'", + "connection-failed": "Verbinding mislukt", + "connection-timeout": "Time-out bij verbinding", + "disconnected": "Verbinding met '{ssid}' verbroken", + "incorrect-password": "Onjuist wachtwoord", + "network-not-found": "Netwerk niet gevonden" + } + }, + "tooltips": { + "add-widget": "Widget toevoegen", + "auto-connect": "Automatisch verbinden voor dit apparaat in-/uitschakelen", + "bluetooth-auto-connect-off": "Automatisch verbinden uitgeschakeld", + "bluetooth-auto-connect-on": "Automatisch verbinden ingeschakeld", + "bluetooth-devices": "Bluetooth-apparaten", + "brightness-at": "Helderheid: {brightness}%", + "click-to-start-recording": "Schermrecorder (opname starten)", + "click-to-stop-recording": "Schermrecorder (opname stoppen)", + "collapse": "Zijbalk inklappen", + "copy-address": "Adres kopiëren", + "delete-notification": "Melding verwijderen", + "dismiss-notification": "Melding sluiten", + "do-not-disturb-enabled": "Niet storen", + "expand": "Zijbalk uitklappen", + "forget-network": "Netwerk vergeten", + "grid-view": "Rasterweergave", + "hidden-files-hide": "Verborgen bestanden", + "hide-unnamed-devices": "Verberg naamloze apparaten", + "home": "Home-map", + "input-muted": "Ingang dempen/dempen opheffen", + "keep-awake": "Wakker houden", + "keyboard-layout": "{layout}-toetsenbordindeling", + "list-view": "Lijstweergave", + "manage-vpn": "VPN-verbindingen", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "Maximum aantal widgets bereikt", + "microphone-volume-at": "Microfoonvolume: {volume}%", + "move-to-section": "Ga naar {section}", + "mute": "Dempen", + "next-media": "Volgende track", + "next-month": "Volgende maand", + "night-light-not-installed": "Nachtlicht (niet beschikbaar)", + "noctalia-performance-enabled": "Noctalia-prestatiemodus", + "open-annotation-tool": "Openen met annotatietool", + "open-control-center": "Bedieningscentrum", + "open-notification-history-enable-dnd": "Meldingsgeschiedenis", + "open-settings": "Instellingen", + "open-tray-dropdown": "Systeemvak", + "output-muted": "Uitgang dempen/dempen opheffen", + "power-profile": "{profile} energieprofiel", + "previous-media": "Vorige track", + "previous-month": "Vorige maand", + "refresh-devices": "Apparaten verversen", + "refresh-wallhaven": "Wallhaven-resultaten verversen", + "refresh-wallpaper-list": "Achtergrondlijst verversen", + "remove-widget": "Widget verwijderen", + "search-close": "Zoeken sluiten", + "session-menu": "Sessiemenu", + "show-all-devices": "Toon alle apparaten", + "switch-to-dark-mode": "Donkere modus", + "switch-to-light-mode": "Lichte modus", + "unmute": "Dempen opheffen", + "up": "Bovenliggende map", + "volume-at": "Uitvoervolume: {volume}%", + "wallpaper-selector": "Achtergrondkiezer" + }, + "wallpaper": { + "browse": { + "empty-directory": "Deze map is leeg.", + "go-root": "Ga naar de root van de achtergrond", + "go-up": "Ga naar de bovenliggende map", + "go-up-hint": "Gebruik de terugknop om omhoog te navigeren." + }, + "configure-directory": "Configureer je achtergrondmap met afbeeldingen.", + "fill-modes": { + "crop": "Bijsnijden (vullend)", + "fit": "Passend (volledig zichtbaar)", + "repeat": "Herhalen (Tegel)", + "stretch": "Uitrekken" + }, + "no-match": "Geen resultaten gevonden.", + "no-wallpaper": "Geen achtergrond gevonden.", + "panel": { + "apikey-help": "Een API-sleutel is vereist om toegang te krijgen tot NSFW-content.", + "apikey-label": "API-sleutel", + "apikey-managed-by-env": "Beheerd via de NOCTALIA_WALLHAVEN_API_KEY omgevingsvariabele.", + "apikey-placeholder": "Voer uw Wallhaven API-sleutel in", + "appearance-dark-tab": "Donker", + "appearance-light-tab": "Licht", + "categories-anime": "Anime", + "categories-label": "Categorieën", + "categories-people": "Mensen", + "color-extraction-disabled": "Achtergrondkleurextractie gebruiken", + "color-extraction-enabled": "Voorgedefinieerde kleurenschema's gebruiken", + "header-devices-apply-all-tooltip": "Kies achtergrond per monitor", + "header-devices-per-monitor-tooltip": "Op alle monitoren toepassen", + "header-link-light-dark-tooltip": "Achtergrond behouden bij het wisselen van thema", + "header-separate-light-dark-tooltip": "Achtergrond automatisch wisselen met thema", + "header-sun-linked-tooltip": "Aparte lichte en donkere achtergronden", + "header-sun-separate-tooltip": "Zelfde achtergrond voor licht en donker", + "order-asc": "Oplopend", + "order-desc": "Aflopend", + "order-label": "Volgorde", + "purity-label": "Inhoudsfilter", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Sketchy", + "ratios-all-portrait": "Alle portret", + "ratios-all-wide": "Alle brede", + "ratios-any": "Alle", + "ratios-label": "Beeldverhouding", + "resolution-atleast": "Ten minste", + "resolution-exact": "Exact", + "resolution-label": "Resolutie", + "resolution-mode-label": "Modus", + "solid-color-tooltip": "Effen kleur achtergrond", + "sort-date-asc": "Sorteren op oudste eerst", + "sort-date-desc": "Sorteren op nieuwste eerst", + "sort-name-asc": "Sorteren op naam (A-Z)", + "sort-name-desc": "Sorteren op naam (Z-A)", + "sort-random": "Sorteer willekeurig", + "sorting-date-added": "Datum toegevoegd", + "sorting-favorites": "Favorieten", + "sorting-label": "Sorteren op", + "sorting-relevance": "Relevantie", + "sorting-toplist": "Toplijst", + "sorting-views": "Weergaven", + "source-label": "Bron", + "source-wallhaven": "Wallhaven", + "title": "Achtergrondkiezer", + "wallhaven-settings-title": "Wallhaven-instellingen" + }, + "transitions": { + "disc": "Schijf", + "fade": "Vervagen", + "honeycomb": "Honingraat", + "pixelate": "Pixeliseren", + "stripes": "Strepen", + "wipe": "Vegen" + }, + "try-different-search": "Probeer een andere zoekopdracht.", + "wallhaven": { + "loading": "Achtergronden laden...", + "no-results": "Geen achtergronden gevonden. Probeer een andere zoekopdracht.", + "page": "{current} van {total}", + "page-prefix": "Pagina", + "page-suffix": "van {total}" + } + }, + "weather": { + "clear-sky": "Onbewolkt", + "drizzle": "Motregen", + "fog": "Mist", + "mainly-clear": "Overwegend helder", + "overcast": "Zwaar bewolkt", + "partly-cloudy": "Gedeeltelijk bewolkt", + "rain-showers": "Regenbuien", + "snow": "Sneeuw", + "thunderstorm": "Onweer" + }, + "widgets": { + "color-picker": { + "palette-description": "Kies uit een breed scala aan vooraf gedefinieerde kleuren.", + "palette-label": "Palet", + "palette-theme-colors": "Snelle toegang tot het kleurenpalet van je thema.", + "title": "Kleurkiezer" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm in kleine letters", + "ampm-uppercase": "AM/PM in hoofdletters", + "common-12hour-time-minutes": "12-uurs tijdnotatie met minuten", + "common-24hour-time-minutes": "24-uurs tijdnotatie met minuten", + "common-24hour-time-seconds": "24-uurs tijdnotatie met seconden", + "common-european-date": "Europese datumnotatie", + "common-iso-date": "ISO-datumnotatie", + "common-us-date": "Amerikaanse datumnotatie", + "common-weekday-date": "Weekdag met datum", + "common-weekday-month-day": "Weekdag, maand en dag", + "day-abbreviated": "Afgekorte naam van de dag", + "day-full": "Volledige naam van de dag", + "day-leading-zero": "Dag met voorloopnul (01-31)", + "day-no-leading-zero": "Dag zonder voorloopnul (1-31)", + "hour-leading-zero": "Uur met voorloopnul (00-23) — 24-uurs formaat", + "hour-no-leading-zero": "Uur zonder voorloopnul (0-23) — 24-uurs formaat", + "minute-leading-zero": "Minuut met voorloopnul (00-59)", + "minute-no-leading-zero": "Minuut zonder voorloopnul (0-59)", + "month-abbreviated": "Afgekorte maandnaam", + "month-full": "Volledige maandnaam", + "month-number-leading-zero": "Maand als getal met voorloopnul (01-12)", + "month-number-no-zero": "Maand als getal zonder voorloopnul (1-12)", + "second-leading-zero": "Seconde met voorloopnul (00-59)", + "second-no-leading-zero": "Seconde zonder voorloopnul (0-59)", + "timezone-abbreviation": "Afkorting van de tijdzone", + "year-four-digit": "Jaar als viercijferig getal", + "year-two-digit": "Jaar als tweecijferig getal (00-99)" + }, + "file-picker": { + "item": "item", + "items": "items", + "search-placeholder": "Zoek bestanden en mappen...", + "select-current": "Huidige selecteren", + "select-file": "Bestand selecteren", + "select-folder": "Map selecteren", + "selected": "Geselecteerd:", + "title": "Bestandskiezer" + }, + "file-picker-title": "Selecteer een bestand", + "icon-picker": { + "title": "Pictogramkiezer" + }, + "tooltip-placeholder": "Plaatshouder" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "Anonieme Identiteit", + "ca-cert": "CA-certificaat", + "eap-method": "EAP-methode", + "password": "Gebruikerswachtwoord", + "phase2-auth": "Fase 2 Authenticatie", + "username": "Identiteit / Gebruikersnaam" + }, + "panel": { + "action-required": "Actie vereist", + "add-hidden-network": "Verborgen netwerk toevoegen", + "add-network": "Netwerk toevoegen", + "available-interfaces": "Beschikbare interfaces", + "available-networks": "Beschikbare netwerken", + "disabled": "Wi-Fi is uitgeschakeld", + "disconnecting": "Verbreken...", + "dns": "DNS", + "enable-message": "Schakel Wi-Fi in om beschikbare netwerken te zien.", + "enter-password": "Voer wachtwoord in...", + "forget": "Vergeten", + "forget-network": "Dit netwerk vergeten?", + "forgetting": "Vergeten...", + "hidden-network": "Verborgen netwerk", + "interface": "Netwerkinterface", + "internet-connected": "Verbonden met internet", + "internet-limited": "Geen internet", + "internet-status": "Internetstatus", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "Bekende netwerken", + "link-speed": "Link snelheid", + "network-name-ssid": "Netwerknaam (SSID)", + "no-ethernet-devices": "Geen Ethernet-apparaten gedetecteerd", + "no-networks": "Geen netwerken gevonden", + "saved": "Opgeslagen", + "scan-again": "Opnieuw scannen", + "searching": "Zoeken naar netwerken in de buurt...", + "security-open": "Openen", + "security-wep": "WEP", + "security-wpa": "WPA/WPA2/WPA3 Persoonlijk", + "security-wpa-ent": "WPA Enterprise", + "security-wpa2-ent": "WPA2 Enterprise", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 Enterprise", + "show-password": "Wachtwoord tonen", + "title": "Wi-Fi" + }, + "signal": { + "excellent": "Uitstekend", + "fair": "Redelijk", + "good": "Goed", + "poor": "Slecht", + "weak": "Zwak" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/nn-HN.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/nn-HN.json new file mode 100644 index 0000000..de1f34d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/nn-HN.json @@ -0,0 +1,241 @@ +{ + "actions": { + "clear-history": "Tøm tidefar", + "close-app": "Steng {app}", + "connect-vpn": "Få samband med {name}", + "cycle-visualizer": "Skift framsyning", + "disable-bluetooth": "Slå av Blåtann", + "disable-dnd": "Slå av \"Ikkje skjepla\"", + "disable-wifi": "Slå av trådlaust net", + "disconnect-vpn": "Enda samband med {name}", + "dock-settings": "Festeinnstillingar", + "enable-bluetooth": "Slå på Blåtann", + "enable-dnd": "Slå på \"Ikkje skjepla\"", + "enable-wifi": "Slå på trådlaust net", + "launcher-settings": "Innstillingar for nøytendeveljar", + "lower-to-bottom": "Send til botnar", + "open-calendar": "Opna almanakka", + "open-display-settings": "Syn innstillingar", + "open-launcher": "Opna nøytendeveljar", + "open-settings": "Opna innstillingar", + "raise-to-top": "Send til toppen", + "random-wallpaper": "Slumpebilæte", + "run-custom-command": "Køyr sjølvgjort styrebod", + "show-active-only": "Syn berre verksam", + "show-all": "Syn alt", + "toggle-mute": "Ljodsløkkjing på/av", + "widget-settings": "Nøytelinnstillingar" + }, + "authentication": { + "error": "Sanningsmistak", + "failed": "Sanningsmistak" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Lita den verksame ruta med hamlitene.", + "hide-mode-description": "Stjorna kor nøytelen ter seg når det ikkje er ei verksam ruta.", + "scrolling-mode-description": "Stjorna når lange rutenomn kann bledjast.", + "show-app-icon-description": "Syn nøytendefråmerke attmed rutenamn.", + "show-app-icon-label": "Syn nøytendefråmerke" + }, + "audio-visualizer": { + "color-name-description": "Kjos lìten på framsyningi.", + "color-name-label": "Fyllelìt", + "hide-when-idle-description": "Framsyningi løynest når dette er på, minder noko spelar av.", + "hide-when-idle-label": "Løyn når ingen ting spelar av", + "width-description": "Sjølvvald lutbreidd." + }, + "battery": { + "device-default": "Fyrevald syningsambod", + "device-description": "Kjos batteriambod du vil syna fram.", + "device-label": "Batteriambod", + "display-mode-description": "Vel korleîs batteriet er synt fram på radi.", + "display-mode-graphic": "Upplysande batteriteikn", + "display-mode-graphic-clean": "Upplysande batteriteikn (ingen %)", + "display-mode-icon-always": "Fråmerke - syn % stødt", + "display-mode-icon-hover": "Fråmerke - syn ved sviv", + "display-mode-icon-only": "Einast teikn", + "hide-if-idle-description": "Løyn nøytelen når batteriet ikkje ladar og ikkje gjev straum.", + "hide-if-idle-label": "Løyn når uverksam", + "hide-if-not-detected-description": "Løyn nøytelen når kjernen ikkje finn eit batteri.", + "hide-if-not-detected-label": "Løyn når ikkje funne", + "low-battery-threshold-label": "Åtvaringstreskald for lågt batteri", + "show-noctalia-performance-description": "Syn brjotaren for Ytestoda frå Noctalia i batterifjøli.
Slær av skuggar og bilætrørslor i Noctalia for å minka emnebruk.", + "show-noctalia-performance-label": "Syn brjotaren for Ytestoda frå Noctalia", + "show-power-profile-description": "Syn veljaren for straumhått i batterifjøli.", + "show-power-profile-label": "Syn straumhåttstoli" + }, + "brightness": { + "apply-all-description": "Brigd på ljosstyrke på kvar skjerm når ein nyttar bladhjulet.", + "apply-all-label": "Gjev bladbrigde åt kvar skjerm" + }, + "clock": { + "clock-display-description": "Legg til merke frå skråi her for å måta til klokka. 'AP'-merket lyt vera med um klokka berre skal hava 12 timar.", + "clock-display-label": "Klokkesyning", + "custom-font-description": "Kjos skriftkynd til klokka.", + "custom-font-label": "Skriftkynd", + "custom-font-placeholder": "Vel skriftkynd ...", + "custom-font-search-placeholder": "Leita i skriftkynder ...", + "horizontal-bar-description": "Råd: Du kann nytta \\n til å skifta til ny lina.", + "horizontal-bar-label": "Vassbein rad", + "preview": "Vis fyreåt", + "tooltip-format-description": "Snidstreng for vitringi som kjem upp når du sviv yver klokka. Haldt tom for fyrevald vitring.", + "tooltip-format-label": "Vitringssnid", + "use-custom-font-description": "Sjå burt frå skriftkyndi elles og nytta ei onnor skriftkynd til klokka.", + "use-custom-font-label": "Nytta onnor skriftkynd", + "use-monospaced-font-description": "Klokka nyttar fastbreiddeskrift når dette er på.", + "use-monospaced-font-label": "Nytta fastbreiddeskrift", + "vertical-bar-description": "Skriv inn eit millomrom for å skifta kvar lut til ei ny lina.", + "vertical-bar-label": "Loddbein rad" + }, + "control-center": { + "browse-file": "Leita etter fil", + "browse-library": "Leita i samning", + "color-selection-description": "Lita nøytelfråmerke med hamlitene.", + "enable-colorization-description": "Lita merket til styreromet med hamlitene.", + "icon-description": "Kjos fråmerke frå samningi eller ei onnor fil.", + "select-custom-icon": "Kjos anna fråmerke", + "use-distro-logo-description": "Nytta fråmerket til Linuxavbrigdet ditt helder en eit anna teikn.", + "use-distro-logo-label": "Nytta avbrigdesfråmerke" + }, + "custom-button": { + "collapse-condition-description": "Knappen løynest um utskrifti er lik dette.", + "collapse-condition-label": "Løyningsvilkor", + "color-selection-description": "Lita fråmerket og teksti med hamlitene.", + "default-tooltip": "Sjølvgjord knapp, stjorna i innstillingane", + "display-command-output-description": "Skriv inn eit styrebod som skal køyra jamlege. Syner fyrste utskriftsradi som tekst.", + "display-command-output-label": "Syn utskrift frå styrebod", + "display-command-output-stream-description": "Skriv inn styrebod som skal køyra heile tidi.", + "dynamic-text": "Kvikskrift", + "enable-colorization-description": "Lita fråmerket og teksti på den sjølvgjorde knappen med hamlitene.", + "enable-colorization-label": "Lita", + "general-tooltip-text-description": "Sjølvvald tekst å syna i hjelpeteksti åt knappen.", + "general-tooltip-text-label": "Sjølvvald hjelpetekst", + "hide-mode-always-expanded": "Stødt vidka", + "hide-mode-description": "Stjornar um nøytelen er til å sjå når styrebodet ikkje kjem med utskrift.", + "hide-mode-expand-with-output": "Vidka ved utskrift", + "hide-mode-label": "Løyndarluna", + "hide-mode-max-transparent": "Vidka heilt men klår", + "icon-description": "Kjos fråmerke frå samningi.", + "ipc-identifier-description": "Sermerke for IPC-styrebod. Nytta dette merket med 'qs -c noctalia-shell ipc call cb [gjerning] [merke]' for å styra knappen med IPC.", + "ipc-identifier-label": "IPC-sermerke", + "left-click-description": "Styrebod som køyrer ved vinstretrykk.", + "left-click-label": "Vinstretrykk", + "left-click-update-text": "Før upp ny tekst ved vinstretrykk", + "max-text-length-horizontal-description": "Øvre grensa på teikn som kann synast på vassbein rad (0 for å løyna).", + "max-text-length-horizontal-label": "Øvre grensa på tekstlengd (vassbein)", + "max-text-length-vertical-description": "Øvre grensa på teikn som kann synast på loddbein rad (0 for å løyna).", + "max-text-length-vertical-label": "Øvre grensa på tekstlengd (loddbein)", + "middle-click-description": "Styrebod som køyrer ved midtrykk.", + "middle-click-label": "Midtrykk", + "middle-click-update-text": "Før upp ny tekst ved midtrykk", + "parse-json-description": "Tyd utfall frå styrebodet som JSON-likam for å stjorna tekst og fråmerke kvikt.", + "parse-json-label": "Tyd utfall som JSON", + "refresh-interval-description": "Bil i millisekundar.", + "refresh-interval-label": "Nykveikjingsbil", + "right-click-description": "Styrebod som køyrer ved høgretrykk.", + "right-click-label": "Høgretrykk", + "right-click-update-text": "Før upp ny tekst ved midtrykk", + "show-exec-tooltip-description": "Syn hjelpetekst med styrebodsupplysningar (vinstre/høgre/midtrykk, hjul).", + "show-exec-tooltip-label": "Syn hjelpetekst med styrebod", + "show-icon-description": "Skifter um nøytelfråmerket er synlegt eller ikkje.", + "show-icon-label": "Syn fråmerke", + "show-text-tooltip-description": "Syn hjelpetekst med utskrifti or styrebodet.", + "show-text-tooltip-label": "Syn kvikskrift hjelpeskrift", + "text-stream-description": "Strøymde utfallssegner frå styrebodet syner seg som tekst på knappen.", + "text-stream-label": "Strøym", + "wheel-description": "Styrebod som køyrer når ein bled hjulet på musi.
Nytta $delta i styrebodet for hjulsbrigdi.", + "wheel-down": "Bled nedetter", + "wheel-down-description": "Styrebod som køyrer når hjulet er bladt nedetter.", + "wheel-down-label": "Styrebod - nedetter", + "wheel-label": "Bladhjul", + "wheel-mode-separate-description": "Hav ulike styrebod når ein bled uppetter og nedetter.", + "wheel-mode-separate-label": "Skilde hjulstyrebod", + "wheel-up": "Bled uppetter", + "wheel-up-description": "Styrebod som køyrer når hjulet er bladt uppetter.", + "wheel-up-label": "Styrebod - uppetter", + "wheel-update-text": "Før upp ny tekst med bledjing" + }, + "keyboard-layout": { + "show-icon-description": "Syn merket for knappefjøluppsett." + }, + "lock-keys": { + "hide-when-off-description": "Løyn sermerket når knappen ikkje er verksam.", + "hide-when-off-label": "Løyn når av", + "show-caps-lock-description": "Syn storlåshåtten", + "show-caps-lock-label": "Storlås", + "show-num-lock-description": "Syn tallåshåtten", + "show-num-lock-label": "Tallås", + "show-scroll-lock-description": "Syn bladlåshåtten", + "show-scroll-lock-label": "Bladlås" + }, + "media-mini": { + "compact-mode-description": "Få spelnøytelsfjøli til å roma mindre.", + "compact-mode-label": "Klemb spelnøytelen", + "hide-mode-description": "Stjorna framferdi åt nøytelen når ingen ting spelar av.", + "max-width-description": "Fastset kor mykje nøytelen kan lengjast vassbeint. Nøytelen dreg seg i hop for å røma stuttare innhald.", + "no-active-player": "Ingi verksame spelarar", + "panel-section-description": "Stjorna skapnaden og framferdi åt spelnøytelsfjøli.", + "panel-section-label": "Spelnøytelsfjøl", + "scrolling-mode-description": "Stjorna når lange spelnomn kann bledjast.", + "scrolling-mode-label": "Bladluna", + "show-album-art-description": "Syn platekunsten til spelet som spelar av no.", + "show-album-art-label": "Syn platekunst", + "show-artist-first-description": "Syn spelmann - spel helder en spel - spelmann", + "show-artist-first-label": "Syn spelmann fyrst", + "show-progress-ring-description": "Syn ein ringvorden framgangsvisar.", + "show-progress-ring-label": "Syn framgangsvisar", + "show-visualizer-description": "Syn ljodframsyning når eitkvart spelar av.", + "show-visualizer-label": "Syn framsyning", + "use-fixed-width-description": "Nøytelen ruvar so mykje råd er helder en å høva seg til innhald, når dette er på.", + "use-fixed-width-label": "Nytta fast breidd", + "visualizer-type-description": "Vel eit slag for ljodframsyningi.", + "visualizer-type-label": "Framsyningsslag" + }, + "notification-history": { + "hide-widget-when-zero-description": "Løyn varselteikn når det ikkje er varsel.", + "hide-widget-when-zero-label": "Løyn teikn når inkje varsel", + "hide-widget-when-zero-unread-description": "Løyn varselteikn når det ikkje er ulesne varsel.", + "hide-widget-when-zero-unread-label": "Løyn teikn når inkje ulesne varsel", + "show-unread-badge-description": "Syn eit merke med kor mange ulesne varsel det er.", + "show-unread-badge-label": "Syn \"ulesne\"-merke", + "unread-badge-color-description": "Kjos lit for merket som syner kor mange ulesne varsel det er.", + "unread-badge-color-label": "\"Ulesne\"-merkelit" + }, + "section-editor": { + "placeholder": "Kjos ein nøytel å leggja til ...", + "search-placeholder": "Leita etter nøytel ..." + }, + "spacer": { + "width-description": "Rombreidd i molar." + }, + "system-monitor": { + "compact-mode-description": "Syn upplysningar som suleteikningar helder en skrift. Stoggar brigde i uppsettet.", + "compact-mode-label": "Klemb nøytelen", + "cpu-frequency-description": "Syn midverksnarleik i GHz.", + "cpu-frequency-label": "Syn midverksøkning", + "cpu-temperature-description": "Syn hitemælingar for midverket um råd.", + "cpu-temperature-label": "Midverkhite", + "cpu-usage-description": "Syn midverkbruk som hundradpart.", + "cpu-usage-label": "Midverkbruk" + } + }, + "common": { + "brightness": "Ljosstyrke", + "select-color-description": "Lita med hamlitene for tyngd." + }, + "options": { + "bar": { + "density-compact": "Klembd" + }, + "launcher-density": { + "compact": "Klembd" + }, + "notification-density": { + "compact": "Klembd" + } + }, + "system": { + "welcome-back": "Velkomen attende," + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/nn-NO.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/nn-NO.json new file mode 100644 index 0000000..6738243 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/nn-NO.json @@ -0,0 +1,2041 @@ +{ + "actions": { + "clear-history": "Tøm historikk", + "close-app": "Steng {app}", + "connect-vpn": "Kopla til {app}", + "cycle-visualizer": "Skift visualiserar", + "disable-bluetooth": "Slå av Bluetooth", + "disable-dnd": "Slå av Ikkje forstyrr", + "disable-wifi": "Slå av Wi-Fi", + "disconnect-vpn": "Kopla frå {name}", + "dock-settings": "Festepunktinstillingar", + "enable-bluetooth": "Slå på Bluetooth", + "enable-dnd": "Slå på Ikkje forstyrr", + "enable-wifi": "Slå på Wi-Fi", + "launcher-settings": "Innstillingar for programveljar", + "lower-to-bottom": "Flytt til botnen", + "open-calendar": "Opna kalender", + "open-display-settings": "Innstillingar for skjerm", + "open-launcher": "Opna programveljar", + "open-settings": "Opna innstillingar", + "raise-to-top": "Flytt til toppen", + "random-wallpaper": "Tilfelleleg bakgrunnsbilete", + "run-custom-command": "Køyr eigendefinert kommando", + "show-active-only": "Vis berre aktive", + "show-all": "Vis alle", + "toggle-mute": "Slå ljod av/på", + "widget-settings": "Innstillingar for miniprogram" + }, + "authentication": { + "error": "Autentiseringsfeil", + "failed": "Autentisering feila" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Fargar ikonet til det aktive vindauga med temafargane.", + "hide-mode-description": "Styrer kva miniprogrammet gjer når det ikkje er eit aktivt vindauga.", + "scrolling-mode-description": "Styrer når rullande tekst slår på for lange vindaugenamn.", + "show-app-icon-description": "Viser programikonet attmed vindaugenamnet.", + "show-app-icon-label": "Vis programikon" + }, + "audio-visualizer": { + "color-name-description": "Vel farge for visualiseraren.", + "color-name-label": "Fyll farge", + "height-description": "Eigendefinert komponentbreidd", + "hide-when-idle-description": "Når dette er på, gøymer visualiseraren seg med mindre noko aktivt spelar.", + "hide-when-idle-label": "Gøym når ingenting spelar av", + "width-description": "Eigendefinert komponentbreidd." + }, + "battery": { + "device-default": "Standard (Skjermeining)", + "device-description": "Vel kva for batterieining å visa.", + "device-label": "Batterieining", + "display-mode-description": "Vel korleis batteriet vert vist på oppgåvelina.", + "display-mode-graphic": "Grafisk batteri", + "display-mode-graphic-clean": "Grafisk batteri (ingen %)", + "display-mode-icon-always": "Ikon - Vis alltid %", + "display-mode-icon-hover": "Ikon - Vis ved sviv", + "display-mode-icon-only": "Einast ikon", + "hide-if-idle-description": "Gøymer miniprogrammet når batteriet ikkje ladar opp eller ut.", + "hide-if-idle-label": "Gøym når i dvale", + "hide-if-not-detected-description": "Gøymer miniprogrammet når inga batterieining er funnen.", + "hide-if-not-detected-label": "Gøym når ikkje funnen", + "low-battery-threshold-label": "Varslingsterskel for lågt batterinivå", + "show-noctalia-performance-description": "Viser ytingsmodusbrytaren for Noctalia i batterifanen.
Slår av skuggar og animasjonar i Noctalia for å minka ressursbruk.", + "show-noctalia-performance-label": "Vis Noctalia Yting-brytaren", + "show-power-profile-description": "Viser veljaren for energiprofil i batterifanen.", + "show-power-profile-label": "Vis kontrollane for energiprofil" + }, + "brightness": { + "apply-all-description": "Endra på ljosstyrken for alle skjermar når ein brukar rullehjulet. ", + "apply-all-label": "Gjev rulleendringar til alle skjermar" + }, + "clock": { + "clock-display-description": "Måtar klokkevisninga ved å leggja til merke frå lista. For å bruka 12-timeformat må du ha med 'AP'-merket.", + "clock-display-label": "Klokkevisar", + "custom-font-description": "Vel eigendefinert skrifttype til klokkevisaren.", + "custom-font-label": "Eigendefinert skrifttype", + "custom-font-placeholder": "Vel eigendefinert skrifttype...", + "custom-font-search-placeholder": "Søk gjennom skrifttypar...", + "horizontal-bar-description": "Tips: Skriv \\n for å byrja ei ny linje", + "horizontal-bar-label": "Vassrett panel", + "preview": "Førevisning", + "tooltip-format-description": "Formater streng til hjelpeteksten som kjem opp når peikaren er over klokka. Haldt dette tomt for å bruka standardteksten.", + "tooltip-format-label": "Hjelpetekstformat", + "use-custom-font-description": "Overstyrer standardskrifttypen med eigendefinert skrifttype til klokka.", + "use-custom-font-label": "Bruk eigendefinert skrifttype", + "use-monospaced-font-description": "Når dette er på, brukar klokka fastbreiddeskrift.", + "use-monospaced-font-label": "Bruk fastbreiddeskrift", + "vertical-bar-description": "Brukar mellomrom for å skilja kvar del på ei ny linje.", + "vertical-bar-label": "Loddrett panel" + }, + "control-center": { + "browse-file": "Leita i filene", + "browse-library": "Leita i biblioteket", + "color-selection-description": "Gjev ikona temafargane.", + "enable-colorization-description": "Fargar kontrollsenterikonet med temafargane.", + "icon-description": "Vel ikon frå biblioteket eller eigendefinert fil.", + "select-custom-icon": "Vel eigendefinert ikon", + "use-distro-logo-description": "Brukar kjennemerket til distribusjonen din heller enn eigendefinert ikon.", + "use-distro-logo-label": "Bruk distrokjennemerke heller enn ikon" + }, + "custom-button": { + "collapse-condition-description": "Dersom utskrifta er lik dette, skyler knappen seg.", + "collapse-condition-label": "Skylevilkår", + "color-selection-description": "Gjev ikon og tekst temafargane.", + "default-tooltip": "Eigendefinert knapp, styr i innstillingane", + "display-command-output-description": "Gjev kommando for å køyra regelrett. Fyrste utskriftslinja vert vist som tekst.", + "display-command-output-label": "Vis kommando utskrift", + "display-command-output-stream-description": "Gjev kommando for å køyra heile tida.", + "dynamic-text": "Dynamisk tekst", + "enable-colorization-description": "Gjev temafargane til ikonet og teksten på den eigendefinerte knappen.", + "enable-colorization-label": "Slå på fargelegging", + "general-tooltip-text-description": "Eigendefinert tekst som skal visast i hjelpeteksta for knappen.", + "general-tooltip-text-label": "Eigendefinert hjelpetekst", + "hide-mode-always-expanded": "Alltid utvida", + "hide-mode-description": "Styrer om miniprogrammet skal visast når kommandoen ikkje har utskrift.", + "hide-mode-expand-with-output": "Utvida med utskrift", + "hide-mode-label": "Skjulemodus", + "hide-mode-max-transparent": "Heilt utvida men gjennomsynleg", + "icon-description": "Vel ikon frå biblioteket.", + "ipc-identifier-description": "Særskild identifikator for IPC kommandoar. Bruk denne identifikatoren med 'qs -c noctalia-shell ipc call cb [handling] [identifikator]' for å styra denne knappen med IPC.", + "ipc-identifier-label": "IPC-identifikator", + "left-click-description": "Kommando som køyrer når knappen er venstreklikka.", + "left-click-label": "Venstreklikk", + "left-click-update-text": "Oppdater tekst som viser seg med venstreklikk", + "max-text-length-horizontal-description": "Øvre grense på kor mange teikn som kan visast i vassrett panel (0 for å skyla tekst).", + "max-text-length-horizontal-label": "Øvre grense på tekstlengd (vassrett)", + "max-text-length-vertical-description": "Øvre grense på kor mange teikn som kan visast i loddrett panel (0 for å skyla tekst).", + "max-text-length-vertical-label": "Øvre grense på tekstlengd (loddrett)", + "middle-click-description": "Kommando som køyrer når knappen er midtklikka.", + "middle-click-label": "Midtklikk", + "middle-click-update-text": "Oppdater tekst som viser seg med midtklikk", + "parse-json-description": "Tolkar kommandoutskrift som JSON-objekt for å setja tekst og ikon dynamisk.", + "parse-json-label": "Tolk utskrift som JSON", + "refresh-interval-description": "Tidsrom i millisekund", + "refresh-interval-label": "Tidsrom mellom oppdateringar", + "right-click-description": "Kommando som køyrer når knappen er høgreklikka.", + "right-click-label": "Høgreklikk", + "right-click-update-text": "Oppdater tekst som viser seg med høgreklikk", + "show-exec-tooltip-description": "Vis hjelpetekst med kommandodetaljar (venstre/høgre/midtklikk, hjul).", + "show-exec-tooltip-label": "Syn kommandohjelpetekst", + "show-icon-description": "Gjer miniprogramsikonet synleg eller ikkje.", + "show-icon-label": "Vis ikon", + "show-text-tooltip-description": "Vis hjelpetekst med utskrifta frå tekstkommandoen.", + "show-text-tooltip-label": "Vis dynamisk hjelpetekst", + "text-stream-description": "Strøymde linjer frå kommandoen viser seg som tekst på knappen.", + "text-stream-label": "Strøym", + "wheel-description": "Kommando som køyrer når ein brukar rullehjulet.
Bruk $delta for rullehjulsdeltaen i kommandoen.", + "wheel-down": "Rull nedetter", + "wheel-down-description": "Kommando som køyrer når ein rullar ned på rullehjulet.", + "wheel-down-label": "Hjul ned-kommando", + "wheel-label": "Rullehjul", + "wheel-mode-separate-description": "Slår på åtskilde kommandoar for når ein rullar opp eller ned.", + "wheel-mode-separate-label": "Skilde hjulekommandoar", + "wheel-up": "Rull oppetter", + "wheel-up-description": "Kommando som køyrer når ein rullar opp på rullehjulet.", + "wheel-up-label": "Hjul opp-kommando", + "wheel-update-text": "Oppdater vist tekst på rulling" + }, + "keyboard-layout": { + "show-icon-description": "Viser ikonet for tastaturoppsett." + }, + "lock-keys": { + "hide-when-off-description": "Skyler teiknet når knappen ikkje er aktiv.", + "hide-when-off-label": "Skyl når avslegen", + "show-caps-lock-description": "Viser status på Caps Lock.", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "Viser status på Num Lock.", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "Viser status på Scroll Lock.", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "Slår på eit oppsett som sparar rom i mediaavspelarpanelet.", + "compact-mode-label": "Tettmodus", + "hide-mode-description": "Styrer kva miniprogrammet gjer når ingenting spelar av.", + "max-width-description": "Set ei øvre grense for kor breid miniprogrammet kan vera. Miniprogrammet minkar for å romma kortare innhald.", + "no-active-player": "Ingen aktiv avspelar", + "panel-section-description": "Styrer kva mediaavspelarpanelet gjer og kor det ser ut.", + "panel-section-label": "Mediaavspelarpanel", + "scrolling-mode-description": "Styrer når rullande tekst slår på med lange songtitlar.", + "scrolling-mode-label": "Rullemodus", + "show-album-art-description": "Viser albumkunst for songen som spelar av no.", + "show-album-art-label": "Vis albumkunst", + "show-artist-first-description": "Viser artist - tittel heller enn tittel - artist.", + "show-artist-first-label": "Vis artist fyrst", + "show-progress-ring-description": "Viser rund sporgangsvisar.", + "show-progress-ring-label": "Vis rund sporgangsvisar", + "show-visualizer-description": "Viser ljodvisualiserar når musikk spelar av.", + "show-visualizer-label": "Vis visualiserar", + "use-fixed-width-description": "Når dette er på, brukar miniprogrammet maksimum breidde heile tida, heller enn å måta seg til innhald dynamisk.", + "use-fixed-width-label": "Bruk fast breidde", + "visualizer-type-description": "Vel kva slags ljodvisualiserer du vil sjå.", + "visualizer-type-label": "Visualiserartype" + }, + "notification-history": { + "hide-widget-when-zero-description": "Skyler varslingsikonet når det ikkje er varsel.", + "hide-widget-when-zero-label": "Skyl ikon når inga varsel", + "hide-widget-when-zero-unread-description": "Skyler varslingsikonet når det ikkje er ulesne varsel.", + "hide-widget-when-zero-unread-label": "Skyl ikon når inga ulesne varsel.", + "show-unread-badge-description": "Viser merke med kor mange ulesne varsel det er.", + "show-unread-badge-label": "Vis \"ulesne\"-merke", + "unread-badge-color-description": "Vel fargen for merket for uleste varslingar.", + "unread-badge-color-label": "Farge på ulest merke" + }, + "section-editor": { + "placeholder": "Vel miniprogram å leggja til...", + "search-placeholder": "Leita i miniprogram..." + }, + "spacer": { + "width-description": "Mellomrom i piksel." + }, + "system-monitor": { + "compact-mode-description": "Viser statistikk som småe stolpediagram heller enn tekst. Stoggar oppsettet frå å skifta.", + "compact-mode-label": "Tettmodus", + "cpu-cores-description": "Syn prosessorkjernebruk åtskilde.", + "cpu-cores-label": "Prosessorkjerner", + "cpu-frequency-description": "Vis prosessorklokkefrekvens no i GHz", + "cpu-frequency-label": "Vis prosessorfrekvens", + "cpu-temperature-description": "Viser temperaturmålingar frå prosessoren om mogeleg.", + "cpu-temperature-label": "Prosessortemperatur", + "cpu-usage-description": "Viser prosessorbruk no i prosenttal.", + "cpu-usage-label": "Prosessorbruk", + "disk-path-description": "Vel kva for monteringspunkt for disk å overvaka.", + "disk-path-label": "Filbane til disk", + "gpu-temperature-description": "Viser temperaturmålingar frå skjermkortet om mogeleg.", + "load-average-description": "Viser snittleg systemlast.", + "load-average-label": "Snittleg last", + "memory-percentage-description": "Viser minnebruk som prosenttal heller enn absolutte verdiar.", + "memory-percentage-label": "Minne som prosent", + "memory-usage-description": "Viser minnebruk no.", + "memory-usage-label": "Minnebruk", + "network-traffic-description": "Viser snøggleik på opplasting og nedlasting på nettverket.", + "network-traffic-label": "Nettverkstraffik", + "storage-as-percentage-description": "Viser diskplass som prosenttal heller enn absolutte tal.", + "storage-as-percentage-label": "Disk som prosenttal", + "storage-available-description": "Viser kor mykje diskplass står att heller enn kor mykje er brukt.", + "storage-available-label": "Tilgjengeleg diskplass", + "storage-usage-description": "Viser diskplassbruk.", + "storage-usage-label": "Diskplassbruk", + "swap-usage-description": "Viser swap-minnebruk.", + "swap-usage-label": "Swap-minnebruk", + "use-monospace-font-description": "Bruk skrift med fast breidde.", + "use-monospace-font-label": "Fastbreiddeskrift", + "use-padding-description": "Legg til utfylling føre teksta for å hindra endring i oppsett.", + "use-padding-description-disabled-monospace-font": "Du må ha fastsett ei fastbreiddeskrift for å bruka dette.", + "use-padding-description-disabled-vertical": "Loddrett oppgåvelina støtter ikkje utfylling.", + "use-padding-label": "Utfyll tekst" + }, + "taskbar": { + "colorize-icons-description": "Gjev ikon på oppgåvelina temafargane.", + "hide-mode-description": "Styrer kva miniprogrammet gjer når det ikkje er samsvarande vindaugo.", + "hide-mode-label": "Gøymemodus", + "icon-scale-description": "Fastset skaleringsfaktor for ikon på oppgåvelina.", + "icon-scale-label": "Ikonskalering", + "max-width-description": "Øvre grensa på kor breid oppgåvelina kan vera i høve til skjermen, i prosent.", + "max-width-label": "Øvre grense på breidde", + "only-active-workspaces-description": "Viser berre appar frå aktive arbeidsrom.", + "only-active-workspaces-label": "Berre frå aktive arbeidsrom", + "only-same-monitor-description": "Viser berre appar frå skjermen med panelet på.", + "only-same-monitor-label": "Berre frå same skjerm", + "show-pinned-apps-description": "Viser feste appar frå festepunktet i oppgåvelina.", + "show-pinned-apps-label": "Vis feste appar", + "show-title-description": "Viser vindaugenamn i oppgåvelina.", + "show-title-description-disabled": "Loddrett panel kan ikkje visa vindaugenamn.", + "show-title-label": "Vis namn", + "smart-width-description": "Måtar til breidde automatisk med talet på oppføringar.", + "smart-width-label": "Smart breidde", + "title-width-description": "Fastset breidde på vindaugenamn i oppgåvelina (i piksel).", + "title-width-label": "Namnebreidde", + "title-width-reset-tooltip": "Tilbakestill namnebreidde" + }, + "tray": { + "chevron-color-description": "Gjev temafargane til skuffepila.", + "chevron-color-label": "Pilfarge", + "colorize-icons-description": "Gjev ikona i systempanelet temafargane.", + "colorize-icons-label": "Farga ikon", + "drawer-enabled-description": "Når dette er på, viser ufeste systempanelobjekt seg i eit skuffepanel.
Når dette er avslege, viser alle systempanelobjekt seg i rad.", + "drawer-enabled-label": "Aktiver skuff", + "hide-passive-description": "Når dette er på, gøymer systempanelobjekt med 'passiv' status seg.", + "hide-passive-label": "Gøym passive objekt" + }, + "volume": { + "display-mode-description": "Vel korleis dette skal visast." + }, + "workspace": { + "character-count-description": "Kor mange teikn som skal visast frå arbeidsromnamn (1 - 10).", + "character-count-label": "Teikntal", + "empty-color-description": "Set bakgrunnsfargen for tomme Workspaces.", + "empty-color-label": "Farge for tomt arbeidsområde", + "enable-scrollwheel-description": "Byter mellom arbeidsrom med rullehjulet på musa.", + "enable-scrollwheel-label": "Rull for å byta arbeidsrom", + "focused-color-description": "Set bakgrunnsfargen for det fokuserte Workspace.", + "focused-color-label": "Fokusert arbeidsområde-farge", + "follow-focused-screen-description": "Viser arbeidsrom frå den aktive skjermen, heller enn skjermen der panelet finn seg.", + "follow-focused-screen-label": "Fylg aktiv skjerm", + "font-weight-description": "Fastset synleg tyngd for tekst innan arbeidsrom.", + "font-weight-label": "Skrifttyngd", + "grouped-border-opacity-description": "Fastset kor ugjennomsynlege grensene på arbeidsromrammene er.", + "grouped-border-opacity-label": "Ugjennomsynlege grenser", + "hide-unoccupied-description": "Viser ikkje arbeidsrom utan vindaugo.", + "hide-unoccupied-label": "Gøym utan vindaugo", + "label-mode-description": "Vel korleis etikettar for arbeidsrom skal visast.", + "label-mode-label": "Etikettmodus", + "occupied-color-description": "Set bakgrunnsfargen for opptekne Workspaces.", + "occupied-color-label": "Farge for oppteke arbeidsområde", + "pill-size-description": "Endrar på kor stor pillene er for kvart arbeidsrom.", + "pill-size-label": "Pillestorleik", + "reverse-scrolling-description": "Reverser retninga for arbeidsområdebytte når du skrollar.", + "reverse-scrolling-label": "Omvendt rulling", + "show-applications-description": "Viser applikasjonsikon innanfor kvart arbeidsrom.", + "show-applications-label": "Vis applikasjonar", + "show-badge-description": "Vis arbeidsområdenummer-merket i gruppert modus.", + "show-badge-label": "Vis arbeidsområde-merke", + "show-labels-only-when-occupied-description": "Viser etiketter for arbeidsrom berre når dei har vindaugo.", + "show-labels-only-when-occupied-label": "Vis etikettar berre med vindaugo", + "unfocused-icons-opacity-description": "Fastset kor ugjennomsynlege ufokuserte applikasjonsikon er.", + "unfocused-icons-opacity-label": "Ugjennomsynlege ufokuserte ikon" + } + }, + "battery": { + "all-batteries": "Alle batteria (i sum)", + "battery-health": "Batterihelse", + "battery-level": "Batterinivå", + "capacity-level": "Kapasitet: {level}", + "charging-rate": "Ladesnøggleik: {rate} W", + "discharging-rate": "Utladingssnøggleik: {rate} W", + "health": "Helse: {percent}%", + "inhibit-idle-description": "Held systemet vake.", + "no-battery-detected": "Inga batteri funnen", + "plugged-in": "Tilkopla", + "power-profile": "Energiprofil", + "time-left": "Tid att: {tid}", + "time-until-full": "Tid til full: {tid}" + }, + "bluetooth": { + "panel": { + "available-devices": "Tilgjengelege einingar", + "blocked": "Blokkert", + "connected-devices": "Kopla einingar", + "device-address": "Einingadresse", + "disabled": "Bluetooth er fråkopla", + "discoverable": "Finnande", + "enable-message": "Slå på Bluetooth for å sjå tilgjengelege einingar.", + "known-devices": "Kjende einingar", + "no-devices": "Ingen einingar funne", + "paired-devices": "Kopla einingar", + "pairing-mode": "Sjå at eininga di er i koplingsmodus.", + "scanning": "Leitar etter einingar...", + "signal-text-excellent": "Tilkopling: Framifrå", + "signal-text-fair": "Tilkopling: Grei", + "signal-text-good": "Tilkopling: God", + "signal-text-poor": "Tilkopling: Dåleg", + "signal-text-unknown": "Tilkopling: Ukjend", + "signal-text-very-poor": "Tilkopling: Svært dåleg" + } + }, + "changelog": { + "error": { + "fetch-failed": "Kan ikkje lasta inn historikk. Prøv att seinare.", + "rate-limit": "Du har nått lastgrensa på GitHub. Prøv att om nokre minutt." + }, + "panel": { + "buttons-discord": "Vert med på Discorden vår", + "buttons-dismiss": "Greitt", + "buttons-feedback": "Gjev attermelding", + "empty": "Versjonsnotatar er ikkje tilgjengelege enno.", + "highlight-title": "Høgdepunkt", + "released": "Gjeven ut på {date}", + "subtitle-fresh": "Takk for at du installerte Noctalia! Her er det som er med i denne utgåva.", + "subtitle-updated": "Oppdatert frå {previousVersion}", + "title": "Nytt i {version}", + "version": "Versjon {version}", + "version-new-user": "Fersk installasjon" + } + }, + "common": { + "actions": "Handlingar", + "active": "Aktiv", + "add": "Legg til", + "appearance": "Utsjånad", + "apply": "Bruk", + "auto-connect": "Kopla til automatisk", + "automation": "Automatisering", + "available": "Tilgjengelege", + "back": "Attende", + "battery": "Batteri", + "behavior": "Åtferd", + "bluetooth": "Bluetooth", + "brightness": "Ljosstyrke", + "browse": "Blad", + "calendar": "Kalender", + "calendar-panel": "Kalenderpanel", + "cancel": "Bryt av", + "cards": "Kort", + "charging": "Ladar", + "clear": "Tøm", + "clipboard": "Utklippstavle", + "close": "Steng", + "color-muted": "Dempa", + "colors": "Fargar", + "command": "Kommando", + "confirm": "Stadfest", + "connect": "Kopla til", + "connected": "Kopla til", + "connecting": "Koplar til...", + "continue": "Hald fram", + "contributors": "Medverkarar", + "copied-to-clipboard": "Kopiert til utklippstavla", + "countdown": "Nedteljing", + "customize": "Måta til", + "date": "Dato", + "default": "Standard", + "delete": "Slett", + "devices": "Einingar", + "disabled": "Avslegen", + "discharging": "Ladar ut", + "disconnect": "Kopla frå", + "disconnected": "Kopla frå", + "disconnecting": "Koplar frå...", + "display-mode": "Visingsmodus", + "documentation": "Dokumentasjon", + "download": "Last ned", + "duration": "Varing", + "dysfunctional": "I uorden", + "edit": "Endra", + "enabled": "Påslegen", + "error": "Feil", + "ethernet": "Ethernet", + "events": "Hende", + "execute": "Utfør", + "faithful": "Tru", + "focus": "Fokus", + "font-weight-bold": "Feit", + "font-weight-light": "Tynn", + "font-weight-medium": "Mellomstor", + "font-weight-regular": "Vanleg", + "font-weight-semibold": "Halvfeit", + "frequency": "Frekvens", + "gateway": "Port", + "general": "Generell", + "height": "Høgd", + "hibernate": "Set i full dvale", + "history": "Historikk", + "icon": "Ikon", + "idle": "Inaktiv", + "info": "Info", + "input": "Inndata", + "install": "Installer", + "installed": "Installerte", + "interface": "Grensesnitt", + "internet": "Internett", + "keybind": "Beinveg", + "language": "Mål", + "loading": "Lastar inn...", + "local": "Lokal", + "location": "Stad", + "lock": "Lås", + "logout": "Skriv ut", + "look": "Sjå", + "margins": "Margar", + "media": "Media", + "media-player": "Mediaavspelar", + "memory": "Minne", + "monitors": "Skjermar", + "network": "Nettverk", + "next": "Neste", + "night-light": "Natteljos", + "no": "Nei", + "no-results": "Ikkje noko utfall", + "no-summary": "Ikkje noko samandrag", + "none": "Ingen", + "not-found": "Ikkje funne", + "notifications": "Varsel", + "official": "Offisiell", + "on-surface": "På overflata", + "output": "Utdata", + "pair": "Par", + "paired": "Para", + "pairing": "Parar...", + "panels": "Panel", + "password": "Passord", + "pause": "Pause", + "performance": "Yting", + "pin": "Fest", + "play": "Spel", + "polling": "Spørjing", + "position": "Posisjon", + "previous": "Førre", + "primary": "Primær", + "random": "Tilfelleleg", + "reboot": "Start å nyo", + "reboot-to-uefi": "Start å nyo til UEFI Fastvare", + "record": "Start opptak", + "refresh": "Lasta inn å nyo", + "required": "(krevst)", + "reset": "Attendestill", + "reset-to-default": "Set att til Standard", + "result-count": "{count} utfall", + "result-count-plural": "{count} utfall", + "resume": "Tak opp att", + "save": "Lagra", + "scanning": "Leitar...", + "screen-corners": "Skjermhyrne", + "search": "Søk", + "second": "{count} sekund", + "second-plural": "{count} sekund", + "secondary": "Sekundær", + "security": "Trygging", + "select": "Vel", + "select-color": "Vel farge", + "select-color-description": "Gjev temafargar for å heva ut.", + "select-icon-color": "Vel ikonfarge", + "settings": "Innstillingar", + "shortcuts": "Beinvegar", + "shutdown": "Slå av", + "signal": "Dekning", + "sound": "Ljod", + "sources": "Kjelder", + "start": "Start", + "stop": "Stogg", + "supporters": "Støttarar", + "suspend": "Set i lett dvale", + "templates": "Malar", + "tertiary": "Tertiær", + "test": "Test", + "thresholds": "Tersklar", + "title": "Tittel", + "toast": "Skjermvarsel", + "trusted": "Lita på", + "uninstall": "Desinstaller", + "unknown": "Ukjend", + "unpair": "Par frå", + "unpin": "Fest av", + "update": "Oppdater", + "upload": "Lasta opp", + "userspace-reboot": "Start brukarromet å nyo", + "version": "Versjon", + "vibrant": "Livleg", + "visualizer": "Visualiserar", + "volume": "Ljodstyrke", + "volumes": "Ljodstyrke", + "wallpaper": "Bakgrunnsbilete", + "weather": "Vêr", + "weather-loading": "Lastar inn vêr", + "week": "Veke", + "widgets": "Miniprogram", + "width": "Breidde", + "wifi": "Wi-Fi", + "windows": "Vindauge", + "yes": "Ja" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Energiprofil", + "tooltip-disabled": "Installer power-profiles-daemon for å bruka energiprofil" + }, + "wifi": { + "label-disconnected": "Kopla frå Wi-Fi", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Alltid gøym", + "always-show": "Alltid vis", + "force-open": "Tving open", + "on-hover": "Ved sviv" + }, + "hide-modes": { + "auto-hide": "Automatisk gøyming", + "hidden": "Gøym når tom", + "idle": "Gøym når inaktiv", + "non-exclusive": "Ikkje uthaldande", + "transparent": "Gjennomsynleg når tom", + "visible": "Alltid synleg" + }, + "launcher": { + "categories": { + "all": "Alt", + "audiovideo": "Ljod & video", + "chat": "Samtale", + "development": "Utvikling", + "education": "Opplæring", + "emoji-activity": "Aktivitetar", + "emoji-animals": "Dyr", + "emoji-flags": "Flagg", + "emoji-food": "Mat og drikke", + "emoji-nature": "Natur", + "emoji-objects": "Ting", + "emoji-people": "Folk & kropp", + "emoji-recent": "Nyleg", + "emoji-symbols": "Symbol", + "emoji-travel": "Reiser & stader", + "game": "Spel", + "graphics": "Grafikk", + "misc": "Ymis", + "office": "Kontor", + "pinned": "Feste", + "system": "System", + "webbrowser": "Nettlesar" + }, + "providers": { + "applications": "Applikasjonar", + "calculator": "Kalkulator", + "calculator-deprecated": "\">calc\"-kommandoen er deprekert og skal snart slettast. Skriv heller matteuttrykk beinveges i søk.", + "calculator-description": "Kalkulator — reknar ut matematiske uttrykk", + "calculator-enter-expression": "Skriv inn eit matematisk uttrykk", + "calculator-press-enter-to-copy": "Trykk Enter for å kopiera utfallet", + "clipboard": "Utklippsbok", + "clipboard-clear-description": "Tøm heile utklippsboka", + "clipboard-clear-description-full": "Slett alt frå utklippsboka", + "clipboard-clear-history": "Tøm utklippsboka", + "clipboard-delete": "Slett oppføring på utklippstavla", + "clipboard-history-disabled": "Utklippsboka avslegen", + "clipboard-history-disabled-description": "Slå på utklippsboka i innstillingane eller installer cliphist", + "clipboard-loading": "Lastar inn utklippsbok...", + "clipboard-search-description": "Leita i utklippsboka", + "command-description": "Køyr skalkommandoar", + "emoji": "Emojiveljar", + "emoji-loading": "Lastar inn emojiar...", + "emoji-loading-description": "Vent litt", + "emoji-no-recent": "Ingen nylege emojiar enno", + "emoji-search-description": "Leita i og kopiera emojiar", + "settings-search-description": "Søk og naviger til innstillingar", + "windows-search-description": "Søk og fokuser opne vindauge" + } + }, + "lock-screen": { + "authenticating": "Autentiserer...", + "password": "Skriv passordet ditt...", + "restart": "Start å nyo", + "shut-down": "Slå av" + }, + "notifications": { + "panel": { + "click-to-collapse": "Klikk for å gøyma", + "click-to-expand": "Klikk for å utvida", + "description": "Varsla dine kjem opp her når dei kjem inn.", + "no-notifications": "Ingen varsel" + }, + "range": { + "earlier": "Tidlegare", + "today": "I dag", + "yesterday": "I går" + }, + "time": { + "diff-d": "For 1 dag sidan", + "diff-dd": "For {diff} dagar sidan", + "diff-h": "For 1 time sidan", + "diff-hh": "For {diff} timar sidan", + "diff-m": "For 1 minutt sidan", + "diff-mm": "For {diff} minutt sidan", + "now": "no" + } + }, + "options": { + "bar": { + "density-comfortable": "Høveleg", + "density-compact": "Tett", + "density-default": "Standard", + "density-mini": "Ørliten", + "density-spacious": "Romsleg", + "display-mode-always-visible": "Alltid synleg", + "type-floating": "Flytande", + "type-framed": "I ramme", + "type-simple": "Endefram" + }, + "control-center": { + "quick-settings-style-classic": "Klassisk", + "quick-settings-style-modern": "Moderne" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "Høveleg", + "compact": "Tett", + "default": "Standard" + }, + "launcher-view-mode": { + "grid": "Rutenett", + "list": "Liste" + }, + "notification-density": { + "compact": "Tett", + "default": "Standard" + }, + "scrolling-modes": { + "always": "Alltid rull", + "hover": "Rull ved sviv", + "never": "Aldri rull" + }, + "session-menu-grid-layout": { + "grid": "Rutenett", + "single-row": "Einskild rad" + }, + "settings-panel-mode": { + "attached": "Panel knytt til lina", + "centered": "Panel i midten", + "window": "Utskilt vindauga" + }, + "visualizer-types": { + "linear": "Lineær", + "mirrored": "Spegla", + "wave": "Bylgja" + }, + "visualizer-visibility": { + "with-background": "Einast med bakgrunn" + }, + "workspace-labels": { + "index": "Register", + "index-and-name": "Register og namn", + "name": "Namn" + } + }, + "panels": { + "about": { + "become-supporter": "Vert med og støtt", + "changelog": "Sjå endringar", + "changelog-on-startup": "Syn endringshistorikk etter oppdatering", + "changelog-on-startup-desc": "Syn endringshistorikken automatisk når Noctalia er oppdatert", + "contributors-description": "Takk og ære til den {count} framifrå medverkaren vår!", + "contributors-description-plural": "Takk og ære til alle dei {count} framifrå medverkarane våre!", + "copy-info": "Kopier info", + "debug-disabled": "Feilsøking av", + "debug-enabled": "Feilsøking på", + "info-copied": "Info kopiert til utklippstavla", + "noctalia-available": "Tilgjengeleg:", + "noctalia-desc": "Eit lettfram og minimalistisk skrivebordsskal skapa med omhug for Wayland, bygt med Quickshell.", + "noctalia-git-commit": "Git commit:", + "noctalia-installed-version": "Installert versjon:", + "noctalia-latest-version": "Nyaste versjon:", + "noctalia-qs-version": "Noctalia QS versjon:", + "noctalia-title": "Noctaliaskal", + "privacy-policy": "Personvernfråsegn", + "support": "Støtt oss", + "supporter-badge": "Støttar", + "supporters-desc": "Ein stor takk til alle framifrå støttarar!", + "supporters-desc-plural": "Ein stor takk til alle {count} framifrå støttarar!", + "supporters-loading": "Lastar inn støttarar...", + "system-board": "Brett:", + "system-cpu": "Prosessor:", + "system-disk": "Disk:", + "system-gpu": "Skjermkort:", + "system-host": "Vert:", + "system-install-hint": "Installer fastfetch for å sjå systemopplysningar", + "system-kernel": "Kjerne:", + "system-loading": "Lastar inn systemopplysningar...", + "system-memory": "Minne:", + "system-monitor": "Skjerm:", + "system-not-installed": "fasfetch er ikkje installert", + "system-os": "OS:", + "system-packages": "Pakkar:", + "system-product": "Vare:", + "system-title": "Systemopplysningar", + "system-uptime": "Driftstid:", + "system-wm": "WM:", + "telemetry-data-copied": "Telemetri kopiert til utklippstavla", + "telemetry-desc": "Hjelp oss gjera Noctalia betre ved å dela anonyme systemopplysningar (skjermoppløysing, kompositor, Linux-utgåve). Sende ein gong med maskinoppstart, inga sporing, data sletta automatisk etter 30 dagar.", + "telemetry-enabled": "Send anonyme systemopplysningar", + "telemetry-show-data": "Sjå data", + "telemetry-title": "Personvern", + "title": "Om", + "up-to-date": "Du er oppdatert!", + "update-available": "Oppdatering tilgjengeleg", + "view-commit": "Sjå commit på GitHub" + }, + "audio": { + "devices-desc": "Set opp tilgjengeleg ljodinn- og ljoduteiningar.", + "devices-input-device-description": "Vel ljodinneininga du vil setja opp.", + "devices-input-device-label": "Inneining", + "devices-output-device-description": "Vel ljoduteininga du vil setja opp.", + "devices-output-device-label": "Uteining", + "devices-title": "Ljodeiningar", + "external-mixer-description": "Skriv inn kommandoen eller applikasjonsbana som køyrer når du slår på funksjonen for den eksterne ljodmiksaren.", + "external-mixer-label": "Kommando for ekstern ljodmiksar", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Set mediaapplikasjonane du likar best og dei du vil ignorera.", + "media-excluded-player-description": "Skriv inn lykelord for avspelarar som systemet skal ignorera. Kvart lykel ord skal vera på ei ny line.", + "media-excluded-player-label": "Borttekne avspelar", + "media-excluded-player-placeholder": "skriv substring og klikk på +", + "media-frame-rate-description": "Høgre frekvens er jamnare men krev fleire ressursar.", + "media-frame-rate-label": "Biletfrekvens", + "media-primary-player-description": "Skriv lykelord for å kjenna att avspelaren du brukar mest.", + "media-primary-player-label": "Hovudspelar", + "media-primary-player-placeholder": "t.d. spotify, vlc, mpv", + "media-scrolling-speed-description": "Tida det tek for tittelen å blada frå start til ende, i sekund.", + "media-scrolling-speed-label": "Bladefart", + "media-scrolling-title-description": "Slå på varig blading for lange mediatitlar.", + "media-scrolling-title-label": "Bladande tittel", + "media-title": "Mediaavspelarar", + "on-middle-clicked-description": "Kommando som køyrer når knappen er midtklikka.", + "panel-applications-empty": "Ingen applikasjonar spelar av ljod no", + "title": "Ljod", + "visualizer-type-description": "Vel visualiseringstypen når ein spelar av media.", + "visualizer-type-label": "Visualiseringstype", + "volumes-desc": "Endrar kontrollane for ljodstyrke og ljodnivå", + "volumes-feedback-sound-file-description": "Bane til ljoden som spelar av når du endrar ljodstyrke.", + "volumes-feedback-sound-file-label": "Ljodstyrkeendring ljodfil", + "volumes-feedback-sound-file-select-title": "Vel ljodfila som spelar av med ljodstyrkeendring", + "volumes-input-volume-description": "Mikrofon inn-nivå", + "volumes-mute-input-description": "Demp standard ljodinneining (mikrofon).", + "volumes-mute-input-label": "Demp ljodinndata", + "volumes-mute-output-description": "Demp hovudljoduteininga på systemet", + "volumes-mute-output-label": "Demp ljodutdata", + "volumes-output-volume-description": "Ljodstyrke på heile systemet", + "volumes-step-size-description": "Endra kor mykje ljodstyrke endrar seg for kvart steg (rullehjul, beinknappar).", + "volumes-step-size-label": "Steg for ljodstyrke", + "volumes-volume-feedback-description": "Spel av svarljod når ein endrar på ljodstyrke.", + "volumes-volume-feedback-label": "Spel av svarljod med ljodstyrke", + "volumes-volume-overdrive-description": "Tillèt at ljodstyrke kan gå over 100%. Ikkje alltid støtta av maskinvare.", + "volumes-volume-overdrive-label": "Tillat overdrift på ljodstyrke" + }, + "bar": { + "appearance-auto-hide-delay-description": "Tid før Bar-en skjuler seg etter at musa forlèt den.", + "appearance-auto-hide-delay-label": "Skjulingsforsinking", + "appearance-auto-hide-exclusive-note": "Merk: Eksklusiv sone blir automatisk deaktivert når auto-skjul er aktivt.", + "appearance-auto-show-delay-description": "Tid før baren viser seg når musa kjem til kanten.", + "appearance-auto-show-delay-label": "Visingsforsinking", + "appearance-background-opacity-description": "Fastset kor ugjennomsynleg oppgåvelina er.", + "appearance-background-opacity-label": "Ugjennomsynleg oppgåvelina", + "appearance-capsule-color-description": "Vel farge for ilåt på oppgåvelina. Dersom du ikkje vel noko, får det standardfargen.", + "appearance-capsule-color-label": "Ilåtsfarge", + "appearance-capsule-opacity-description": "Set kor ugjennomsynlege bakgrunnen er for miniprogram når ilåtet er på skjermen.", + "appearance-capsule-opacity-label": "Ugjennomsynleg ilåt", + "appearance-content-padding-description": "Måta til utfyllinga mellom panelrender og miniprogram.", + "appearance-content-padding-label": "Innhaldsutfylling", + "appearance-density-description": "Fastset utfylling på lina for å gjera henne tett eller romsleg.", + "appearance-density-label": "Tettleik på oppgåvelina", + "appearance-desc": "Måtar til kor oppgåvelina ser ut og kvar ho er.", + "appearance-display-mode-description": "Vel når baren er synleg.", + "appearance-enable-exclusion-zone-inset-description": "Minska ekskluderingsona med 1 fysisk piksel slik at vindaugo som er kant-i-kant bløder saman.", + "appearance-enable-exclusion-zone-inset-label": "Innrykt ekskluderingssone", + "appearance-floating-description": "Viser oppgåvelina som flytande 'pille'.", + "appearance-floating-label": "Flytande oppgåvelina", + "appearance-font-scale-description": "Måta til skala for skriftstorleik for skrift på panelen.", + "appearance-font-scale-label": "Skriftskala", + "appearance-frame-radius": "Innradius", + "appearance-frame-settings-description": "Endrar rammetjukn og innradius på hyrnet", + "appearance-frame-settings-label": "Innstillingar for ramma", + "appearance-frame-thickness": "Tjukn", + "appearance-hide-on-overview-description": "Gøymer oppgåvelina og stengjer panel når oversyna for samansetjaren er aktiv.", + "appearance-hide-on-overview-label": "Gøym opggåvelina i oversyn", + "appearance-margins-description": "Endrar på margane kring den flytande oppgåvelina.", + "appearance-margins-horizontal": "Vassrett", + "appearance-margins-vertical": "Loddrett", + "appearance-outer-corners-description": "Viser hyrner som bøygjer ut på oppgåvelina.", + "appearance-outer-corners-label": "Ytterhyrne", + "appearance-position-description": "Vel kvar oppgåvelina skal vera på skjermen.", + "appearance-position-label": "Plassering på oppgåvelina", + "appearance-show-capsule-description": "Vis bakgrunn for miniprogram.", + "appearance-show-capsule-label": "Vis ilåt", + "appearance-show-outline-description": "Viser ei synleg grense kring kvart miniprogram.", + "appearance-show-outline-label": "Vis grenser på miniprogram", + "appearance-type-description": "Vel stil for oppgåvelina: Endefram, Flytande eller Innramma.", + "appearance-type-label": "Oppgåvelinestil", + "appearance-use-separate-opacity-description": "Slå dette på for å bruka eit anna nivå for kor ugjennomsynleg bakgrunnen for oppgåvelina er.", + "appearance-use-separate-opacity-label": "Bruk anna nivå for ugjennomsynleg oppgåvelina", + "monitor-configure-widgets": "Set opp miniprogram", + "monitor-override-settings": "Overstyr globale innstillingar", + "monitor-override-settings-description": "Bruk eigendefinerte innstillingar for denne skjermen.", + "monitor-reset-all": "Nullstill alt", + "monitor-widgets-title": "Miniprogramoppsett - {monitor}", + "monitors-desc": "Viser oppgåvelina på visse skjermar. Standarden er alle, dersom du ikkje vel noko.", + "monitors-desc-new": "Vel kva for skjermar viser oppgåvelina, og måta til innstillingar for kvar skjerm.", + "monitors-title": "Vising på skjermar", + "title": "Oppgåvelina", + "tray-blacklist-description": "Legg til unntaksreglar for systempanelet. Støttar varateikn (*).", + "tray-blacklist-label": "Svarteliste", + "tray-blacklist-placeholder": "t.d. nm-applet, Fcitx*", + "tray-pin-application": "Fest applikasjon", + "tray-unpin-application": "Løys applikasjon", + "use-global-widgets": "Bruk globale miniprogram", + "widgets-desc": "Drag miniprogram for å endra på rekkjefylgjd. Bruk høgreklikksmenyen for å senda miniprogram andre stader eller sletta dei.", + "widgets-title": "Miniprogramplassering" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Lagar fargeskala frå bakgrunnsbiletet ditt. Vel fargar automatisk for å skapa ein samanhengande skala.", + "color-source-use-wallpaper-colors-label": "Bruk bakgrunnsfargar", + "dark-mode-mode-description": "Slår på automatisk byte mellom ljos- og mørkemodus.", + "dark-mode-mode-label": "Tidsplan for mørkemodus", + "dark-mode-mode-manual": "For hand", + "dark-mode-mode-off": "Av", + "dark-mode-switch-description": "Byter til mørkare tema for å lettare sjå om natta.", + "delete-error-description": "Mislukka med å sletta {scheme}", + "delete-error-title": "Sletting mislukka", + "delete-success-description": "Lukkast med å sletta {scheme}", + "delete-success-title": "Fargeskala sletta", + "download-button": "Last ned fleire", + "download-downloading": "Lastar ned...", + "download-empty": "Ingen tilgjengelege fargeskalaer", + "download-error-api-error": "API-feil: {status}", + "download-error-description": "Kunne ikkje lasta inn {scheme}", + "download-error-download-failed": "Nedlasting mislukka med feilkode: {code}", + "download-error-invalid-response": "Ugyldig svarformat frå API", + "download-error-no-files": "Ingen filer funne for skala", + "download-error-parse-failed": "Kunne ikkje tolka API-svar: {error}", + "download-error-rate-limit": "Lastgrensa på GitHub-API nådd", + "download-error-title": "Nedlasting mislukka", + "download-fetching": "Hentar tilgjengelege fargeskalaer...", + "download-success-description": "Lasta inn {scheme} utan feil", + "download-success-title": "Fargeskala lasta ned", + "download-title": "Last ned fargeskalaer", + "method-description": { + "content": "Material Design-skala som tek fargane ut nøye slik at fargane samstavast med kjeldefargane.", + "dysfunctional": "Som Faithful, men vel den nest vanlegaste fargefamilien som primær.", + "faithful": "Prøver å halda seg nær til kjeldefargen men samstundes halda paletten harmonisk.", + "fruit-salad": "Material Design-skala som gjev ein livleg og leiksam palett med ymse ulike grunnfargar.", + "monochrome": "Material Design-skala som brukar éin grunnfarge i grått med minimal fargelegging.", + "muted": "Gjev mjukare, umetta fargar som er rolege og avdempa. ", + "rainbow": "Material Design-skala som gjev fargar frå heile spekteret for å auka fargemangfald.", + "tonal-spot": "Standard Material Design-algoritme. Gjev jamne palettar med toneymsing frå grunnfargen.", + "vibrant": "Maksimera metting for djerve, audsynlege palettar med sterke tonar." + }, + "predefined-desc": "Vel frå ei samling førevalde fargeskalaer.", + "predefined-generate-templates-label": "Laga malar for førevalde skalaer", + "predefined-title": "Førevalde fargeskalaer", + "templates-desc": "Gjev fargane til eksterne applikasjonar.", + "templates-filter-description": "Vis malar frå ein viss kategori.", + "templates-filter-label": "Filtrer med kategori", + "templates-misc-description": "Lag dine eigne malar.", + "templates-misc-label": "Avansert", + "templates-misc-user-templates-description": "Slå på berre om du veit kva du gjer — sjå på dokumentasjonen på nettet.", + "templates-misc-user-templates-label": "Slå på brukarmalar", + "templates-none-detected": "Ingen funne", + "templates-write-path": "Skriv: {filepath}", + "title": "Fargeskala", + "wallpaper-method-description": "Vel måten Noctalia lagar fargeskala på.", + "wallpaper-method-label": "Fargeskala lagingsmåte", + "wallpaper-monitor-source-description": "Vel kva for skjerm Noctalia ser på for å velja bakgrunnsfargar.", + "wallpaper-monitor-source-label": "Kjelde for fargeval" + }, + "connections": { + "authentication-required": "Krev autentisering", + "bluetooth-devices-unnamed": "Einingar utan namn er ikkje viste.", + "bluetooth-discoverable": "Ein kan finna eininga som {hostName} medan denne innstillingsfana er open.", + "bluetooth-rssi-polling-description": "Prøv RSSI jamt for å finna kopla einingar med bluetoothctl. Kanskje ikkje tilgjengeleg for alle einingar; brukar minimale ressursar når på.", + "bluetooth-rssi-polling-interval-description": "Styr kor ofte dekningsstyrke skal oppdaterast for tilkopla einingar.", + "bluetooth-rssi-polling-interval-label": "Spørjingsintervall", + "bluetooth-rssi-polling-label": "Bluetooth-dekningsspørjing", + "disable-discoverability-description": "Gøym eininga di frå nære Bluetooth-eininga.", + "disable-discoverability-label": "Gjer eininga usynleg", + "hide-unnamed-devices-description": "Gøym einingar som syner seg berre som Bluetooth-adresser.", + "hide-unnamed-devices-label": "Gøym einingar utan namn", + "pin-instructions": "Skriv inn PIN-koden som eininga gjev deg.", + "title": "Koplingar" + }, + "control-center": { + "cards-desc": "Vel kva for kontrollar skal visast i kontrollsenteret og rekkjefylgda.", + "desc": "Styrer stad og åtferd for kontrollsenteret.", + "position-description": "Vel kvar på skjermen kontrollsenteret opnar seg.", + "shortcuts-custom-button-command-description": "Kommandoen som køyrer når du klikkar på knappen.", + "shortcuts-custom-button-enable-on-state-logic-description": "Slå på eit anna ikon og 'varmt' status frå ein sjekkekommando.", + "shortcuts-custom-button-enable-on-state-logic-label": "Slå på \"på-stode\"-logikk", + "shortcuts-custom-button-icon-description": "Vel eit ikon frå biblioteket.", + "shortcuts-custom-button-on-clicked-label": "Kommando på venstreklikk", + "shortcuts-custom-button-on-middle-clicked-description": "Kommando som køyrer når knappen er midtklikka.", + "shortcuts-custom-button-on-state-command-description": "Kommando som køyrer for å sjå om knappen er i 'på'-stoda.", + "shortcuts-custom-button-on-state-command-label": "På-stode sjekkekommando", + "shortcuts-custom-button-on-state-icon-description": "Ikonet for knappen når han er i 'på'-stoda.", + "shortcuts-custom-button-on-state-icon-label": "På-stode ikon", + "shortcuts-custom-button-state-checks-add": "Legg til stodesjekk", + "shortcuts-custom-button-state-checks-command": "Kommando som køyrer for denne stodesjekken.", + "shortcuts-custom-button-state-checks-label": "Stodesjekkar", + "shortcuts-custom-button-state-checks-remove": "Slett", + "shortcuts-custom-button-tooltip-description": "Hjelpeteksten som viser seg ved musesviv.", + "shortcuts-custom-button-tooltip-label": "Hjelpetekst", + "shortcuts-title": "Miniprogram for beinvegar", + "system-monitor-disk-path-description": "Vel kva for diskmonteringspunkt systemvaktskortet i kontrollsenteret skal vakta.", + "system-monitor-disk-path-label": "Systemvakt filbane", + "title": "Kontrollsenter" + }, + "desktop-widgets": { + "clock-enabled-description": "Vis ei klokkeminiprogram på skrivebordet.", + "clock-enabled-label": "Slå på klokka", + "clock-format-label": "Format", + "clock-height-description": "Høgd på klokkeprogrammet i piksel", + "clock-height-label": "Høgd", + "clock-minimal-mode-description": "Bruk endefram klokkevisning med tid og dato.", + "clock-minimal-mode-label": "Endefram-modus", + "clock-rounded-corners-description": "Bruk avrunda hyrne for bakgrunnen til miniprogrammet.", + "clock-rounded-corners-label": "Avrunda hyrne", + "clock-show-background-description": "Vis bakgrunnsilåta for klokkeprogrammet.", + "clock-show-background-label": "Vis bakgrunn", + "clock-show-date-description": "Vis datoen i dag under tida.", + "clock-show-date-label": "Vis dato", + "clock-show-seconds-description": "Vis sekund i tida.", + "clock-show-seconds-label": "Vis sekund", + "clock-style-analog": "Analog", + "clock-style-binary": "Binær", + "clock-style-description": "Vel klokkevisning.", + "clock-style-digital": "Digital", + "clock-style-label": "Klokkestil", + "clock-style-minimal": "Endefram", + "clock-width-description": "Breidde på klokkeprogrammet i piksel", + "edit-mode-button-label": "Endringsmodus", + "edit-mode-controls-explanation": "Venstreklikk & drag: Flytt eller endra storleik. Høgreklikk: opna val for samanhengsmenyen.", + "edit-mode-description": "Slå på endringsmodus for å flytta på skrivebordsminiprogram. Når dette er på, viser miniprogram ei grense og dei kan flyttast på.", + "edit-mode-exit-button": "Kom ut av endringsmodus", + "edit-mode-grid-snap-label": "Snap til rutenett", + "edit-mode-label": "Endringsmodus", + "enabled-description": "Slå av/på skrivebordsminiprogram heilt.", + "enabled-label": "Slå på skrivebordsminiprogram", + "general-desc": "Styr miniprogram som står på skrivebordet ditt.", + "general-title": "Skrivebordsminiprogram", + "media-player-enabled-description": "Vis mediaavspelar på skrivebordet.", + "media-player-enabled-label": "Slå på mediaminiprogram", + "media-player-rounded-corners-description": "Slå på avrunda hyrne på miniprogramsrendene.", + "media-player-show-album-art-description": "Vis albumkunst og spordata (tittel og artist).", + "media-player-show-album-art-label": "Vis albumkunst og tittel", + "media-player-show-background-description": "Vis bakgrunnsilåta.", + "media-player-show-buttons-description": "Vis mediakontrollane (spel/pause, førre, neste).", + "media-player-show-buttons-label": "Vis knappar", + "media-player-show-visualizer-description": "Vis overlegg for ljodvisualiseraren.", + "media-player-visualizer-type-description": "Vel visualiseringstype.", + "system-stat-layout-bottom": "Nedst", + "system-stat-layout-description": "Vel korleis legenden viser seg i høve til grafen.", + "system-stat-layout-label": "Oppsett", + "system-stat-layout-side": "Side", + "system-stat-rounded-corners-description": "Bruk avrunda hyrne for bakgrunnsbilete til miniprogrammet.", + "system-stat-rounded-corners-label": "Avrunda Hjørne", + "system-stat-show-background-description": "Viser bakgrunnsilåt til miniprogrammet for systemstatistikk.", + "system-stat-show-background-label": "Vis bakgrunn", + "system-stat-stat-type-description": "Vel kva for systemstatistikk som skal visast.", + "system-stat-stat-type-label": "Statistikktype", + "title": "Skrivebordsminiprogram", + "weather-enabled-description": "Viser miniprogram for vêr på skrivebordet.", + "weather-enabled-label": "Slå på miniprogram for vêr", + "weather-show-background-description": "Viser bakgrunnsilåt for miniprogrammet for vêr." + }, + "display": { + "monitors-brightness-step-description": "Endra på kor store endringane i ljosstyrke er med beinvegar og rullehjul.", + "monitors-brightness-step-label": "Ljosstyrkesteg", + "monitors-brightness-unavailable-ddc-disabled": "Kan ikkje endra på ljosstyrke. Slå på \"Ekstern ljosstyrkestøtte\" for å endra på ljosstyrken på denne skjermen.", + "monitors-brightness-unavailable-generic": "Kan ikkje endra på ljosstyrke på denne skjermen.", + "monitors-desc": "Endra på ljosstyrke på kvar skjerm for seg.", + "monitors-enforce-minimum-description": "På somme skjermar slår bakljoset seg av med 0% ljosstyrke. Dette løyser problemet.", + "monitors-enforce-minimum-label": "Set minimum ljosstyrke til 1%", + "monitors-external-brightness-description": "Slår på støtte for DDCUtil for å styra ljosstyrke på eksterne skjermar med DDC/CI-protokol.", + "monitors-external-brightness-label": "Ekstern ljosstyrkestøtte", + "monitors-global-brightness-description": "Endra på ljosstyrke for alle skjermar samstundes. ", + "monitors-global-brightness-label": "Alle skjermar", + "monitors-title": "Innstillingar for kvar skjerm for seg", + "night-light-auto-schedule-description": "Utifrå solglad og solrenning der du er ({location}) — råd.", + "night-light-auto-schedule-label": "Automatisk timeplan", + "night-light-desc": "Minkar blåljos for å hjelpa med svevn og mot trøytte augo.", + "night-light-enable-description": "Gjev eit varmt fargefilter for å minka blåljos", + "night-light-enable-label": "Slå på natteljos", + "night-light-force-activation-description": "Ser bort ifrå timeplanen og set nattefilteret på med det same.", + "night-light-force-activation-label": "Tving filter", + "night-light-manual-schedule-description": "Set eigendefinerte tider for solrenning og solglad.", + "night-light-manual-schedule-label": "Timeplan for hand", + "night-light-manual-schedule-select-start": "Vel starttid", + "night-light-manual-schedule-select-stop": "Vel stoggtid", + "night-light-manual-schedule-sunrise": "Solgladetid", + "night-light-manual-schedule-sunset": "Solrenningstid", + "night-light-temperature-day": "Dag", + "night-light-temperature-day-description": "Styrer temperatur om dagen.", + "night-light-temperature-description": "Set fargevarmen om natta og om dagen.", + "night-light-temperature-label": "Fargetemperatur", + "night-light-temperature-night": "Natt", + "night-light-temperature-night-description": "Styrer temperaturen om natta.", + "title": "Vising" + }, + "dock": { + "appearance-background-opacity-description": "Endrar på kor ugjennomsynleg festepunktbakgrunnen er.", + "appearance-border-radius-description": "Endra på grenseradiusen på festepunktet.", + "appearance-border-radius-label": "Grenseradius", + "appearance-colorize-icons-description": "Gjev temafargane til appikon på festepunktet (berre uaktive appar).", + "appearance-colorize-icons-label": "Farga ikon", + "appearance-dead-opacity-description": "Endrar på kor ugjennomsynlege appikon er når appen ikkje køyrer.", + "appearance-dead-opacity-label": "Ugjennomsynlege daude appar", + "appearance-desc": "Måta til korleis festepunktet ser ut og ter seg.", + "appearance-display-auto-hide": "Gøym automatisk", + "appearance-display-description": "Vel korleis festepunktet ter seg", + "appearance-display-exclusive": "Uthaldande", + "appearance-dock-indicator-description": "Syn ein framgangsvisar når festepunktet er gøymt.", + "appearance-dock-indicator-label": "Festepunktvisar", + "appearance-floating-distance-description": "Set avstanden mellom festepunktet og skjermranda.", + "appearance-floating-distance-label": "Festepunkt flyteavstand", + "appearance-hide-show-speed-description": "Endrar på kor fort festepunktet viser og gøymer seg.", + "appearance-hide-show-speed-label": "Vis/gøym fart", + "appearance-icon-size-description": "Endra på kor stor festepunktet er.", + "appearance-icon-size-label": "Storleik", + "appearance-inactive-indicators-description": "Viser ei pille for kvar app, ikkje berre den opne.", + "appearance-inactive-indicators-label": "Køyreteikn", + "appearance-indicator-color-description": "Vel fargen på indikatoren for den skjulte dokken.", + "appearance-indicator-color-label": "Indikatorfarge", + "appearance-indicator-opacity-description": "Juster gjennomsiktigheita for indikatoren for den skjulte dokken.", + "appearance-indicator-opacity-label": "Gjennomsiktigheit for indikator", + "appearance-indicator-thickness-description": "Bruk ein tjukkare indikator for den skjulte dokken (6 px i staden for 3 px).", + "appearance-indicator-thickness-label": "Tjukkare indikator", + "appearance-pinned-static-description": "Alltid sender feste appikon til venstre i statisk rekkjefylgd.", + "appearance-pinned-static-label": "Statiske feste appar", + "appearance-position-description": "Vel kvar festepunktet står på skjermen.", + "appearance-position-label": "Stad", + "appearance-sit-on-frame-description": "Set festepunktet i ramma heller enn å ha det på toppen.", + "appearance-sit-on-frame-label": "Festepunktet sit på ramma", + "appearance-type-attached": "Statisk", + "appearance-type-description": "Vel mellom flytande pille eller statisk oppgåveline fest til randa.", + "appearance-type-floating": "Flytande", + "appearance-type-label": "Festepunktstil", + "enabled-description": "Viser eller gøymer festepunktet heilt.", + "enabled-label": "Slå på festepunkt", + "monitors-desc": "Viser festepunktet på visse skjermar. Dersom du ikkje vel noko, viser det seg på alle.", + "monitors-only-same-monitor-description": "Viser berre appar på skjermen som festepunktet står på.", + "monitors-only-same-monitor-label": "Berre appar frå same skjermen", + "monitors-title": "Skjermvising", + "title": "Festepunkt" + }, + "general": { + "copy-settings": "Kopier innstillingar", + "fonts-default-description": "Skrifttypen brukt mest gjennom heile grensesnittet", + "fonts-default-label": "Standardskrifttype", + "fonts-default-placeholder": "Vel standardskrifttype...", + "fonts-default-scale-description": "Aukar eller minkar kor stor standardskrifta er.", + "fonts-default-scale-label": "Standard skriftstorleik", + "fonts-default-search-placeholder": "Leit etter skrifttype...", + "fonts-desc": "Vel skrifttypane brukt gjennom heile grensesnittet.", + "fonts-monospace-description": "Fastbreiddeskrift brukt for tal og statistikkvising.", + "fonts-monospace-label": "Fastbreiddeskrift", + "fonts-monospace-placeholder": "Vel fastbreiddeskrift...", + "fonts-monospace-scale-description": "Aukar eller minkar kor stor fastbreiddeskrifta er.", + "fonts-monospace-scale-label": "Fastbreiddeskriftstorleik", + "fonts-monospace-search-placeholder": "Leit etter fastbreiddeskrift...", + "fonts-title": "Skrifttypar", + "keybinds-conflict-description": "Tastkombinasjonen er alt gjeven til {action}.", + "keybinds-conflict-title": "Beinknapp-konflikt", + "keybinds-description": "Styr navigasjonsknappar for panel og programveljaren.", + "keybinds-down": "Flytt nedover", + "keybinds-enter": "Stadfest / Hende", + "keybinds-escape": "Steng / Attende", + "keybinds-left": "Flytt til venstre", + "keybinds-modifier-description": "Beinknappar må ha styringstast (Ctrl eller Alt).", + "keybinds-modifier-title": "Krev styringstast", + "keybinds-remove": "Fjern", + "keybinds-right": "Flytt til høgre", + "keybinds-title": "Navigasjonsknappar", + "keybinds-up": "Flytt oppover", + "language-desc": "Vel språket du vil sjå i applikasjonen.", + "language-select-auto-detect": "Automatisk", + "language-select-description": "Vel språket bruk gjennom applikasjonsgrensesnittet.", + "language-select-label": "Applikasjonsspråk", + "launch-setup-wizard": "Oppsetthjelpar", + "profile-desc": "Endra på brukardetaljane dine og brukarbiletet ditt.", + "profile-picture-description": "Brukarbiletet ditt som vert vist gjennom grensesnittet.", + "profile-picture-label": "Brukarbilete", + "profile-select-avatar": "Vel brukarbilete", + "profile-title": "Profil", + "profile-tooltip": "Brukarbilete", + "reverse-scrolling-description": "Snu om den tolka rulleretninga.", + "reverse-scrolling-label": "Omsnudd rulling", + "screen-corners-desc": "Måta til avrunding på skjermhyrna og synlege effektar.", + "screen-corners-radius-description": "Endra på dei avrunda hyrna på skjermen.", + "screen-corners-radius-label": "Skjermhyrneradius", + "screen-corners-radius-reset": "Nullstill skjermhyrneradius", + "screen-corners-show-corners-description": "Viser avrunda hyrne på skjermranda.", + "screen-corners-show-corners-label": "Vis skjermhyrne", + "screen-corners-solid-black-description": "Bruk heilsvart heller enn bakgrunnsfargen til oppgåvelina.", + "screen-corners-solid-black-label": "Heilsvarte hyrne", + "screen-corners-title": "Skjermhyrne", + "settings-copied": "Innstillingar kopierte til utklippstavla", + "tab-basics": "Grunnleggjande", + "tab-keybinds": "Beinknappar" + }, + "hooks": { + "info-command-info-description": "• Kommandoar køyrer i skal (sh -lc)
• Kommandoar køyrer i bakgrunnen (ufeste)
• Prøveknappar køyrer med noverande verdiar", + "info-parameters-description": "Bakgrunnskrok: $1 = bakgrunnsbane, $2 = skjermnamn
• Tema brytarkrok: $1 = sann/usann (mørkemodus-status)
• Skjermopplåsings-/ulåsingskrokar: Ingen parametrar
• Ytingsmoduskrokar: Ingen parametrar
• Øktskrok: $1 = handling (Slå av/starta å nyo)", + "info-parameters-label": "Tilgjengelege parametrar", + "noctalia-started-description": "Kommandoen som køyrer når Noctalia er ferdig med å lasta inn.", + "noctalia-started-label": "Noctalia byrja", + "noctalia-started-placeholder": "t.d. notify-send 'Noctalia lasta inn'", + "performance-mode-disabled-description": "Kommandoen som køyrer når Noctalia ytingsmodus er avslege.", + "performance-mode-disabled-label": "Ytingsmodus av", + "performance-mode-disabled-placeholder": "t.d. notify-send \"Yting\" \"Modus avslege\"", + "performance-mode-enabled-description": "Kommandoen som køyrer når Noctalia ytingsmodus er på.", + "performance-mode-enabled-label": "Ytingsmodus på", + "performance-mode-enabled-placeholder": "t.d. notify-send \"Yting\" \"Modus påslege\"", + "screen-lock-description": "Kommandoen som køyrer når skjermen er låst.", + "screen-lock-label": "Skjerm låst", + "screen-lock-placeholder": "t.d. notify-send \"Skjerm\" \"Låst\"", + "screen-unlock-description": "Kommandoen som køyrer når låseskjermen vert låst opp.", + "screen-unlock-label": "Skjerm låst opp", + "screen-unlock-placeholder": "t.d. notify-send \"Skjerm\" \"Låst opp\"", + "session-description": "Kommandoen som køyrer før maskina er avslegen eller starta å nyo. Får handlingtype som $1 (slå av/start å nyo).", + "session-label": "Øktslutt", + "session-placeholder": "t.d. notify-send \"Økt\" \"$1\"", + "system-hooks-desc": "Set opp kommandoar som køyrer når systemhende skjer.", + "system-hooks-enable-description": "Slår av eller på krokkommandoar.", + "system-hooks-enable-label": "Slå på krokar", + "system-hooks-title": "Systemkrokar", + "theme-changed-description": "Kommandoen som køyrer når temaet byter mellom Mørke- og Ljosmodus.", + "theme-changed-label": "Tema endra", + "theme-changed-placeholder": "e.g. notify-send \"Tema\" \"Bytte\"", + "title": "Krokar", + "wallpaper-changed-description": "Kommandoen som køyrer når bakgrunnsbiletet endrar.", + "wallpaper-changed-label": "Bakgrunnsbiletet endra", + "wallpaper-changed-placeholder": "t.d. notify-send \"Bakgrunnsbilete\" \"Endra\"" + }, + "indicator": { + "default-value": "Standard: {value}", + "system-default": "Systemstandard" + }, + "launcher": { + "clipboard-desc": "Sjå og styra utklippsboka di frå programveljaren.", + "execute-desc": "Styrer korleis applikasjonar er køyrde", + "execute-title": "Køyring", + "settings-annotation-tool-description": "Kommando som køyrer når ein klikkar på merknadsknappen i utklippsboka. Biletet vert sendt med røyr til denne kommandoen.", + "settings-annotation-tool-label": "Merknadsverktøy", + "settings-annotation-tool-placeholder": "t.d. 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Limar inn det valde utklippet automatisk. Krev wtype.", + "settings-auto-paste-label": "Automatisk liming", + "settings-clip-preview-description": "Viser førebilete av utklippsinnhaldet når ein nyttar >clip kommandoen.", + "settings-clip-preview-label": "Slå på klippførebilete", + "settings-clip-wrap-text-description": "Bryt tekst i utklippsboka heller enn å korta han av.", + "settings-clip-wrap-text-label": "Tekstbryting i utklipp", + "settings-clipboard-history-description": "Sjå ting som var kopiert tidlegare i programveljaren.", + "settings-clipboard-history-label": "Slå på utklippsboka", + "settings-clipboard-watch-image-description": "Heile kommandostreng send til wl-paste for bileteendringar (krev omstart).", + "settings-clipboard-watch-image-label": "Biletvaktskommando", + "settings-clipboard-watch-text-description": "Heile kommandostreng send til wl-paste for tekstendringar (krev omstart).", + "settings-clipboard-watch-text-label": "Tekstvaktskommando", + "settings-custom-launch-prefix-description": "Førefester kommandoar med eigendefinert programveljar (t.d. 'runapp' for systemd-integrering).", + "settings-custom-launch-prefix-enabled-description": "Brukar eigendefinert førefeste for å starta applikasjonar heller enn standardmåten.", + "settings-custom-launch-prefix-enabled-label": "Slå på eigendefinert startførefeste", + "settings-custom-launch-prefix-label": "Eigendefinert startførefeste", + "settings-density-description": "Endra på kor store applikasjonsikon er, og kor tette dei er i veljaren.", + "settings-density-label": "Tettleik", + "settings-desc": "Vel korleis programveljaren ser ut og kva det gjer.", + "settings-enable-session-search-description": "Vis øktshandlingar (lås, slå av, start å nyo, osb.) ved søk i veljaren.", + "settings-enable-session-search-label": "Slå på øktssøk", + "settings-enable-settings-search-description": "Viser innstillingar når ein leitar i programveljaren.", + "settings-enable-settings-search-label": "Søk i innstillingar", + "settings-enable-windows-search-description": "Søk og fokuser aktive vindauge.", + "settings-enable-windows-search-label": "Aktiver vindaugssøk", + "settings-grid-view-description": "Viser utfall i rutenett heller enn som liste.", + "settings-icon-mode-description": "Brukar heimlege systemikon heller enn Tabler-ikon.", + "settings-icon-mode-label": "Bruk heimlege ikon", + "settings-ignore-mouse-input-description": "Slå av musekontakt og rullehjul i programveljaren.", + "settings-ignore-mouse-input-label": "Ikkje vyrd musekontakt", + "settings-overlay-layer-description": "Syn programveljaren på overlaget, over fullskjermsvindaugo. Når dette er på, koplar ikkje veljaren seg til oppgåvelina.", + "settings-overlay-layer-label": "Syn over fullskjerm", + "settings-position-description": "Vel kvar programveljarpanelet opnar seg.", + "settings-show-categories-description": "Viser kategorifanar for å granska applikasjonar", + "settings-show-categories-label": "Vis kategori", + "settings-show-icon-background-description": "Viser ein avrunda firkant som bakgrunn bak ikon.", + "settings-show-icon-background-label": "Vis ikonbakgrunn", + "settings-sort-by-usage-description": "Når dette er på, står dei appane du brukar mest fyrst på lista.", + "settings-sort-by-usage-label": "Sortera ved bruk", + "settings-terminal-command-description": "Kommando som køyrer frå terminal. T.d. 'kitty -e' eller 'gnome-terminal --'.", + "settings-terminal-command-label": "Terminalkommando", + "settings-use-app2unit-description": "Brukar annan oppstartsmåte for å betre kunna ordna app-prosessar og for å hindra feil.", + "settings-use-app2unit-label": "Bruk App2Unit for å køyra applikasjonar", + "settings-view-mode-description": "Vel oppsettet for veljarobjekt.", + "settings-view-mode-label": "Synsmodus", + "title": "Programveljar" + }, + "location": { + "calendar-cards-desc": "Ordna og slå av/på kort i kalenderpanelet.", + "calendar-cards-title": "Kalenderkort", + "calendar-header-label": "Kalendertopptekst", + "calendar-month-label": "Kalendermånad", + "date-time-12hour-format-description": "Viser tid i 12-timeformat på låseskjermen og kalenderen. Klokka på oppgåvelina har eigne innstillingar.", + "date-time-12hour-format-label": "Bruk 12-timeformat", + "date-time-desc": "Måta til korleis dato og tid ser ut.", + "date-time-first-day-of-week-automatic": "Automatisk (bruk systemlokal)", + "date-time-first-day-of-week-description": "Vel kva for dag byrjar veka på kalenderen.", + "date-time-first-day-of-week-label": "Fyrste vekedag", + "date-time-show-events-description": "Vis arrangement på kalenderpanelet.", + "date-time-show-events-label": "Vis kalenderarrangement", + "date-time-title": "Dato og tid", + "date-time-use-analog-description": "Vis analogklokke på kalendervindauga og låseskjermen.", + "date-time-use-analog-label": "Bruk analogklokke", + "date-time-week-numbers-description": "Vis veketal (t.d. veke 38) på kalenderen.", + "date-time-week-numbers-label": "Vis veketal", + "location-desc": "Få vêr og natteljosplanleggjing ved å segja kvar du er.", + "location-search-description": "t.d. Mosjøen, Nordland", + "location-search-label": "Leit etter stad", + "location-search-placeholder": "Skriv inn stadnamn", + "location-title": "Der du er", + "weather-desc": "Vel temperatureininga du vil sjå.", + "weather-enabled-description": "Vis vêrdata gjennom grensesnittet og henta vêrdata. Når dette er avslege, gøymer alle vêrobjekt seg og Noctalia skal ikkje spørja etter dei på nettverket.", + "weather-enabled-label": "Slå på vêr", + "weather-fahrenheit-description": "Vis temperatur i Fahrenheit heller enn Celsius.", + "weather-fahrenheit-label": "Vis temperatur i Fahrenheit (°F)", + "weather-hide-city-description": "Gøym bynamnet på vêrvisningar gjennom grensesnittet.", + "weather-hide-city-label": "Gøym bynamn", + "weather-hide-timezone-description": "Gøym tidssone på vêrvisningar gjennom grensesnittet.", + "weather-hide-timezone-label": "Gøym tidssone", + "weather-show-effects-description": "Vis fleire synlege effektar (som regn, snø, lyn) på vêrkortet.", + "weather-show-effects-label": "Vis vêreffektar", + "weather-show-in-calendar-description": "Vis det daglege vêrvarselet rett i kalenderen.", + "weather-show-in-calendar-label": "Vis vêr i kalenderen" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "Når fprintd (fingeravtrykkautentisering) er aktiv, lar dette alternativet deg framleis logge inn med passordet ditt i staden for eit fingeravtrykk.", + "allow-password-with-fprintd-label": "Tillat passordpålogging med fprintd", + "auto-start-auth-description": "Startar automatisk fingeravtrykksautentisering utan å krevje eit tastetrykk eller eit knappetrykk.", + "auto-start-auth-label": "Autostart autentisering", + "clock-format-description": "Måta til klokkeformatet med dato/tidssyntaksteikn.", + "clock-format-label": "Klokkeformat", + "clock-style-analog": "Analog", + "clock-style-custom": "Eigendefinert", + "clock-style-description": "Vel korleis klokka ser ut på låseskjermen.", + "clock-style-digital": "Digital", + "clock-style-label": "Klokkestil", + "compact-lockscreen-description": "Viser berre inndata for å logga inn og systemkontrollar, gøymer miniprogramma for vêr og media.", + "compact-lockscreen-label": "Endefram låseskjerm", + "lock-on-suspend-description": "Låser skjermen automatisk når ein suspenderer systemet.", + "lock-on-suspend-label": "Lås på suspendering", + "lock-screen-animations-description": "Slå på/av animasjonar på låseskjermen.", + "lock-screen-animations-label": "Animasjonar på låseskjermen", + "lock-screen-blur-strength-description": "Gjev ein uklår effekt til bilætet på låseskjermen.", + "lock-screen-blur-strength-label": "Uklåre på låseskjermen", + "lock-screen-tint-strength-description": "Gjev ein fargesnev i overlag til biletet på låseskjermen.", + "lock-screen-tint-strength-label": "Fargestyrke på låseskjermen", + "monitors-desc": "Syn låseskjerm på visse skjermar. Dersom du ikkje vel noko, syner han seg på alle.", + "show-hibernate-description": "Viser 'Set i full dvale' i straumkontrollane.", + "show-hibernate-label": "Vis full dvale", + "show-session-buttons-description": "Tillèt at ein kann bruka straumkontrollane frå låseskjermen.", + "show-session-buttons-label": "Straumkontrollar", + "title": "Låseskjerm" + }, + "network": { + "bluetooth-description": "Slå på Bluetooth-handsaming.", + "bluetooth-rssi-polling-description": "Prøvetak RSSI jamt for tilkopla einingar med bluetoothctl. Det kan henda dette ikkje er tilgjengeleg for alle einingar; brukar minimale ressursar når påslege.", + "bluetooth-rssi-polling-label": "Bluetooth-dekningsspørjing", + "desc": "Styr Wi-Fi- og Bluetooth-koplingar", + "wifi-description": "Styr trådlause nettverk (krev NetworkManager)." + }, + "notifications": { + "duration-critical-urgency-description": "Kor lenge kritiske varsel er synlege.", + "duration-critical-urgency-label": "Kritisk", + "duration-desc": "Styr kor lenge varsel er synlege utifrå kor viktige dei er.", + "duration-low-urgency-description": "Kor lenge mindre viktige varsel er synlege.", + "duration-low-urgency-label": "Mindre viktig", + "duration-normal-urgency-description": "Kor lenge vanlege varsel er synlege.", + "duration-normal-urgency-label": "Vanleg", + "duration-respect-expire-description": "Bruk tidsavbrotet fastsett i varselet.", + "duration-respect-expire-label": "Vyrd tidsavbrot", + "duration-title": "Varsellengd", + "history-clear-dismiss-description": "Slett varsel frå historikk når avvist.", + "history-clear-dismiss-label": "Slett når avvist", + "history-critical-urgency-description": "Lagrar kritiske varsel i historikken.", + "history-critical-urgency-label": "Lagra kritiske varsel i historikken", + "history-desc": "Styrer kva for varsel vert lagra i historikken utifrå kor viktige dei er.", + "history-low-urgency-description": "Lagrar mindre viktige varsel i historikken.", + "history-low-urgency-label": "Lagra mindre viktige varsel i historikken.", + "history-normal-urgency-description": "Lagrar vanlege varsel i historikken.", + "history-normal-urgency-label": "Lagra vanlege varsel i historikken.", + "monitors-desc": "Viser varsel på visse skjermar. Dersom du ikkje vel noko, viser varsel seg på alle.", + "settings-always-on-top-description": "Viser varsel over fullskjermsvindaugo og andre lag.", + "settings-background-opacity-description": "Endrar på kor ugjennomsynlege varselbakgrunnar er.", + "settings-density-description": "Vel varselkorttettleik", + "settings-density-label": "Tettleik", + "settings-desc": "Styrer korleis varsel ser ut og ter seg.", + "settings-do-not-disturb-description": "Slår av varselpoppopp når dette er på.", + "settings-enabled-description": "Slår på eller av varseldaemonen, krev at du startar Noctaliaskal å nyo.", + "settings-enabled-label": "Slå på varsel", + "settings-location-description": "Der varsel står opp på skjermen.", + "settings-markdown-description": "Gjev att innhaldet i varselet med Markdown-formattering.", + "settings-markdown-label": "Slå på Markdown", + "sounds-desc": "Styrer varselljod og ljodstyrke.", + "sounds-enabled-description": "Slår på ljodeffektar for innkomande varsel.", + "sounds-enabled-label": "Slå på varselljod", + "sounds-excluded-apps-description": "Ikkje spel av varselljoden for visse applikasjonar som har eigne ljodar.", + "sounds-excluded-apps-label": "Borttekne applikasjonar", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Filbana til ljodfila som spelar av med kritiske varsel.", + "sounds-files-critical-label": "Kritisk varselljod", + "sounds-files-critical-select-title": "Vel fil for kritisk varselljod", + "sounds-files-desc": "Styrer eigendefinerte ljodfiler for varsel med grunnlag i kor viktige dei er.", + "sounds-files-low-description": "Filbana til ljodfila som spelar av for mindre viktige varsel.", + "sounds-files-low-label": "Mindre viktig varselljod", + "sounds-files-low-select-title": "Vel fil for mindre viktig varselljod", + "sounds-files-normal-description": "Filbana til ljodfila som spelar av for vanlege varsel.", + "sounds-files-normal-label": "Vanleg varselljod", + "sounds-files-normal-select-title": "Vel fil for vanleg varselljod", + "sounds-files-placeholder": "Vel filbana til ljodfila", + "sounds-files-select-file": "Vel ljodfil", + "sounds-files-title": "Ljodfiler", + "sounds-files-unified-description": "Filbana til ljoden som spelar av for varsel.", + "sounds-files-unified-label": "Varselljod", + "sounds-files-unified-select-title": "Vel fil for varselljod", + "sounds-separate-description": "Bruk ulike ljodfiler for mindre viktige, vanlege og kritiske varsel.", + "sounds-separate-label": "Bruk ulike ljoder for kvart prioritetsnivå", + "sounds-title": "Ljodinnstillingar", + "sounds-unavailable-description": "Installer Qt6 Multimedia for å slå på varselljodar.", + "sounds-unavailable-label": "Varselljodar ikkje tilgjengelege", + "sounds-volume-description": "Styre ljodstyrke for varselljodar.", + "sounds-volume-label": "Ljodstyrke", + "toast-battery-description": "Viser åtvaring når batterinivået går under dette prosenttalet.", + "toast-battery-label": "Batteriåtvaring", + "toast-desc": "Styrer korleis varsel ser ut og ter seg.", + "toast-keyboard-description": "Viser skjermvarsel når knappefjøloppsett er endra.", + "toast-keyboard-label": "Knappefjøloppsett", + "toast-media-description": "Viser skjermvarsel når det er ei endring i mediaavspelingsstatus.", + "toast-media-label": "Media" + }, + "osd": { + "always-on-top-description": "Vis Skjermvising over fullskjermsvindaugo og andre lag.", + "always-on-top-label": "Alltid øvst", + "background-opacity-description": "Styrer kor gjennomsynleg skjermvisingsbakgrunnen er.", + "background-opacity-label": "Ugjennomsynleg bakgrunn", + "description": "Styrer varsel på skjermen som overlaga for ljodstyrke og ljosstyrke.", + "duration-auto-hide-description": "Endra på tida for skjermvisinga kverv.", + "duration-auto-hide-label": "Gøym etter", + "duration-desc": "Kor lenge skjermvisinga er synleg før ho gøymer seg.", + "duration-title": "Gøymetidsavbrot", + "enabled-description": "Vis endringar i ljodstyrke og ljosstyrke i sanntid.", + "enabled-label": "Slå på skjermvisinga", + "events-desc": "Vel kva for hende som slår skjermvisinga på.", + "general-desc": "Styrer om skjermvisinga er synleg og korleis ho ter seg.", + "location-description": "Der skjermvisingar står opp.", + "monitors-desc": "Vis skjermvisingar på visse skjermar. Dersom du ikkje vel noko, står dei opp på alle.", + "title": "Skjermvising", + "types-brightness-description": "Vis skjermvising med endring i ljosstyrke.", + "types-brightness-label": "Ljosstyrke", + "types-custom-text-description": "Vis skjermvising med eigendefinerte tekstmeldingar frå IPC.", + "types-custom-text-label": "Eigendefinert tekst", + "types-desc": "Vel kva for hende som slår skjermvisinga på.", + "types-input-volume-description": "Vis skjermvising med endring i mikrofonljodstyrke.", + "types-input-volume-label": "Inndata-ljodstyrke", + "types-lockkey-description": "Vis skjermvising med byte på Caps Lock, Num Lock eller Scroll Lock.", + "types-lockkey-label": "Låseknappar", + "types-media-description": "Vis skjermvising med endring i mediaavspeling (spel, pause, hopp over).", + "types-media-label": "Mediaavspeling", + "types-title": "Skjermvisingshende", + "types-volume-description": "Vis skjermvisinga med endring i utdataljodstyrke.", + "types-volume-label": "Utdata-ljodstyrke" + }, + "plugins": { + "auto-update": "Oppdater tillegg automatisk", + "auto-update-description": "Oppdaterer alle tillegga automatisk når skalet byrjar.", + "available-description": "Leit etter og installer tillegg frå tilmåta kjelder.", + "available-label": "Tilgjengelege tillegg", + "available-no-plugins-description": "Sjekk tilleggkjeldene dine eller lasta inn lista å nyo.", + "available-no-plugins-label": "Ingen tilgjengelege tillegg", + "check-for-updates": "Leit etter oppdateringar", + "checking-for-updates": "Leitar etter oppdateringar...", + "collision-already-installed": "Dette tillegget er alt installert", + "collision-custom-version-exists": "Ein annan versjon frå \"{source}\" er alt installert", + "collision-official-version-exists": "Den ofisielle versjonen av dette tillegget er alt installert", + "development-disable": "Slå av utviklingsmodus for tillegget", + "development-enable": "Slå på utviklingsmodus for tillegget", + "filter-downloaded": "Nedlasta", + "filter-not-downloaded": "Ikkje nedlasta", + "filter-tags-description": "Filtrer tillegg med kategori eller status på nedlasting.", + "filter-tags-label": "Taggar", + "hot-reload-description": "Last inn tillegg å nyo automatisk når filene er endra. Nyttig for tilleggutvikling.", + "hot-reload-label": "Innlasting på farta (utviklingsmodus)", + "hot-reloaded": "Lasta inn tillegg å nyo: {name}", + "install-error": "Kunne ikkje installera: {error}", + "install-incompatible": "{plugin} krev Noctalia v{version} eller nyare", + "install-success": "Installerte {plugin}", + "installed-description": "Handsama og styr alle lokalinstallerte tillegg.", + "installed-label": "Installerte tillegg", + "installed-no-plugins-description": "Installer tillegg frå \"Tilgjengelege\"-fana.", + "installed-no-plugins-label": "Ingen installerte tillegg", + "installing": "Installerer {plugin}...", + "open-plugin-page": "Opna tilleggsida", + "open-plugins-tab": "Opna tillegginnstillingar", + "plugin-settings-title": "{plugin}-innstillingar", + "refresh-refreshing": "Lastar inn tilleggslista å nyo...", + "refresh-tooltip": "Last inn tilgjengelege tillegg å nyo", + "settings-error-not-loaded": "Tillegg ikkje lasta inn", + "settings-saved": "Tillegginnstillingar lagra", + "settings-tooltip": "Tillegginnstillingar", + "source-custom": "Eigendefinert kjelde", + "sources-add-custom": "Legg til eigendefinert lager", + "sources-add-dialog-description": "Legg til eit GitHub-lager som tilleggkjelde.", + "sources-add-dialog-error": "Kunne ikkje leggja til tilleggkjelde", + "sources-add-dialog-name": "Lagernamn", + "sources-add-dialog-name-placeholder": "Mine eigne tillegg", + "sources-add-dialog-success": "La til tilleggkjelde", + "sources-add-dialog-title": "Legg til tilleggkjelde", + "sources-add-dialog-url": "Lageradresse", + "sources-description": "Handsama tillegglager", + "sources-placeholder": "Det tøffe lageret mitt", + "sources-remove-tooltip": "Slett tilleggkjelde", + "title": "Tillegg", + "translations-reloaded": "Lasta inn omsetjingar å nyo for {name}", + "uninstall-dialog-description": "Er du viss på at du vil desinstallera {plugin}? Dette slettar alt av tilleggdata.", + "uninstall-dialog-title": "Desinstaller tillegg", + "uninstall-error": "Kunne ikkje desinstallera: {error}", + "uninstall-success": "Desinstallerte {plugin}", + "uninstalling": "Desinstallerer {plugin}...", + "update-all": "Oppdater alle ({count})", + "update-all-success": "Klarte å oppdatere alle tillegga", + "update-available": "Ny tilleggoppdatering tilgjengeleg", + "update-available-plural": "Tilleggoppdateringar tilgjengelege ({count})", + "update-error": "Kunne ikkje oppdatera tillegg: {plugin}: {error}", + "update-pending": "v{current} → v{new} (krev Noctalia v{required})", + "update-success": "Oppdaterte {plugin} til v{version}", + "update-version": "v{current} → v{new}", + "updating": "Oppdaterer..." + }, + "region": { + "title": "Region" + }, + "session-menu": { + "countdown-duration-description": "Fastset kor lenge nedteljinga varar før straumhandlingar køyrer.", + "countdown-duration-label": "Nedteljing", + "enable-countdown-description": "Viser nedteljing før straumhandlingar køyrer.", + "enable-countdown-label": "Slå på nedteljing", + "entries-desc": "Vel kva for straumhandlingar står i øktsmenyen og rekkjefylgja dei står i.", + "entries-title": "Straumhandlingar", + "entry-settings-command-description": "Eigendefinert kommando som køyrer med denne handlinga. Haldt tom for å bruka systemstandarden.", + "entry-settings-command-placeholder": "t.d. systemctl poweroff", + "entry-settings-default-command-lock": "Intern låseskjerm (ingen kommando)", + "entry-settings-default-command-logout": "Intern utlogging (ingen kommando)", + "entry-settings-default-info-description": "Dersom du ikkje ha definert ein kommando, køyrer systemstandarden.", + "entry-settings-default-info-label": "Standardkommando", + "entry-settings-keybind-description": "Vel knappane som utløyser handlinga når øktsmenyen er open.", + "entry-settings-keybind-placeholder": "Trykk \"Tak opp\" for å setja opp knapp...", + "entry-settings-keybind-recording": "Tek opp... Trykk knappar", + "entry-settings-title": "Set opp {entry}", + "entry-settings-tooltip": "Set opp kommando", + "general-desc": "Set opp korleis øktsmenyen ser ut og kva det gjer.", + "large-buttons-layout-description": "Vel korleis knappar viser seg på øktsmenyen.", + "large-buttons-layout-label": "Oppsett med store knappar", + "large-buttons-style-description": "Viser øktsmenyen med store knappar i rutenett.", + "large-buttons-style-label": "Stil med store knappar", + "position-description": "Vel kvar øktsmenyen opnar seg på skjermen når du opnar han.", + "show-header-description": "Viser tittel og stengeknapp øvst på øktsmenyen.", + "show-header-label": "Vis topptekst", + "show-keybinds-description": "Syn knappefjøltips på øktval", + "show-keybinds-label": "Syn beinknappar" + }, + "system-monitor": { + "critical-color-label": "Kritisk farge", + "custom-highlight-colors-title-label": "Eigendefinerte uthevingsfargar", + "disk-available-label": "Disk tilgjengeleg", + "disk-section-label": "Diskbruk", + "enable-dgpu-monitoring-description": "Åtvaring: Dette vekkjer det diskrete skjermkortet ditt (NVIDIA/AMD), som kan ha stor påverknad på batterilivet på berande datamaskiner med hypridgrafikk.", + "enable-dgpu-monitoring-label": "Slå på vakt for diskret skjermkort", + "external-monitor-description": "Skriv inn kommandoen eller applikasjonbanen som køyrer når du slår på eksterne systemvaktapplikasjonar", + "external-monitor-label": "Ekstern systemvaktkommando", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "Set opp korleis systemvakta ser ut og ter seg.", + "gpu-section-label": "Skjermkorttemperatur", + "highlight-colors-section-label": "Uthevingsfargar", + "polling-interval-label": "Spørjingsintervall", + "polling-section-description": "Set opp kor ofte kvart systemmål vert oppdatert", + "polling-section-label": "Spørjingsintervall", + "threshold-critical": "Kritisk terskel", + "threshold-warning": "Åtvaringsterskel", + "thresholds-section-description": "Endra åtvarings-/kritiske tersklar for kvart systemmål.", + "use-custom-highlight-colors-description": "Når dette er avslege, brukar systemvakta uthevingsfargen frå temaet.", + "use-custom-highlight-colors-label": "Bruk eigendefinerte uthevingsfargar", + "warning-color-label": "Åtvaringsfarge" + }, + "user-interface": { + "allow-panels-without-bar-description": "Når dette er på kan panel opna på kvar som helst skjerm. Når dette er av, opnar dei berre på skjermar med oppgåvelina. Dette kan minka minnebruk.", + "allow-panels-without-bar-label": "Tillat panel på skjermar utan oppgåvelina", + "animation-disable-description": "Slår av alle animasjonar for ei kjappar oppleving med betre svartid.", + "animation-disable-label": "Slå av UI-animasjonar", + "animation-speed-description": "Styr global animasjonsfart", + "animation-speed-label": "Animasjonsfart", + "animation-speed-reset": "Nullstill animasjonsfart", + "appearance-desc": "Måta til synlege objekt som hjelpetekst, grenser og skuggar.", + "box-border-description": "Vis ei grense kring område med innhald.", + "box-border-label": "Ilåtgrense", + "box-border-radius-description": "Styrer kor avrunda hyrna er på viktige oppsettsområde, til dømes på stolpar, kort og innhaldspanel.", + "box-border-radius-label": "Ilåtradius", + "control-border-radius-description": "Styrer kor bøygde interaktiv delar er, til dømes knappar, brytarar og tekstfelt.", + "control-border-radius-label": "Inndataradius", + "desc": "Måta til korleis grensesnittet ser ut og ter seg.", + "dim-desktop-description": "Mørknar skrivebordet når panel eller menyar er opne.", + "dim-desktop-label": "Mørkna skrivebord", + "dimmer-opacity-description": "Set kor mørkt det vert når skrivebordet mørknar.", + "dimmer-opacity-label": "Skrivebordsmørker", + "panel-background-opacity-description": "Set kor ugjennomsynleg bakgrunnen er for alle panel (oppgåvelina, programveljar, innstillingar osb.).", + "panel-background-opacity-label": "Ugjennomsynlege panelbakgrunnar", + "panels-attached-to-bar-description": "Panel låser seg til oppgåvelina og skjermrendene, som gjev ein stilig utsjånad med innbøygde hyrne.", + "panels-attached-to-bar-label": "Fest panel til render", + "panels-overlay-description": "Tryggjer at panel og oppgåvelina held seg synlege, jamvel over fullskjermsappar.", + "panels-overlay-label": "Haldt panel og oppgåvelina øvst", + "scaling-description": "Endrar kor stor brukargrensesnittet er, sett bort ifrå oppgåvelina.", + "scaling-label": "Grensesnittskalering", + "settings-panel-mode-description": "Vel oppsett for innstillingar (kan krevja at du må opna dei på nytt).", + "settings-panel-mode-label": "Oppsett for innstillingar", + "shadows-description": "Slår på skuggar under liner og panel.", + "shadows-direction-description": "Vel kvar skuggen kjem frå.", + "shadows-direction-label": "Skuggeretning", + "shadows-label": "Skuggar", + "title": "Brukargrensesnitt", + "tooltips-description": "Slå på eller av hjelpetekst gjennom brukargrensesnittet", + "tooltips-label": "Vis hjelpetekst" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Alfabetisk", + "automation-change-mode-description": "Vel korleis Noctalia vel bakgrunnsbilete når dei endrar automatisk.", + "automation-change-mode-label": "Endringsmodus", + "automation-custom-interval-description": "Skriv inn tid som TT:MM (t.d. 01:30).", + "automation-custom-interval-label": "Eigendefinert tidsrom", + "automation-interval-description": "Kor ofte bakgrunnsbiletet endrar seg automatisk.", + "automation-interval-label": "Endringstidsrom", + "automation-random-wallpaper-description": "Planlegg tilfellelege endringar på bakgrunnsbilete jamleg.", + "automation-scheduled-change-description": "Endrar bakgrunnsbilete automatisk og jamleg.", + "automation-scheduled-change-label": "Planlagd endring", + "look-feel-edge-smoothness-description": "Når biletet endrar seg får rendene ein mjuk, fjørvoren effekt.", + "look-feel-edge-smoothness-label": "Mykna endringsrand", + "look-feel-fill-color-description": "Vel fyllfarge som kan visast bak bakgrunnsbiletet.", + "look-feel-fill-mode-description": "Vel korleis biletet skal skalerast for å høva skjermoppløysinga di.", + "look-feel-fill-mode-label": "Fyllmodus", + "look-feel-skip-startup-transition-description": "Hopp over bakgrunnsanimasjonen når skalet byrjar.", + "look-feel-skip-startup-transition-label": "Hopp over startanimasjon", + "look-feel-title": "Utsjånad", + "look-feel-transition-duration-description": "Kor lenge endringsanimasjonane tek i sekund.", + "look-feel-transition-duration-label": "Endringstid", + "look-feel-transition-type-description": "Animasjonstype når bakgrunnsbiletet endrar seg.", + "look-feel-transition-type-label": "Endringstype", + "settings-desc": "Styr korleis bakgrunnsbiletet vert handsama og viste.", + "settings-enable-management-description": "Styr bakgrunnsbilete med Noctalia. Slå av dersom du vil heller bruka eit anna program.", + "settings-enable-management-label": "Slå på bakgrunnshandsaming", + "settings-enable-overview-description": "Gjev oversynsskjermen eit uskarpt og dempa bakgrunnsbilete.", + "settings-enable-overview-label": "Slå på oversynsbilete", + "settings-folder-description": "Filbane til mappa der du har bakgrunnsbileta dine.", + "settings-folder-label": "Bakgrunnsbiletemappe", + "settings-hide-wallpaper-filenames-tooltip-hide": "Gøym filnamn", + "settings-hide-wallpaper-filenames-tooltip-show": "Vis filnamn", + "settings-monitor-specific-description": "Fastset ulike biletemapper for kvar skjerm.", + "settings-monitor-specific-label": "Skjermvise mapper", + "settings-monitor-specific-tooltip": "Vakta bakgrunnsbiletemappa", + "settings-overview-blur-strength-description": "Gjev uklåre til overlaget.", + "settings-overview-blur-strength-label": "Overlagsuklåre", + "settings-overview-tint-description": "Gjev fargesnev til oversyna.", + "settings-overview-tint-label": "Oversynsfargestyrke", + "settings-recursive-search-description": "Leit etter bakgrunnsbilete i underordna mapper i biletemappa òg.", + "settings-recursive-search-label": "Leit i underordna mapper", + "settings-select-monitor-folder": "Vel bakgrunnsbiletemappe for skjerm", + "settings-selector-description": "Vel bakgrunnsbilete.", + "settings-selector-position-description": "Vel kvar bileteveljarpanelet skal visast.", + "settings-show-hidden-files-tooltip-hide": "Gøym gøymde filer", + "settings-show-hidden-files-tooltip-show": "Vis gøymde filer", + "settings-title": "Innstillingar for bakgrunnsbilete", + "settings-view-mode-description": "Vel korleis bakgrunnsbilete skal visast frå mappa di.", + "settings-view-mode-label": "Visningsmodus", + "view-mode-browse": "Søk i mapper", + "view-mode-cycle-tooltip": "Visningsmodus: {mode} (klikk for å endra)", + "view-mode-recursive": "Flate undermapper", + "view-mode-single": "Rotmappe" + } + }, + "placeholders": { + "add-new-keybind": "Legg til ny beinknapp", + "command-example": "echo \"Hei, verd\"", + "enter-command": "Skriv inn kommando som skal køyra (app eller eigendefinert skript)", + "enter-ipc-identifier": "Skriv inn unik identifikator for IPC kommandoar", + "enter-path": "Skriv inn filbane...", + "enter-text-to-collapse": "t.d. 'ingenting spelar av'. Bruk /regex/ til mønster.", + "enter-tooltip": "Skriv inn hjelpetekst", + "enter-width-pixels": "Skriv inn breidde i piksel", + "keybind-recording": "Tek opp beinknapp...", + "search": "Søk...", + "search-icons": "t.d. noctalia, niri, battery, cloud", + "search-launcher": "Søk i treff... eller bruk > for kommandoar", + "search-wallhaven": "Leit på Wallhaven...", + "search-wallpapers": "Skriv for å filtrera bakgrunnsbilete..." + }, + "positions": { + "bottom": "Nedst", + "bottom-center": "Nede i midten", + "bottom-left": "Nede til venstre", + "bottom-right": "Nede til høgre", + "center": "I midten", + "center-left": "I midten til venstre", + "center-right": "I midten til høgre", + "close-to-bar": "Attmed lineknappen", + "follow-bar": "Fylg oppgåvelina", + "left": "Venstre", + "right": "Høgre", + "top": "Øvst", + "top-center": "Øvst i midten", + "top-left": "Øvst til venstre", + "top-right": "Øvst til høgre" + }, + "session-menu": { + "action-in-seconds": "{action} i {seconds} sekund...", + "cancel-timer": "Avbryt nedteljing", + "title": "Øktsmeny" + }, + "setup": { + "all-done": "Ferdig!", + "appearance": { + "subheader": "Vel mørkemodus og fargekjelder (bakgrunnsbilete eller eigendefinert)." + }, + "customize": { + "header": "Skreddersy opplevinga di", + "subheader": "Endra kvar oppgåvelina er, tettleik, skalering og meir." + }, + "skip-setup": "Hopp over oppsett", + "telemetry-wizard-done": "Skjønar!", + "telemetry-wizard-note": "Du har kontroll — slå dette av eller på når som helst i innstillingar", + "telemetry-wizard-subtitle": "Vi har lagt til anonyme analytikk for å hjelpa gjera Noctalia betre.", + "telemetry-wizard-title": "Oppdatering om personvern", + "wallpaper": { + "choose-dir": "Vel ei mappe som har bakgrunnsbileta dine i seg", + "dir-browse": "Søk etter bakgrunnsmappe", + "dir-description": "Vel mappa som har bakgrunnsbileta dine i seg.", + "dir-label": "Bakgrunnsmappe", + "dir-select-title": "Vel bakgrunnsmappe", + "header": "Vel bakgrunnsbiletet ditt", + "no-dir": "Inga bakgrunnsmappe vald", + "no-valid": "Fann ikkje gilde biletefiler i: {dir}", + "none-in-dir": "Fann ikkje bakgrunnsbilete i mappa", + "preview-error": "Kunne ikkje lasta inn biletet", + "select-prompt": "Vel bakgrunnsbilete under", + "subheader": "Måta til stemninga med eit fagert bakgrunnsbilete." + }, + "welcome-note": "Berre nokre lette ting for å få deg opp og gåande — alle vala finn du i innstillingane.", + "welcome-subtitle": "Lat oss gjera skrivebordet ditt", + "welcome-title": "Velkomen åt Noctalia!" + }, + "system": { + "cpu-temperature": "{temp} °C", + "disk-usage": "{percent} %", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "Ingen mediaavspelar funnen", + "scaling-percentage": "{percentage} %", + "signal-strength": "{signal} %", + "unknown-app": "Ukjend app", + "uptime": "Oppetid: {uptime}", + "user-requested": "Brukaretterspurd", + "welcome-back": "Velkomen attende,", + "widget-settings-title": "{widget}innstillingar" + }, + "system-monitor": { + "cpu-temp": "Prosessortemperatur", + "cpu-usage": "Prosessorbruk", + "disk": "Disk", + "download-speed": "Nedlastingsfart", + "gpu-temp": "Skjermkorttemperatur", + "load-average": "Snittleg last", + "title": "Systemvakt", + "upload-speed": "Opplastingsfart" + }, + "toast": { + "airplane-mode": { + "title": "Flymodus" + }, + "battery": { + "critical": "Kritisk batterinivå", + "critical-desc": "Batterinivået er på {percent} % - kopla til ladaren no!", + "low": "Lågt batteri", + "low-desc": "Batterinivået er {percent} %. Du bør kopla til ladaren." + }, + "bluetooth": { + "address-copied": "Adresse kopiert til utklippstavla", + "confirm-code": "Stadfest koden {value} på hi eininga.", + "connect-failed": "Mislukka tilkopling", + "disconnect-failed": "Mislukka fråkopling", + "discoverable-change-failed": "Kunne ikkje endra oppdagingstilstand", + "discoverable-disabled": "Ikkje finnande", + "discoverable-enabled": "Finnande", + "display-code": "Skriv inn koden {value} på hi eininga.", + "forget-failed": "Kunne ikkje gløyma eininga", + "pair-failed": "Kunne ikkje kopla til eininga", + "passkey-required": "Eininga krev passord.", + "pincode-required": "Eininga krev PIN-kode.", + "state-change-failed": "Kunne ikkje endra Bluetoothstatus" + }, + "clipboard": { + "long-text": "Lang tekst", + "unavailable": "Utklippsboka ikkje tilgjengeleg", + "unavailable-desc": "'cliphist' er ikkje installert. Du må installera han for å kunne bruka utklippsboka." + }, + "do-not-disturb": { + "disabled": "\"Ikkje forstyrr\" av", + "disabled-desc": "Viser alle varsel", + "enabled": "\"Ikkje forstyrr\" på", + "enabled-desc": "Du finn desse varsla i historikken din" + }, + "donation-opened": "Donasjonsside opna i nettlesaren din", + "internet-limited": "Kopla til utan internett", + "keyboard-layout": { + "changed": "Oppsett endra til {layout}", + "title": "Knappefjøl" + }, + "missing-control-center": { + "description": "Miniprogrammet for kontrollsenteret er ikkje lenger på oppgåvelina. For å få tilgang frå oppgåvelina, må du leggja det til att. Du kan opna det ved å høgreklikka på oppgåvelina òg.", + "label": "Siste miniprogram for kontrollsenter sletta" + }, + "night-light": { + "forced": "Tvinga aktivering", + "normal": "Vanleg modus", + "not-installed": "wlsunset ikkje installert" + }, + "noctalia-performance": { + "disabled": "Ytingsmodus av", + "enabled": "Ytingsmodus på", + "label": "Noctalia Yting" + }, + "power-profile": { + "changed": "Straumprofil endra", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "Noctalia støytte på ein feil medan det handsama malar", + "title": "Malhandsaming mislukka" + }, + "vpn": { + "connected": "Kopla til '{name}'", + "disconnected": "Kopla frå '{name}'" + }, + "wallpaper-colors": { + "disabled": "Bakgrunnsfargar av", + "enabled": "Bakgrunnsfargar på", + "label": "Bakgrunnsfargar" + }, + "wifi": { + "connected": "Kopla til '{ssid}'", + "connection-failed": "Kopling mislukka", + "connection-timeout": "Tidsavbrot med kopling", + "disconnected": "Kopla frå '{ssid}'", + "incorrect-password": "Gale passord", + "network-not-found": "Nettverk ikkje funne" + } + }, + "tooltips": { + "add-widget": "Legg til miniprogram", + "bluetooth-devices": "Bluetooth-einingar", + "brightness-at": "Ljosstyrke: {brightness} %", + "click-to-start-recording": "Skjermopptak (start opptak)", + "click-to-stop-recording": "Skjermopptak (stogg opptak)", + "collapse": "Gøym sidestolpen", + "copy-address": "Kopier adresse", + "delete-notification": "Slett varsel", + "dismiss-notification": "Avvis varsel", + "do-not-disturb-enabled": "Ikkje forstyrr", + "expand": "Brett ut sidestolpen", + "forget-network": "Gløym nettverk", + "grid-view": "Rutenett", + "hidden-files-hide": "Gøymde filer", + "hide-unnamed-devices": "Gøym einingar utan namn", + "home": "Heimemappe", + "input-muted": "Slå av/på ljod i inngang", + "keep-awake": "Haldt vaken", + "keyboard-layout": "{layout} knappefjøloppsett", + "list-view": "Liste", + "manage-vpn": "VPN-tilkoplingar", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "Øvre grense på miniprogramm nådd", + "microphone-volume-at": "Ljodstyrke på mikrofon: {volume} %", + "move-to-section": "Flytt til {section}", + "mute": "Slå av ljod", + "next-media": "Neste song", + "next-month": "Neste månad", + "night-light-not-installed": "Natteljos (ikkje tilgjengeleg)", + "noctalia-performance-enabled": "Noctalia ytingsmodus", + "open-annotation-tool": "Opna med merknadsverktøyet", + "open-control-center": "Kontrollsenter", + "open-notification-history-enable-dnd": "Varselhistorikk", + "open-settings": "Innstillingar", + "open-tray-dropdown": "Systemkorg", + "output-muted": "Slå av/på ljod i utgang", + "power-profile": "{profile} straumprofil", + "previous-media": "Førre song", + "previous-month": "Førre månad", + "refresh-devices": "Last inn einingar å nyo", + "refresh-wallhaven": "Last inn Wallhaven-utfall å nyo", + "refresh-wallpaper-list": "Last inn bakgrunnslista å nyo", + "remove-widget": "Slett miniprogram", + "search-close": "Steng søk", + "session-menu": "Øktsmeny", + "show-all-devices": "Vis alle einingar", + "switch-to-dark-mode": "Mørkemodus", + "switch-to-light-mode": "Ljosmodus", + "unmute": "Slå på ljod", + "up": "Overordna mappe", + "volume-at": "Ljod i utgang: {volume} %", + "wallpaper-selector": "Bakgrunnsveljar" + }, + "wallpaper": { + "browse": { + "empty-directory": "Denne mappa er tom.", + "go-root": "Gå til bakgrunnsbileterota", + "go-up": "Gå til overordna mappe", + "go-up-hint": "Bruk \"attende\"-knappen for å gå opp eitt nivå." + }, + "configure-directory": "Styr bakgrunnsbiletemappa med bilete.", + "fill-modes": { + "crop": "Skjer av (Fyll)", + "fit": "Måta til (Haldt)", + "repeat": "Tak opp att (Flislegg)", + "stretch": "Tøyg" + }, + "no-match": "Ingen treff.", + "no-wallpaper": "Ingen bakgrunnsbilete funne.", + "panel": { + "apikey-help": "Ein API-lykel krevst for å få tilgang til NSFW-innhald", + "apikey-label": "API-lykel", + "apikey-managed-by-env": "Styrt med NOCTALIA_WALLHAVEN_API_KEY miljøvariabel.", + "apikey-placeholder": "Skriv inn API-lyklen din for Wallhaven", + "categories-anime": "Anime", + "categories-label": "Kategoriar", + "categories-people": "Folk", + "color-extraction-disabled": "Tak fargane frå bakgrunnsbiletet", + "color-extraction-enabled": "Bruk føredefinerte fargeskalaar", + "order-asc": "Oppover", + "order-desc": "Nedover", + "order-label": "Rekkjefylgd", + "purity-label": "Innhaldsfilter", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "På randa", + "ratios-all-portrait": "Alle ståande", + "ratios-all-wide": "Alle breide", + "ratios-any": "Kva som helst", + "ratios-label": "Sidetilhøve", + "resolution-atleast": "I minsto", + "resolution-exact": "Nøye", + "resolution-label": "Oppløysing", + "resolution-mode-label": "Modus", + "solid-color-tooltip": "Einsfarga bakgrunn", + "sorting-date-added": "Dato lagt til", + "sorting-favorites": "Favorittar", + "sorting-label": "Sorter ved", + "sorting-relevance": "Relevans", + "sorting-toplist": "Toppliste", + "sorting-views": "Visningar", + "source-label": "Kjelde", + "source-wallhaven": "Wallhaven", + "title": "Wallhaven-veljar", + "wallhaven-settings-title": "Wallhaven-innstillingar" + }, + "transitions": { + "disc": "Disk", + "fade": "Bleikna", + "honeycomb": "Vokstavle", + "pixelate": "Pikseler", + "stripes": "Striper", + "wipe": "Viska" + }, + "try-different-search": "Prøv å søkja noko anna.", + "wallhaven": { + "loading": "Lastar inn bakgrunnsbilete...", + "no-results": "Fann ingen bakgrunnsbilete. Prøv å søkja noko anna.", + "page": "{current} av {total}", + "page-prefix": "Side", + "page-suffix": "av {total}" + } + }, + "weather": { + "clear-sky": "Klår himmel", + "drizzle": "Yr", + "fog": "Skodde", + "mainly-clear": "For det meste klår", + "overcast": "Skyete", + "partly-cloudy": "Nokre fåe skyer", + "rain-showers": "Regnskurer", + "snow": "Snø", + "thunderstorm": "Storm" + }, + "widgets": { + "color-picker": { + "palette-description": "Vel frå mange ulike fargar.", + "palette-label": "Palett", + "palette-theme-colors": "Kjapp tilgang til fargeskalaen for temaet ditt.", + "title": "Fargeveljar" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm med småe bokstavar", + "ampm-uppercase": "AM/PM med store bokstavar", + "common-12hour-time-minutes": "Tid i 12-timar med minutt", + "common-24hour-time-minutes": "Tid i 24-timar med minutt", + "common-24hour-time-seconds": "Tid i 24-timar med sekund", + "common-european-date": "Europeisk datoformat", + "common-iso-date": "ISO datoformat", + "common-us-date": "Amerikansk datoformat", + "common-weekday-date": "Vekedag med dato", + "common-weekday-month-day": "Vekedag, månad og dag", + "day-abbreviated": "Forkorta dagsnamn", + "day-full": "Fullt dagsnamn", + "day-leading-zero": "Dag med null føre (01-31)", + "day-no-leading-zero": "Dag utan null føre (1-31)", + "hour-leading-zero": "Time med null føre (00-23) — 24t-format", + "hour-no-leading-zero": "Time utan null føre (0-23) — 24t-format", + "minute-leading-zero": "Minutt med null føre (00-59)", + "minute-no-leading-zero": "Minutt utan null føre (0-59)", + "month-abbreviated": "Forkorta månadsnamn", + "month-full": "Fullt månadsnamn", + "month-number-leading-zero": "Månad som tal med null føre (01-12)", + "month-number-no-zero": "Månad som tal utan null føre (1-12)", + "second-leading-zero": "Sekund med null føre (00-59)", + "second-no-leading-zero": "Sekund utan null føre (0-59)", + "timezone-abbreviation": "Forkorta tidssone", + "year-four-digit": "År som tal med fire siffer", + "year-two-digit": "År som tal med to siffer (00-99)" + }, + "file-picker": { + "item": "objekt", + "items": "objekt", + "search-placeholder": "Leita i filer og mapper...", + "select-current": "Vel noverande", + "select-file": "Vel fil", + "select-folder": "Vel mappe", + "selected": "Valt:", + "title": "Filveljar" + }, + "file-picker-title": "Vel ei fil", + "icon-picker": { + "title": "Ikonveljar" + }, + "tooltip-placeholder": "Plasshaldar" + }, + "wifi": { + "panel": { + "action-required": "Krev handling", + "available-interfaces": "Tilgjengelege grensesnitt", + "available-networks": "Tilgjengelege nettverk", + "disabled": "Wi-Fi er slege av", + "disconnecting": "Koplar frå...", + "dns": "DNS", + "enable-message": "Slå på Wi-Fi for å sjå tilgjengelege nettverk.", + "enter-password": "Skriv inn Wi-Fi-passord...", + "forget": "Gløym", + "forget-network": "Gløym dette nettverket", + "forgetting": "Gløymer...", + "interface": "Nettverkgrensesnitt", + "internet-connected": "Kopla til internettet", + "internet-limited": "Ikkje kopla til internettet", + "internet-status": "Internettstatus", + "ipv4": "IPv4", + "known-networks": "Kjende nettverk", + "link-speed": "Lenkjefart", + "no-ethernet-devices": "Ingen etherneteiningar funne", + "no-networks": "Ingen Wi-Fi-nettverk funne", + "saved": "Lagra", + "scan-again": "Leit atter", + "searching": "Søkjer etter nettverk...", + "title": "Wi-Fi" + }, + "signal": { + "excellent": "Framifrå", + "fair": "Grei", + "good": "God", + "poor": "Dåleg" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/pl.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/pl.json new file mode 100644 index 0000000..115049c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/pl.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "Wyczyść historię", + "close-app": "Zamknij {app}", + "connect-vpn": "Połącz z {name}", + "cycle-visualizer": "Przełącz wizualizator", + "disable-bluetooth": "Wyłącz Bluetooth", + "disable-dnd": "Wyłącz 'Nie przeszkadzać'", + "disable-wifi": "Wyłącz Wi-Fi", + "disconnect-vpn": "Odłącz {name}", + "dock-settings": "Ustawienia docka", + "enable-bluetooth": "Włącz Bluetooth", + "enable-dnd": "Włącz 'Nie przeszkadzać'", + "enable-wifi": "Włącz Wi-Fi", + "launcher-settings": "Ustawienia programu uruchamiającego", + "lower-to-bottom": "Opuść na dół", + "open-calendar": "Otwórz kalendarz", + "open-display-settings": "Ustawienia ekranu", + "open-launcher": "Otwórz launcher", + "open-settings": "Otwórz ustawienia", + "raise-to-top": "Przenieś na górę", + "random-wallpaper": "Losowa tapeta", + "run-custom-command": "Uruchom własne polecenie", + "show-active-only": "Pokaż tylko aktywne", + "show-all": "Pokaż wszystko", + "toggle-mute": "Przełącz wyciszenie", + "widget-settings": "Ustawienia widżetu" + }, + "authentication": { + "error": "Błąd uwierzytelniania", + "failed": "Uwierzytelnianie nieudane" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Zastosuj kolory motywu do ikony aktywnego okna.", + "hide-mode-description": "Kontroluje zachowanie widżetu, gdy żadne okno nie jest aktywne.", + "scrolling-mode-description": "Kontroluje, kiedy włączone jest przewijanie tekstu dla długich tytułów okien.", + "show-app-icon-description": "Wyświetl ikonę aplikacji obok tytułu okna.", + "show-app-icon-label": "Pokaż ikonę aplikacji", + "show-app-text-description": "Wyświetl tytuł aktywnego okna.", + "show-app-text-label": "Pokaż tytuł okna" + }, + "audio-visualizer": { + "color-name-description": "Wybierz kolor dla wizualizatora.", + "color-name-label": "Kolor wypełnienia", + "height-description": "Niestandardowa szerokość komponentu.", + "hide-when-idle-description": "Gdy włączone, wizualizator jest ukryty, chyba że odtwarzacz jest aktywny.", + "hide-when-idle-label": "Ukryj, gdy nic nie jest odtwarzane", + "width-description": "Niestandardowa szerokość komponentu." + }, + "battery": { + "device-default": "Domyślne (urządzenie wyświetlające)", + "device-description": "Wybierz, które urządzenie baterii ma być wyświetlane.", + "device-label": "Urządzenie baterii", + "display-mode-description": "Wybierz, jak bateria jest wyświetlana na pasku.", + "display-mode-graphic": "Graficzna bateria", + "display-mode-graphic-clean": "Graficzna bateria (bez %)", + "display-mode-icon-always": "Ikona - Zawsze pokazuj %", + "display-mode-icon-hover": "Ikona - Pokaż po najechaniu", + "display-mode-icon-only": "Tylko ikona", + "hide-if-idle-description": "Ukryj widżet, gdy bateria nie jest ładowana ani rozładowywana.", + "hide-if-idle-label": "Ukryj, gdy bezczynny", + "hide-if-not-detected-description": "Ukryj widżet, gdy w systemie nie zostanie wykryta bateria.", + "hide-if-not-detected-label": "Ukryj się, jeśli nie wykryto", + "low-battery-threshold-label": "Próg ostrzegania o niskim poziomie baterii", + "show-noctalia-performance-description": "Wyświetl przełącznik trybu wydajności Noctalia w panelu baterii.
Wyłącza cienie i animacje w Noctalia, aby zmniejszyć zużycie zasobów.", + "show-noctalia-performance-label": "Pokaż przełącznik Wydajności Noctalia", + "show-power-profile-description": "Wyświetl wybór profilu zasilania w panelu baterii.", + "show-power-profile-label": "Pokaż kontrolki profilu zasilania" + }, + "brightness": { + "apply-all-description": "Zmień jasność dla wszystkich monitorów podczas używania kółka przewijania.", + "apply-all-label": "Zastosuj zmiany przewijania do wszystkich monitorów" + }, + "clock": { + "clock-display-description": "Dostosuj wyświetlanie zegara, dodając tokeny z poniższej listy. Aby użyć formatu 12-godzinnego, musisz dołączyć token 'AP'.", + "clock-display-label": "Wyświetlanie zegara", + "custom-font-description": "Wybierz niestandardową czcionkę dla zegara.", + "custom-font-label": "Niestandardowa czcionka", + "custom-font-placeholder": "Wybierz czcionkę...", + "custom-font-search-placeholder": "Szukaj czcionek...", + "horizontal-bar-description": "Wskazówka: Użyj \\n, aby utworzyć podział linii.", + "horizontal-bar-label": "Pasek poziomy", + "preview": "Podgląd", + "tooltip-format-description": "Ciąg formatujący dla etykietki wyświetlanej po najechaniu na zegar. Pozostaw puste, aby użyć domyślnej etykietki.", + "tooltip-format-label": "Format etykietki", + "use-custom-font-description": "Zastąp domyślną czcionkę zegara czcionką niestandardową.", + "use-custom-font-label": "Użyj niestandardowej czcionki", + "use-monospaced-font-description": "Gdy włączone, zegar będzie używał czcionki o stałej szerokości.", + "use-monospaced-font-label": "Użyj czcionki monospaced", + "vertical-bar-description": "Użyj spacji, aby oddzielić każdą część w nowej linii.", + "vertical-bar-label": "Pasek pionowy" + }, + "control-center": { + "browse-file": "Przeglądaj pliki", + "browse-library": "Przeglądaj bibliotekę", + "color-selection-description": "Zastosuj kolory motywu do ikon.", + "enable-colorization-description": "Włącz koloryzację ikony, stosując kolory motywu.", + "icon-description": "Wybierz ikonę z biblioteki lub niestandardowy plik.", + "select-custom-icon": "Wybierz własną ikonę", + "use-distro-logo-description": "Użyj logo twojej dystrybucji zamiast niestandardowej ikony.", + "use-distro-logo-label": "Użyj logo dystrybucji zamiast ikony" + }, + "custom-button": { + "collapse-condition-description": "Jeśli tekst wyjściowy pasuje do tej wartości, przycisk zostanie zwinięty.", + "collapse-condition-label": "Warunek zwinięcia", + "color-selection-description": "Zastosuj kolory motywu do ikony i tekstu.", + "default-tooltip": "Przycisk niestandardowy, konfiguruj w ustawieniach", + "display-command-output-description": "Wpisz polecenie do uruchamiania w regularnych odstępach czasu. Pierwsza linia wyniku zostanie wyświetlona jako tekst.", + "display-command-output-label": "Wyświetl wynik polecenia", + "display-command-output-stream-description": "Wpisz polecenie do uruchamiania w trybie ciągłym.", + "dynamic-text": "Dynamiczny tekst", + "enable-colorization-description": "Włącz kolorowanie ikony i tekstu niestandardowego przycisku, stosując kolory motywu.", + "enable-colorization-label": "Włącz kolorowanie", + "general-tooltip-text-description": "Niestandardowy tekst do wyświetlenia w dymku przycisku.", + "general-tooltip-text-label": "Niestandardowy tekst podpowiedzi", + "hide-mode-always-expanded": "Zawsze rozwinięty", + "hide-mode-description": "Kontroluje widoczność widżetu, gdy polecenie nie zwraca wyniku.", + "hide-mode-expand-with-output": "Rozwiń, gdy jest wynik", + "hide-mode-label": "Tryb ukrywania", + "hide-mode-max-transparent": "Rozwinięty, ale przezroczysty", + "icon-description": "Wybierz ikonę z biblioteki.", + "ipc-identifier-description": "Unikalny identyfikator dla poleceń IPC. Użyj tego identyfikatora z 'qs -c noctalia-shell ipc call cb [akcja] [identyfikator]', aby sterować tym przyciskiem za pomocą IPC.", + "ipc-identifier-label": "Identyfikator IPC", + "left-click-description": "Polecenie do wykonania po kliknięciu lewym przyciskiem myszy.", + "left-click-label": "Lewy przycisk", + "left-click-update-text": "Aktualizuj tekst po kliknięciu lewym przyciskiem", + "max-text-length-horizontal-description": "Maksymalna liczba znaków na pasku poziomym (0, aby ukryć tekst).", + "max-text-length-horizontal-label": "Max długość tekstu (poziomo)", + "max-text-length-vertical-description": "Maksymalna liczba znaków na pasku pionowym (0, aby ukryć tekst).", + "max-text-length-vertical-label": "Max długość tekstu (pionowo)", + "middle-click-description": "Polecenie do wykonania po kliknięciu środkowym przyciskiem myszy.", + "middle-click-label": "Środkowy przycisk", + "middle-click-update-text": "Aktualizuj tekst po kliknięciu środkowym przyciskiem", + "parse-json-description": "Przetwórz wynik polecenia jako obiekt JSON, aby dynamicznie ustawić tekst i ikonę.", + "parse-json-label": "Parsuj wynik jako JSON", + "refresh-interval-description": "Interwał w milisekundach.", + "refresh-interval-label": "Interwał odświeżania", + "right-click-description": "Polecenie do wykonania po kliknięciu prawym przyciskiem myszy.", + "right-click-label": "Prawy przycisk", + "right-click-update-text": "Aktualizuj tekst po kliknięciu prawym przyciskiem", + "show-exec-tooltip-description": "Pokaż podpowiedzi ze szczegółami poleceń (lewy/prawy/środkowy klik, kółko).", + "show-exec-tooltip-label": "Pokaż podpowiedzi poleceń", + "show-icon-description": "Przełącza widoczność ikony widżetu.", + "show-icon-label": "Pokaż ikonę", + "show-text-tooltip-description": "Pokaż dymki z podpowiedzią z wyjściem polecenia tekstowego.", + "show-text-tooltip-label": "Pokaż dynamiczne etykietki tekstowe", + "text-stream-description": "Linie przesyłane strumieniowo z polecenia będą wyświetlane jako tekst na przycisku.", + "text-stream-label": "Strumień", + "wheel-description": "Polecenie do wykonania przy użyciu kółka myszy.
Użyj $delta dla wartości przesunięcia kółka w poleceniu.", + "wheel-down": "Rolka w dół", + "wheel-down-description": "Polecenie do wykonania przy przewijaniu kółkiem w dół.", + "wheel-down-label": "Polecenie kółka w dół", + "wheel-label": "Kółko myszy", + "wheel-mode-separate-description": "Włącz oddzielne polecenia dla przewijania w górę i w dół.", + "wheel-mode-separate-label": "Oddzielne polecenia kółka", + "wheel-up": "Rolka w górę", + "wheel-up-description": "Polecenie do wykonania przy przewijaniu kółkiem w górę.", + "wheel-up-label": "Polecenie kółka w górę", + "wheel-update-text": "Aktualizuj tekst po użyciu kółka" + }, + "keyboard-layout": { + "show-icon-description": "Wyświetl ikonę układu klawiatury." + }, + "lock-keys": { + "hide-when-off-description": "Ukryj wskaźnik, gdy klawisz jest nieaktywny.", + "hide-when-off-label": "Ukryj gdy wyłączony", + "show-caps-lock-description": "Wyświetl status Caps Lock.", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "Wyświetl status Num Lock.", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "Wyświetl status Scroll Lock.", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "Włącz układ oszczędzający miejsce dla panelu odtwarzacza multimedialnego.", + "compact-mode-label": "Tryb kompaktowy", + "hide-mode-description": "Kontroluje zachowanie widżetu, gdy żadne media nie są odtwarzane.", + "max-width-description": "Ustawia maksymalną szerokość poziomą widżetu. Widżet skurczy się, aby dopasować się do krótszej zawartości.", + "no-active-player": "Brak aktywnego odtwarzacza", + "panel-section-description": "Skonfiguruj wygląd i zachowanie panelu odtwarzacza multimediów.", + "panel-section-label": "Panel odtwarzacza multimedialnego", + "scrolling-mode-description": "Kontroluje, kiedy włączone jest przewijanie tekstu dla długich tytułów utworów.", + "scrolling-mode-label": "Tryb przewijania", + "show-album-art-description": "Wyświetl okładkę albumu aktualnie odtwarzanego utworu.", + "show-album-art-label": "Pokaż okładkę", + "show-artist-first-description": "Wyświetl artystę - tytuł zamiast tytuł - artysta.", + "show-artist-first-label": "Pokaż najpierw artystę", + "show-progress-ring-description": "Wyświetl kołowy wskaźnik postępu utworu.", + "show-progress-ring-label": "Pokaż pierścień postępu", + "show-visualizer-description": "Wyświetl wizualizator dźwięku podczas odtwarzania muzyki.", + "show-visualizer-label": "Pokaż wizualizator", + "use-fixed-width-description": "Gdy włączone, widżet zawsze będzie używał maksymalnej szerokości zamiast dynamicznie dostosowywać się do treści.", + "use-fixed-width-label": "Użyj stałej szerokości", + "visualizer-type-description": "Wybierz styl wyświetlanego wizualizatora dźwięku.", + "visualizer-type-label": "Typ wizualizatora" + }, + "notification-history": { + "hide-widget-when-zero-description": "Ukryj ikonę powiadomień, gdy nie ma żadnych powiadomień.", + "hide-widget-when-zero-label": "Ukryj ikonę bez powiadomień", + "hide-widget-when-zero-unread-description": "Ukryj ikonę powiadomień, gdy nie ma nieprzeczytanych powiadomień.", + "hide-widget-when-zero-unread-label": "Ukryj ikonę bez nieprzeczytanych", + "show-unread-badge-description": "Wyświetl plakietkę z liczbą nieprzeczytanych powiadomień.", + "show-unread-badge-label": "Pokaż plakietkę nieprzeczytanych", + "unread-badge-color-description": "Wybierz kolor dla plakietki nieprzeczytanych powiadomień.", + "unread-badge-color-label": "Kolor plakietki nieprzeczytanych" + }, + "section-editor": { + "placeholder": "Wybierz widżet do dodania...", + "search-placeholder": "Szukaj widżetu..." + }, + "spacer": { + "width-description": "Szerokość odstępu w pikselach." + }, + "system-monitor": { + "compact-mode-description": "Wyświetlaj statystyki jako mini wykresy słupkowe zamiast wartości tekstowych. Zapobiega przesunięciom układu.", + "compact-mode-label": "Tryb kompaktowy", + "cpu-cores-description": "Wyświetlaj użycie rdzeni CPU indywidualnie.", + "cpu-cores-label": "Rdzenie CPU", + "cpu-frequency-description": "Wyświetl aktualną prędkość zegara CPU w GHz.", + "cpu-frequency-label": "Pokaż częstotliwość CPU", + "cpu-temperature-description": "Pokaż odczyty temperatury procesora (jeśli dostępne).", + "cpu-temperature-label": "Temperatura CPU", + "cpu-usage-description": "Wyświetl aktualne użycie procesora w procentach.", + "cpu-usage-label": "Użycie CPU", + "disk-path-description": "Wybierz punkt montowania dysku do monitorowania.", + "disk-path-label": "Ścieżka dysku", + "gpu-temperature-description": "Pokaż odczyty temperatury GPU (jeśli dostępne).", + "load-average-description": "Wyświetl średnie obciążenie systemu.", + "load-average-label": "Średnie obciążenie", + "memory-percentage-description": "Pokaż użycie pamięci w procentach zamiast wartości bezwzględnych.", + "memory-percentage-label": "Pamięć w procentach", + "memory-usage-description": "Wyświetl aktualne użycie pamięci RAM.", + "memory-usage-label": "Użycie pamięci", + "network-traffic-description": "Wyświetl prędkość wysyłania i pobierania danych w sieci.", + "network-traffic-label": "Ruch sieciowy", + "storage-as-percentage-description": "Wyświetlaj miejsce na dysku jako procent zamiast wartości bezwzględnych.", + "storage-as-percentage-label": "Dysk jako procent", + "storage-available-description": "Pokazuje, ile miejsca na dysku jest dostępne, zamiast ile jest używane.", + "storage-available-label": "Dostępne miejsce na dysku", + "storage-usage-description": "Pokaż informacje o użyciu miejsca na dysku.", + "storage-usage-label": "Użycie dysku", + "swap-usage-description": "Pokaż użycie pamięci wymiany.", + "swap-usage-label": "Użycie pamięci wymiany", + "use-monospace-font-description": "Użyj czcionki o stałej szerokości znaków dla zachowania spójnej szerokości znaków.", + "use-monospace-font-label": "Czcionka o stałej szerokości znaków", + "use-padding-description": "Wypełnia wartości tekstowe spacjami wiodącymi, aby zapobiec przesunięciom układu.", + "use-padding-description-disabled-monospace-font": "Do tej funkcji potrzebna jest czcionka monospace.", + "use-padding-description-disabled-vertical": "Pionowy pasek zadań nie obsługuje wypełnienia tekstowego.", + "use-padding-label": "Wypełnienie Tekstu" + }, + "taskbar": { + "colorize-icons-description": "Zastosuj kolory motywu do ikon paska zadań.", + "hide-mode-description": "Kontroluje zachowanie widżetu, gdy nie ma pasujących okien.", + "hide-mode-label": "Tryb ukrywania", + "icon-scale-description": "Ustawia współczynnik skalowania ikon na pasku zadań.", + "icon-scale-label": "Skalowanie ikon", + "max-width-description": "Maksymalna szerokość paska zadań jako procent szerokości ekranu.", + "max-width-label": "Maksymalna szerokość", + "only-active-workspaces-description": "Pokaż tylko aplikacje z aktywnych obszarów roboczych.", + "only-active-workspaces-label": "Tylko z aktywnych obszarów", + "only-same-monitor-description": "Pokaż tylko aplikacje z monitora, na którym znajduje się pasek.", + "only-same-monitor-label": "Tylko z tego samego monitora", + "show-pinned-apps-description": "Pokaż przypięte aplikacje z doku na pasku zadań.", + "show-pinned-apps-label": "Pokaż przypięte aplikacje", + "show-title-description": "Wyświetl tytuły okien na pasku zadań.", + "show-title-description-disabled": "Pionowy pasek zadań nie obsługuje wyświetlania tytułów.", + "show-title-label": "Pokaż tytuł", + "smart-width-description": "Automatycznie dostosuj szerokość wpisów na podstawie ich liczby.", + "smart-width-label": "Inteligentna szerokość", + "title-width-description": "Ustaw szerokość tytułów okien na pasku zadań (w pikselach).", + "title-width-label": "Szerokość tytułu", + "title-width-reset-tooltip": "Resetuj szerokość tytułu" + }, + "tray": { + "chevron-color-description": "Zastosuj kolory motywu do ikony strzałki szuflady.", + "chevron-color-label": "Kolor strzałki", + "colorize-icons-description": "Zastosuj kolory motywu do ikon zasobnika.", + "colorize-icons-label": "Koloryzuj ikony", + "drawer-enabled-description": "Gdy włączone, nieprzypięte elementy zasobnika są pokazywane w panelu szuflady.
Gdy wyłączone, wszystkie elementy są pokazywane w linii.", + "drawer-enabled-label": "Włącz szufladę", + "hide-passive-description": "Gdy włączone, elementy zasobnika o statusie 'Passive' będą ukryte.", + "hide-passive-label": "Ukryj pasywne elementy" + }, + "volume": { + "display-mode-description": "Wybierz sposób wyświetlania tej wartości." + }, + "workspace": { + "character-count-description": "Liczba znaków do wyświetlenia z nazw obszarów roboczych (1-10).", + "character-count-label": "Liczba znaków", + "empty-color-description": "Ustaw kolor tła dla pustych Workspaceów.", + "empty-color-label": "Kolor pustego obszaru roboczego", + "enable-scrollwheel-description": "Przełączaj między obszarami roboczymi za pomocą kółka myszy.", + "enable-scrollwheel-label": "Przewiń, aby przełączyć obszary", + "focused-color-description": "Ustaw kolor tła dla aktywnego Workspace.", + "focused-color-label": "Kolor aktywnego obszaru roboczego", + "follow-focused-screen-description": "Wyświetl obszary robocze z aktualnie aktywnego ekranu, zamiast ekranu, na którym znajduje się pasek.", + "follow-focused-screen-label": "Podążaj za skupionym ekranem", + "font-weight-description": "Ustaw wizualną wagę tekstu w obszarze roboczym.", + "font-weight-label": "Grubość czcionki", + "grouped-border-opacity-description": "Ustaw poziom krycia dla obramowań kontenerów obszaru roboczego.", + "grouped-border-opacity-label": "Krycie obramowania", + "hide-unoccupied-description": "Nie wyświetlaj obszarów roboczych bez okien.", + "hide-unoccupied-label": "Ukryj nieobsadzone", + "label-mode-description": "Wybierz sposób wyświetlania etykiet obszarów roboczych.", + "label-mode-label": "Tryb etykiet", + "occupied-color-description": "Ustaw kolor tła dla zajętych Workspace'ów.", + "occupied-color-label": "Kolor zajętego obszaru roboczego", + "pill-size-description": "Dostosuj rozmiar pigułek obszaru roboczego (50%-100%).", + "pill-size-label": "Rozmiar pigułki", + "reverse-scrolling-description": "Odwróć kierunek przełączania obszarów roboczych podczas przewijania.", + "reverse-scrolling-label": "Odwróć przewijanie", + "show-applications-description": "Wyświetl ikony aplikacji wewnątrz każdego obszaru roboczego.", + "show-applications-hover-description": "Wyświetl ikony aplikacji w każdym obszarze roboczym po najechaniu na obszar roboczy.", + "show-applications-hover-label": "Pokaż aplikacje po najechaniu kursorem", + "show-applications-label": "Pokaż aplikacje", + "show-badge-description": "Pokaż plakietkę z numerem obszaru roboczego w trybie grupowym.", + "show-badge-label": "Pokaż odznakę obszaru roboczego", + "show-labels-only-when-occupied-description": "Pokazuj etykiety obszarów roboczych tylko wtedy, gdy zawierają okna.", + "show-labels-only-when-occupied-label": "Pokaż etykiety tylko gdy obsadzone", + "unfocused-icons-opacity-description": "Ustaw poziom przezroczystości dla ikon nieaktywnych aplikacji.", + "unfocused-icons-opacity-label": "Krycie ikon bez ostrości" + } + }, + "battery": { + "all-batteries": "Wszystkie baterie (łącznie)", + "battery-health": "Stan baterii", + "battery-level": "Poziom baterii", + "capacity-level": "Pojemność: {level}", + "charging-rate": "Prędkość ładowania: {rate} W", + "discharging-rate": "Prędkość rozładowywania: {rate} W", + "health": "Kondycja: {percent}%", + "inhibit-idle-description": "Zapobiega zasypianiu systemu.", + "no-battery-detected": "Nie wykryto baterii", + "plugged-in": "Podłączony", + "power-profile": "Profil zasilania", + "time-left": "Pozostały czas: {time}", + "time-until-full": "Czas do pełnego naładowania: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Dostępne urządzenia", + "blocked": "Zablokowane", + "connected-devices": "Połączone urządzenia", + "device-address": "Adres urządzenia", + "disabled": "Bluetooth jest wyłączony", + "discoverable": "Wykrywalny", + "enable-message": "Włącz Bluetooth, aby zobaczyć dostępne urządzenia.", + "known-devices": "Znane urządzenia", + "no-devices": "Brak dostępnych urządzeń", + "paired-devices": "Sparowane urządzenia", + "pairing-mode": "Upewnij się, że urządzenie jest w trybie parowania.", + "scanning": "Skanowanie urządzeń...", + "signal-text-excellent": "Sygnał: Doskonały", + "signal-text-fair": "Sygnał: Dobry", + "signal-text-good": "Sygnał: Dobry", + "signal-text-poor": "Sygnał: Słaby", + "signal-text-unknown": "Sygnał: Nieznany", + "signal-text-very-poor": "Sygnał: Bardzo słaby" + } + }, + "changelog": { + "error": { + "fetch-failed": "Nie można załadować dziennika zmian. Spróbuj ponownie później.", + "rate-limit": "Przekroczono limit zapytań GitHub. Spróbuj ponownie za kilka minut." + }, + "panel": { + "buttons-discord": "Dołącz do Discorda", + "buttons-dismiss": "OK", + "buttons-feedback": "Przekaż opinię", + "empty": "Informacje o wydaniu nie są jeszcze dostępne.", + "highlight-title": "Najważniejsze zmiany", + "released": "Wydano: {date}", + "subtitle-fresh": "Dziękujemy za zainstalowanie Noctalia! Oto co zawiera ta kompilacja.", + "subtitle-updated": "Zaktualizowano z {previousVersion}", + "title": "Co nowego w {version}", + "version": "Wersja {version}", + "version-new-user": "Nowa instalacja" + } + }, + "common": { + "actions": "Akcje", + "active": "Aktywny", + "add": "Dodaj", + "appearance": "Wygląd", + "apply": "Zastosuj", + "auto-connect": "Automatyczne łączenie", + "automation": "Automatyzacja", + "available": "Dostępne", + "back": "Powrót", + "battery": "Bateria", + "behavior": "Zachowanie", + "bluetooth": "Bluetooth", + "brightness": "Jasność", + "browse": "Przeglądaj", + "calendar": "Kalendarz", + "calendar-panel": "Panel kalendarza", + "cancel": "Anuluj", + "cards": "Karty", + "charging": "Ładowanie", + "clear": "Wyczyść", + "clipboard": "Schowek", + "close": "Zamknij", + "color-muted": "Wyciszony", + "colors": "Kolory", + "command": "Komenda", + "confirm": "Potwierdź", + "connect": "Połącz", + "connected": "Połączone", + "connecting": "Łączenie...", + "continue": "Kontynuuj", + "contributors": "Współtwórcy", + "copied-to-clipboard": "Skopiowano do schowka", + "countdown": "Odliczanie", + "customize": "Dostosuj", + "date": "Data", + "default": "Domyślny", + "delete": "Usuń", + "devices": "Urządzenia", + "disabled": "Wyłączone", + "discharging": "Wypisywanie", + "disconnect": "Rozłącz", + "disconnected": "Rozłączone", + "disconnecting": "Rozłączanie...", + "display-mode": "Tryb wyświetlania", + "documentation": "Dokumentacja", + "download": "Pobierz", + "duration": "Czas trwania", + "dysfunctional": "Dysfunkcyjny", + "edit": "Edytuj", + "enabled": "Włączone", + "error": "Błąd", + "ethernet": "Ethernet", + "events": "Wydarzenia", + "execute": "Wykonaj", + "faithful": "Wierny", + "focus": "Skupienie", + "font-weight-bold": "Pogrubienie", + "font-weight-light": "Cienka", + "font-weight-medium": "Średni", + "font-weight-regular": "Zwykły", + "font-weight-semibold": "Półgruby", + "frequency": "Częstotliwość", + "gateway": "Brama", + "general": "Ogólne", + "height": "Wysokość", + "hibernate": "Hibernacja", + "history": "Historia", + "icon": "Ikona", + "idle": "Bezczynne", + "info": "Informacje", + "input": "Wprowadź", + "install": "Zainstaluj", + "installed": "Zainstalowano", + "interface": "Interfejs", + "internet": "Internet", + "keybind": "Skrót klawiszowy", + "language": "Język", + "loading": "Ładowanie...", + "local": "Lokalny", + "location": "Lokalizacja", + "lock": "Zablokuj", + "logout": "Wyloguj", + "look": "Wygląd", + "margins": "Marginesy", + "media": "Media", + "media-player": "Odtwarzacz multimedialny", + "memory": "Pamięć", + "monitors": "Monitory", + "move-down": "Przenieś w dół", + "move-up": "Przenieś w górę", + "network": "Sieć", + "next": "Następny", + "night-light": "Podświetlenie nocne", + "no": "Nie", + "no-results": "Brak wyników", + "no-summary": "Brak podsumowania", + "none": "Brak", + "not-found": "Nie znaleziono", + "notifications": "Powiadomienia", + "official": "Oficjalne", + "on-surface": "Na powierzchni", + "output": "Wyjście", + "pair": "Paruj", + "paired": "Sparowane", + "pairing": "Parowanie...", + "panels": "Panele", + "password": "Hasło", + "pause": "Pauza", + "performance": "Wydajność", + "pin": "Przypnij", + "play": "Odtwórz", + "polling": "Odpytywanie", + "position": "Pozycja", + "previous": "Poprzedni", + "primary": "Podstawowy", + "random": "Losowy", + "reboot": "Uruchom ponownie", + "reboot-to-uefi": "Uruchom ponownie do UEFI", + "record": "Rozpocznij nagrywanie", + "refresh": "Odśwież", + "required": "(wymagane)", + "reset": "Zresetuj", + "reset-to-default": "Przywróć do Domyślnych", + "result-count": "{count} wynik", + "result-count-plural": "{count} wyniki", + "resume": "Wznów", + "save": "Zapisz", + "scanning": "Skanowanie...", + "screen-corners": "Narożniki ekranu", + "search": "Szukaj", + "second": "{count} sekunda", + "second-plural": "{count} sekund", + "secondary": "Dodatkowy", + "security": "Bezpieczeństwo", + "select": "Wybierz", + "select-color": "Wybierz kolor", + "select-color-description": "Zastosuj kolory motywu dla podkreślenia.", + "select-icon-color": "Wybierz kolor ikony", + "settings": "Ustawienia", + "shortcuts": "Skróty", + "shutdown": "Wyłącz", + "signal": "Sygnał", + "sound": "Dźwięk", + "sources": "Źródła", + "start": "Start", + "stop": "Stop", + "supporters": "Wspierający", + "suspend": "Wstrzymaj", + "templates": "Szablony", + "tertiary": "Trzeciorzędowy", + "test": "Test", + "thresholds": "Progi", + "title": "Tytuł", + "toast": "Toast", + "trusted": "Zaufany", + "uninstall": "Odinstaluj", + "unknown": "Nieznany", + "unpair": "Rozłącz parę", + "unpin": "Odepnij", + "update": "Aktualizacja", + "upload": "Wyślij", + "username": "Nazwa użytkownika", + "userspace-reboot": "Uruchom ponownie przestrzeń użytkownika", + "version": "Wersja", + "vibrant": "Żywy", + "visualizer": "Wizualizator", + "volume": "Objętość", + "volumes": "Tomy", + "wallpaper": "Tapeta", + "weather": "Pogoda", + "weather-loading": "Ładowanie pogody...", + "weather-no-location": "Ustaw lokalizację w Ustawieniach, aby zobaczyć pogodę", + "week": "Tydzień", + "widgets": "Widżety", + "width": "Szerokość", + "wifi": "Wi-Fi", + "windows": "Okna", + "yes": "Tak" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Profil zasilania", + "tooltip-disabled": "Zainstaluj power-profiles-daemon, aby korzystać z profili zasilania" + }, + "wifi": { + "label-disconnected": "Wi-Fi rozłączone", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Zawsze się ukrywaj", + "always-show": "Zawsze pokazuj", + "force-open": "Wyważyć", + "on-hover": "Po najechaniu" + }, + "hide-modes": { + "auto-hide": "Automatyczne ukrywanie", + "hidden": "Ukryj, gdy puste", + "idle": "Ukryj, gdy bezczynny", + "non-exclusive": "Niewyłączny", + "transparent": "Przezroczysty, gdy pusty", + "visible": "Zawsze widoczny" + }, + "launcher": { + "categories": { + "all": "Wszystkie", + "audiovideo": "Audio i wideo", + "chat": "Czat", + "development": "Programowanie", + "education": "Edukacja", + "emoji-activity": "Aktywność", + "emoji-animals": "Zwierzęta", + "emoji-flags": "Flagi", + "emoji-food": "Jedzenie i napoje", + "emoji-nature": "Natura", + "emoji-objects": "Obiekty", + "emoji-people": "Ludzie i ciało", + "emoji-recent": "Ostatni", + "emoji-symbols": "Symbole", + "emoji-travel": "Podróże i miejsca", + "game": "Gry", + "graphics": "Grafika", + "misc": "Różne", + "office": "Biuro", + "pinned": "Przypięte", + "system": "System", + "webbrowser": "Przeglądarka www" + }, + "providers": { + "applications": "Aplikacje", + "calculator": "Kalkulator", + "calculator-deprecated": "Polecenie >calc jest przestarzałe i wkrótce zostanie usunięte. Zamiast tego wpisuj wyrażenia matematyczne bezpośrednio w wyszukiwarce.", + "calculator-description": "Kalkulator — obliczaj wyrażenia matematyczne", + "calculator-enter-expression": "Wpisz wyrażenie matematyczne", + "calculator-press-enter-to-copy": "Naciśnij Enter, aby skopiować wynik", + "clipboard": "Historia schowka", + "clipboard-clear-description": "Wyczyść całą historię schowka", + "clipboard-clear-description-full": "Usuń wszystkie elementy z historii schowka", + "clipboard-clear-history": "Wyczyść historię schowka", + "clipboard-delete": "Usuń wpis ze schowka", + "clipboard-history-disabled": "Historia schowka wyłączona", + "clipboard-history-disabled-description": "Włącz historię schowka w ustawieniach lub zainstaluj cliphist", + "clipboard-loading": "Ładowanie historii schowka...", + "clipboard-search-description": "Szukaj w historii schowka", + "command-description": "Uruchamiaj polecenia powłoki", + "emoji": "Wybór Emoji", + "emoji-loading": "Ładowanie emoji...", + "emoji-loading-description": "Proszę czekać", + "emoji-no-recent": "Brak ostatnich emoji", + "emoji-search-description": "Szukaj i kopiuj emoji", + "settings-search-description": "Szukaj i przejdź do ustawień", + "windows-search-description": "Wyszukaj i aktywuj otwarte okna" + } + }, + "lock-screen": { + "authenticating": "Uwierzytelnianie...", + "password": "Wprowadź hasło...", + "restart": "Restart", + "shut-down": "Wyłącz" + }, + "notifications": { + "panel": { + "click-to-collapse": "Kliknij, aby zwinąć", + "click-to-expand": "Kliknij, aby rozwinąć", + "description": "Twoje powiadomienia pojawią się tutaj, gdy nadejdą.", + "no-notifications": "Brak powiadomień" + }, + "range": { + "earlier": "Wcześniej", + "today": "Dzisiaj", + "yesterday": "Wczoraj" + }, + "time": { + "diff-d": "1 dzień temu", + "diff-dd": "{diff} dni temu", + "diff-h": "1 godzinę temu", + "diff-hh": "{diff} godzin temu", + "diff-m": "1 minutę temu", + "diff-mm": "{diff} minut temu", + "now": "teraz" + } + }, + "options": { + "bar": { + "density-comfortable": "Komfortowy", + "density-compact": "Kompaktowy", + "density-default": "Domyślny", + "density-mini": "Mini", + "density-spacious": "Przestronny", + "display-mode-always-visible": "Zawsze widoczne", + "type-floating": "Pływający", + "type-framed": "Obramowany", + "type-simple": "Prosty" + }, + "control-center": { + "quick-settings-style-classic": "Klasyczny", + "quick-settings-style-modern": "Nowoczesny" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "Komfortowy", + "compact": "Kompaktowy", + "default": "Domyślny" + }, + "launcher-view-mode": { + "grid": "Siatka", + "list": "Lista" + }, + "notification-density": { + "compact": "Kompaktowy", + "default": "Domyślny" + }, + "scrolling-modes": { + "always": "Zawsze przewijaj", + "hover": "Przewijaj po najechaniu", + "never": "Nigdy nie przewijaj" + }, + "session-menu-grid-layout": { + "grid": "Siatka", + "single-row": "Pojedynczy wiersz" + }, + "settings-panel-mode": { + "attached": "Panel przypięty do paska", + "centered": "Panel wyśrodkowany", + "window": "Oddzielne okno" + }, + "visualizer-types": { + "linear": "Liniowy", + "mirrored": "Lustrzany", + "wave": "Fala" + }, + "visualizer-visibility": { + "with-background": "Tylko z tłem" + }, + "workspace-labels": { + "index": "Indeks", + "index-and-name": "Indeks i nazwa", + "name": "Nazwa" + } + }, + "panels": { + "about": { + "become-supporter": "Zostań wspierającym", + "changelog": "Zobacz dziennik zmian", + "changelog-on-startup": "Pokaż dziennik zmian po aktualizacji", + "changelog-on-startup-desc": "Automatycznie pokazuj dziennik zmian po aktualizacji Noctalia.", + "contributors-description": "Podziękowania dla naszego {count} niesamowitego współtwórcy!", + "contributors-description-plural": "Podziękowania dla naszych {count} niesamowitych współtwórców!", + "copy-info": "Kopiuj informacje", + "debug-disabled": "Tryb debugowania wyłączony", + "debug-enabled": "Tryb debugowania włączony", + "info-copied": "Informacje skopiowane do schowka", + "noctalia-available": "Dostępne:", + "noctalia-desc": "Elegancka i minimalistyczna powłoka pulpitu stworzona dla Wayland, zbudowana w Quickshell.", + "noctalia-git-commit": "Git commit:", + "noctalia-installed-version": "Zainstalowana wersja:", + "noctalia-latest-version": "Najnowsza wersja:", + "noctalia-qs-version": "Noctalia QS wersja:", + "noctalia-title": "Noctalia shell", + "privacy-policy": "Polityka prywatności", + "support": "Wesprzyj nas", + "supporter-badge": "Wspierający", + "supporters-desc": "Ogromne podziękowania dla naszego wspaniałego wspierającego!", + "supporters-desc-plural": "Ogromne podziękowania dla naszych {count} wspaniałych wspierających!", + "supporters-loading": "Ładowanie wspierających...", + "system-board": "Płyta główna:", + "system-cpu": "Procesor:", + "system-disk": "Dysk:", + "system-gpu": "GPU:", + "system-host": "Gospodarz:", + "system-install-hint": "Zainstaluj fastfetch, aby wyświetlić informacje o systemie", + "system-kernel": "Jądro:", + "system-loading": "Ładowanie informacji o systemie...", + "system-memory": "Pamięć:", + "system-monitor": "Monitor:", + "system-not-installed": "fastfetch nie jest zainstalowany", + "system-os": "System operacyjny:", + "system-packages": "Paczki:", + "system-product": "Produkt:", + "system-title": "Informacje o systemie", + "system-uptime": "Czas pracy:", + "system-wm": "WM:", + "telemetry-data-copied": "Dane telemetryczne skopiowane do schowka.", + "telemetry-desc": "Pomóż ulepszyć Noctalię, udostępniając anonimowe informacje o systemie (rozdzielczość ekranu, kompozytor, dystrybucja). Wysyłane raz przy uruchomieniu, bez śledzenia, dane usuwane automatycznie po 30 dniach.", + "telemetry-enabled": "Wyślij anonimowe informacje o systemie.", + "telemetry-show-data": "Wyświetl dane", + "telemetry-title": "Prywatność", + "title": "O programie", + "up-to-date": "Jesteś na bieżąco!", + "update-available": "Dostępna aktualizacja", + "view-commit": "Wyświetl zatwierdzenie na GitHubie" + }, + "audio": { + "devices-desc": "Konfiguruj dostępne urządzenia wejściowe i wyjściowe audio.", + "devices-input-device-description": "Wybierz żądane urządzenie wejściowe dźwięku.", + "devices-input-device-label": "Urządzenie wejściowe", + "devices-output-device-description": "Wybierz żądane urządzenie wyjściowe dźwięku.", + "devices-output-device-label": "Urządzenie wyjściowe", + "devices-title": "Urządzenia audio", + "external-mixer-description": "Wpisz polecenie lub ścieżkę do aplikacji, aby uruchomić zewnętrzny mikser dźwięku.", + "external-mixer-label": "Polecenie zewnętrznego miksera", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Ustaw preferowane i ignorowane aplikacje multimedialne.", + "media-excluded-player-description": "Dodaj słowa kluczowe dla odtwarzaczy, które system ma ignorować. Każde słowo w nowej linii.", + "media-excluded-player-label": "Wykluczony odtwarzacz", + "media-excluded-player-placeholder": "wpisz fragment i naciśnij +", + "media-frame-rate-description": "Wyższe wartości są płynniejsze, ale zużywają więcej zasobów.", + "media-frame-rate-label": "Liczba klatek (FPS)", + "media-primary-player-description": "Wpisz słowo kluczowe, aby zidentyfikować swój główny odtwarzacz.", + "media-primary-player-label": "Główny odtwarzacz", + "media-primary-player-placeholder": "np. spotify, vlc, mpv", + "media-scrolling-speed-description": "Czas w sekundach na przewinięcie tytułu od początku do końca.", + "media-scrolling-speed-label": "Prędkość przewijania", + "media-scrolling-title-description": "Włącz ciągłe przewijanie długich tytułów mediów.", + "media-scrolling-title-label": "Przewijany tytuł", + "media-title": "Odtwarzacze mediów", + "on-middle-clicked-description": "Komenda do wykonania po kliknięciu środkowym przyciskiem myszy.", + "panel-applications-empty": "Żadna aplikacja nie odtwarza obecnie dźwięku", + "spectrum-mirrored-description": "Odbij spektrum tak, aby niskie częstotliwości były w centrum. Gdy wyłączone, niskie częstotliwości są po lewej, a wysokie po prawej.", + "spectrum-mirrored-label": "Lustrzane spektrum", + "title": "Audio", + "visualizer-type-description": "Wybierz typ wizualizacji odtwarzania multimediów.", + "visualizer-type-label": "Typ wizualizacji", + "volumes-desc": "Dostosuj głośność i poziomy dźwięku.", + "volumes-feedback-sound-file-description": "Ścieżka do pliku dźwiękowego odtwarzanego podczas zmiany głośności.", + "volumes-feedback-sound-file-label": "Plik dźwiękowy informacji zwrotnej głośności", + "volumes-feedback-sound-file-select-title": "Wybierz plik dźwiękowy informacji zwrotnej o głośności", + "volumes-input-volume-description": "Poziom głośności wejścia mikrofonu.", + "volumes-mute-input-description": "Wycisz domyślne wejście audio (mikrofon).", + "volumes-mute-input-label": "Wycisz wejście audio", + "volumes-mute-output-description": "Wycisz główne wyjście audio systemu.", + "volumes-mute-output-label": "Wycisz wyjście audio", + "volumes-output-volume-description": "Poziom głośności całego systemu.", + "volumes-step-size-description": "Dostosuj skok zmiany głośności (kółko myszy, skróty klawiszowe).", + "volumes-step-size-label": "Skok głośności", + "volumes-volume-feedback-description": "Odtwarzaj dźwięk zwrotny podczas regulacji głośności.", + "volumes-volume-feedback-label": "Odtwórz dźwięk sprzężenia zwrotnego głośności", + "volumes-volume-overdrive-description": "Pozwól na zwiększenie głośności powyżej 100%. Może nie być obsługiwane przez każdy sprzęt.", + "volumes-volume-overdrive-label": "Zezwalaj na wzmocnienie głośności (overdrive)" + }, + "bar": { + "appearance-auto-hide-delay-description": "Czas, po którym Bar zniknie po opuszczeniu go przez mysz.", + "appearance-auto-hide-delay-label": "Opóźnienie ukrywania", + "appearance-auto-hide-exclusive-note": "Uwaga: Strefa wyłączna jest automatycznie wyłączana, gdy autoukrywanie jest aktywne.", + "appearance-auto-show-delay-description": "Czas, zanim pasek pojawi się, gdy mysz wejdzie na krawędź.", + "appearance-auto-show-delay-label": "Opóźnienie Wyświetlania", + "appearance-background-opacity-description": "Ustaw przezroczystość tła konkretnie dla paska.", + "appearance-background-opacity-label": "Krycie tła paska", + "appearance-capsule-color-description": "Wybierz kolor dla kapsuł paska, lub nie używaj żadnego dla domyślnego koloru powierzchni.", + "appearance-capsule-color-label": "Kolor kapsuły", + "appearance-capsule-opacity-description": "Ustaw poziom przezroczystości tła widżetów, gdy kapsuła jest widoczna.", + "appearance-capsule-opacity-label": "Przezroczystość kapsuły", + "appearance-content-padding-description": "Dostosuj odstęp między krawędziami paska a widżetami.", + "appearance-content-padding-label": "Odstęp treści", + "appearance-density-description": "Dostosuj odstępy paska dla kompaktowego lub przestronnego wyglądu.", + "appearance-density-label": "Zagęszczenie paska", + "appearance-desc": "Dostosuj wygląd i pozycję paska.", + "appearance-display-mode-description": "Wybierz, kiedy pasek jest widoczny.", + "appearance-enable-exclusion-zone-inset-description": "Zmniejsz strefę wykluczenia o 1 fizyczny piksel, aby okna przylegające idealnie zachodziły pod krawędź paska.", + "appearance-enable-exclusion-zone-inset-label": "Wewnętrzna strefa wykluczenia", + "appearance-floating-description": "Wyświetl pasek jako pływającą 'pigułkę'.", + "appearance-floating-label": "Pływający pasek", + "appearance-font-scale-description": "Dostosuj skalę rozmiaru czcionki dla tekstu wyświetlanego na pasku.", + "appearance-font-scale-label": "Skala czcionki", + "appearance-frame-radius": "Promień wewnętrzny", + "appearance-frame-settings-description": "Dostosuj grubość ramki i wewnętrzny promień narożników", + "appearance-frame-settings-label": "Ustawienia ramki", + "appearance-frame-thickness": "Grubość", + "appearance-hide-on-overview-description": "Ukryj pasek i zamknij panele, gdy aktywny jest przegląd kompozytora.", + "appearance-hide-on-overview-label": "Ukryj pasek w przeglądzie", + "appearance-margins-description": "Dostosuj marginesy wokół pływającego paska.", + "appearance-margins-horizontal": "Margines poziomy", + "appearance-margins-vertical": "Margines pionowy", + "appearance-outer-corners-description": "Wyświetl zaokrąglone narożniki na zewnątrz paska.", + "appearance-outer-corners-label": "Narożniki zewnętrzne", + "appearance-position-description": "Wybierz miejsce umieszczenia paska na ekranie.", + "appearance-position-label": "Pozycja paska", + "appearance-show-capsule-description": "Pokaż tła widżetów.", + "appearance-show-capsule-label": "Pokaż kapsułę", + "appearance-show-on-workspace-switch-description": "Automatycznie krótko wyświetlaj Bar, gdy zmieni się obszar roboczy.", + "appearance-show-on-workspace-switch-label": "Pokaż pasek przy zmianie obszaru roboczego", + "appearance-show-outline-description": "Wyświetla widoczne obramowanie wokół każdego widżetu.", + "appearance-show-outline-label": "Pokaż obramowanie widżetów", + "appearance-type-description": "Wybierz styl paska: Prosty, Pływający lub Obramowany.", + "appearance-type-label": "Typ paska", + "appearance-use-separate-opacity-description": "Umożliwia użycie oddzielnej wartości krycia dla tła paska.", + "appearance-use-separate-opacity-label": "Użyj osobnej przezroczystości słupków", + "appearance-widget-spacing-description": "Dostosuj odstępy między każdym widżetem na pasku.", + "appearance-widget-spacing-label": "Odstępy między widżetami", + "behavior-middle-click-command-description": "Polecenie do wykonania po środkowym kliknięciu.", + "behavior-middle-click-command-label": "Polecenie Środkowego Kliknięcia", + "behavior-middle-click-command-placeholder": "niri msg action przełącz przegląd", + "behavior-middle-click-description": "Wybierz, co robi środkowe kliknięcie na pustych obszarach Paska.", + "behavior-middle-click-follow-mouse-description": "Otwórz wybrany panel środkowego kliknięcia w pozycji kursora.", + "behavior-middle-click-follow-mouse-label": "Środkowe kliknięcie podąża za kursorem", + "behavior-middle-click-label": "Akcja środkowego kliknięcia Paska", + "behavior-right-click-command-description": "Polecenie do wykonania po kliknięciu prawym przyciskiem.", + "behavior-right-click-command-label": "Polecenie Prawego Przycisku Myszy", + "behavior-right-click-command-placeholder": "notify-send \"Prawy przycisk myszy\"", + "behavior-right-click-description": "Wybierz, co robi prawy przycisk myszy na pustych obszarach paska.", + "behavior-right-click-follow-mouse-description": "Otwórz wybrany panel prawego przycisku myszy w pozycji kursora.", + "behavior-right-click-follow-mouse-label": "Prawy przycisk myszy podąża za kursorem", + "behavior-right-click-label": "Akcja prawego przycisku myszy na Pasku", + "behavior-wheel-wrap-description": "Po włączeniu przewijanie kontynuuje się od ostatniego elementu do pierwszego.", + "behavior-wheel-wrap-label": "Cyklicznie", + "behavior-workspace-scroll-description": "Wybierz, co robi kółko myszy na pustych obszarach paska.", + "behavior-workspace-scroll-label": "Akcja kółka myszy Paska", + "behavior-workspace-scroll-option-content": "Treść", + "behavior-workspace-scroll-option-workspace": "Obszar roboczy", + "monitor-configure-widgets": "Skonfiguruj widżety", + "monitor-override-settings": "Zastąp ustawienia globalne", + "monitor-override-settings-description": "Użyj niestandardowych ustawień dla tego monitora.", + "monitor-reset-all": "Zresetuj wszystko", + "monitor-widgets-title": "Konfiguracja widgetu - {monitor}", + "monitors-desc": "Pokaż pasek na konkretnych monitorach. Domyślnie na wszystkich, jeśli żaden nie zostanie wybrany.", + "monitors-desc-new": "Skonfiguruj, które monitory wyświetlają Bar i dostosuj ustawienia dla każdego monitora.", + "monitors-title": "Wyświetlanie na monitorach", + "title": "Pasek", + "tray-blacklist-description": "Dodaj reguły wykluczenia zasobnika, obsługuje maski (*).", + "tray-blacklist-label": "Czarna lista", + "tray-blacklist-placeholder": "np. nm-applet, Fcitx*", + "tray-pin-application": "Przypnij aplikację", + "tray-unpin-application": "Odepnij aplikację", + "use-global-widgets": "Używaj globalnych widżetów", + "widgets-desc": "Przeciągnij widżety, aby zmienić ich kolejność. Użyj menu pod prawym przyciskiem, aby przenosić lub usuwać widżety.", + "widgets-title": "Pozycjonowanie widżetów" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Generuj schematy kolorów z tapety. Automatycznie wyodrębnia kolory, aby stworzyć spójny motyw.", + "color-source-use-wallpaper-colors-label": "Użyj kolorów tapety", + "dark-mode-mode-description": "Włącza automatyczne przełączanie między trybem jasnym a ciemnym.", + "dark-mode-mode-label": "Harmonogram trybu ciemnego", + "dark-mode-mode-manual": "Ręcznie", + "dark-mode-mode-off": "Wyłączony", + "dark-mode-switch-description": "Przełącza na ciemniejszy motyw dla łatwiejszego oglądania w nocy.", + "delete-error-description": "Nie udało się usunąć {scheme}", + "delete-error-title": "Usuwanie nieudane", + "delete-success-description": "Pomyślnie usunięto {scheme}", + "delete-success-title": "Schemat kolorów usunięty", + "download-button": "Pobierz więcej", + "download-downloading": "Pobieranie...", + "download-empty": "Brak dostępnych schematów kolorów", + "download-error-api-error": "Błąd API: {status}", + "download-error-description": "Nie udało się pobrać {scheme}", + "download-error-download-failed": "Pobieranie nieudane z kodem: {code}", + "download-error-invalid-response": "Nieprawidłowy format odpowiedzi API", + "download-error-no-files": "Nie znaleziono plików dla schematu", + "download-error-parse-failed": "Błąd przetwarzania odpowiedzi API: {error}", + "download-error-rate-limit": "Przekroczono limit zapytań GitHub API", + "download-error-title": "Pobieranie nieudane", + "download-fetching": "Pobieranie dostępnych schematów kolorów...", + "download-success-description": "Pomyślnie pobrano {scheme}", + "download-success-title": "Schemat kolorów pobrany", + "download-title": "Pobierz schematy kolorów", + "method-description": { + "content": "Schemat Material Design z ekstrakcją kolorów o wysokiej wierności, która ściśle odpowiada rzeczywistym kolorom treści źródłowej.", + "dysfunctional": "Jak Faithful, ale wybiera drugą najbardziej dominującą rodzinę kolorów jako główną.", + "faithful": "Próbuje pozostać blisko koloru źródłowego, jednocześnie generując harmonijną paletę.", + "fruit-salad": "Schemat Material Design, który tworzy radosną, żywą paletę z różnorodnymi odcieniami.", + "monochrome": "Schemat Material Design wykorzystujący jednobarwną skalę szarości z minimalną zawartością chromatyczną.", + "muted": "Generuje bardziej miękkie, odbarwione tony dla spokojnego i stonowanego wyglądu.", + "rainbow": "Schemat Material Design, które generuje kolory w całym spektrum, aby uzyskać maksymalną różnorodność kolorów.", + "tonal-spot": "Domyślny algorytm Material Design. Tworzy zbalansowane palety, wykorzystując tonalne wariacje koloru źródłowego.", + "vibrant": "Maksymalizuje nasycenie kolorów dla odważnych, przyciągających wzrok palet o żywych tonach." + }, + "predefined-desc": "Wybierz z kolekcji predefiniowanych schematów kolorów.", + "predefined-generate-templates-label": "Generuj szablony dla predefiniowanych schematów", + "predefined-title": "Predefiniowane schematy kolorów", + "sync-gsettings-description": "Dopasuj motyw systemowy do aktywnego wariantu jasnego lub ciemnego.", + "sync-gsettings-label": "Synchronizuj motyw systemowy", + "templates-desc": "Zastosuj kolory w aplikacjach zewnętrznych.", + "templates-filter-description": "Pokaż szablony z określonej kategorii.", + "templates-filter-label": "Filtruj według kategorii", + "templates-misc-description": "Twórz własne szablony.", + "templates-misc-label": "Zaawansowane", + "templates-misc-user-templates-description": "Włącz tylko jeśli wiesz co robisz — zapoznaj się z dokumentacją online.", + "templates-misc-user-templates-label": "Włącz szablony użytkownika", + "templates-none-detected": "Nie wykryto", + "templates-write-path": "Zapisuje: {filepath}", + "title": "Schemat kolorów", + "wallpaper-method-description": "Wybierz swoją ulubioną metodę generowania palety.", + "wallpaper-method-label": "Metoda generowania palety", + "wallpaper-monitor-source-description": "Wybierz monitor, który ma być używany do wydobywania kolorów tapety.", + "wallpaper-monitor-source-label": "Źródło generowania kolorów" + }, + "connections": { + "authentication-required": "Wymagana autentykacja", + "bluetooth-auto-connect-description": "Automatycznie łącz się z zaufanymi sparowanymi urządzeniami, gdy Bluetooth jest włączony.", + "bluetooth-auto-connect-label": "Automatyczne łączenie sparowanych urządzeń", + "bluetooth-devices-unnamed": "Nienazwane urządzenia nie są wyświetlane.", + "bluetooth-discoverable": "To urządzenie jest wykrywalne jako {hostName}, dopóki ta zakładka ustawień jest otwarta.", + "bluetooth-rssi-polling-description": "Okresowo próbkuje RSSI dla podłączonych urządzeń za pośrednictwem bluetoothctl. Może nie być dostępne dla wszystkich urządzeń; zużywa minimalne zasoby po włączeniu.", + "bluetooth-rssi-polling-interval-description": "Skonfiguruj, jak często aktualizować siłę sygnału dla podłączonych urządzeń.", + "bluetooth-rssi-polling-interval-label": "Interwał odpytywania", + "bluetooth-rssi-polling-label": "Odpytywanie sygnału Bluetooth", + "disable-discoverability-description": "Ukryj swoje urządzenie przed pobliskimi urządzeniami Bluetooth.", + "disable-discoverability-label": "Wyłącz widoczność urządzenia", + "hide-unnamed-devices-description": "Ukryj urządzenia, które pojawiają się tylko jako adresy Bluetooth.", + "hide-unnamed-devices-label": "Ukryj nienazwane urządzenia", + "pin-instructions": "Wprowadź kod PIN wyświetlony na Twoim urządzeniu.", + "title": "Połączenia", + "wifi-header-text": "Użyj Wi-Fi, aby zobaczyć dostępne sieci i połączyć się z internetem." + }, + "control-center": { + "cards-desc": "Dostosuj, które kontrolki pojawiają się w centrum sterowania i w jakiej kolejności.", + "desc": "Konfiguruj pozycjonowanie i zachowanie panelu centrum sterowania.", + "open-at-mouse-description": "Po kliknięciu prawym przyciskiem myszy na pusty obszar Bar, otwórz Centrum Sterowania blisko kursora. Gdy wyłączone, zamiast tego używane jest powyższe ustawienie pozycji.", + "open-at-mouse-label": "Otwórz w miejscu kursora po prawym kliknięciu", + "position-description": "Wybierz, gdzie pojawia się panel centrum sterowania po otwarciu.", + "shortcuts-custom-button-command-description": "Polecenie do wykonania po kliknięciu przycisku.", + "shortcuts-custom-button-enable-on-state-logic-description": "Włącz drugą ikonę i stan 'aktywny' na podstawie polecenia sprawdzającego.", + "shortcuts-custom-button-enable-on-state-logic-label": "Włącz logikę stanu 'on'", + "shortcuts-custom-button-icon-description": "Wybierz ikonę z biblioteki.", + "shortcuts-custom-button-on-clicked-label": "Polecenie lewego kliknięcia", + "shortcuts-custom-button-on-middle-clicked-description": "Polecenie do wykonania po kliknięciu środkowym przyciskiem myszy.", + "shortcuts-custom-button-on-state-command-description": "Polecenie sprawdzające, czy przycisk powinien być w stanie 'on'. Zwraca 0 dla on, inne dla off.", + "shortcuts-custom-button-on-state-command-label": "Polecenie sprawdzające stan 'on'", + "shortcuts-custom-button-on-state-icon-description": "Ikona przycisku, gdy jest on w stanie 'on'.", + "shortcuts-custom-button-on-state-icon-label": "Ikona stanu 'on'", + "shortcuts-custom-button-state-checks-add": "Dodaj sprawdzanie stanu", + "shortcuts-custom-button-state-checks-command": "Polecenie do wykonania dla tego sprawdzenia stanu", + "shortcuts-custom-button-state-checks-label": "Sprawdzanie stanu", + "shortcuts-custom-button-state-checks-remove": "Usuń", + "shortcuts-custom-button-tooltip-description": "Podpowiedź wyświetlana po najechaniu na przycisk.", + "shortcuts-custom-button-tooltip-label": "Podpowiedź", + "shortcuts-title": "Widżety skrótów", + "system-monitor-disk-path-description": "Wybierz, który punkt montowania dysku ma być monitorowany przez kartę monitora systemu w centrum sterowania.", + "system-monitor-disk-path-label": "Ścieżka dysku monitora systemu", + "title": "Centrum sterowania" + }, + "desktop-widgets": { + "clock-enabled-description": "Pokaż widżet zegara na pulpicie.", + "clock-enabled-label": "Włącz widżet zegara", + "clock-format-label": "Format", + "clock-height-description": "Wysokość widżetu zegara w pikselach.", + "clock-height-label": "Wysokość", + "clock-minimal-mode-description": "Użyj minimalistycznego zegara z czasem i datą.", + "clock-minimal-mode-label": "Tryb minimalistyczny", + "clock-rounded-corners-description": "Użyj zaokrąglonych rogów dla tła widżetu.", + "clock-rounded-corners-label": "Zaokrąglone rogi", + "clock-show-background-description": "Pokaż tło dla widżetu zegara.", + "clock-show-background-label": "Pokaż tło", + "clock-show-date-description": "Wyświetl aktualną datę poniżej godziny.", + "clock-show-date-label": "Pokaż datę", + "clock-show-seconds-description": "Wyświetl sekundy.", + "clock-show-seconds-label": "Pokaż sekundy", + "clock-style-analog": "Analogowy", + "clock-style-binary": "Binarny", + "clock-style-description": "Wybierz styl wyświetlania zegara.", + "clock-style-digital": "Cyfrowy", + "clock-style-label": "Styl zegara", + "clock-style-minimal": "Minimalistyczny", + "clock-width-description": "Szerokość widżetu zegara w pikselach.", + "cpu-intensive-note": "Widgety oznaczone ! zużywają więcej CPU i powinny być włączane z rozwagą.", + "edit-mode-button-label": "Wejdź w tryb edycji", + "edit-mode-controls-explanation": "Lewy przycisk myszy: Przesuń widżet.\nPrawy przycisk myszy: Zmień rozmiar widżetu.", + "edit-mode-description": "Włącz tryb edycji, aby przesuwać i zmieniać pozycję widżetów pulpitu. Gdy włączone, widżety pokazują ramkę przeciągania.", + "edit-mode-exit-button": "Wyjdź z trybu edycji", + "edit-mode-grid-snap-label": "Przyciągaj do siatki", + "edit-mode-grid-snap-scale-label": "Skala przyciągania do siatki", + "edit-mode-label": "Tryb edycji", + "enabled-description": "Włącz lub całkowicie wyłącz widżety pulpitu.", + "enabled-label": "Włącz widżety pulpitu", + "general-desc": "Konfiguruj widżety pojawiające się na pulpicie.", + "general-title": "Widżety pulpitu", + "media-player-enabled-description": "Pokaż widżet odtwarzacza mediów na pulpicie.", + "media-player-enabled-label": "Włącz widżet odtwarzacza", + "media-player-rounded-corners-description": "Włącz zaokrąglone rogi na krawędziach widżetu.", + "media-player-show-album-art-description": "Pokaż okładkę albumu i informacje o utworach (tytuł i wykonawca).", + "media-player-show-album-art-label": "Pokaż okładkę albumu i tytuł", + "media-player-show-background-description": "Pokaż tło dla widżetu odtwarzacza.", + "media-player-show-buttons-description": "Pokaż przyciski sterowania multimediami (odtwarzanie/pauza, poprzedni, następny) na widżecie odtwarzacza multimedialnego.", + "media-player-show-buttons-label": "Pokaż przyciski", + "media-player-show-visualizer-description": "Pokaż nakładkę wizualizatora audio.", + "media-player-visualizer-type-description": "Wybierz typ wizualizacji dla tła odtwarzacza pulpitu.", + "overview-enabled-description": "Pokaż widżety pulpitu, gdy przegląd kompozytora jest otwarty.", + "overview-enabled-label": "Pokaż w przeglądzie", + "system-stat-layout-bottom": "Dół", + "system-stat-layout-description": "Wybierz, jak legenda jest wyświetlana względem wykresu.", + "system-stat-layout-label": "Układ", + "system-stat-layout-side": "Strona", + "system-stat-rounded-corners-description": "Użyj zaokrąglonych rogów dla tła widżetu.", + "system-stat-rounded-corners-label": "Zaokrąglone Narożniki", + "system-stat-show-background-description": "Pokaż kontener tła dla widżetu statystyk systemowych.", + "system-stat-show-background-label": "Pokaż tło", + "system-stat-stat-type-description": "Wybierz, którą statystykę systemową wyświetlić.", + "system-stat-stat-type-label": "Typ Statystyki", + "title": "Widżety pulpitu", + "weather-enabled-description": "Pokaż widżet pogody na pulpicie.", + "weather-enabled-label": "Włącz widżet pogody", + "weather-show-background-description": "Pokaż tło dla widżetu pogody." + }, + "display": { + "monitors-backlight-device-auto-option": "Domyślny", + "monitors-backlight-device-description": "Wybierz urządzenie podświetlenia dla tego wyjścia.", + "monitors-backlight-device-label": "Urządzenie Podświetlenia", + "monitors-brightness-step-description": "Dostosuj skok zmiany jasności (kółko myszy i skróty klawiszowe).", + "monitors-brightness-step-label": "Skok jasności", + "monitors-brightness-unavailable-ddc-disabled": "Sterowanie jasnością niedostępne. Włącz \"Obsługa zewnętrznej jasności\", aby sterować jasnością tego ekranu.", + "monitors-brightness-unavailable-generic": "Sterowanie jasnością nie jest dostępne dla tego ekranu.", + "monitors-desc": "Dostosuj jasność dla każdego ekranu.", + "monitors-enforce-minimum-description": "Rozwiązuje problem całkowitego wyłączania podświetlenia na niektórych ekranach przy 0% jasności.", + "monitors-enforce-minimum-label": "Wymuś minimalną jasność (1%)", + "monitors-external-brightness-description": "Włącz obsługę DDCUtil dla sterowania jasnością zewnętrznych monitorów przez protokół DDC/CI.", + "monitors-external-brightness-label": "Obsługa zewnętrznej jasności", + "monitors-global-brightness-description": "Dostosuj jasność wszystkich monitorów jednocześnie.", + "monitors-global-brightness-label": "Wszystkie monitory", + "monitors-title": "Ustawienia per-monitor", + "night-light-auto-schedule-description": "Oparte na czasie zachodu i wschodu słońca w {location} — zalecane.", + "night-light-auto-schedule-label": "Automatyczny harmonogram", + "night-light-desc": "Zredukuj emisję niebieskiego światła, aby lepiej spać i mniej męczyć oczy.", + "night-light-enable-description": "Zastosuj ciepły filtr kolorów, aby zredukować emisję niebieskiego światła.", + "night-light-enable-label": "Włącz podświetlenie nocne", + "night-light-force-activation-description": "Ignoruje harmonogram i natychmiast stosuje filtr nocny.", + "night-light-force-activation-label": "Wymuś aktywację", + "night-light-manual-schedule-description": "Ustaw własne godziny wschodu i zachodu słońca.", + "night-light-manual-schedule-label": "Ręczny harmonogram", + "night-light-manual-schedule-select-start": "Wybierz czas rozpoczęcia", + "night-light-manual-schedule-select-stop": "Wybierz czas zakończenia", + "night-light-manual-schedule-sunrise": "Godzina wschodu", + "night-light-manual-schedule-sunset": "Godzina zachodu", + "night-light-temperature-day": "Dzień", + "night-light-temperature-day-description": "Kontroluje temperaturę barwową w ciągu dnia.", + "night-light-temperature-description": "Ustaw ciepło barw dla nocy i dnia.", + "night-light-temperature-label": "Temperatura barwowa", + "night-light-temperature-night": "Noc", + "night-light-temperature-night-description": "Kontroluje temperaturę barwową w nocy.", + "title": "Ekran" + }, + "dock": { + "appearance-background-opacity-description": "Dostosuj przezroczystość tła doku.", + "appearance-border-radius-description": "Dostosuj zaokrąglenie krawędzi doku.", + "appearance-border-radius-label": "Promień narożników", + "appearance-colorize-icons-description": "Zastosuj kolory motywu do ikon aplikacji w doku (tylko dla nieaktywnych aplikacji).", + "appearance-colorize-icons-label": "Koloryzuj ikony", + "appearance-dead-opacity-description": "Dostosuj przezroczystość ikon aplikacji, które nie są uruchomione.", + "appearance-dead-opacity-label": "Przezroczystość nieaktywnych", + "appearance-desc": "Dostosuj zachowanie i wygląd doku.", + "appearance-display-auto-hide": "Auto-ukrywanie", + "appearance-display-description": "Wybierz, jak ma zachowywać się dok.", + "appearance-display-exclusive": "Ekskluzywny", + "appearance-dock-indicator-description": "Pokaż mały wskaźnik, gdy dock jest ukryty.", + "appearance-dock-indicator-label": "Wskaźnik docka", + "appearance-floating-distance-description": "Ustaw odległość między dokiem a krawędzią ekranu.", + "appearance-floating-distance-label": "Odległość pływania doku", + "appearance-group-apps-description": "Grupuj wiele okien z tej samej aplikacji w jeden wpis Docka.", + "appearance-group-apps-label": "Grupuj te same aplikacje", + "appearance-group-click-action-cycle": "Przełączaj okna", + "appearance-group-click-action-description": "Wybierz, co robi lewe kliknięcie dla zgrupowanych aplikacji.", + "appearance-group-click-action-label": "Akcja kliknięcia zgrupowanej aplikacji", + "appearance-group-click-action-list": "Otwórz listę okien", + "appearance-group-context-menu-mode-description": "Wybierz, jak zachowuje się menu kontekstowe dla zgrupowanych aplikacji.", + "appearance-group-context-menu-mode-extended": "Rozszerzony", + "appearance-group-context-menu-mode-label": "Tryb pogrupowanego menu aplikacji", + "appearance-group-context-menu-mode-list": "Lista okien", + "appearance-group-indicator-style-description": "Wybierz, jak zgrupowane wskaźniki uruchomionych aplikacji wyświetlają stan aktywnego okna.", + "appearance-group-indicator-style-dots": "Kropki", + "appearance-group-indicator-style-label": "Styl zgrupowanego wskaźnika", + "appearance-group-indicator-style-number": "Numer", + "appearance-hide-show-speed-description": "Dostosuj prędkość animacji ukrywania/pokazywania doku.", + "appearance-hide-show-speed-label": "Prędkość ukrywania/pokazywania", + "appearance-icon-size-description": "Dostosuj ogólny rozmiar doku.", + "appearance-icon-size-label": "Rozmiar doku", + "appearance-inactive-indicators-description": "Wyświetlaj wskaźniki dla wszystkich uruchomionych aplikacji, nie tylko dla aktywnej.", + "appearance-inactive-indicators-label": "Wskaźniki uruchomionych", + "appearance-indicator-color-description": "Wybierz kolor indykatora ukrytego doku.", + "appearance-indicator-color-label": "Kolor indykatora", + "appearance-indicator-opacity-description": "Dostosuj przezroczystość indykatora ukrytego doku.", + "appearance-indicator-opacity-label": "Przezroczystość indykatora", + "appearance-indicator-thickness-description": "Włącz grubszy indykator ukrytego doku (6 px zamiast 3 px).", + "appearance-indicator-thickness-label": "Grubszy indykator", + "appearance-launcher-icon-description": "Wybierz ikonę używaną dla Dock Launchera.", + "appearance-launcher-icon-label": "Ikona Launchera", + "appearance-launcher-position-description": "Wybierz, gdzie ikona programu uruchamiającego pojawi się w docku.", + "appearance-launcher-position-end": "Koniec", + "appearance-launcher-position-label": "Pozycja uruchamiacza", + "appearance-launcher-position-start": "Start", + "appearance-launcher-use-distro-logo-description": "Użyj logo swojej dystrybucji jako ikony Dock Launcher.", + "appearance-launcher-use-distro-logo-label": "Użyj logo dystrybucji zamiast ikony", + "appearance-pinned-static-description": "Zawsze spychaj przypięte aplikacje na lewo w stałej kolejności.", + "appearance-pinned-static-label": "Statyczne przypięte aplikacje", + "appearance-position-description": "Wybierz, gdzie dok ma się pojawiać na ekranie.", + "appearance-position-label": "Pozycja", + "appearance-show-launcher-icon-description": "Pokaż ikonę programu uruchamiającego aplikacje w docku.", + "appearance-show-launcher-icon-label": "Pokaż launcher aplikacji", + "appearance-sit-on-frame-description": "Wyrównaj dock wewnątrz ramki, zamiast umieszczać go na wierzchu.", + "appearance-sit-on-frame-label": "Dock znajduje się na ramce", + "appearance-type-attached": "Zadokowany", + "appearance-type-description": "Wybierz pomiędzy pływającą pigułką a statycznym paskiem przymocowanym do krawędzi.", + "appearance-type-floating": "Pływający", + "appearance-type-label": "Styl docka", + "enabled-description": "Pokaż lub całkowicie ukryj dok.", + "enabled-label": "Włącz dok", + "monitors-desc": "Pokaż dok na konkretnych monitorach. Domyślnie na wszystkich, jeśli żaden nie zostanie wybrany.", + "monitors-only-same-monitor-description": "Pokaż tylko aplikacje z monitora, na którym znajduje się dok.", + "monitors-only-same-monitor-label": "Tylko aplikacje z tego samego monitora", + "monitors-title": "Wyświetlanie na monitorach", + "title": "Dok" + }, + "general": { + "copy-settings": "Kopiuj ustawienia", + "fonts-default-description": "Główna czcionka używana w całym interfejsie.", + "fonts-default-label": "Czcionka domyślna", + "fonts-default-placeholder": "Wybierz czcionkę domyślną...", + "fonts-default-scale-description": "Zwiększ lub zmniejsz rozmiar standardowego tekstu.", + "fonts-default-scale-label": "Rozmiar czcionki domyślnej", + "fonts-default-search-placeholder": "Szukaj czcionki...", + "fonts-desc": "Wybierz czcionki używane w interfejsie.", + "fonts-monospace-description": "Czcionka o stałej szerokości używana do liczb i statystyk.", + "fonts-monospace-label": "Czcionka monospace", + "fonts-monospace-placeholder": "Wybierz czcionkę monospace...", + "fonts-monospace-scale-description": "Zwiększ lub zmniejsz rozmiar tekstu o stałej szerokości.", + "fonts-monospace-scale-label": "Rozmiar czcionki monospace", + "fonts-monospace-search-placeholder": "Szukaj czcionki monospace...", + "fonts-title": "Czcionki", + "keybinds-conflict-description": "Kombinacja klawiszy jest już przypisana do {action}.", + "keybinds-conflict-title": "Konflikt skrótów klawiszowych", + "keybinds-description": "Skonfiguruj globalne klawisze nawigacyjne dla paneli i launchera.", + "keybinds-down": "Przenieś w dół", + "keybinds-enter": "Potwierdź / Akcja", + "keybinds-escape": "Zamknij / Wstecz", + "keybinds-left": "Przesuń w lewo", + "keybinds-modifier-description": "Skróty muszą zawierać klawisz modyfikujący (Ctrl lub Alt).", + "keybinds-modifier-title": "Wymagany modyfikator", + "keybinds-remove": "Usuń / Skasuj", + "keybinds-right": "Przesuń w prawo", + "keybinds-title": "Skróty klawiszowe nawigacji", + "keybinds-up": "Przenieś w górę", + "language-desc": "Wybierz preferowany język aplikacji.", + "language-select-auto-detect": "Automatyczny", + "language-select-description": "Wybierz język używany w interfejsie aplikacji.", + "language-select-label": "Język aplikacji", + "launch-setup-wizard": "Kreator konfiguracji", + "profile-desc": "Edytuj dane użytkownika i awatar.", + "profile-picture-description": "Twoje zdjęcie profilowe pojawiające się w interfejsie.", + "profile-picture-label": "Zdjęcie profilowe", + "profile-select-avatar": "Wybierz obraz awatara", + "profile-title": "Profil", + "profile-tooltip": "Zdjęcie profilowe", + "reverse-scrolling-description": "Odwróć zinterpretowany kierunek przewijania.", + "reverse-scrolling-label": "Odwróć przewijanie", + "screen-corners-desc": "Dostosuj zaokrąglenia krawędzi ekranu i efekty wizualne.", + "screen-corners-radius-description": "Dostosuj zaokrąglenie narożników ekranu.", + "screen-corners-radius-label": "Promień narożników ekranu", + "screen-corners-radius-reset": "Resetuj promień narożników", + "screen-corners-show-corners-description": "Wyświetlaj zaokrąglone narożniki na krawędzi ekranu.", + "screen-corners-show-corners-label": "Pokaż narożniki ekranu", + "screen-corners-solid-black-description": "Użyj jednolitej czerni zamiast koloru tła paska.", + "screen-corners-solid-black-label": "Jednolicie czarne narożniki", + "screen-corners-title": "Narożniki ekranu", + "settings-copied": "Ustawienia skopiowane do schowka", + "smooth-scrolling-description": "Animuj przewijanie list, aby uzyskać płynniejsze wrażenia z kółka.", + "smooth-scrolling-label": "Płynne przewijanie", + "tab-basics": "Podstawy", + "tab-keybinds": "Skróty klawiszowe" + }, + "hooks": { + "color-generation-description": "Polecenie do wykonania po zakończeniu generowania kolorów (np. po motywowaniu na podstawie tapety).", + "color-generation-label": "Kolory wygenerowane", + "color-generation-placeholder": "np. notify-send \"Kolory\" \"Wygenerowano\"", + "info-command-info-description": "• Polecenia są wykonywane przez powłokę (sh -lc)
• Polecenia działają w tle (detached)
• Przyciski testowe wykonują się z aktualnymi wartościami", + "info-parameters-description": "• Wallpaper hook: $1 = ścieżka tapety, $2 = nazwa ekranu, $3 = motyw (ciemny/jasny)
• Theme toggle hook: $1 = true/false (stan trybu ciemnego)
• Screen lock/unlock hooks: $1 = lock/unlock (stan blokady ekranu)
• Performance mode hooks: Brak parametrów
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "Dostępne parametry", + "noctalia-started-description": "Polecenie do wykonania po zakończeniu ładowania Noctalii.", + "noctalia-started-label": "Noctalia uruchomiona", + "noctalia-started-placeholder": "np. notify-send 'Noctalia załadowany'", + "performance-mode-disabled-description": "Komenda do wykonania, gdy tryb wydajności Noctalia zostanie wyłączony.", + "performance-mode-disabled-label": "Tryb wydajności wyłączony", + "performance-mode-disabled-placeholder": "np. notify-send \"Wydajność\" \"Tryb wyłączony\"", + "performance-mode-enabled-description": "Komenda do wykonania, gdy tryb wydajności Noctalia jest włączony.", + "performance-mode-enabled-label": "Tryb wydajności włączony", + "performance-mode-enabled-placeholder": "np. notify-send \"Wydajność\" \"Tryb włączony\"", + "screen-lock-description": "Polecenie do wykonania, gdy ekran zostanie zablokowany.", + "screen-lock-label": "Ekran zablokowany", + "screen-lock-placeholder": "np. notify-send \"Ekran\" \"Zablokowany\"", + "screen-unlock-description": "Polecenie do wykonania, gdy ekran zostanie odblokowany.", + "screen-unlock-label": "Ekran odblokowany", + "screen-unlock-placeholder": "np. notify-send \"Ekran\" \"Odblokowany\"", + "session-description": "Polecenie do wykonania przed zamknięciem lub ponownym uruchomieniem. Otrzymuje typ akcji jako $1 (zamknięcie/ponowne uruchomienie).", + "session-label": "Koniec sesji", + "session-placeholder": "np. notify-send \"Session\" \"$1\"", + "system-hooks-desc": "Konfiguruj polecenia do wykonania podczas zdarzeń systemowych.", + "system-hooks-enable-description": "Włącz lub wyłącz wszystkie polecenia hook.", + "system-hooks-enable-label": "Włącz hooks", + "system-hooks-title": "Hooks systemowe", + "theme-changed-description": "Polecenie do wykonania podczas przełączania między trybem ciemnym a jasnym.", + "theme-changed-label": "Zmiana motywu", + "theme-changed-placeholder": "np. notify-send \"Motyw\" \"Przełączony\"", + "title": "Hooks", + "wallpaper-changed-description": "Polecenie do wykonania podczas zmiany tapety.", + "wallpaper-changed-label": "Zmiana tapety", + "wallpaper-changed-placeholder": "np. notify-send \"Tapeta\" \"Zmieniona\"" + }, + "idle": { + "custom-add": "Dodaj polecenie", + "custom-description": "Uruchom polecenie shella po okresie bezczynności.", + "custom-entry-command": "Polecenie", + "custom-entry-delete": "Usuń", + "custom-entry-edit": "Edytuj Niestandardowe Polecenie", + "custom-entry-name": "Nazwa", + "custom-entry-name-placeholder": "np. Wyłącz światła", + "custom-entry-new": "Nowe Niestandardowe Polecenie", + "custom-entry-timeout": "Czas bezczynności", + "custom-entry-timeout-format": "{count} sekunda", + "custom-entry-timeout-format-plural": "{count} sekund", + "custom-entry-unnamed": "Nienazwana komenda", + "custom-label": "Niestandardowe polecenia bezczynności", + "enable-description": "Automatycznie wyłącz ekran, zablokuj lub uśpij po okresie bezczynności.", + "enable-label": "Włącz zarządzanie bezczynnością", + "fade-duration-description": "Sekundy animacji wygaszania do czerni przed uruchomieniem każdej akcji. Każdy ruch myszy anuluje wygaszanie.", + "fade-duration-label": "Czas trwania zanikania", + "lock-description": "Sekundy bezczynności przed aktywacją ekranu blokady.", + "lock-label": "Zablokuj ekran", + "resume-command-label": "Wznów polecenie", + "screen-off-description": "Sekundy bezczynności, zanim monitory zostaną wyłączone.", + "screen-off-label": "Wyłącz ekran", + "status-description": "Czas bezczynności zgłoszony przez compositora.", + "status-label": "Czas bezczynności", + "suspend-description": "Sekundy bezczynności przed wstrzymaniem systemu.", + "tab-behavior": "Zachowanie", + "tab-custom": "Niestandardowy", + "timeouts-description": "Ustaw na 0, aby wyłączyć etap. Limity czasu są wstrzymane, gdy 'Nie usypiaj' jest aktywny.", + "timeouts-label": "Limity czasu", + "title": "Bezczynność", + "unavailable": "Natywne monitorowanie bezczynności nie jest dostępne na tym Compositorze." + }, + "indicator": { + "default-value": "Domyślnie: {value}", + "system-default": "Domyślne ustawienia systemowe" + }, + "launcher": { + "clipboard-desc": "Dostęp i zarządzanie historią schowka z poziomu launchera.", + "execute-desc": "Skonfiguruj sposób uruchamiania aplikacji.", + "execute-title": "Wykonanie", + "settings-annotation-tool-description": "Komenda do uruchomienia po kliknięciu przycisku adnotacji w historii schowka. Obraz zostanie przekazany do tej komendy.", + "settings-annotation-tool-label": "Narzędzie do adnotacji", + "settings-annotation-tool-placeholder": "np. 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Automatyczne wklejanie wybranego elementu ze schowka. Wymaga wtype.", + "settings-auto-paste-label": "Automatyczne wklejanie", + "settings-clip-chips-description": "Pokaż pasek zakładek, aby filtrować historię schowka według typu (Obrazy, Linki, Pliki, Kod itp.).", + "settings-clip-chips-label": "Włącz chipy kategorii", + "settings-clip-preview-description": "Pokaż podgląd zawartości schowka przy użyciu polecenia >clip.", + "settings-clip-preview-label": "Włącz podgląd schowka", + "settings-clip-smart-icons-description": "Pokazuj specyficzne ikony dla linków, plików, kolorów i kodu zamiast ogólnej ikony schowka.", + "settings-clip-smart-icons-label": "Włącz inteligentne ikony", + "settings-clip-wrap-text-description": "Zawijaj tekst na liście schowka zamiast go obcinać.", + "settings-clip-wrap-text-label": "Zawijaj tekst ze schowka", + "settings-clipboard-history-description": "Dostęp do wcześniej skopiowanych elementów z poziomu launchera.", + "settings-clipboard-history-label": "Włącz historię schowka", + "settings-clipboard-watch-image-description": "Pełny ciąg poleceń przekazywany do wl-paste dla zmian obrazu (wymaga ponownego uruchomienia).", + "settings-clipboard-watch-image-label": "Polecenie Monitorowania Obrazów", + "settings-clipboard-watch-text-description": "Pełny ciąg poleceń przekazywany do wl-paste dla zmian tekstu (wymaga ponownego uruchomienia).", + "settings-clipboard-watch-text-label": "Polecenie monitorowania tekstu", + "settings-custom-launch-prefix-description": "Poprzedzaj polecenia własnym prefiksem (np. 'runapp' dla integracji z systemd).", + "settings-custom-launch-prefix-enabled-description": "Używaj własnego prefiksu do uruchamiania aplikacji zamiast metody domyślnej.", + "settings-custom-launch-prefix-enabled-label": "Włącz własny prefiks", + "settings-custom-launch-prefix-label": "Własny prefiks uruchamiania", + "settings-density-description": "Dostosuj rozmiar ikon aplikacji i gęstość Launchera.", + "settings-density-label": "Gęstość", + "settings-desc": "Dostosuj zachowanie i wygląd launchera.", + "settings-enable-session-search-description": "Pokaż akcje sesji (blokowanie, wyłączanie, ponowne uruchamianie itp.) podczas wyszukiwania w Launcherze.", + "settings-enable-session-search-label": "Włącz wyszukiwanie sesji", + "settings-enable-settings-search-description": "Wyświetlaj wyniki ustawień podczas wyszukiwania w Launcherze.", + "settings-enable-settings-search-label": "Włącz wyszukiwanie w ustawieniach", + "settings-enable-windows-search-description": "Wyszukaj i skup aktywne okna.", + "settings-enable-windows-search-label": "Włącz wyszukiwanie okien", + "settings-grid-view-description": "Wyświetlaj elementy w układzie siatki zamiast listy.", + "settings-icon-mode-description": "Używaj natywnych ikon systemowych zamiast ikon Tabler.", + "settings-icon-mode-label": "Używaj natywnych ikon", + "settings-ignore-mouse-input-description": "Wyłącz interakcję myszy i kółko przewijania w launcherze.", + "settings-ignore-mouse-input-label": "Ignoruj dane wejściowe myszy", + "settings-overlay-layer-description": "Wyświetl program uruchamiający na warstwie nakładki, nad oknami pełnoekranowymi. Po włączeniu program uruchamiający nie zostanie przypięty do paska.", + "settings-overlay-layer-label": "Pokaż nad pełnym ekranem", + "settings-position-description": "Wybierz, gdzie pojawia się panel launchera.", + "settings-show-categories-description": "Pokaż karty kategorii do filtrowania aplikacji.", + "settings-show-categories-label": "Pokaż kategorie", + "settings-show-icon-background-description": "Pokaż zaokrąglone tło prostokątne za ikonami.", + "settings-show-icon-background-label": "Pokaż tło ikony", + "settings-sort-by-usage-description": "Gdy włączone, najczęściej uruchamiane aplikacje pojawiają się na początku listy.", + "settings-sort-by-usage-label": "Sortuj według użycia", + "settings-terminal-command-description": "Polecenie uruchamiające terminal. Np. 'kitty -e' lub 'gnome-terminal --'.", + "settings-terminal-command-label": "Polecenie terminala", + "settings-use-app2unit-description": "Używa alternatywnej metody uruchamiania, aby lepiej zarządzać procesami i zapobiegać problemom.", + "settings-use-app2unit-label": "Użyj App2Unit do uruchamiania aplikacji", + "settings-view-mode-description": "Wybierz układ dla wpisów programu uruchamiającego.", + "settings-view-mode-label": "Tryb widoku", + "title": "Launcher" + }, + "location": { + "auto-locate-description": "Automatycznie wykrywaj swoją lokalizację okresowo, używając adresu IP.", + "auto-locate-label": "Automatyczne Wykrywanie", + "calendar-cards-desc": "Organizuj i włączaj/wyłączaj karty w panelu kalendarza.", + "calendar-cards-title": "Karty kalendarza", + "calendar-header-label": "Nagłówek kalendarza", + "calendar-month-label": "Miesiąc kalendarza", + "date-time-12hour-format-description": "Wyświetlaj czas w formacie 12-godzinnym na ekranie blokady i w kalendarzu. Zegar paska ma własne ustawienia.", + "date-time-12hour-format-label": "Użyj formatu 12-godzinnego", + "date-time-desc": "Dostosuj sposób wyświetlania daty i czasu.", + "date-time-first-day-of-week-automatic": "Automatycznie (według systemu)", + "date-time-first-day-of-week-description": "Wybierz, który dzień zaczyna tydzień w kalendarzu.", + "date-time-first-day-of-week-label": "Pierwszy dzień tygodnia", + "date-time-show-events-description": "Wyświetlaj wydarzenia w panelu kalendarza.", + "date-time-show-events-label": "Pokaż wydarzenia kalendarza", + "date-time-title": "Data i czas", + "date-time-use-analog-description": "Pokaż zegar w stylu analogowym w oknie kalendarza.", + "date-time-use-analog-label": "Użyj zegara analogowego", + "date-time-week-numbers-description": "Wyświetlaj numer tygodnia w roku (np. tydzień 38) w kalendarzu.", + "date-time-week-numbers-label": "Pokaż numery tygodni", + "geolocate-now-button": "Zlokalizuj teraz", + "location-desc": "Ustaw lokalizację, aby otrzymywać dokładną pogodę i harmonogram podświetlenia nocnego.", + "location-search-description": "np. Warszawa, PL", + "location-search-label": "Szukaj lokalizacji", + "location-search-placeholder": "Wpisz nazwę lokalizacji", + "location-title": "Twoja lokalizacja", + "weather-desc": "Wybierz preferowaną jednostkę temperatury.", + "weather-enabled-description": "Włącz pobieranie danych pogodowych i ich wyświetlanie. Gdy wyłączone, wszystkie elementy pogody zostaną ukryte i nie będą wysyłane zapytania sieciowe.", + "weather-enabled-label": "Włącz pogodę", + "weather-fahrenheit-description": "Wyświetlaj temperaturę w stopniach Fahrenheita zamiast Celsjusza.", + "weather-fahrenheit-label": "Pokaż temperaturę w Fahrenheitach (°F)", + "weather-hide-city-description": "Ukryj nazwę miasta na wyświetlaczach pogody w całym interfejsie.", + "weather-hide-city-label": "Ukryj nazwę miasta", + "weather-hide-timezone-description": "Ukryj skrót strefy czasowej na wyświetlaczach pogody w całym interfejsie.", + "weather-hide-timezone-label": "Ukryj strefę czasową", + "weather-show-effects-description": "Pokaż dodatkowe efekty wizualne (deszcz, śnieg, błyskawice) na karcie pogody.", + "weather-show-effects-label": "Wyświetlaj efekty pogodowe", + "weather-show-in-calendar-description": "Pokaż dzienną prognozę pogody bezpośrednio w widoku kalendarza.", + "weather-show-in-calendar-label": "Wyświetlaj pogodę w kalendarzu", + "weather-talia-mascot-always-description": "Zawsze używaj Talia emojis zamiast tabler icons.", + "weather-talia-mascot-always-label": "Zawsze pokazuj emotikony pogody Talia" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "Gdy fprintd (uwierzytelnianie odciskiem palca) jest aktywne, ta opcja pozwala nadal logować się za pomocą hasła zamiast odcisku palca.", + "allow-password-with-fprintd-label": "Zezwól na logowanie hasłem za pomocą fprintd", + "auto-start-auth-description": "Automatycznie uruchamia uwierzytelnianie odciskiem palca bez konieczności naciskania klawisza lub klikania przycisku.", + "auto-start-auth-label": "Automatyczne uruchamianie uwierzytelniania", + "clock-format-description": "Dostosuj format zegara, używając tokenów składni daty/czasu.", + "clock-format-label": "Format zegara", + "clock-style-analog": "Analogowy", + "clock-style-custom": "Niestandardowy", + "clock-style-description": "Wybierz styl wizualny zegara na ekranie blokady.", + "clock-style-digital": "Cyfrowy", + "clock-style-label": "Styl Zegara", + "compact-lockscreen-description": "Pokaż tylko logowanie i kontrolki systemowe, ukrywając widżety pogody i mediów.", + "compact-lockscreen-label": "Kompaktowy ekran blokady", + "enable-lockscreen-media-controls-description": "Pokaż interaktywne elementy sterujące odtwarzaniem multimediów na ekranie blokady.", + "enable-lockscreen-media-controls-label": "Sterowanie Multimediami na Ekranie Blokady", + "lock-on-suspend-description": "Automatycznie blokuj ekran podczas uśpienia systemu.", + "lock-on-suspend-label": "Blokuj przy uśpieniu", + "lock-screen-animations-description": "Włącz lub wyłącz animacje ekranu blokady.", + "lock-screen-animations-label": "Animacje Ekranu Blokady", + "lock-screen-blur-strength-description": "Stosuje efekt rozmycia do tapety ekranu blokady.", + "lock-screen-blur-strength-label": "Siła rozmycia ekranu blokady", + "lock-screen-tint-strength-description": "Stosuje nakładkę z odcieniem do tapety ekranu blokady.", + "lock-screen-tint-strength-label": "Siła zabarwienia ekranu blokady", + "monitors-desc": "Pokaż ekran blokady na określonych monitorach. Domyślnie na wszystkich, jeśli żadne nie zostaną wybrane.", + "password-chars-description": "Urocze ikony używane do ukrywania hasła.", + "password-chars-label": "Losowe ikony haseł", + "show-hibernate-description": "Pokaż opcję 'hibernacja' w kontrolkach zasilania.", + "show-hibernate-label": "Pokaż hibernację", + "show-session-buttons-description": "Zezwalaj na dostęp do ustawień zasilania z poziomu ekranu blokady.", + "show-session-buttons-label": "Kontrolki zasilania", + "title": "Ekran blokady" + }, + "network": { + "bluetooth-description": "Włącz zarządzanie Bluetooth.", + "bluetooth-rssi-polling-description": "Okresowo próbkuje RSSI dla podłączonych urządzeń przez bluetoothctl. Może nie być dostępne dla wszystkich urządzeń; zużywa minimalne zasoby, gdy jest włączone.", + "bluetooth-rssi-polling-label": "Odpytywanie sygnału Bluetooth", + "desc": "Zarządzaj połączeniami Wi-Fi i Bluetooth.", + "wifi-description": "Zarządzanie sieciami bezprzewodowymi (wymaga NetworkManager)." + }, + "notifications": { + "duration-critical-urgency-description": "Jak długo widoczne są powiadomienia o priorytecie krytycznym.", + "duration-critical-urgency-label": "Pilność krytyczna", + "duration-desc": "Konfiguruj, jak długo powiadomienia pozostają widoczne w zależności od poziomu pilności.", + "duration-low-urgency-description": "Jak długo widoczne są powiadomienia o priorytecie niskim.", + "duration-low-urgency-label": "Pilność niska", + "duration-normal-urgency-description": "Jak długo widoczne są powiadomienia o priorytecie normalnym.", + "duration-normal-urgency-label": "Pilność normalna", + "duration-respect-expire-description": "Używaj czasu wygasania ustawionego w samym powiadomieniu.", + "duration-respect-expire-label": "Respektuj czas wygasania (expire timeout)", + "duration-title": "Czas trwania powiadomienia", + "history-clear-dismiss-description": "Wyczyść powiadomienie z historii po odrzuceniu.", + "history-clear-dismiss-label": "Wyczyść po odrzuceniu", + "history-critical-urgency-description": "Zapisuj powiadomienia o krytycznym priorytecie w historii.", + "history-critical-urgency-label": "Zachowaj krytyczną pilność dla historii", + "history-desc": "Kontroluj, które powiadomienia są zapisywane w historii na podstawie ich poziomu ważności.", + "history-low-urgency-description": "Zapisuj powiadomienia o niskim priorytecie w historii.", + "history-low-urgency-label": "Odeślij niską pilność do historii", + "history-normal-urgency-description": "Zapisuj powiadomienia o normalnym priorytecie w historii.", + "history-normal-urgency-label": "Zapisz normalny priorytet do historii", + "monitors-desc": "Pokazuj powiadomienia na konkretnych monitorach. Domyślnie na wszystkich.", + "rules-action-block": "Blok", + "rules-action-block-desc": "Całkowicie pomija.", + "rules-action-hide": "Ukryj", + "rules-action-hide-desc": "Bez wyskakującego okienka, bez dźwięku, dodaje do historii.", + "rules-action-label": "Akcja", + "rules-action-mute": "Wycisz", + "rules-action-mute-desc": "Brak dźwięku, nadal wyświetla wyskakujące okienko i w historii.", + "rules-add": "Dodaj regułę", + "rules-delete": "Usuń regułę", + "rules-description": "Dopasuj nazwę aplikacji lub zawartość. Reguły są sprawdzane po kolei, a pierwsze dopasowanie jest stosowane.", + "rules-edit": "Edytuj regułę", + "rules-label": "Reguły Filtrowania", + "rules-pattern-label": "Wzór", + "rules-pattern-placeholder": "firefox, discord, lub /regex/", + "rules-tab": "Zasady", + "settings-always-on-top-description": "Wyświetlaj powiadomienia nad oknami pełnoekranowymi i innymi warstwami.", + "settings-background-opacity-description": "Dostosuj przezroczystość tła powiadomień.", + "settings-density-description": "Wybierz gęstość karty powiadomień.", + "settings-density-label": "Gęstość", + "settings-desc": "Konfiguruj wygląd i zachowanie powiadomień.", + "settings-do-not-disturb-description": "Wyłącz wszystkie wyskakujące powiadomienia.", + "settings-enabled-description": "Włącz lub wyłącz demona powiadomień (wymaga restartu Noctalia).", + "settings-enabled-label": "Włącz powiadomienia", + "settings-location-description": "Miejsce na ekranie, w którym pojawiają się powiadomienia.", + "settings-markdown-description": "Renderuj treść powiadomień, używając formatowania Markdown.", + "settings-markdown-label": "Włącz Markdown", + "sounds-desc": "Konfiguruj efekty dźwiękowe i głośność powiadomień.", + "sounds-enabled-description": "Włącz efekty dźwiękowe dla przychodzących powiadomień.", + "sounds-enabled-label": "Włącz dźwięki powiadomień", + "sounds-excluded-apps-description": "Pomiń odtwarzanie dźwięku systemowego dla aplikacji, które mają własne wbudowane powiadomienia dźwiękowe.", + "sounds-excluded-apps-label": "Wykluczone aplikacje", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Ścieżka do pliku dźwiękowego dla powiadomień krytycznych.", + "sounds-files-critical-label": "Dźwięk pilności krytycznej", + "sounds-files-critical-select-title": "Wybierz dźwięk pilności krytycznej", + "sounds-files-desc": "Konfiguruj własne pliki dźwiękowe dla różnych poziomów pilności.", + "sounds-files-low-description": "Ścieżka do pliku dźwiękowego dla powiadomień o niskim priorytecie.", + "sounds-files-low-label": "Dźwięk pilności niskiej", + "sounds-files-low-select-title": "Wybierz dźwięk pilności niskiej", + "sounds-files-normal-description": "Ścieżka do pliku dźwiękowego dla powiadomień normalnych.", + "sounds-files-normal-label": "Dźwięk pilności normalnej", + "sounds-files-normal-select-title": "Wybierz dźwięk pilności normalnej", + "sounds-files-placeholder": "Wprowadź ścieżkę do pliku dźwiękowego", + "sounds-files-select-file": "Wybierz plik dźwiękowy", + "sounds-files-title": "Pliki dźwiękowe", + "sounds-files-unified-description": "Ścieżka do pliku dźwiękowego dla wszystkich powiadomień.", + "sounds-files-unified-label": "Dźwięk powiadomienia", + "sounds-files-unified-select-title": "Wybierz plik dźwięku powiadomienia", + "sounds-separate-description": "Używaj różnych dźwięków dla niskiej, normalnej i krytycznej pilności.", + "sounds-separate-label": "Różne dźwięki zależnie od priorytetu", + "sounds-title": "Ustawienia dźwięku", + "sounds-unavailable-description": "Zainstaluj Qt6 Multimedia, aby włączyć dźwięki powiadomień.", + "sounds-unavailable-label": "Dźwięki powiadomień niedostępne", + "sounds-volume-description": "Dostosuj poziom głośności dźwięków powiadomień.", + "sounds-volume-label": "Głośność dźwięku", + "toast-battery-description": "Wyświetl ostrzeżenie, gdy poziom baterii spadnie poniżej tego procentu.", + "toast-battery-label": "Ostrzeżenie o baterii", + "toast-desc": "Konfiguruj wygląd i zachowanie dymków powiadomień.", + "toast-keyboard-description": "Pokaż dymek (toast), gdy zmieni się układ klawiatury.", + "toast-keyboard-label": "Układ klawiatury", + "toast-media-description": "Wyświetl toast, gdy zmieni się stan odtwarzania multimediów.", + "toast-media-label": "Media" + }, + "osd": { + "always-on-top-description": "Wyświetlaj OSD nad oknami pełnoekranowymi i innymi warstwami.", + "always-on-top-label": "Zawsze na wierzchu", + "background-opacity-description": "Kontroluje przezroczystość tła OSD.", + "background-opacity-label": "Przezroczystość tła", + "description": "Konfiguruj wskaźniki ekranowe, takie jak nakładki głośności i jasności.", + "duration-auto-hide-description": "Dostosuj czas, po którym OSD zniknie.", + "duration-auto-hide-label": "Ukryj automatycznie po", + "duration-desc": "Jak długo OSD pozostaje widoczne przed automatycznym ukryciem.", + "duration-title": "Czas wygasania OSD", + "enabled-description": "Pokazuj zmiany głośności i jasności w czasie rzeczywistym.", + "enabled-label": "Włącz wyświetlanie ekranowe (OSD)", + "events-desc": "Wybierz, które zdarzenia mają wywoływać wyświetlanie ekranowe.", + "general-desc": "Skonfiguruj widoczność i zachowanie OSD.", + "location-description": "Miejsce na ekranie, w którym pojawia się OSD.", + "monitors-desc": "Pokazuj OSD na konkretnych monitorach. Domyślnie na wszystkich.", + "title": "Wyświetlanie ekranowe", + "types-brightness-description": "Pokazuj OSD podczas zmiany jasności ekranu.", + "types-brightness-label": "Jasność", + "types-custom-text-description": "Pokazuj OSD dla własnych wiadomości tekstowych z IPC.", + "types-custom-text-label": "Własny tekst", + "types-desc": "Wybierz zdarzenia, które wyzwalają OSD.", + "types-input-volume-description": "Pokazuj OSD podczas zmiany głośności mikrofonu.", + "types-input-volume-label": "Głośność wejściowa", + "types-lockkey-description": "Pokazuj OSD podczas przełączania Caps Lock, Num Lock lub Scroll Lock.", + "types-lockkey-label": "Klawisze blokady", + "types-media-description": "Wyświetlaj OSD, gdy zmieni się stan odtwarzania multimediów (odtwarzanie, pauza, pomijanie).", + "types-media-label": "Odtwarzanie multimediów", + "types-title": "Zdarzenia wyzwalające OSD", + "types-volume-description": "Pokazuj OSD podczas zmiany głośności wyjścia audio.", + "types-volume-label": "Głośność wyjściowa" + }, + "plugins": { + "auto-update": "Automatyczna aktualizacja wtyczek", + "auto-update-description": "Automatycznie aktualizuj wszystkie wtyczki po uruchomieniu shella.", + "available-description": "Przeglądaj i instaluj wtyczki ze skonfigurowanych źródeł.", + "available-label": "Dostępne wtyczki", + "available-no-plugins-description": "Sprawdź źródła wtyczek lub odśwież listę.", + "available-no-plugins-label": "Brak dostępnych wtyczek", + "check-for-updates": "Sprawdź aktualizacje", + "checking-for-updates": "Sprawdzanie aktualizacji...", + "collision-already-installed": "Ta wtyczka jest już zainstalowana", + "collision-custom-version-exists": "Niestandardowa wersja z \"{source}\" jest już zainstalowana", + "collision-official-version-exists": "Oficjalna wersja tej wtyczki jest już zainstalowana", + "development-disable": "Wyłącz tryb deweloperski dla tej wtyczki", + "development-enable": "Włącz tryb deweloperski dla tej wtyczki.", + "filter-downloaded": "Pobrane", + "filter-not-downloaded": "Niepobrane", + "filter-tags-description": "Filtruj wtyczki według kategorii lub statusu pobierania.", + "filter-tags-label": "Tagi", + "hot-reload-description": "Automatycznie przeładuj wtyczki przy zmianie ich plików. Przydatne przy tworzeniu wtyczek.", + "hot-reload-label": "Gorące przeładowanie (tryb deweloperski)", + "hot-reloaded": "Przeładowano wtyczkę: {name}", + "install-error": "Błąd instalacji: {error}", + "install-incompatible": "{plugin} wymaga Noctalii w wersji v{version} lub wyższej", + "install-success": "Pomyślnie zainstalowano {plugin}", + "installed-description": "Zarządzaj i konfiguruj lokalnie zainstalowane wtyczki.", + "installed-label": "Zainstalowane wtyczki", + "installed-no-plugins-description": "Zainstaluj wtyczki z sekcji \"Dostępne\".", + "installed-no-plugins-label": "Brak zainstalowanych wtyczek", + "installing": "Instalowanie {plugin}...", + "notify-updates": "Powiadomienia o aktualizacjach wtyczek", + "notify-updates-description": "Wyświetl wyskakujące powiadomienie, gdy dostępne są nowe aktualizacje wtyczek.", + "open-plugin-page": "Otwórz stronę wtyczki", + "open-plugins-tab": "Otwórz ustawienia wtyczek", + "plugin-settings-title": "Ustawienia {plugin}", + "refresh-refreshing": "Odświeżanie listy wtyczek...", + "refresh-tooltip": "Odśwież dostępne wtyczki", + "settings-error-not-loaded": "Wtyczka nie została załadowana", + "settings-saved": "Ustawienia wtyczki zapisane", + "settings-tooltip": "Ustawienia wtyczki", + "source-custom": "Źródło niestandardowe", + "sources-add-custom": "Dodaj własne repozytorium", + "sources-add-dialog-description": "Dodaj repozytorium GitHub jako źródło wtyczek.", + "sources-add-dialog-error": "Nie udało się dodać źródła wtyczek", + "sources-add-dialog-name": "Nazwa repozytorium", + "sources-add-dialog-name-placeholder": "Moje własne wtyczki", + "sources-add-dialog-success": "Źródło wtyczek dodane pomyślnie", + "sources-add-dialog-title": "Dodaj źródło wtyczek", + "sources-add-dialog-url": "Adres URL repozytorium", + "sources-description": "Zarządzaj repozytoriami wtyczek.", + "sources-placeholder": "Moje fajne repozytorium", + "sources-remove-tooltip": "Usuń źródło wtyczek", + "title": "Wtyczki", + "translations-reloaded": "Przeładowano tłumaczenia: {name}", + "uninstall-dialog-description": "Czy na pewno chcesz odinstalować {plugin}? Spowoduje to usunięcie wszystkich danych wtyczki.", + "uninstall-dialog-title": "Odinstaluj wtyczkę", + "uninstall-error": "Błąd odinstalowywania: {error}", + "uninstall-success": "Pomyślnie odinstalowano {plugin}", + "uninstalling": "Odinstalowywanie {plugin}...", + "update-all": "Aktualizuj wszystkie ({count})", + "update-all-success": "Wszystkie wtyczki zaktualizowane pomyślnie", + "update-available": "Dostępna nowa aktualizacja wtyczki", + "update-available-plural": "Dostępne nowe aktualizacje wtyczek ({count})", + "update-error": "Błąd aktualizacji wtyczki {plugin}: {error}", + "update-pending": "v{current} → v{new} (wymaga Noctalii v{required})", + "update-success": "Zaktualizowano {plugin} do v{version}", + "update-version": "v{current} → v{new}", + "updating": "Aktualizowanie {plugin}..." + }, + "region": { + "title": "Region" + }, + "session-menu": { + "countdown-duration-description": "Ustaw czas odliczania przed wykonaniem akcji zasilania.", + "countdown-duration-label": "Czas trwania odliczania", + "enable-countdown-description": "Pokaż minutnik odliczający przed wykonaniem akcji zasilania.", + "enable-countdown-label": "Włącz minutnik odliczania", + "entries-desc": "Dostosuj, które akcje zasilania pojawiają się w menu sesji i w jakiej kolejności.", + "entries-title": "Akcje zasilania", + "entry-settings-command-description": "Własne polecenie dla tej akcji. Zostaw puste, aby użyć domyślnego polecenia systemowego.", + "entry-settings-command-placeholder": "np. systemctl poweroff", + "entry-settings-default-command-lock": "Wewnętrzna blokada (brak polecenia)", + "entry-settings-default-command-logout": "Wewnętrzne wylogowanie (brak polecenia)", + "entry-settings-default-info-description": "Jeśli nie podano własnego polecenia, zostanie użyte domyślne polecenie systemowe.", + "entry-settings-default-info-label": "Domyślne polecenie", + "entry-settings-keybind-description": "Przypisz kombinację klawiszy, aby wywołać tę akcję, gdy menu sesji jest otwarte.", + "entry-settings-keybind-placeholder": "Naciśnij Nagraj, aby przypisać klawisz...", + "entry-settings-keybind-recording": "Nagrywanie... Naciśnij dowolną kombinację klawiszy", + "entry-settings-title": "Konfiguruj {entry}", + "entry-settings-tooltip": "Konfiguruj polecenie", + "general-desc": "Konfiguruj zachowanie i wygląd panelu menu sesji.", + "large-buttons-layout-description": "Wybierz sposób wyświetlania przycisków menu sesji.", + "large-buttons-layout-label": "Układ z dużymi przyciskami", + "large-buttons-style-description": "Wyświetlaj menu sesji z dużymi przyciskami w układzie siatki.", + "large-buttons-style-label": "Styl dużych przycisków", + "position-description": "Wybierz, gdzie pojawia się panel menu sesji po otwarciu.", + "show-header-description": "Wyświetlaj tytuł i przycisk zamknięcia na górze menu sesji.", + "show-header-label": "Pokaż nagłówek", + "show-keybinds-description": "Wyświetl podpowiedzi skrótów klawiszowych w opcjach sesji.", + "show-keybinds-label": "Pokaż skróty klawiszowe" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Zachowaj widoczność widżetów pulpitu, gdy tryb wydajności Noctalia jest włączony.", + "noctalia-performance-disable-desktop-widgets-label": "Włącz widżety pulpitu w trybie wydajności", + "noctalia-performance-disable-wallpaper-description": "Zachowaj widoczne tapety pulpitu, przeglądu i ekranu blokady, gdy tryb wydajności Noctalia jest włączony.", + "noctalia-performance-disable-wallpaper-label": "Włącz renderowanie tapety w trybie wydajności", + "title": "System" + }, + "system-monitor": { + "critical-color-label": "Kolor krytyczny", + "custom-highlight-colors-title-label": "Własne kolory podświetlenia", + "disk-available-label": "Dostępne miejsce na dysku", + "disk-section-label": "Użycie dysku", + "enable-dgpu-monitoring-description": "Ostrzeżenie: To obudzi twoją dedykowaną kartę graficzną (NVIDIA/AMD), co może znacząco wpłynąć na żywotność baterii w laptopach z hybrydową grafiką.", + "enable-dgpu-monitoring-label": "Włącz monitorowanie dedykowanej karty graficznej", + "external-monitor-description": "Wprowadź polecenie lub ścieżkę aplikacji do uruchomienia przy aktywacji zewnętrznej aplikacji monitora systemu.", + "external-monitor-label": "Polecenie zewnętrznego monitora systemu", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "Konfiguruj zachowanie i wygląd monitora systemu.", + "gpu-section-label": "Temperatura GPU", + "highlight-colors-section-label": "Kolory podświetlenia", + "polling-interval-label": "Interwał odświeżania", + "polling-section-description": "Skonfiguruj, jak często każda metryka systemowa jest aktualizowana.", + "polling-section-label": "Interwały odpytywania", + "threshold-critical": "Krytyczny", + "threshold-warning": "Ostrzeżenie", + "thresholds-section-description": "Dostosuj progi ostrzegawcze/krytyczne oraz interwały odświeżania dla każdej metryki systemu.", + "use-custom-highlight-colors-description": "Gdy wyłączone, używane są domyślne kolory podświetlenia motywu.", + "use-custom-highlight-colors-label": "Użyj własnych kolorów podświetlenia", + "warning-color-label": "Kolor ostrzegawczy" + }, + "user-interface": { + "allow-panels-without-bar-description": "Gdy włączone, panele mogą otwierać się na dowolnym ekranie. Gdy wyłączone, panele otworzą się tylko na ekranach z paskiem, co może zmniejszyć zużycie pamięci.", + "allow-panels-without-bar-label": "Zezwalaj na panele na ekranach bez paska", + "animation-disable-description": "Wyłącz wszystkie animacje dla szybszego i bardziej responsywnego działania.", + "animation-disable-label": "Wyłącz animacje UI", + "animation-speed-description": "Dostosuj globalną prędkość animacji.", + "animation-speed-label": "Prędkość animacji", + "animation-speed-reset": "Resetuj prędkość animacji", + "appearance-desc": "Dostosuj elementy wizualne, takie jak podpowiedzi, obramowania i cienie.", + "blur-behind-description": "Rozmywa obszar za paskami i panelami, używając protokołu rozmycia kompozytora.", + "blur-behind-label": "Rozmycie tła", + "box-border-description": "Wyświetla obrys wokół obszarów zawartości.", + "box-border-label": "Obrys kontenera", + "box-border-radius-description": "Dostosowuje zaokrąglenie narożników głównych sekcji układu, takich jak paski boczne, karty i panele treści.", + "box-border-radius-label": "Promień kontenerów", + "control-border-radius-description": "Kontroluje zaokrąglenie elementów interaktywnych, w tym przycisków, przełączników i pól tekstowych.", + "control-border-radius-label": "Promień wejść", + "desc": "Dostosuj wygląd i zachowanie interfejsu.", + "dim-desktop-description": "Przyciemniaj pulpit, gdy panele lub menu są otwarte.", + "dim-desktop-label": "Przyciemniaj pulpit", + "dimmer-opacity-description": "Ustaw poziom przezroczystości przyciemnienia pulpitu.", + "dimmer-opacity-label": "Przezroczystość przyciemnienia", + "panel-background-opacity-description": "Ustaw przezroczystość tła dla wszystkich paneli (pasek, launcher, ustawienia itp.).", + "panel-background-opacity-label": "Przezroczystość tła paneli", + "panels-attached-to-bar-description": "Panele przypinają się do paska i krawędzi ekranu, tworząc spójny wygląd ze stylowymi odwróconymi narożnikami.", + "panels-attached-to-bar-label": "Przyciągaj panele do krawędzi", + "panels-overlay-description": "Zapewnia, że panele i pasek pozostają widoczne nawet nad aplikacjami pełnoekranowymi.", + "panels-overlay-label": "Trzymaj panele i pasek na wierzchu", + "scaling-description": "Zmienia rozmiar ogólnego interfejsu użytkownika, z wyłączeniem paska.", + "scaling-label": "Skalowanie interfejsu", + "scrollbar-always-visible-description": "Paski przewijania zawsze widoczne, gdy zawartość jest przewijalna, zamiast pokazywać je tylko po najechaniu myszą.", + "scrollbar-always-visible-label": "Zawsze pokazuj paski przewijania", + "settings-panel-header": "Panel Ustawień", + "settings-panel-mode-description": "Wybierz układ ustawień (może wymagać ponownego otwarcia).", + "settings-panel-mode-label": "Tryb panelu ustawień", + "settings-panel-sidebar-card-style-description": "Otacza pasek boczny ustawień wypełnionym tłem z zaokrąglonymi rogami.", + "settings-panel-sidebar-card-style-label": "Styl kart paska bocznego", + "shadows-description": "Włącza cienie pod paskami i panelami.", + "shadows-direction-description": "Wybierz, gdzie rzucany jest cień.", + "shadows-direction-label": "Kierunek cienia", + "shadows-label": "Cienie", + "title": "Interfejs użytkownika", + "tooltips-description": "Włącz lub wyłącz podpowiedzi w całym interfejsie.", + "tooltips-label": "Pokaż podpowiedzi", + "translucent-widgets-description": "Spraw, aby przyciski, zakładki i inne widżety w panelach były półprzezroczyste.", + "translucent-widgets-label": "Półprzezroczyste widżety" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Alfabetyczny", + "automation-change-mode-description": "Wybierz, jak wybierane są tapety podczas automatycznej zmiany.", + "automation-change-mode-label": "Tryb zmiany", + "automation-custom-interval-description": "Wprowadź czas jako GG:MM (np. 01:30).", + "automation-custom-interval-label": "Niestandardowy interwał", + "automation-interval-description": "Jak często automatycznie zmieniać tapety.", + "automation-interval-label": "Interwał tapet", + "automation-random-wallpaper-description": "Harmonogram losowych zmian tapet w regularnych odstępach czasu.", + "automation-scheduled-change-description": "Automatycznie zmieniaj tapety w regularnych odstępach czasu.", + "automation-scheduled-change-label": "Zaplanowana zmiana", + "look-feel-edge-smoothness-description": "Stosuje efekt miękkiego wtapiania na krawędziach przejść.", + "look-feel-edge-smoothness-label": "Zmiękcz krawędzie przejścia", + "look-feel-fill-color-description": "Wybierz kolor wypełnienia, który może pojawić się pod tapetą.", + "look-feel-fill-mode-description": "Wybierz, jak obraz ma się skalować, aby pasować do rozdzielczości monitora.", + "look-feel-fill-mode-label": "Tryb wypełnienia", + "look-feel-skip-startup-transition-description": "Pomiń animację tapety przy starcie powłoki.", + "look-feel-skip-startup-transition-label": "Pomiń animację startową", + "look-feel-title": "Wygląd i styl", + "look-feel-transition-duration-description": "Czas trwania animacji przejścia w sekundach.", + "look-feel-transition-duration-label": "Czas trwania przejścia", + "look-feel-transition-type-description": "Typ animacji podczas przełączania tapet.", + "look-feel-transition-type-label": "Typ przejścia", + "settings-clear-cache-button": "Wyczyść pamięć podręczną", + "settings-clear-cache-description": "Wyczyść buforowane, przeskalowane tapety, aby zwolnić miejsce na dysku.", + "settings-clear-cache-label": "Pamięć podręczna tapet", + "settings-clear-cache-toast": "Pamięć podręczna tapet wyczyszczona", + "settings-desc": "Kontroluj sposób zarządzania i wyświetlania tapet.", + "settings-enable-management-description": "Zarządzaj tapetami za pomocą Noctalia. Odznacz, jeśli wolisz używać innej aplikacji.", + "settings-enable-management-label": "Włącz zarządzanie tapetami", + "settings-enable-overview-description": "Stosuje rozmytą i przyciemnioną tapetę na ekranie podglądu (overview).", + "settings-enable-overview-label": "Włącz tapetę podglądu", + "settings-folder-description": "Ścieżka do głównego folderu z tapetami.", + "settings-folder-label": "Folder tapet", + "settings-hide-wallpaper-filenames-tooltip-hide": "Ukryj nazwy plików", + "settings-hide-wallpaper-filenames-tooltip-show": "Pokaż nazwy plików", + "settings-monitor-specific-description": "Ustaw inny folder tapet dla każdego monitora.", + "settings-monitor-specific-label": "Katalogi per-monitor", + "settings-monitor-specific-tooltip": "Folder tapet monitora", + "settings-overview-blur-strength-description": "Stosuje siłę rozmycia do przeglądu.", + "settings-overview-blur-strength-label": "Siła rozmycia przeglądu", + "settings-overview-tint-description": "Stosuje siłę odcienia do przeglądu.", + "settings-overview-tint-label": "Siła zabarwienia przeglądu", + "settings-recursive-search-description": "Szukaj tapet również w podfolderach katalogu głównego.", + "settings-recursive-search-label": "Przeszukuj podfoldery", + "settings-select-monitor-folder": "Wybierz folder tapet monitora", + "settings-selector-description": "Wybierz swoją tapetę.", + "settings-selector-position-description": "Wybierz, gdzie pojawia się panel wyboru tapety.", + "settings-show-hidden-files-tooltip-hide": "Ukryj ukryte pliki", + "settings-show-hidden-files-tooltip-show": "Pokaż ukryte pliki", + "settings-title": "Ustawienia tapet", + "settings-use-original-images-description": "Pomiń zmianę rozmiaru tapet przed wyświetleniem. Oszczędza miejsce na dysku i zmniejsza zużycie CPU podczas zmiany tapet, ale może zużywać więcej pamięci dla bardzo dużych obrazów.", + "settings-use-original-images-label": "Użyj oryginalnych obrazów", + "settings-view-mode-description": "Wybierz sposób wyświetlania tapet z Twojego katalogu.", + "settings-view-mode-label": "Tryb wyświetlania", + "view-mode-browse": "Przeglądaj katalogi", + "view-mode-cycle-tooltip": "Tryb widoku: {mode} (kliknij, aby zmienić)", + "view-mode-recursive": "Spłaszczone podkatalogi", + "view-mode-single": "Katalog główny" + } + }, + "placeholders": { + "add-new-keybind": "Dodaj nowy skrót klawiszowy", + "command-example": "echo \"Witaj świecie\"", + "enter-command": "Wpisz polecenie do wykonania (aplikacja lub skrypt)", + "enter-ipc-identifier": "Wprowadź unikalny identyfikator dla poleceń IPC", + "enter-path": "Wprowadź ścieżkę...", + "enter-text-to-collapse": "np. 'nic nie jest odtwarzane'. Użyj /regex/ dla wzorców.", + "enter-tooltip": "Wpisz tekst podpowiedzi", + "enter-width-pixels": "Wpisz szerokość w pikselach", + "keybind-recording": "Nagrywanie skrótu klawiszowego...", + "search": "Szukaj...", + "search-icons": "np. noctalia, niri, battery, cloud", + "search-launcher": "Szukaj wpisów... lub użyj > dla poleceń", + "search-wallhaven": "Szukaj w Wallhaven...", + "search-wallpapers": "Wpisz, aby filtrować tapety..." + }, + "positions": { + "bottom": "Dół", + "bottom-center": "Dół środek", + "bottom-left": "Dół lewo", + "bottom-right": "Dół prawo", + "center": "Środek", + "center-left": "Środek lewo", + "center-right": "Środek prawo", + "close-to-bar": "Blisko przycisku paska", + "follow-bar": "Podążaj za paskiem", + "left": "Lewo", + "right": "Prawo", + "top": "Góra", + "top-center": "Góra środek", + "top-left": "Góra lewo", + "top-right": "Góra prawo" + }, + "session-menu": { + "action-in-seconds": "{action} za {seconds} sekund...", + "cancel-timer": "Anuluj timer", + "title": "Menu sesji" + }, + "setup": { + "all-done": "Gotowe!", + "appearance": { + "subheader": "Wybierz tryb ciemny i źródła kolorów (tapeta lub predefiniowane)." + }, + "customize": { + "header": "Dostosuj swoje doświadczenie", + "subheader": "Dostosuj pozycję paska, zagęszczenie, skalowanie i inne." + }, + "skip-setup": "Pomiń konfigurację", + "telemetry-wizard-done": "Rozumiem!", + "telemetry-wizard-note": "Masz kontrolę — możesz to włączyć lub wyłączyć w dowolnym momencie w ustawieniach", + "telemetry-wizard-subtitle": "Dodaliśmy anonimowe analizy, aby pomóc w ulepszaniu Noctalia", + "telemetry-wizard-title": "Aktualizacja prywatności", + "wallpaper": { + "choose-dir": "Wybierz katalog zawierający obrazy tapet", + "dir-browse": "Przeglądaj folder tapet", + "dir-description": "Wybierz folder zawierający twoje tapety.", + "dir-label": "Katalog tapet", + "dir-select-title": "Wybierz folder tapet", + "header": "Wybierz tapetę", + "no-dir": "Nie wybrano katalogu tapet", + "no-valid": "Nie znaleziono prawidłowych plików obrazów w: {dir}", + "none-in-dir": "Nie znaleziono tapet w katalogu", + "preview-error": "Błąd ładowania obrazu", + "select-prompt": "Wybierz tapetę poniżej", + "subheader": "Ustaw nastrój dzięki pięknemu tłu." + }, + "welcome": "Witaj", + "welcome-note": "Tylko kilka podstaw na początek — pełne opcje znajdują się w ustawieniach", + "welcome-subtitle": "Sprawmy, aby Twój pulpit był wyjątkowy", + "welcome-title": "Witamy w Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "Nie wykryto odtwarzacza mediów", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "Nieznana aplikacja", + "uptime": "Czas pracy: {uptime}", + "user-requested": "Na prośbę użytkownika", + "welcome-back": "Witaj z powrotem,", + "widget-settings-title": "Ustawienia {widget}" + }, + "system-monitor": { + "core-usage": "Użycie rdzenia {id}", + "cpu-temp": "Temperatura CPU", + "cpu-usage": "Użycie CPU", + "disk": "Dysk", + "download-speed": "Prędkość pobierania", + "gpu-temp": "Temperatura GPU", + "load-average": "Śr. obciążenie", + "title": "Monitor systemu", + "upload-speed": "Prędkość wysyłania" + }, + "toast": { + "airplane-mode": { + "description": "Wyłącz wszystkie komunikacje bezprzewodowe.", + "title": "Tryb samolotowy" + }, + "battery": { + "critical": "Krytyczny poziom baterii", + "critical-desc": "Bateria naładowana w {percent}% — proszę natychmiast podłączyć ładowarkę", + "low": "Słaba bateria", + "low-desc": "Bateria naładowana w {percent}% — proszę podłączyć ładowarkę" + }, + "bluetooth": { + "address-copied": "Adres skopiowany do schowka", + "auto-connect-disabled": "Automatyczne łączenie wyłączone", + "auto-connect-enabled": "Automatyczne łączenie włączone", + "auto-connecting": "Łączenie z {count} urządzeniem/urządzeniami...", + "confirm-code": "Potwierdź kod {value} na drugim urządzeniu.", + "connect-failed": "Nie udało się połączyć z urządzeniem", + "disconnect-failed": "Nie udało się rozłączyć z urządzeniem", + "discoverable-change-failed": "Nie udało się zmienić stanu wykrywalności", + "discoverable-disabled": "Wykrywalność wyłączona", + "discoverable-enabled": "Wykrywalność włączona", + "display-code": "Wprowadź kod {value} na drugim urządzeniu.", + "forget-failed": "Nie udało się zapomnieć urządzenia", + "pair-failed": "Nie udało się sparować urządzenia", + "passkey-required": "Wymagany klucz dostępu przez urządzenie.", + "pincode-required": "Kod PIN wymagany przez urządzenie.", + "state-change-failed": "Nie udało się zmienić stanu Bluetooth" + }, + "clipboard": { + "long-text": "Długi tekst", + "unavailable": "Historia schowka niedostępna", + "unavailable-desc": "Aplikacja 'cliphist' nie jest zainstalowana. Zainstaluj ją, aby korzystać z historii schowka" + }, + "custom-command-failed": { + "description": "Polecenie nie powiodło się: {command}\\nKod wyjścia: {code}", + "title": "Niestandardowe polecenie nie powiodło się" + }, + "do-not-disturb": { + "disabled": "Nie przeszkadzać wyłączone", + "disabled-desc": "Pokazywanie wszystkich powiadomień", + "enabled": "Nie przeszkadzać włączone", + "enabled-desc": "Powiadomienia znajdziesz w swojej historii" + }, + "donation-opened": "Strona darowizny otwarta w Twojej przeglądarce", + "internet-limited": "Połączono bez dostępu do internetu", + "keyboard-layout": { + "changed": "Układ klawiatury zmieniony na {layout}", + "title": "Klawiatura" + }, + "missing-control-center": { + "description": "Widżet centrum sterowania został usunięty z paska. Aby mieć do niego dostęp, musisz go dodać ponownie. Możesz go również otworzyć, klikając prawym przyciskiem na pasek", + "label": "Ostatni widżet centrum sterowania usunięty" + }, + "night-light": { + "forced": "Wymuszona aktywacja", + "normal": "Tryb normalny", + "not-installed": "wlsunset nie jest zainstalowany" + }, + "noctalia-performance": { + "disabled": "Tryb wydajności wyłączony", + "enabled": "Tryb wydajności włączony", + "label": "Wydajność Noctalia" + }, + "power-profile": { + "changed": "Profil zasilania zmieniony", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "Napotkano błąd podczas przetwarzania szablonów", + "title": "Przetwarzanie szablonu nie powiodło się" + }, + "vpn": { + "connected": "Połączono z '{name}'", + "disconnected": "Rozłączono z '{name}'" + }, + "wallpaper-colors": { + "disabled": "Kolory tapety wyłączone", + "enabled": "Kolory tapety włączone", + "label": "Generowanie kolorów tapety" + }, + "wifi": { + "connected": "Połączono z '{ssid}'", + "connection-failed": "Połączenie nieudane", + "connection-timeout": "Przekroczenie czasu połączenia", + "disconnected": "Rozłączono z '{ssid}'", + "incorrect-password": "Nieprawidłowe hasło", + "network-not-found": "Nie znaleziono sieci" + } + }, + "tooltips": { + "add-widget": "Dodaj widżet", + "auto-connect": "Przełącz automatyczne łączenie dla tego urządzenia", + "bluetooth-auto-connect-off": "Automatyczne łączenie wyłączone", + "bluetooth-auto-connect-on": "Automatyczne łączenie włączone", + "bluetooth-devices": "Urządzenia Bluetooth", + "brightness-at": "Jasność: {brightness}%", + "click-to-start-recording": "Nagrywanie ekranu (start)", + "click-to-stop-recording": "Nagrywanie ekranu (stop)", + "collapse": "Zwiń pasek boczny", + "copy-address": "Kopiuj adres", + "delete-notification": "Usuń powiadomienie", + "dismiss-notification": "Odrzuć powiadomienie", + "do-not-disturb-enabled": "Nie przeszkadzać", + "expand": "Rozwiń pasek boczny", + "forget-network": "Zapomnij sieć", + "grid-view": "Widok siatki", + "hidden-files-hide": "Ukryte pliki", + "hide-unnamed-devices": "Ukryj urządzenia bez nazwy", + "home": "Katalog domowy", + "input-muted": "Przełącz wyciszenie wejścia", + "keep-awake": "Nie usypiaj", + "keyboard-layout": "Układ klawiatury {layout}", + "list-view": "Widok listy", + "manage-vpn": "Połączenia VPN", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "Osiągnięto maksymalną liczbę widżetów", + "microphone-volume-at": "Głośność mikrofonu: {volume}%", + "move-to-section": "Przejdź do {sekcja}", + "mute": "Wycisz", + "next-media": "Następny utwór", + "next-month": "Następny miesiąc", + "night-light-not-installed": "Podświetlenie nocne (niedostępne)", + "noctalia-performance-enabled": "Tryb wydajności Noctalia", + "open-annotation-tool": "Otwórz za pomocą narzędzia do adnotacji", + "open-control-center": "Centrum sterowania", + "open-notification-history-enable-dnd": "Historia powiadomień", + "open-settings": "Ustawienia", + "open-tray-dropdown": "Zasobnik systemowy", + "output-muted": "Przełącz wyciszenie wyjścia", + "power-profile": "Profil zasilania {profile}", + "previous-media": "Poprzedni utwór", + "previous-month": "Poprzedni miesiąc", + "refresh-devices": "Odśwież urządzenia", + "refresh-wallhaven": "Odśwież wyniki Wallhaven", + "refresh-wallpaper-list": "Odśwież listę tapet", + "remove-widget": "Usuń widżet", + "search-close": "Zamknij wyszukiwanie", + "session-menu": "Menu sesji", + "show-all-devices": "Pokaż wszystkie urządzenia", + "switch-to-dark-mode": "Tryb ciemny", + "switch-to-light-mode": "Tryb jasny", + "unmute": "Wyłącz wyciszenie", + "up": "Katalog nadrzędny", + "volume-at": "Głośność wyjściowa: {volume}%", + "wallpaper-selector": "Wybór tapety" + }, + "wallpaper": { + "browse": { + "empty-directory": "Ten katalog jest pusty.", + "go-root": "Przejdź do katalogu głównego tapety", + "go-up": "Przejdź do folderu nadrzędnego", + "go-up-hint": "Użyj przycisku wstecz, aby przejść w górę." + }, + "configure-directory": "Skonfiguruj swój katalog z obrazami tapet.", + "fill-modes": { + "crop": "Przytnij (Wypełnij)", + "fit": "Dopasuj (Zmieść)", + "repeat": "Powtórz (Kafel)", + "stretch": "Rozciągnij" + }, + "no-match": "Nie znaleziono dopasowania.", + "no-wallpaper": "Nie znaleziono tapety.", + "panel": { + "apikey-help": "Do dostępu do treści NSFW wymagany jest klucz API.", + "apikey-label": "Klucz API", + "apikey-managed-by-env": "Zarządzane za pomocą zmiennej środowiskowej NOCTALIA_WALLHAVEN_API_KEY.", + "apikey-placeholder": "Wprowadź swój klucz API Wallhaven", + "appearance-dark-tab": "Ciemny", + "appearance-light-tab": "Jasny", + "categories-anime": "Anime", + "categories-label": "Kategorie", + "categories-people": "Ludzie", + "color-extraction-disabled": "Użyj ekstrakcji kolorów tapety", + "color-extraction-enabled": "Użyj predefiniowanych schematów kolorów", + "header-devices-apply-all-tooltip": "Wybierz tapetę na monitor", + "header-devices-per-monitor-tooltip": "Zastosuj do wszystkich monitorów", + "header-link-light-dark-tooltip": "Zachowaj tapetę przy zmianie motywu", + "header-separate-light-dark-tooltip": "Automatyczna zmiana tapety z motywem", + "header-sun-linked-tooltip": "Oddzielne jasne i ciemne tapety", + "header-sun-separate-tooltip": "Ta sama tapeta dla trybu jasnego i ciemnego", + "order-asc": "Rosnąco", + "order-desc": "Malejąco", + "order-label": "Kolejność", + "purity-label": "Filtr treści", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Sketchy", + "ratios-all-portrait": "Wszystkie pionowe", + "ratios-all-wide": "Wszystkie szerokie", + "ratios-any": "Dowolny", + "ratios-label": "Współczynnik proporcji", + "resolution-atleast": "Przynajmniej", + "resolution-exact": "Dokładnie", + "resolution-label": "Rozdzielczość", + "resolution-mode-label": "Tryb", + "solid-color-tooltip": "Jednolite tło kolorystyczne", + "sort-date-asc": "Sortuj od najstarszych", + "sort-date-desc": "Sortuj od najnowszych", + "sort-name-asc": "Sortuj według nazwy (A-Z)", + "sort-name-desc": "Sortuj według nazwy (Z-A)", + "sort-random": "Sortuj losowo", + "sorting-date-added": "Data dodania", + "sorting-favorites": "Ulubione", + "sorting-label": "Sortuj według", + "sorting-relevance": "Trafność", + "sorting-toplist": "Najlepsze", + "sorting-views": "Wyświetlenia", + "source-label": "Źródło", + "source-wallhaven": "Wallhaven", + "title": "Selektor tapety", + "wallhaven-settings-title": "Ustawienia Wallhaven" + }, + "transitions": { + "disc": "Dysk", + "fade": "Zanikanie", + "honeycomb": "Plaster miodu", + "pixelate": "Pikselizuj", + "stripes": "Paski", + "wipe": "Wycieranie" + }, + "try-different-search": "Spróbuj innego zapytania.", + "wallhaven": { + "loading": "Ładowanie tapet...", + "no-results": "Nie znaleziono tapet. Spróbuj innego zapytania.", + "page": "{current} z {total}", + "page-prefix": "Strona", + "page-suffix": "z {total}" + } + }, + "weather": { + "clear-sky": "Czyste niebo", + "drizzle": "Mżawka", + "fog": "Mgła", + "mainly-clear": "Głównie bezchmurnie", + "overcast": "Zachmurzenie całkowite", + "partly-cloudy": "Częściowe zachmurzenie", + "rain-showers": "Przelotne opady deszczu", + "snow": "Śnieg", + "thunderstorm": "Burza" + }, + "widgets": { + "color-picker": { + "palette-description": "Wybierz z szerokiej gamy predefiniowanych kolorów.", + "palette-label": "Paleta", + "palette-theme-colors": "Szybki dostęp do palety kolorów twojego motywu.", + "title": "Próbnik kolorów" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm małymi literami", + "ampm-uppercase": "AM/PM wielkimi literami", + "common-12hour-time-minutes": "Czas 12-godzinny z minutami", + "common-24hour-time-minutes": "Czas 24-godzinny z minutami", + "common-24hour-time-seconds": "Czas 24-godzinny z sekundami", + "common-european-date": "Europejski format daty", + "common-iso-date": "Format daty ISO", + "common-us-date": "Amerykański format daty", + "common-weekday-date": "Dzień tygodnia z datą", + "common-weekday-month-day": "Dzień tygodnia, miesiąc i dzień", + "day-abbreviated": "Skrócona nazwa dnia", + "day-full": "Pełna nazwa dnia", + "day-leading-zero": "Dzień z zerem wiodącym (01-31)", + "day-no-leading-zero": "Dzień bez zera wiodącego (1-31)", + "hour-leading-zero": "Godzina z zerem wiodącym (00-23) — format 24h", + "hour-no-leading-zero": "Godzina bez zera wiodącego (0-23) — format 24h", + "minute-leading-zero": "Minuta z zerem wiodącym (00-59)", + "minute-no-leading-zero": "Minuta bez zera wiodącego (0-59)", + "month-abbreviated": "Skrócona nazwa miesiąca", + "month-full": "Pełna nazwa miesiąca", + "month-number-leading-zero": "Miesiąc jako liczba z zerem wiodącym (01-12)", + "month-number-no-zero": "Miesiąc jako liczba bez zera wiodącego (1-12)", + "second-leading-zero": "Sekunda z zerem wiodącym (00-59)", + "second-no-leading-zero": "Sekunda bez zera wiodącego (0-59)", + "timezone-abbreviation": "Skrót strefy czasowej", + "year-four-digit": "Rok jako liczba czterocyfrowa", + "year-two-digit": "Rok jako liczba dwucyfrowa (00-99)" + }, + "file-picker": { + "item": "element", + "items": "elementy", + "search-placeholder": "Szukaj plików i folderów...", + "select-current": "Wybierz bieżący", + "select-file": "Wybierz plik", + "select-folder": "Wybierz folder", + "selected": "Wybrano:", + "title": "Wybór plików" + }, + "file-picker-title": "Wybierz plik", + "icon-picker": { + "title": "Wybór ikon" + }, + "tooltip-placeholder": "Miejsce na tekst" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "Anonimowa Tożsamość", + "ca-cert": "Certyfikat CA", + "eap-method": "Metoda EAP", + "password": "Hasło użytkownika", + "phase2-auth": "Uwierzytelnianie Fazy 2", + "username": "Tożsamość / Nazwa Użytkownika" + }, + "panel": { + "action-required": "Wymagane działanie", + "add-hidden-network": "Dodaj ukrytą sieć", + "add-network": "Dodaj sieć", + "available-interfaces": "Dostępne interfejsy", + "available-networks": "Dostępne sieci", + "disabled": "Wi-Fi jest wyłączone", + "disconnecting": "Rozłączanie...", + "dns": "DNS", + "enable-message": "Włącz Wi-Fi, aby zobaczyć dostępne sieci.", + "enter-password": "Wpisz hasło...", + "forget": "Zapomnij", + "forget-network": "Zapomnieć tę sieć?", + "forgetting": "Zapominanie...", + "hidden-network": "Ukryta sieć", + "interface": "Interfejs sieciowy", + "internet-connected": "Połączony z internetem", + "internet-limited": "Brak internetu", + "internet-status": "Status internetu", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "Znane sieci", + "link-speed": "Prędkość łącza", + "network-name-ssid": "Nazwa sieci (SSID)", + "no-ethernet-devices": "Nie wykryto żadnych urządzeń Ethernet", + "no-networks": "Nie znaleziono żadnych sieci", + "saved": "Zapisano", + "scan-again": "Skanuj ponownie", + "searching": "Szukanie pobliskich sieci...", + "security-open": "Otwórz", + "security-wep": "WEP", + "security-wpa": "WPA/WPA2/WPA3 Osobiste", + "security-wpa-ent": "WPA Enterprise", + "security-wpa2-ent": "WPA2 Enterprise", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 Enterprise", + "show-password": "Pokaż hasło", + "title": "Wi-Fi" + }, + "signal": { + "excellent": "Doskonały", + "fair": "Przeciętny", + "good": "Dobry", + "poor": "Zły", + "weak": "Słaby" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/pt.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/pt.json new file mode 100644 index 0000000..2080d9d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/pt.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "Limpar histórico", + "close-app": "Fechar {app}", + "connect-vpn": "Conectar-se a {name}", + "cycle-visualizer": "Visualizador de ciclo", + "disable-bluetooth": "Desativar Bluetooth", + "disable-dnd": "Desativar 'Não perturbe'", + "disable-wifi": "Desativar Wi-Fi", + "disconnect-vpn": "Desconectar {name}", + "dock-settings": "Definições do dock", + "enable-bluetooth": "Ativar Bluetooth", + "enable-dnd": "Ativar 'Não perturbe''", + "enable-wifi": "Ativar Wi-Fi", + "launcher-settings": "Configurações do iniciador", + "lower-to-bottom": "Abaixar para o fundo", + "open-calendar": "Abrir calendário", + "open-display-settings": "Configurações de exibição", + "open-launcher": "Abrir iniciador", + "open-settings": "Abrir configurações", + "raise-to-top": "Elevar ao topo", + "random-wallpaper": "Papel de parede aleatório", + "run-custom-command": "Executar comando personalizado", + "show-active-only": "Mostrar apenas ativos", + "show-all": "Mostrar tudo", + "toggle-mute": "Alternar mudo", + "widget-settings": "Configurações do widget" + }, + "authentication": { + "error": "Erro de autenticação", + "failed": "Falha na autenticação" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Aplicar cores do tema ao ícone da janela ativa.", + "hide-mode-description": "Controla o comportamento do widget quando nenhuma janela está ativa.", + "scrolling-mode-description": "Controlar quando a rolagem de texto está habilitada para títulos de janela longos.", + "show-app-icon-description": "Exibir o ícone do aplicativo ao lado do título da janela.", + "show-app-icon-label": "Mostrar ícone do aplicativo", + "show-app-text-description": "Exibir o título da janela ativa.", + "show-app-text-label": "Mostrar título da janela" + }, + "audio-visualizer": { + "color-name-description": "Selecione a cor para o visualizador.", + "color-name-label": "Cor de preenchimento", + "height-description": "Largura de componente personalizada.", + "hide-when-idle-description": "Quando ativado, o visualizador fica oculto a menos que haja reprodução ativa.", + "hide-when-idle-label": "Ocultar quando não houver reprodução", + "width-description": "Largura do componente personalizado." + }, + "battery": { + "device-default": "Padrão (Dispositivo de exibição)", + "device-description": "Selecione qual dispositivo de bateria exibir.", + "device-label": "Dispositivo de bateria", + "display-mode-description": "Escolha como a bateria é exibida na barra.", + "display-mode-graphic": "Bateria gráfica", + "display-mode-graphic-clean": "Bateria gráfica (sem %)", + "display-mode-icon-always": "Ícone - Sempre mostrar %", + "display-mode-icon-hover": "Ícone - Mostrar ao passar", + "display-mode-icon-only": "Apenas ícone", + "hide-if-idle-description": "Ocultar o widget quando a bateria não estiver carregando ou descarregando.", + "hide-if-idle-label": "Ocultar quando inativo", + "hide-if-not-detected-description": "Oculte o widget quando nenhuma bateria for detectada no sistema.", + "hide-if-not-detected-label": "Ocultar se não detectado", + "low-battery-threshold-label": "Limite de aviso de bateria fraca", + "show-noctalia-performance-description": "Exibir o interruptor do modo de desempenho Noctalia no painel da bateria.
Desativa sombras e animações no Noctalia para reduzir o uso de recursos.", + "show-noctalia-performance-label": "Mostrar alternância Noctalia Desempenho", + "show-power-profile-description": "Exibir a seleção de perfil de energia no painel da bateria.", + "show-power-profile-label": "Mostrar controles de perfil de energia" + }, + "brightness": { + "apply-all-description": "Alterar o brilho de todos os monitores ao usar a roda de rolagem.", + "apply-all-label": "Aplicar alterações de rolagem a todos os monitores" + }, + "clock": { + "clock-display-description": "Personalize a exibição do seu relógio adicionando tokens da lista abaixo. Para usar o formato de 12 horas, você deve incluir o token 'AP'.", + "clock-display-label": "Exibição do relógio", + "custom-font-description": "Selecione uma fonte personalizada para a exibição do relógio.", + "custom-font-label": "Fonte personalizada", + "custom-font-placeholder": "Selecionar fonte personalizada...", + "custom-font-search-placeholder": "Procurar fontes...", + "horizontal-bar-description": "Dica: Use \\n para criar uma quebra de linha.", + "horizontal-bar-label": "Barra horizontal", + "preview": "Pré-visualização", + "tooltip-format-description": "String de formato para a dica de ferramenta exibida ao passar o mouse sobre o relógio. Deixe vazio para usar a dica de ferramenta padrão.", + "tooltip-format-label": "Formato da dica de ferramenta", + "use-custom-font-description": "Substitua a fonte padrão por uma fonte personalizada para o relógio.", + "use-custom-font-label": "Usar fonte personalizada", + "use-monospaced-font-description": "Quando ativado, o relógio usará a fonte monoespaçada.", + "use-monospaced-font-label": "Usar fonte monoespaçada", + "vertical-bar-description": "Use um espaço para separar cada parte em uma nova linha.", + "vertical-bar-label": "Barra vertical" + }, + "control-center": { + "browse-file": "Navegar por arquivo", + "browse-library": "Navegar na biblioteca", + "color-selection-description": "Aplica as cores do tema aos ícones.", + "enable-colorization-description": "Ativar colorização para o ícone, aplicando as cores do tema.", + "icon-description": "Selecione um ícone da biblioteca ou um arquivo personalizado.", + "select-custom-icon": "Selecione um ícone personalizado", + "use-distro-logo-description": "Usar o logo da sua distribuição em vez de um ícone personalizado.", + "use-distro-logo-label": "Usar logo da distribuição em vez de ícone" + }, + "custom-button": { + "collapse-condition-description": "Se o texto de saída corresponder a este valor, o botão será recolhido.", + "collapse-condition-label": "Condição de recolhimento", + "color-selection-description": "Aplicar cores do tema ao ícone e ao texto.", + "default-tooltip": "Botão personalizado, configurar nas configurações", + "display-command-output-description": "Digite um comando para ser executado em um intervalo regular. A primeira linha da sua saída será exibida como texto.", + "display-command-output-label": "Exibir saída de comando", + "display-command-output-stream-description": "Insira um comando para executar continuamente.", + "dynamic-text": "Texto dinâmico", + "enable-colorization-description": "Ativar colorização para o ícone e texto do botão personalizado, aplicando as cores do tema.", + "enable-colorization-label": "Ativar colorização", + "general-tooltip-text-description": "Texto personalizado para exibir na dica de ferramenta do botão.", + "general-tooltip-text-label": "Texto de dica de ferramenta personalizado", + "hide-mode-always-expanded": "Sempre expandido", + "hide-mode-description": "Controla a visibilidade do widget quando o comando não tem saída.", + "hide-mode-expand-with-output": "Expandir quando tem saída", + "hide-mode-label": "Modo de ocultação", + "hide-mode-max-transparent": "Totalmente expandido mas transparente", + "icon-description": "Selecione um ícone da biblioteca.", + "ipc-identifier-description": "Identificador exclusivo para comandos IPC. Use este identificador com 'qs -c noctalia-shell ipc call cb [ação] [identificador]' para controlar este botão via IPC.", + "ipc-identifier-label": "Identificador IPC", + "left-click-description": "Comando a executar quando o botão é clicado com o botão esquerdo.", + "left-click-label": "Clique esquerdo", + "left-click-update-text": "Atualizar texto exibido ao clicar com o botão esquerdo", + "max-text-length-horizontal-description": "Número máximo de caracteres a serem exibidos na barra horizontal (0 para ocultar o texto).", + "max-text-length-horizontal-label": "Comprimento máximo do texto (horizontal)", + "max-text-length-vertical-description": "Número máximo de caracteres a serem exibidos na barra vertical (0 para ocultar o texto).", + "max-text-length-vertical-label": "Comprimento máximo do texto (vertical)", + "middle-click-description": "Comando a executar quando o botão é clicado com o botão do meio.", + "middle-click-label": "Clique do meio", + "middle-click-update-text": "Atualizar texto exibido com clique do meio", + "parse-json-description": "Analisa a saída do comando como um objeto JSON para definir dinamicamente o texto e o ícone.", + "parse-json-label": "Analisar saída como JSON", + "refresh-interval-description": "Intervalo em milissegundos.", + "refresh-interval-label": "Intervalo de atualização", + "right-click-description": "Comando a executar quando o botão é clicado com o botão direito.", + "right-click-label": "Clique direito", + "right-click-update-text": "Atualizar o texto exibido ao clicar com o botão direito", + "show-exec-tooltip-description": "Mostrar tooltips com detalhes do comando (clique esquerdo/direito/meio, roda).", + "show-exec-tooltip-label": "Mostrar dicas de ferramentas de comando", + "show-icon-description": "Alterna a visibilidade do ícone do widget.", + "show-icon-label": "Mostrar ícone", + "show-text-tooltip-description": "Mostrar dicas de ferramentas com a saída do comando de texto.", + "show-text-tooltip-label": "Mostrar dicas de texto dinâmicas", + "text-stream-description": "As linhas transmitidas do comando serão exibidas como texto no botão.", + "text-stream-label": "Transmissão", + "wheel-description": "Comando a executar quando a roda de rolagem é usada.
Use $delta para o delta da roda de rolagem no comando.", + "wheel-down": "Roda baixo", + "wheel-down-description": "Comando a executar quando a roda de rolagem é rolada para baixo.", + "wheel-down-label": "Comando de roda para baixo", + "wheel-label": "Roda de rolagem", + "wheel-mode-separate-description": "Ativar comandos separados para roda para cima e para baixo.", + "wheel-mode-separate-label": "Comandos de roda separados", + "wheel-up": "Roda cima", + "wheel-up-description": "Comando a executar quando a roda de rolagem é rolada para cima.", + "wheel-up-label": "Comando de roda para cima", + "wheel-update-text": "Atualizar texto exibido ao rolar" + }, + "keyboard-layout": { + "show-icon-description": "Exibir o ícone do layout do teclado." + }, + "lock-keys": { + "hide-when-off-description": "Ocultar o indicador quando a tecla não está ativa.", + "hide-when-off-label": "Ocultar quando desativado", + "show-caps-lock-description": "Exibir o status do Bloq Caps.", + "show-caps-lock-label": "Bloq Caps", + "show-num-lock-description": "Exibir o status do Bloq Num.", + "show-num-lock-label": "Bloq Num", + "show-scroll-lock-description": "Exibir o status do Bloq Rol.", + "show-scroll-lock-label": "Bloq Rol" + }, + "media-mini": { + "compact-mode-description": "Ativar um layout que economize espaço para o painel do reprodutor de mídia.", + "compact-mode-label": "Modo compacto", + "hide-mode-description": "Controla o comportamento do widget quando nenhuma mídia está sendo reproduzida.", + "max-width-description": "Define o tamanho horizontal máximo do widget. O widget será reduzido para se adequar a conteúdo mais curto.", + "no-active-player": "Nenhum player ativo", + "panel-section-description": "Configure a aparência e o comportamento do painel do reprodutor de mídia.", + "panel-section-label": "Painel do reprodutor de mídia", + "scrolling-mode-description": "Controlar quando a rolagem de texto está habilitada para títulos de faixa longos.", + "scrolling-mode-label": "Modo de rolagem", + "show-album-art-description": "Exibir a arte do álbum da faixa atualmente sendo reproduzida.", + "show-album-art-label": "Mostrar arte do álbum", + "show-artist-first-description": "Exibir artista - título em vez de título - artista.", + "show-artist-first-label": "Mostrar o artista primeiro", + "show-progress-ring-description": "Exibir um indicador de progresso circular mostrando o progresso da faixa.", + "show-progress-ring-label": "Mostrar anel de progresso", + "show-visualizer-description": "Exibir um visualizador de áudio quando música está sendo reproduzida.", + "show-visualizer-label": "Mostrar visualizador", + "use-fixed-width-description": "Quando ativado, o widget sempre usará a largura máxima em vez de ajustar dinamicamente ao conteúdo.", + "use-fixed-width-label": "Usar largura fixa", + "visualizer-type-description": "Escolher o estilo de visualizador de áudio a exibir.", + "visualizer-type-label": "Tipo de visualizador" + }, + "notification-history": { + "hide-widget-when-zero-description": "Ocultar o ícone de notificações quando não houver notificações.", + "hide-widget-when-zero-label": "Ocultar ícone sem notificações", + "hide-widget-when-zero-unread-description": "Ocultar o ícone de notificações quando não houver notificações não lidas.", + "hide-widget-when-zero-unread-label": "Ocultar ícone sem notificações não lidas", + "show-unread-badge-description": "Exibir um distintivo mostrando o número de notificações não lidas.", + "show-unread-badge-label": "Mostrar distintivo de não lidos", + "unread-badge-color-description": "Selecione a cor para o emblema de notificação não lida.", + "unread-badge-color-label": "Cor do distintivo de não lido" + }, + "section-editor": { + "placeholder": "Selecione um widget para adicionar...", + "search-placeholder": "Pesquisar widgets..." + }, + "spacer": { + "width-description": "Largura do espaçamento em pixels." + }, + "system-monitor": { + "compact-mode-description": "Exibir as estatísticas como mini gráficos de barras em vez de valores de texto. Evita mudanças no layout.", + "compact-mode-label": "Modo compacto", + "cpu-cores-description": "Exibir o uso dos núcleos da CPU individualmente.", + "cpu-cores-label": "Núcleos de CPU", + "cpu-frequency-description": "Exibir a velocidade de clock atual da CPU em GHz.", + "cpu-frequency-label": "Mostrar frequência da CPU", + "cpu-temperature-description": "Mostrar as leituras de temperatura da CPU se disponíveis.", + "cpu-temperature-label": "Temperatura da CPU", + "cpu-usage-description": "Exibir o percentual atual de uso da CPU.", + "cpu-usage-label": "Uso da CPU", + "disk-path-description": "Selecione qual ponto de montagem de disco monitorar.", + "disk-path-label": "Caminho do disco", + "gpu-temperature-description": "Mostrar as leituras de temperatura da GPU se disponíveis.", + "load-average-description": "Exibir a carga média do sistema.", + "load-average-label": "Carga média", + "memory-percentage-description": "Mostrar o uso de memória como percentual em vez de valores absolutos.", + "memory-percentage-label": "Memória como percentual", + "memory-usage-description": "Exibir as informações atuais de uso da RAM.", + "memory-usage-label": "Uso de memória", + "network-traffic-description": "Exibir as velocidades de upload e download da rede.", + "network-traffic-label": "Tráfego de rede", + "storage-as-percentage-description": "Mostrar o espaço em disco como percentagem em vez de valores absolutos.", + "storage-as-percentage-label": "Disco como percentagem", + "storage-available-description": "Mostra quanto espaço em disco está disponível em vez de quanto é usado.", + "storage-available-label": "Espaço em disco disponível", + "storage-usage-description": "Mostrar as informações de uso do espaço em disco.", + "storage-usage-label": "Uso de armazenamento", + "swap-usage-description": "Mostrar o uso da memória swap.", + "swap-usage-label": "Utilização de swap", + "use-monospace-font-description": "Use fonte monoespaçada para largura de caractere consistente.", + "use-monospace-font-label": "Fonte monoespaçada", + "use-padding-description": "Preenche os valores de texto com espaços iniciais para evitar o deslocamento do layout.", + "use-padding-description-disabled-monospace-font": "É necessária uma fonte monoespaçada para esta funcionalidade.", + "use-padding-description-disabled-vertical": "A barra de tarefas vertical não suporta preenchimento de texto.", + "use-padding-label": "Preenchimento do Texto" + }, + "taskbar": { + "colorize-icons-description": "Aplicar cores do tema aos ícones da barra de tarefas.", + "hide-mode-description": "Controla como o widget se comporta quando não há janelas correspondentes.", + "hide-mode-label": "Modo de ocultação", + "icon-scale-description": "Define o fator de escala para ícones da barra de tarefas.", + "icon-scale-label": "Escalonamento de ícones", + "max-width-description": "Largura máxima da barra de tarefas como porcentagem da largura da tela.", + "max-width-label": "Largura máxima", + "only-active-workspaces-description": "Mostrar apenas aplicativos de áreas de trabalho ativas.", + "only-active-workspaces-label": "Apenas de espaços de trabalho ativos", + "only-same-monitor-description": "Mostrar apenas aplicativos do monitor onde a barra está localizada.", + "only-same-monitor-label": "Apenas do mesmo monitor", + "show-pinned-apps-description": "Mostrar aplicativos fixados do Dock na barra de tarefas.", + "show-pinned-apps-label": "Mostrar aplicativos fixados", + "show-title-description": "Exibir títulos de janelas na barra de tarefas.", + "show-title-description-disabled": "A barra de tarefas vertical não suporta exibir títulos.", + "show-title-label": "Mostrar título", + "smart-width-description": "Ajustar automaticamente a largura das entradas com base no número de entradas.", + "smart-width-label": "Largura inteligente", + "title-width-description": "Definir a largura dos títulos de janelas na barra de tarefas (em pixels).", + "title-width-label": "Largura do título", + "title-width-reset-tooltip": "Redefinir largura do título" + }, + "tray": { + "chevron-color-description": "Aplicar as cores do tema ao ícone de seta da gaveta.", + "chevron-color-label": "Cor da seta", + "colorize-icons-description": "Aplicar cores do tema aos ícones da bandeja do sistema.", + "colorize-icons-label": "Colorir ícones", + "drawer-enabled-description": "Quando habilitado, os itens da bandeja não fixados são exibidos em um painel gaveta.
Quando desabilitado, todos os itens da bandeja são exibidos inline.", + "drawer-enabled-label": "Habilitar gaveta", + "hide-passive-description": "Quando ativado, os itens da bandeja com status 'Passivo' serão ocultados.", + "hide-passive-label": "Ocultar itens passivos" + }, + "volume": { + "display-mode-description": "Escolha como você gostaria que este valor aparecesse." + }, + "workspace": { + "character-count-description": "Número de caracteres a exibir dos nomes de espaços de trabalho (1-10).", + "character-count-label": "Número de caracteres", + "empty-color-description": "Definir a cor de fundo para Workspaces vazios.", + "empty-color-label": "Cor do espaço de trabalho vazio", + "enable-scrollwheel-description": "Alternar entre áreas de trabalho usando a roda do mouse.", + "enable-scrollwheel-label": "Role para alternar entre áreas de trabalho", + "focused-color-description": "Definir a cor de fundo para o Workspace focado.", + "focused-color-label": "Cor do workspace focado", + "follow-focused-screen-description": "Exibir áreas de trabalho da tela atualmente em foco, em vez da tela onde a barra está localizada.", + "follow-focused-screen-label": "Seguir tela em foco", + "font-weight-description": "Definir o peso visual para o texto na área de trabalho.", + "font-weight-label": "Peso da fonte", + "grouped-border-opacity-description": "Definir o nível de opacidade para as bordas do contêiner do espaço de trabalho.", + "grouped-border-opacity-label": "Opacidade da borda", + "hide-unoccupied-description": "Não exibir áreas de trabalho sem janelas.", + "hide-unoccupied-label": "Ocultar desocupados", + "label-mode-description": "Escolher como os rótulos de espaço de trabalho são exibidos.", + "label-mode-label": "Modo de rótulo", + "occupied-color-description": "Definir a cor de fundo para Workspaces ocupados.", + "occupied-color-label": "Cor do espaço de trabalho ocupado", + "pill-size-description": "Ajuste o tamanho das pílulas da área de trabalho (50%-100%).", + "pill-size-label": "Tamanho da pílula", + "reverse-scrolling-description": "Inverter a direção da troca de áreas de trabalho ao rolar.", + "reverse-scrolling-label": "Rolagem invertida", + "show-applications-description": "Exibir ícones de aplicativos dentro de cada espaço de trabalho.", + "show-applications-hover-description": "Exibir ícones de aplicativos dentro de cada área de trabalho quando a área de trabalho é sobreposta.", + "show-applications-hover-label": "Mostrar aplicativos ao passar o mouse", + "show-applications-label": "Mostrar aplicativos", + "show-badge-description": "Mostrar o selo do número da área de trabalho no modo agrupado.", + "show-badge-label": "Mostrar selo da área de trabalho", + "show-labels-only-when-occupied-description": "Mostrar rótulos de área de trabalho apenas quando contiverem janelas.", + "show-labels-only-when-occupied-label": "Mostrar rótulos apenas quando ocupado", + "unfocused-icons-opacity-description": "Definir o nível de opacidade para ícones de aplicativos não focados.", + "unfocused-icons-opacity-label": "Opacidade dos ícones desfocados" + } + }, + "battery": { + "all-batteries": "Todas as baterias (combinadas)", + "battery-health": "Estado da bateria", + "battery-level": "Nível da bateria", + "capacity-level": "Capacidade: {level}", + "charging-rate": "Taxa de carregamento: {rate} W", + "discharging-rate": "Taxa de descarregamento: {rate} W", + "health": "Saúde: {percent}%", + "inhibit-idle-description": "Mantém o sistema acordado.", + "no-battery-detected": "Nenhuma bateria detectada", + "plugged-in": "Conectado", + "power-profile": "Perfil de energia", + "time-left": "Tempo restante: {time}", + "time-until-full": "Tempo até carga completa: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Dispositivos disponíveis", + "blocked": "Bloqueado", + "connected-devices": "Dispositivos conectados", + "device-address": "Endereço do dispositivo", + "disabled": "O Bluetooth está desativado", + "discoverable": "Visível", + "enable-message": "Ative o Bluetooth para ver os dispositivos disponíveis.", + "known-devices": "Dispositivos conhecidos", + "no-devices": "Nenhum dispositivo disponível", + "paired-devices": "Dispositivos emparelhados", + "pairing-mode": "Certifique-se de que seu dispositivo está em modo de pareamento.", + "scanning": "Procurando por dispositivos...", + "signal-text-excellent": "Sinal: Excelente", + "signal-text-fair": "Sinal: Justo", + "signal-text-good": "Sinal: Bom", + "signal-text-poor": "Sinal: Fraco", + "signal-text-unknown": "Sinal: Desconhecido", + "signal-text-very-poor": "Sinal: Muito fraco" + } + }, + "changelog": { + "error": { + "fetch-failed": "Não foi possível carregar os dados do changelog. Tente novamente mais tarde.", + "rate-limit": "Limite do GitHub atingido. Tente novamente em alguns minutos." + }, + "panel": { + "buttons-discord": "Entre no nosso Discord", + "buttons-dismiss": "OK", + "buttons-feedback": "Dar feedback", + "empty": "As notas da versão ainda não estão disponíveis.", + "highlight-title": "Destaques", + "released": "Lançado em {date}", + "subtitle-fresh": "Obrigado por instalar o Noctalia! Veja o que está incluído nesta compilação.", + "subtitle-updated": "Atualizado a partir da {previousVersion}", + "title": "Novidades na {version}", + "version": "Versão {version}", + "version-new-user": "Nova instalação" + } + }, + "common": { + "actions": "Ações", + "active": "Ativo", + "add": "Adicionar", + "appearance": "Aparência", + "apply": "Aplicar", + "auto-connect": "Conexão automática", + "automation": "Automação", + "available": "Disponível", + "back": "Voltar", + "battery": "Bateria", + "behavior": "Comportamento", + "bluetooth": "Bluetooth", + "brightness": "Brilho", + "browse": "Navegar", + "calendar": "Calendário", + "calendar-panel": "Painel do calendário", + "cancel": "Cancelar", + "cards": "Cartões", + "charging": "Carregando", + "clear": "Limpar", + "clipboard": "Área de transferência", + "close": "Fechar", + "color-muted": "Silenciado", + "colors": "Cores", + "command": "Comando", + "confirm": "Confirmar", + "connect": "Conectar", + "connected": "Conectado(a)", + "connecting": "Conectando...", + "continue": "Continuar", + "contributors": "Colaboradores", + "copied-to-clipboard": "Copiado para a área de transferência", + "countdown": "Contagem regressiva", + "customize": "Personalizar", + "date": "Data", + "default": "Padrão", + "delete": "Apagar", + "devices": "Dispositivos", + "disabled": "Desativado", + "discharging": "Descarregando", + "disconnect": "Desconectar", + "disconnected": "Desconectado(a)", + "disconnecting": "Desconectando...", + "display-mode": "Modo de exibição", + "documentation": "Documentação", + "download": "Baixar", + "duration": "Duração", + "dysfunctional": "Disfuncional", + "edit": "Editar", + "enabled": "Ativado", + "error": "Erro", + "ethernet": "Ethernet", + "events": "Eventos", + "execute": "Executar", + "faithful": "Fiel", + "focus": "Foco", + "font-weight-bold": "Negrito", + "font-weight-light": "Leve", + "font-weight-medium": "Médio", + "font-weight-regular": "Regular", + "font-weight-semibold": "Seminegrito", + "frequency": "Frequência", + "gateway": "Porta de entrada", + "general": "Geral", + "height": "Altura", + "hibernate": "Hibernar", + "history": "História", + "icon": "Ícone", + "idle": "Ocioso", + "info": "Informação", + "input": "Entrada", + "install": "Instalar", + "installed": "Instalado", + "interface": "Interface", + "internet": "Internet", + "keybind": "Atalho de teclado", + "language": "Linguagem", + "loading": "Carregando...", + "local": "Local", + "location": "Localização", + "lock": "Bloquear", + "logout": "Sair", + "look": "Aparência", + "margins": "Margens", + "media": "Mídia", + "media-player": "Reprodutor de mídia", + "memory": "Memória", + "monitors": "Monitores", + "move-down": "Mover para baixo", + "move-up": "Mover para cima", + "network": "Rede", + "next": "Próximo", + "night-light": "Luz noturna", + "no": "Não", + "no-results": "Nenhum resultado", + "no-summary": "Sem resumo", + "none": "Nenhum", + "not-found": "Não encontrado", + "notifications": "Notificações", + "official": "Oficial", + "on-surface": "Na Superfície", + "output": "Saída", + "pair": "Emparelhar", + "paired": "Emparelhado", + "pairing": "Emparelhando...", + "panels": "Painéis", + "password": "Senha", + "pause": "Pausar", + "performance": "Desempenho", + "pin": "Fixar", + "play": "Reproduzir", + "polling": "Sondagem", + "position": "Posição", + "previous": "Anterior", + "primary": "Primário", + "random": "Aleatório", + "reboot": "Reiniciar", + "reboot-to-uefi": "Reiniciar para UEFI", + "record": "Iniciar gravação", + "refresh": "Atualizar", + "required": "(obrigatório)", + "reset": "Reiniciar", + "reset-to-default": "Redefinir para o Padrão", + "result-count": "{count} resultado", + "result-count-plural": "{count} resultados", + "resume": "Retomar", + "save": "Salvar", + "scanning": "A digitalização está em andamento...", + "screen-corners": "Cantos da tela", + "search": "Pesquisar", + "second": "{count} segundo", + "second-plural": "{count} segundos", + "secondary": "Secundário", + "security": "Segurança", + "select": "Selecionar", + "select-color": "Selecionar cor", + "select-color-description": "Aplicar cores do tema para enfatizar.", + "select-icon-color": "Selecionar cor do ícone", + "settings": "Definições", + "shortcuts": "Atalhos", + "shutdown": "Desligar", + "signal": "Sinal", + "sound": "Som", + "sources": "Fontes", + "start": "Começar", + "stop": "Pare", + "supporters": "Apoiadores", + "suspend": "Suspender", + "templates": "Modelos", + "tertiary": "Terciário", + "test": "Teste", + "thresholds": "Limiares", + "title": "Título", + "toast": "Toast", + "trusted": "Confiável", + "uninstall": "Desinstalar", + "unknown": "Desconhecido", + "unpair": "Desemparelhar", + "unpin": "Desafixar", + "update": "Atualização", + "upload": "Enviar", + "username": "Nome de usuário", + "userspace-reboot": "Reiniciar espaço de usuário", + "version": "Versão", + "vibrant": "Vibrante", + "visualizer": "Visualizador", + "volume": "Volume", + "volumes": "Volumes", + "wallpaper": "Papel de parede", + "weather": "Tempo", + "weather-loading": "Carregando o clima...", + "weather-no-location": "Defina um local nas Definições para ver o tempo", + "week": "Semana", + "widgets": "Widgets", + "width": "Largura", + "wifi": "Wi-Fi", + "windows": "Janelas", + "yes": "Sim" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Perfil de energia", + "tooltip-disabled": "Instale power-profiles-daemon para usar perfis de energia" + }, + "wifi": { + "label-disconnected": "Wi-Fi desconectado", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Sempre se esconda", + "always-show": "Sempre mostrar", + "force-open": "Forçar a abertura", + "on-hover": "Ao passar" + }, + "hide-modes": { + "auto-hide": "Ocultar automaticamente", + "hidden": "Ocultar quando vazio", + "idle": "Ocultar quando inativo", + "non-exclusive": "Não exclusivo", + "transparent": "Transparente quando vazio", + "visible": "Sempre visível" + }, + "launcher": { + "categories": { + "all": "Tudo", + "audiovideo": "Áudio e vídeo", + "chat": "Bate-papo", + "development": "Desenvolvimento", + "education": "Educação", + "emoji-activity": "Atividade", + "emoji-animals": "Animais", + "emoji-flags": "Bandeiras", + "emoji-food": "Comida e bebida", + "emoji-nature": "Natureza", + "emoji-objects": "Objetos", + "emoji-people": "Pessoas e corpo", + "emoji-recent": "Recentes", + "emoji-symbols": "Símbolos", + "emoji-travel": "Viagens e lugares", + "game": "Jogos", + "graphics": "Gráficos", + "misc": "Diversos", + "office": "Escritório", + "pinned": "Fixado", + "system": "Sistema", + "webbrowser": "Navegador web" + }, + "providers": { + "applications": "Aplicativos", + "calculator": "Calculadora", + "calculator-deprecated": "O comando >calc está obsoleto e será removido em breve. Por favor, digite expressões matemáticas diretamente na pesquisa.", + "calculator-description": "Calculadora — avalie expressões matemáticas", + "calculator-enter-expression": "Digite uma expressão matemática", + "calculator-press-enter-to-copy": "Pressione Enter para copiar o resultado", + "clipboard": "Histórico da área de transferência", + "clipboard-clear-description": "Limpar todo o histórico da área de transferência", + "clipboard-clear-description-full": "Remover todos os itens do histórico da área de transferência", + "clipboard-clear-history": "Limpar histórico da área de transferência", + "clipboard-delete": "Apagar entrada da área de transferência", + "clipboard-history-disabled": "Histórico da área de transferência desativado", + "clipboard-history-disabled-description": "Ative o histórico da área de transferência nas configurações ou instale o cliphist", + "clipboard-loading": "Carregando histórico da área de transferência...", + "clipboard-search-description": "Pesquisar no histórico da área de transferência", + "command-description": "Executar comandos shell", + "emoji": "Seletor de emojis", + "emoji-loading": "Carregando emojis...", + "emoji-loading-description": "Por favor, aguarde", + "emoji-no-recent": "Ainda não há emojis recentes", + "emoji-search-description": "Buscar e copiar emojis", + "settings-search-description": "Pesquisar e navegar para as configurações", + "windows-search-description": "Pesquisar e focar janelas abertas" + } + }, + "lock-screen": { + "authenticating": "Autenticando...", + "password": "Digite sua senha...", + "restart": "Reiniciar", + "shut-down": "Desligar" + }, + "notifications": { + "panel": { + "click-to-collapse": "Clique para recolher", + "click-to-expand": "Clique para expandir", + "description": "Suas notificações aparecerão aqui assim que chegarem.", + "no-notifications": "Nenhuma notificação" + }, + "range": { + "earlier": "Antigas", + "today": "Hoje", + "yesterday": "Ontem" + }, + "time": { + "diff-d": "há 1 dia", + "diff-dd": "há {diff} dias", + "diff-h": "há 1 hora", + "diff-hh": "há {diff} horas", + "diff-m": "há 1 minuto", + "diff-mm": "há {diff} minutos", + "now": "agora" + } + }, + "options": { + "bar": { + "density-comfortable": "Confortável", + "density-compact": "Compacta", + "density-default": "Padrão", + "density-mini": "Mini", + "density-spacious": "Espaçoso(a)", + "display-mode-always-visible": "Sempre Visível", + "type-floating": "Flutuante", + "type-framed": "Emoldurada", + "type-simple": "Simples" + }, + "control-center": { + "quick-settings-style-classic": "Clássico", + "quick-settings-style-modern": "Moderno" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "Confortável", + "compact": "Compacto", + "default": "Padrão" + }, + "launcher-view-mode": { + "grid": "Grade", + "list": "Lista" + }, + "notification-density": { + "compact": "Compacto", + "default": "Padrão" + }, + "scrolling-modes": { + "always": "Rolar sempre", + "hover": "Rolar ao passar", + "never": "Nunca rolar" + }, + "session-menu-grid-layout": { + "grid": "Grade", + "single-row": "Linha única" + }, + "settings-panel-mode": { + "attached": "Painel anexado à barra", + "centered": "Painel centralizado", + "window": "Janela separada" + }, + "visualizer-types": { + "linear": "Linear", + "mirrored": "Espelhado", + "wave": "Onda" + }, + "visualizer-visibility": { + "with-background": "Apenas com fundo" + }, + "workspace-labels": { + "index": "Índice", + "index-and-name": "Índice e Nome", + "name": "Nome" + } + }, + "panels": { + "about": { + "become-supporter": "Torne-se um apoiador", + "changelog": "Ver histórico de alterações", + "changelog-on-startup": "Mostrar changelog na atualização", + "changelog-on-startup-desc": "Mostrar automaticamente o registo de alterações quando o Noctalia for atualizado.", + "contributors-description": "Agradecimentos ao nosso incrível colaborador!", + "contributors-description-plural": "Agradecimentos aos nossos {count} incríveis colaboradores!", + "copy-info": "Copiar informações", + "debug-disabled": "Modo de depuração desativado", + "debug-enabled": "Modo de depuração ativado", + "info-copied": "Informação copiada para a área de transferência", + "noctalia-available": "Disponível:", + "noctalia-desc": "Um shell de desktop elegante e minimalista, cuidadosamente criado para Wayland, construído com Quickshell.", + "noctalia-git-commit": "Commit Git:", + "noctalia-installed-version": "Versão instalada:", + "noctalia-latest-version": "Última versão:", + "noctalia-qs-version": "Noctalia QS versão:", + "noctalia-title": "Shell Noctalia", + "privacy-policy": "Política de privacidade", + "support": "Apoie-nos", + "supporter-badge": "Apoiador", + "supporters-desc": "Um enorme obrigado ao nosso incrível apoiador!", + "supporters-desc-plural": "Um enorme obrigado aos nossos {count} incríveis apoiadores!", + "supporters-loading": "Carregando apoiadores...", + "system-board": "Placa-mãe:", + "system-cpu": "CPU:", + "system-disk": "Disco:", + "system-gpu": "GPU:", + "system-host": "Host:", + "system-install-hint": "Instale o fastfetch para visualizar informações do sistema", + "system-kernel": "Kernel:", + "system-loading": "Carregando informações do sistema...", + "system-memory": "Memória:", + "system-monitor": "Monitor:", + "system-not-installed": "fastfetch não está instalado", + "system-os": "SO:", + "system-packages": "Pacotes:", + "system-product": "Produto:", + "system-title": "Informações do Sistema", + "system-uptime": "Tempo de atividade:", + "system-wm": "WM:", + "telemetry-data-copied": "Dados de telemetria copiados para a área de transferência.", + "telemetry-desc": "Ajude a melhorar o Noctalia compartilhando informações anônimas do sistema (resolução da tela, compositor, distribuição). Enviado uma vez na inicialização, sem rastreamento, os dados são excluídos automaticamente após 30 dias.", + "telemetry-enabled": "Enviar informações anônimas do sistema", + "telemetry-show-data": "Visualizar dados", + "telemetry-title": "Privacidade", + "title": "Sobre", + "up-to-date": "Você está atualizado!", + "update-available": "Atualização disponível", + "view-commit": "Ver commit no GitHub" + }, + "audio": { + "devices-desc": "Configure os dispositivos de entrada e saída de áudio disponíveis.", + "devices-input-device-description": "Selecione o dispositivo de entrada de áudio desejado.", + "devices-input-device-label": "Dispositivo de entrada", + "devices-output-device-description": "Selecione o dispositivo de saída de áudio desejado.", + "devices-output-device-label": "Dispositivo de saída", + "devices-title": "Dispositivos de áudio", + "external-mixer-description": "Insira o comando ou o caminho do aplicativo para iniciar ao ativar o recurso de mixer de áudio externo.", + "external-mixer-label": "Comando de mixer de áudio externo", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Defina seus aplicativos de mídia preferidos e ignorados.", + "media-excluded-player-description": "Adicione palavras-chave para os reprodutores que você deseja que o sistema ignore. Cada palavra-chave deve estar em uma nova linha.", + "media-excluded-player-label": "Reprodutor excluído", + "media-excluded-player-placeholder": "digite a substring e pressione +", + "media-frame-rate-description": "Taxas mais altas são mais suaves, mas consomem mais recursos.", + "media-frame-rate-label": "Taxa de quadros", + "media-primary-player-description": "Digite uma palavra-chave para identificar seu reprodutor principal.", + "media-primary-player-label": "Reprodutor principal", + "media-primary-player-placeholder": "p. ex. spotify, vlc, mpv", + "media-scrolling-speed-description": "Tempo em segundos para o título rolar do início ao fim.", + "media-scrolling-speed-label": "Velocidade de rolagem", + "media-scrolling-title-description": "Ativar rolagem contínua para títulos de mídia longos.", + "media-scrolling-title-label": "Título com rolagem", + "media-title": "Reprodutores de mídia", + "on-middle-clicked-description": "Comando a ser executado quando o botão do meio é clicado.", + "panel-applications-empty": "Nenhum aplicativo está reproduzindo áudio no momento", + "spectrum-mirrored-description": "Espelha o espectro para que as frequências baixas fiquem no centro. Quando desativado, as frequências baixas ficam à esquerda e as altas à direita.", + "spectrum-mirrored-label": "Espectro espelhado", + "title": "Áudio", + "visualizer-type-description": "Escolha um tipo de visualização para reprodução de mídia.", + "visualizer-type-label": "Tipo de visualização", + "volumes-desc": "Ajuste os controles de volume e os níveis de áudio.", + "volumes-feedback-sound-file-description": "Caminho para o arquivo de som reproduzido ao mudar o volume.", + "volumes-feedback-sound-file-label": "Arquivo de som de feedback de volume", + "volumes-feedback-sound-file-select-title": "Selecionar arquivo de som de feedback de volume", + "volumes-input-volume-description": "Nível de volume de entrada do microfone.", + "volumes-mute-input-description": "Silencia a entrada de áudio padrão (microfone).", + "volumes-mute-input-label": "Silenciar entrada de áudio", + "volumes-mute-output-description": "Silencia a saída de áudio principal do sistema.", + "volumes-mute-output-label": "Silenciar saída de áudio", + "volumes-output-volume-description": "Nível de volume de todo o sistema.", + "volumes-step-size-description": "Ajuste o tamanho do passo para alterações de volume (roda do mouse, atalhos de teclado).", + "volumes-step-size-label": "Tamanho do passo do volume", + "volumes-volume-feedback-description": "Reproduzir um som de feedback ao ajustar o volume.", + "volumes-volume-feedback-label": "Reproduzir som de feedback de volume", + "volumes-volume-overdrive-description": "Permite aumentar o volume para além dos 100%. Poderá não ser suportado por todo o hardware.", + "volumes-volume-overdrive-label": "Permitir volume acima de 100%" + }, + "bar": { + "appearance-auto-hide-delay-description": "Tempo até a Bar se ocultar depois que o rato a deixa.", + "appearance-auto-hide-delay-label": "Atraso para Ocultar", + "appearance-auto-hide-exclusive-note": "Nota: A zona exclusiva é automaticamente desativada quando o auto-ocultar está ativo.", + "appearance-auto-show-delay-description": "Tempo até a barra aparecer quando o mouse entra na borda.", + "appearance-auto-show-delay-label": "Atraso de Exibição", + "appearance-background-opacity-description": "Defina a opacidade do fundo especificamente para a barra.", + "appearance-background-opacity-label": "Opacidade do fundo da barra", + "appearance-capsule-color-description": "Escolha uma cor para as cápsulas da barra, ou não use nenhuma para a cor de superfície padrão.", + "appearance-capsule-color-label": "Cor da cápsula", + "appearance-capsule-opacity-description": "Define o nível de opacidade para fundos dos widgets quando a cápsula é mostrada.", + "appearance-capsule-opacity-label": "Opacidade da cápsula", + "appearance-content-padding-description": "Ajusta o espaçamento entre as bordas da barra e os widgets.", + "appearance-content-padding-label": "Espaçamento do conteúdo", + "appearance-density-description": "Ajuste o preenchimento da barra para um visual compacto ou espaçoso.", + "appearance-density-label": "Densidade da barra", + "appearance-desc": "Personalize a aparência e a posição da barra.", + "appearance-display-mode-description": "Escolha quando a barra está visível.", + "appearance-enable-exclusion-zone-inset-description": "Reduza a zona de exclusão em 1 pixel físico para que as janelas alinhadas se estendam perfeitamente sob a borda da barra.", + "appearance-enable-exclusion-zone-inset-label": "Zona de exclusão interna", + "appearance-floating-description": "Exibe a barra como uma 'pílula' flutuante.", + "appearance-floating-label": "Barra flutuante", + "appearance-font-scale-description": "Ajustar a escala do tamanho da fonte para o texto exibido na barra.", + "appearance-font-scale-label": "Escala da fonte", + "appearance-frame-radius": "Raio Interno", + "appearance-frame-settings-description": "Ajuste a espessura da moldura e o raio interno", + "appearance-frame-settings-label": "Configurações da Moldura", + "appearance-frame-thickness": "Espessura", + "appearance-hide-on-overview-description": "Ocultar a barra e fechar os painéis quando a visão geral do compositor estiver ativa.", + "appearance-hide-on-overview-label": "Ocultar barra na visão geral", + "appearance-margins-description": "Ajuste as margens ao redor da barra flutuante.", + "appearance-margins-horizontal": "Margem horizontal", + "appearance-margins-vertical": "Margem vertical", + "appearance-outer-corners-description": "Exibe cantos curvados para fora na barra.", + "appearance-outer-corners-label": "Cantos externos", + "appearance-position-description": "Escolha onde posicionar a barra na tela.", + "appearance-position-label": "Posição da barra", + "appearance-show-capsule-description": "Mostra o fundo dos widgets.", + "appearance-show-capsule-label": "Mostrar cápsula", + "appearance-show-on-workspace-switch-description": "Mostrar a Bar automaticamente por um breve período quando o espaço de trabalho mudar.", + "appearance-show-on-workspace-switch-label": "Mostrar barra ao mudar de área de trabalho", + "appearance-show-outline-description": "Exibe uma borda visível ao redor de cada widget.", + "appearance-show-outline-label": "Mostrar contornos do widget", + "appearance-type-description": "Escolha o estilo da barra: Simples, Flutuante ou Emoldurada.", + "appearance-type-label": "Tipo de barra", + "appearance-use-separate-opacity-description": "Permitir usar um valor de opacidade separado para o fundo da barra.", + "appearance-use-separate-opacity-label": "Usar opacidade separada para as barras", + "appearance-widget-spacing-description": "Ajusta o espaçamento entre cada widget na barra.", + "appearance-widget-spacing-label": "Espaçamento de widgets", + "behavior-middle-click-command-description": "Comando a executar no clique do meio.", + "behavior-middle-click-command-label": "Comando do Clique do Meio", + "behavior-middle-click-command-placeholder": "niri msg action alternar visão geral", + "behavior-middle-click-description": "Escolha o que o clique do meio faz nas áreas vazias da Barra.", + "behavior-middle-click-follow-mouse-description": "Abrir o painel de clique do meio selecionado na posição do cursor.", + "behavior-middle-click-follow-mouse-label": "Clique do meio segue o mouse", + "behavior-middle-click-label": "Ação de clique do meio na Barra", + "behavior-right-click-command-description": "Comando a executar ao clicar com o botão direito.", + "behavior-right-click-command-label": "Comando do Botão Direito", + "behavior-right-click-command-placeholder": "notify-send \"Clicar com o botão direito\"", + "behavior-right-click-description": "Escolha o que o clique direito faz nas áreas vazias da barra.", + "behavior-right-click-follow-mouse-description": "Abrir o painel de clique direito selecionado na posição do cursor.", + "behavior-right-click-follow-mouse-label": "Clique direito segue o rato", + "behavior-right-click-label": "Ação de clique direito na Barra", + "behavior-wheel-wrap-description": "Quando ativado, a rolagem continua do último item para o primeiro.", + "behavior-wheel-wrap-label": "Circular", + "behavior-workspace-scroll-description": "Escolha o que a roda do mouse faz em áreas vazias da barra.", + "behavior-workspace-scroll-label": "Ação da roda do rato da Barra", + "behavior-workspace-scroll-option-content": "Conteúdo", + "behavior-workspace-scroll-option-workspace": "Área de trabalho", + "monitor-configure-widgets": "Configurar widgets", + "monitor-override-settings": "Substituir configurações globais", + "monitor-override-settings-description": "Usar definições personalizadas para este monitor.", + "monitor-reset-all": "Redefinir tudo", + "monitor-widgets-title": "Configuração do Widget - {monitor}", + "monitors-desc": "Mostra a barra em monitores específicos. O padrão é todos, se nenhum for escolhido.", + "monitors-desc-new": "Configure quais monitores exibem a Bar e personalize as configurações por monitor.", + "monitors-title": "Exibição nos monitores", + "title": "Barra", + "tray-blacklist-description": "Adicione regras de exclusão para a bandeja do sistema, suporta curingas (*).", + "tray-blacklist-label": "Lista Negra", + "tray-blacklist-placeholder": "p. ex. nm-applet, Fcitx*", + "tray-pin-application": "Fixar aplicativo", + "tray-unpin-application": "Desfixar aplicativo", + "use-global-widgets": "Usar widgets globais", + "widgets-desc": "Arraste os widgets para alterar a ordem. Use o menu do botão direito para transferir widgets entre seções ou removê-los.", + "widgets-title": "Posicionamento dos widgets" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Gere esquemas de cores a partir do seu papel de parede. Extrai automaticamente as cores para criar um tema coeso.", + "color-source-use-wallpaper-colors-label": "Usar cores do papel de parede", + "dark-mode-mode-description": "Ativa a mudança automática entre o modo claro e o modo escuro.", + "dark-mode-mode-label": "Agendamento do modo escuro", + "dark-mode-mode-manual": "Manual", + "dark-mode-mode-off": "Desligado", + "dark-mode-switch-description": "Muda para um tema mais escuro para facilitar a visualização à noite.", + "delete-error-description": "Falha ao excluir {scheme}", + "delete-error-title": "Falha ao excluir", + "delete-success-description": "{scheme} excluído com sucesso", + "delete-success-title": "Esquema de cores excluído", + "download-button": "Baixar mais", + "download-downloading": "Baixando...", + "download-empty": "Nenhum esquema de cores disponível", + "download-error-api-error": "Erro da API: {status}", + "download-error-description": "Falha ao baixar {scheme}", + "download-error-download-failed": "Falha no download com código de saída: {code}", + "download-error-invalid-response": "Formato de resposta da API inválido", + "download-error-no-files": "Nenhum arquivo encontrado para o esquema", + "download-error-parse-failed": "Falha ao analisar a resposta da API: {error}", + "download-error-rate-limit": "Limite de taxa da API do GitHub excedido", + "download-error-title": "Falha no download", + "download-fetching": "Buscando esquemas de cores disponíveis...", + "download-success-description": "{scheme} baixado com sucesso", + "download-success-title": "Esquema de cores baixado", + "download-title": "Baixar esquemas de cores", + "method-description": { + "content": "Esquema de Material Design com extração de cores de alta fidelidade que corresponde de perto às cores reais do conteúdo de origem.", + "dysfunctional": "Como Faithful, mas escolhe a segunda família de cores mais dominante como primária.", + "faithful": "Tenta manter-se próximo da cor de origem, enquanto gera uma paleta harmoniosa.", + "fruit-salad": "Esquema de Material Design que produz uma paleta divertida e vibrante com tons diversos e variados.", + "monochrome": "Esquema de Material Design usando uma escala de cinza de tom único com conteúdo cromático mínimo.", + "muted": "Gera tons mais suaves e dessaturados para uma aparência calma e discreta.", + "rainbow": "Esquema de Material Design que gera cores em todo o espectro para máxima diversidade de cores.", + "tonal-spot": "Algoritmo padrão do Material Design. Cria paletas equilibradas usando variações tonais da cor de origem.", + "vibrant": "Maximiza a saturação da cor para paletas ousadas e atraentes com tons vívidos." + }, + "predefined-desc": "Escolha entre uma coleção de esquemas de cores predefinidos.", + "predefined-generate-templates-label": "Gerar modelos para esquemas predefinidos", + "predefined-title": "Esquemas de cores predefinidos", + "sync-gsettings-description": "Corresponder o tema do sistema à variante clara ou escura ativa.", + "sync-gsettings-label": "Sincronizar tema do sistema", + "templates-desc": "Aplicar cores a aplicações externas.", + "templates-filter-description": "Mostrar templates de uma categoria específica.", + "templates-filter-label": "Filtrar por categoria", + "templates-misc-description": "Crie seus próprios modelos.", + "templates-misc-label": "Avançado", + "templates-misc-user-templates-description": "Ative apenas se souber o que está fazendo — consulte nossa documentação online.", + "templates-misc-user-templates-label": "Ativar modelos do usuário", + "templates-none-detected": "Nenhum detectado", + "templates-write-path": "Escreve: {filepath}", + "title": "Esquema de cores", + "wallpaper-method-description": "Escolha o seu método de geração de paleta favorito.", + "wallpaper-method-label": "Método de geração de paleta", + "wallpaper-monitor-source-description": "Selecione qual monitor usar para extrair as cores do papel de parede.", + "wallpaper-monitor-source-label": "Fonte de geração de cores" + }, + "connections": { + "authentication-required": "Autenticação necessária", + "bluetooth-auto-connect-description": "Conectar automaticamente a dispositivos emparelhados confiáveis quando o Bluetooth estiver ativado.", + "bluetooth-auto-connect-label": "Conectar automaticamente dispositivos emparelhados", + "bluetooth-devices-unnamed": "Dispositivos sem nome não são exibidos.", + "bluetooth-discoverable": "Este dispositivo é detectável como {hostName} enquanto esta aba de configurações estiver aberta.", + "bluetooth-rssi-polling-description": "Amostra periodicamente o RSSI de dispositivos conectados via bluetoothctl. Pode não estar disponível para todos os dispositivos; usa recursos mínimos quando ativado.", + "bluetooth-rssi-polling-interval-description": "Configurar com que frequência atualizar a intensidade do sinal para dispositivos conectados.", + "bluetooth-rssi-polling-interval-label": "Intervalo de sondagem", + "bluetooth-rssi-polling-label": "Sondagem de sinal Bluetooth", + "disable-discoverability-description": "Ocultar o seu dispositivo de dispositivos Bluetooth próximos.", + "disable-discoverability-label": "Desativar visibilidade do dispositivo", + "hide-unnamed-devices-description": "Ocultar dispositivos que aparecem apenas como endereços Bluetooth.", + "hide-unnamed-devices-label": "Ocultar dispositivos sem nome", + "pin-instructions": "Por favor, insira o código PIN exibido no seu dispositivo.", + "title": "Conexões", + "wifi-header-text": "Use o Wi-Fi para ver as redes disponíveis e conectar-se à internet." + }, + "control-center": { + "cards-desc": "Personalize quais controles aparecem na central de controle e em que ordem.", + "desc": "Configurar o posicionamento e o comportamento do painel da central de controle.", + "open-at-mouse-description": "Ao clicar com o botão direito numa área vazia da Bar, abre o Centro de Controle perto do cursor. Quando desativado, a definição de posição acima é usada em vez disso.", + "open-at-mouse-label": "Abrir no cursor ao clicar com o botão direito", + "position-description": "Escolha onde o painel da central de controle aparece quando aberto.", + "shortcuts-custom-button-command-description": "O comando a ser executado quando o botão é clicado.", + "shortcuts-custom-button-enable-on-state-logic-description": "Ativa um segundo ícone e um estado 'quente' com base em um comando de verificação.", + "shortcuts-custom-button-enable-on-state-logic-label": "Ativar lógica de estado ativado", + "shortcuts-custom-button-icon-description": "Selecione um ícone da biblioteca.", + "shortcuts-custom-button-on-clicked-label": "Comando de clique esquerdo", + "shortcuts-custom-button-on-middle-clicked-description": "Comando a executar quando o botão for clicado com o botão do meio.", + "shortcuts-custom-button-on-state-command-description": "Comando a ser executado para verificar se o botão deve estar no estado 'ligado'. Retorna 0 para ligado, diferente de zero para desligado.", + "shortcuts-custom-button-on-state-command-label": "Comando de verificação de estado ativado", + "shortcuts-custom-button-on-state-icon-description": "O ícone para o botão quando ele está no estado 'ligado'.", + "shortcuts-custom-button-on-state-icon-label": "Ícone de estado ativado", + "shortcuts-custom-button-state-checks-add": "Adicionar verificação de estado", + "shortcuts-custom-button-state-checks-command": "Comando a ser executado para esta verificação de estado", + "shortcuts-custom-button-state-checks-label": "Verificações de estado", + "shortcuts-custom-button-state-checks-remove": "Remover", + "shortcuts-custom-button-tooltip-description": "A dica de ferramenta a ser exibida ao passar o mouse sobre o botão.", + "shortcuts-custom-button-tooltip-label": "Dica de ferramenta", + "shortcuts-title": "Widgets de atalhos", + "system-monitor-disk-path-description": "Selecione qual ponto de montagem do disco o cartão do monitor do sistema no centro de controle deve monitorar.", + "system-monitor-disk-path-label": "Caminho do disco do monitor do sistema", + "title": "Central de controle" + }, + "desktop-widgets": { + "clock-enabled-description": "Mostrar um widget de relógio na área de trabalho.", + "clock-enabled-label": "Ativar widget de relógio", + "clock-format-label": "Formato", + "clock-height-description": "Altura do widget do relógio em pixels.", + "clock-height-label": "Altura", + "clock-minimal-mode-description": "Usar uma exibição de relógio minimalista com hora e data.", + "clock-minimal-mode-label": "Modo minimalista", + "clock-rounded-corners-description": "Use cantos arredondados para o fundo do widget.", + "clock-rounded-corners-label": "Cantos arredondados", + "clock-show-background-description": "Mostrar o contêiner de fundo para o widget de relógio.", + "clock-show-background-label": "Mostrar plano de fundo", + "clock-show-date-description": "Exiba a data atual abaixo da hora.", + "clock-show-date-label": "Mostrar data", + "clock-show-seconds-description": "Exibir segundos na hora.", + "clock-show-seconds-label": "Mostrar segundos", + "clock-style-analog": "Analógico", + "clock-style-binary": "Binário", + "clock-style-description": "Escolha o estilo de exibição do relógio.", + "clock-style-digital": "Digital", + "clock-style-label": "Estilo do relógio", + "clock-style-minimal": "Minimal", + "clock-width-description": "Largura do widget do relógio em pixels.", + "cpu-intensive-note": "Os widgets marcados com ! usam mais CPU e devem ser ativados com atenção.", + "edit-mode-button-label": "Entrar no modo de edição", + "edit-mode-controls-explanation": "Clique esquerdo e arraste: Mover ou redimensionar o widget.\nClique direito: Abrir as opções do menu de contexto.", + "edit-mode-description": "Ative o modo de edição para mover e reposicionar widgets do desktop. Quando ativado, os widgets exibem um contorno de arrastar e podem ser reposicionados.", + "edit-mode-exit-button": "Sair do modo de edição", + "edit-mode-grid-snap-label": "Alinhar à grade", + "edit-mode-grid-snap-scale-label": "Escala de ajuste à grelha", + "edit-mode-label": "Modo de edição", + "enabled-description": "Ativar ou desativar widgets do desktop completamente.", + "enabled-label": "Ativar widgets do desktop", + "general-desc": "Configure widgets que aparecem na sua área de trabalho.", + "general-title": "Widgets para desktop", + "media-player-enabled-description": "Mostrar um widget de reprodutor de mídia na área de trabalho.", + "media-player-enabled-label": "Ativar widget do reprodutor de mídia", + "media-player-rounded-corners-description": "Ativar cantos arredondados nas bordas do widget.", + "media-player-show-album-art-description": "Mostrar a capa do álbum e as informações da faixa (título e artista).", + "media-player-show-album-art-label": "Mostrar a capa do álbum e o título", + "media-player-show-background-description": "Mostrar o contêiner de fundo para o widget do reprodutor de mídia.", + "media-player-show-buttons-description": "Mostrar botões de controle de mídia (reproduzir/pausar, anterior, próximo) no widget do reprodutor de mídia.", + "media-player-show-buttons-label": "Mostrar botões", + "media-player-show-visualizer-description": "Mostrar a sobreposição do visualizador de áudio.", + "media-player-visualizer-type-description": "Escolha um tipo de visualização para o fundo do reprodutor de mídia na área de trabalho.", + "overview-enabled-description": "Mostrar widgets do desktop enquanto a visão geral do compositor estiver aberta.", + "overview-enabled-label": "Mostrar na visão geral", + "system-stat-layout-bottom": "Inferior", + "system-stat-layout-description": "Escolha como a legenda é exibida em relação ao gráfico.", + "system-stat-layout-label": "Layout", + "system-stat-layout-side": "Lado", + "system-stat-rounded-corners-description": "Usar cantos arredondados para o fundo do widget.", + "system-stat-rounded-corners-label": "Cantos Arredondados", + "system-stat-show-background-description": "Mostrar o contêiner de fundo para o widget de estatísticas do sistema.", + "system-stat-show-background-label": "Mostrar plano de fundo", + "system-stat-stat-type-description": "Escolhe qual estatística do sistema exibir.", + "system-stat-stat-type-label": "Tipo de Estatística", + "title": "Widgets do desktop", + "weather-enabled-description": "Mostrar um widget de clima na área de trabalho.", + "weather-enabled-label": "Ativar widget de clima", + "weather-show-background-description": "Mostrar o contêiner de fundo para o widget de clima." + }, + "display": { + "monitors-backlight-device-auto-option": "Padrão", + "monitors-backlight-device-description": "Selecione um dispositivo de retroiluminação para esta saída.", + "monitors-backlight-device-label": "Dispositivo de Retroiluminação", + "monitors-brightness-step-description": "Ajuste o tamanho do passo para alterações de brilho (roda do mouse e atalhos de teclado).", + "monitors-brightness-step-label": "Tamanho do passo do brilho", + "monitors-brightness-unavailable-ddc-disabled": "Controle de brilho indisponível. Ative \"Suporte de brilho externo\" para controlar o brilho desta tela.", + "monitors-brightness-unavailable-generic": "O controle de brilho não está disponível para esta tela.", + "monitors-desc": "Ajustar o brilho das configurações para cada tela.", + "monitors-enforce-minimum-description": "Resolve o problema da retroiluminação apagar completamente em alguns monitores com brilho em 0%.", + "monitors-enforce-minimum-label": "Forçar brilho mínimo (1%)", + "monitors-external-brightness-description": "Ativar suporte DDCUtil para controlar o brilho em monitores externos através do protocolo DDC/CI.", + "monitors-external-brightness-label": "Suporte de brilho externo", + "monitors-global-brightness-description": "Ajustar o brilho de todos os monitores de uma vez.", + "monitors-global-brightness-label": "Todos os monitores", + "monitors-title": "Configurações por monitor", + "night-light-auto-schedule-description": "Baseado no horário do pôr do sol e do nascer do sol em {location} — recomendado.", + "night-light-auto-schedule-label": "Agendamento automático", + "night-light-desc": "Reduz a emissão de luz azul para ajudar você a dormir melhor e diminuir o cansaço visual.", + "night-light-enable-description": "Aplica um filtro de cor quente para reduzir a emissão de luz azul.", + "night-light-enable-label": "Ativar luz noturna", + "night-light-force-activation-description": "Ignora o agendamento e aplica o filtro noturno imediatamente.", + "night-light-force-activation-label": "Forçar ativação", + "night-light-manual-schedule-description": "Defina horários personalizados para o nascer e o pôr do sol.", + "night-light-manual-schedule-label": "Agendamento manual", + "night-light-manual-schedule-select-start": "Selecionar hora de início", + "night-light-manual-schedule-select-stop": "Selecionar hora de término", + "night-light-manual-schedule-sunrise": "Horário do nascer do sol", + "night-light-manual-schedule-sunset": "Horário do pôr do sol", + "night-light-temperature-day": "Dia", + "night-light-temperature-day-description": "Controla a temperatura durante o dia.", + "night-light-temperature-description": "Defina o quão quente a cor será durante a noite e o dia.", + "night-light-temperature-label": "Temperatura da cor", + "night-light-temperature-night": "Noite", + "night-light-temperature-night-description": "Controla a temperatura durante a noite.", + "title": "Tela" + }, + "dock": { + "appearance-background-opacity-description": "Ajuste a opacidade do fundo da dock.", + "appearance-border-radius-description": "Ajustar o raio da borda da dock.", + "appearance-border-radius-label": "Raio da borda", + "appearance-colorize-icons-description": "Aplicar cores do tema aos ícones de aplicativos da dock (apenas aplicativos não focados).", + "appearance-colorize-icons-label": "Colorir ícones", + "appearance-dead-opacity-description": "Ajustar a opacidade dos ícones de aplicativos que não estão em execução.", + "appearance-dead-opacity-label": "Opacidade morta", + "appearance-desc": "Personalize o comportamento e a aparência da dock.", + "appearance-display-auto-hide": "Ocultar automaticamente", + "appearance-display-description": "Escolha como a dock se comporta.", + "appearance-display-exclusive": "Exclusivo", + "appearance-dock-indicator-description": "Mostrar um pequeno indicador quando o dock estiver oculto.", + "appearance-dock-indicator-label": "Indicador do dock", + "appearance-floating-distance-description": "Ajuste a distância de flutuação da borda da tela.", + "appearance-floating-distance-label": "Distância de flutuação da dock", + "appearance-group-apps-description": "Agrupar várias janelas da mesma app em uma única entrada do Dock.", + "appearance-group-apps-label": "Agrupar aplicativos semelhantes", + "appearance-group-click-action-cycle": "Alternar janelas", + "appearance-group-click-action-description": "Escolha o que o clique esquerdo faz para aplicações agrupadas.", + "appearance-group-click-action-label": "Ação ao clicar em aplicação agrupada", + "appearance-group-click-action-list": "Abrir lista de janelas", + "appearance-group-context-menu-mode-description": "Escolha como o menu de contexto se comporta para aplicativos agrupados.", + "appearance-group-context-menu-mode-extended": "Estendido", + "appearance-group-context-menu-mode-label": "Modo de menu de aplicativos agrupado", + "appearance-group-context-menu-mode-list": "Lista de janelas", + "appearance-group-indicator-style-description": "Escolha como os indicadores de execução agrupados exibem o estado da janela focada.", + "appearance-group-indicator-style-dots": "Pontos", + "appearance-group-indicator-style-label": "Estilo de indicador agrupado", + "appearance-group-indicator-style-number": "Número", + "appearance-hide-show-speed-description": "Ajuste a velocidade da animação de ocultar/mostrar da dock.", + "appearance-hide-show-speed-label": "Velocidade de ocultar/mostrar", + "appearance-icon-size-description": "Ajustar o tamanho geral do dock.", + "appearance-icon-size-label": "Tamanho do dock", + "appearance-inactive-indicators-description": "Exibir indicadores de pílula para todos os aplicativos, não apenas para o aplicativo ativo no momento.", + "appearance-inactive-indicators-label": "Indicadores de execução", + "appearance-indicator-color-description": "Escolha a cor do indicador da dock oculta.", + "appearance-indicator-color-label": "Cor do indicador", + "appearance-indicator-opacity-description": "Ajuste a opacidade do indicador da dock oculta.", + "appearance-indicator-opacity-label": "Opacidade do indicador", + "appearance-indicator-thickness-description": "Ativar um indicador da dock oculta mais espesso (6 px em vez de 3 px).", + "appearance-indicator-thickness-label": "Indicador mais espesso", + "appearance-launcher-icon-description": "Escolha o ícone usado para o Dock Launcher.", + "appearance-launcher-icon-label": "Ícone do Lançador", + "appearance-launcher-position-description": "Escolha onde o ícone do lançador aparece no dock.", + "appearance-launcher-position-end": "Fim", + "appearance-launcher-position-label": "Posição do lançador", + "appearance-launcher-position-start": "Iniciar", + "appearance-launcher-use-distro-logo-description": "Use o logo da sua distribuição como o ícone do Dock Launcher.", + "appearance-launcher-use-distro-logo-label": "Usar o logótipo da distro em vez do ícone", + "appearance-pinned-static-description": "Sempre posicione os ícones de aplicativos fixados à esquerda em ordem estática.", + "appearance-pinned-static-label": "Aplicativos fixados estaticamente", + "appearance-position-description": "Escolha onde o dock aparece na tela.", + "appearance-position-label": "Posição", + "appearance-show-launcher-icon-description": "Mostrar o ícone do lançador de aplicativos na dock.", + "appearance-show-launcher-icon-label": "Mostrar lançador de aplicações", + "appearance-sit-on-frame-description": "Alinhe o dock dentro da borda do quadro em vez de ficar por cima.", + "appearance-sit-on-frame-label": "O dock fica no quadro", + "appearance-type-attached": "Ancorado", + "appearance-type-description": "Escolha entre uma pílula flutuante ou uma barra estática anexada à borda.", + "appearance-type-floating": "Flutuante", + "appearance-type-label": "Estilo do dock", + "enabled-description": "Mostrar ou ocultar o dock completamente.", + "enabled-label": "Ativar dock", + "monitors-desc": "Mostrar dock em monitores específicos. O padrão é todos se nenhum for escolhido.", + "monitors-only-same-monitor-description": "Mostrar apenas aplicativos do monitor onde a dock está localizada.", + "monitors-only-same-monitor-label": "Apenas aplicativos do mesmo monitor", + "monitors-title": "Exibição no monitor", + "title": "Dock" + }, + "general": { + "copy-settings": "Copiar configurações", + "fonts-default-description": "Fonte principal usada em toda a interface.", + "fonts-default-label": "Fonte padrão", + "fonts-default-placeholder": "Selecione a fonte padrão...", + "fonts-default-scale-description": "Aumentar ou diminuir o tamanho do texto padrão.", + "fonts-default-scale-label": "Tamanho de fonte padrão", + "fonts-default-search-placeholder": "Pesquisar fontes...", + "fonts-desc": "Escolha as fontes usadas em toda a interface.", + "fonts-monospace-description": "Fonte monoespaçada usada para exibição de números e estatísticas.", + "fonts-monospace-label": "Fonte monoespaçada", + "fonts-monospace-placeholder": "Selecione a fonte monoespaçada...", + "fonts-monospace-scale-description": "Aumentar ou diminuir o tamanho do texto monoespaçado.", + "fonts-monospace-scale-label": "Tamanho da fonte monoespaçada", + "fonts-monospace-search-placeholder": "Pesquisar fontes monoespaçadas...", + "fonts-title": "Fontes", + "keybinds-conflict-description": "A combinação de teclas já está atribuída a {action}.", + "keybinds-conflict-title": "Conflito de atalhos de teclado", + "keybinds-description": "Configure as teclas de navegação globais para painéis e o lançador.", + "keybinds-down": "Mover para baixo", + "keybinds-enter": "Confirmar / Ação", + "keybinds-escape": "Fechar / Voltar", + "keybinds-left": "Mover para a esquerda", + "keybinds-modifier-description": "Atalhos devem incluir uma tecla modificadora (Ctrl ou Alt).", + "keybinds-modifier-title": "Modificador necessário", + "keybinds-remove": "Remover", + "keybinds-right": "Mover para a direita", + "keybinds-title": "Atalhos de teclado de navegação", + "keybinds-up": "Mover para cima", + "language-desc": "Escolha o seu idioma preferido para a aplicação.", + "language-select-auto-detect": "Automático", + "language-select-description": "Selecione o idioma usado na interface da aplicação.", + "language-select-label": "Idioma da aplicação", + "launch-setup-wizard": "Assistente de configuração", + "profile-desc": "Edite os detalhes do seu usuário e avatar.", + "profile-picture-description": "Sua foto de perfil que aparece em toda a interface.", + "profile-picture-label": "Foto de perfil", + "profile-select-avatar": "Selecionar imagem de avatar", + "profile-title": "Perfil", + "profile-tooltip": "Foto de perfil", + "reverse-scrolling-description": "Inverter a direção de rolagem interpretada.", + "reverse-scrolling-label": "Rolagem invertida", + "screen-corners-desc": "Personalize o arredondamento dos cantos da tela e os efeitos visuais.", + "screen-corners-radius-description": "Ajuste os cantos arredondados da tela.", + "screen-corners-radius-label": "Raio dos cantos da tela", + "screen-corners-radius-reset": "Redefinir raio dos cantos da tela", + "screen-corners-show-corners-description": "Exibe cantos arredondados na borda da tela.", + "screen-corners-show-corners-label": "Mostrar cantos da tela", + "screen-corners-solid-black-description": "Use preto sólido em vez da cor de fundo da barra.", + "screen-corners-solid-black-label": "Cantos pretos sólidos", + "screen-corners-title": "Cantos da tela", + "settings-copied": "Configurações copiadas para a área de transferência", + "smooth-scrolling-description": "Animar a rolagem de listas para uma experiência de roda mais suave.", + "smooth-scrolling-label": "Rolagem suave", + "tab-basics": "Básico", + "tab-keybinds": "Atalhos de teclado" + }, + "hooks": { + "color-generation-description": "Comando a ser executado quando a geração de cores for concluída (p. ex., após a tematização baseada no papel de parede).", + "color-generation-label": "Cores geradas", + "color-generation-placeholder": "ex. notify-send \"Cores\" \"Gerados\"", + "info-command-info-description": "• Comandos são executados via shell (sh -lc)
• Comandos rodam em segundo plano (desanexados)
• Botões de teste executam com os valores atuais", + "info-parameters-description": "• Wallpaper hook: $1 = caminho do papel de parede, $2 = nome da tela, $3 = tema (escuro/claro)
• Theme toggle hook: $1 = true/false (estado do modo escuro)
• Screen lock/unlock hooks: $1 = lock/unlock (estado de bloqueio de tela)
• Performance mode hooks: Sem parâmetros
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "Parâmetros disponíveis", + "noctalia-started-description": "Comando para executar quando o Noctalia terminar de carregar.", + "noctalia-started-label": "Noctalia iniciado", + "noctalia-started-placeholder": "p. ex. notify-send 'Noctalia Carregado'", + "performance-mode-disabled-description": "Comando a ser executado quando o modo de desempenho Noctalia é desabilitado.", + "performance-mode-disabled-label": "Modo de desempenho desabilitado", + "performance-mode-disabled-placeholder": "p. ex. notify-send \"Desempenho\" \"Modo desabilitado\"", + "performance-mode-enabled-description": "Comando a ser executado quando o modo de desempenho Noctalia é habilitado.", + "performance-mode-enabled-label": "Modo de desempenho habilitado", + "performance-mode-enabled-placeholder": "p. ex. notify-send \"Desempenho\" \"Modo habilitado\"", + "screen-lock-description": "Comando a ser executado quando a tela é bloqueada.", + "screen-lock-label": "Tela bloqueada", + "screen-lock-placeholder": "p. ex. notify-send \"Tela\" \"Bloqueada\"", + "screen-unlock-description": "Comando a ser executado quando a tela é desbloqueada.", + "screen-unlock-label": "Tela desbloqueada", + "screen-unlock-placeholder": "p. ex. notify-send \"Tela\" \"Desbloqueada\"", + "session-description": "Comando a ser executado antes do desligamento ou reinicialização. Recebe o tipo de ação como $1 (desligamento/reinicialização).", + "session-label": "Fim da sessão", + "session-placeholder": "p. ex. notify-send \"Session\" \"$1\"", + "system-hooks-desc": "Configure comandos para serem executados quando eventos do sistema ocorrerem.", + "system-hooks-enable-description": "Ativa ou desativa todos os comandos de hook.", + "system-hooks-enable-label": "Ativar hooks", + "system-hooks-title": "Ganchos do sistema", + "theme-changed-description": "Comando a ser executado quando o tema alterna entre modo escuro e claro.", + "theme-changed-label": "Tema alterado", + "theme-changed-placeholder": "p. ex. notify-send \"Tema\" \"Alternado\"", + "title": "Hooks", + "wallpaper-changed-description": "Comando a ser executado quando o papel de parede muda.", + "wallpaper-changed-label": "Papel de parede alterado", + "wallpaper-changed-placeholder": "p. ex. notify-send \"Papel de parede\" \"Alterado\"" + }, + "idle": { + "custom-add": "Adicionar comando", + "custom-description": "Executa um comando de shell após um período de inatividade.", + "custom-entry-command": "Comando", + "custom-entry-delete": "Excluir", + "custom-entry-edit": "Editar Comando Personalizado", + "custom-entry-name": "Nome", + "custom-entry-name-placeholder": "p. ex. Desligar luzes", + "custom-entry-new": "Novo Comando Personalizado", + "custom-entry-timeout": "Tempo de inatividade", + "custom-entry-timeout-format": "{count} segundo", + "custom-entry-timeout-format-plural": "{count} segundos", + "custom-entry-unnamed": "Comando sem nome", + "custom-label": "Comandos de inatividade personalizados", + "enable-description": "Desligar a tela, bloquear ou suspender automaticamente após um período de inatividade.", + "enable-label": "Ativar gerenciamento de inatividade", + "fade-duration-description": "Segundos para a animação de esmaecimento para preto antes de cada ação ser acionada. Qualquer movimento do rato cancela o esmaecimento.", + "fade-duration-label": "Duração do esmaecimento", + "lock-description": "Segundos de inatividade antes da ativação da tela de bloqueio.", + "lock-label": "Bloquear tela", + "resume-command-label": "Retomar comando", + "screen-off-description": "Segundos de inatividade antes que os monitores sejam desligados.", + "screen-off-label": "Desligar tela", + "status-description": "Tempo ocioso conforme relatado pelo compositor.", + "status-label": "Tempo ocioso", + "suspend-description": "Segundos de inatividade antes que o sistema suspenda.", + "tab-behavior": "Comportamento", + "tab-custom": "Personalizado", + "timeouts-description": "Defina como 0 para desativar uma etapa. Os tempos limite são pausados enquanto o 'Manter acordado' estiver ativo.", + "timeouts-label": "Tempos limite", + "title": "Inatividade", + "unavailable": "A monitorização nativa de inatividade não está disponível neste Compositor." + }, + "indicator": { + "default-value": "Padrão: {value}", + "system-default": "Padrão do sistema" + }, + "launcher": { + "clipboard-desc": "Acesse e gerencie seu histórico da área de transferência a partir do iniciador.", + "execute-desc": "Configurar como os aplicativos são iniciados.", + "execute-title": "Execução", + "settings-annotation-tool-description": "Comando para executar ao clicar no botão de anotação no histórico da área de transferência. A imagem será enviada para este comando.", + "settings-annotation-tool-label": "Ferramenta de anotação", + "settings-annotation-tool-placeholder": "p. ex. 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Colar automaticamente o item selecionado da área de transferência. Requer wtype.", + "settings-auto-paste-label": "Colar automaticamente", + "settings-clip-chips-description": "Mostrar uma barra de abas para filtrar o histórico da área de transferência por tipo (Imagens, Links, Ficheiros, Código, etc.).", + "settings-clip-chips-label": "Ativar chips de categoria", + "settings-clip-preview-description": "Mostra uma pré-visualização do conteúdo da área de transferência ao usar o comando >clip.", + "settings-clip-preview-label": "Ativar pré-visualização da área de transferência", + "settings-clip-smart-icons-description": "Mostrar ícones específicos para links, arquivos, cores e código em vez de um ícone de área de transferência genérico.", + "settings-clip-smart-icons-label": "Ativar ícones inteligentes", + "settings-clip-wrap-text-description": "Quebrar o texto na lista da área de transferência em vez de truncá-lo.", + "settings-clip-wrap-text-label": "Quebrar texto da área de transferência", + "settings-clipboard-history-description": "Acesse itens copiados anteriormente a partir do lançador.", + "settings-clipboard-history-label": "Ativar histórico da área de transferência", + "settings-clipboard-watch-image-description": "Cadeia de comando completa passada para wl-paste para alterações de imagem (requer reinício).", + "settings-clipboard-watch-image-label": "Comando de Vigilância de Imagens", + "settings-clipboard-watch-text-description": "Cadeia de comando completa passada para wl-paste para alterações de texto (requer reinício).", + "settings-clipboard-watch-text-label": "Comando de observação de texto", + "settings-custom-launch-prefix-description": "Prefixar comandos com um inicializador personalizado (p. ex. 'runapp' para integração systemd).", + "settings-custom-launch-prefix-enabled-description": "Usar um prefixo personalizado para inicializar aplicativos em vez do método padrão.", + "settings-custom-launch-prefix-enabled-label": "Habilitar prefixo de inicialização personalizado", + "settings-custom-launch-prefix-label": "Prefixo de inicialização personalizado", + "settings-density-description": "Ajuste o tamanho dos ícones de aplicativos e a densidade do Launcher.", + "settings-density-label": "Densidade", + "settings-desc": "Personalize o comportamento e a aparência do lançador.", + "settings-enable-session-search-description": "Mostrar ações de sessão (bloquear, desligar, reiniciar, etc.) ao pesquisar no Launcher.", + "settings-enable-session-search-label": "Ativar pesquisa de sessão", + "settings-enable-settings-search-description": "Mostrar os resultados das configurações ao pesquisar no Launcher.", + "settings-enable-settings-search-label": "Ativar pesquisa nas configurações", + "settings-enable-windows-search-description": "Pesquisar e focar janelas ativas.", + "settings-enable-windows-search-label": "Ativar pesquisa de janelas", + "settings-grid-view-description": "Exibir itens em uma grade em vez de uma lista.", + "settings-icon-mode-description": "Use ícones nativos do sistema em vez de ícones Tabler.", + "settings-icon-mode-label": "Usar ícones nativos", + "settings-ignore-mouse-input-description": "Desativar a interação do mouse e a roda de rolagem no iniciador.", + "settings-ignore-mouse-input-label": "Ignorar entrada do mouse", + "settings-overlay-layer-description": "Exibe o lançador na camada de sobreposição, acima de janelas em tela cheia. Quando ativado, o lançador não será anexado à barra.", + "settings-overlay-layer-label": "Mostrar acima da tela cheia", + "settings-position-description": "Escolha onde o painel do lançador aparece.", + "settings-show-categories-description": "Mostrar abas de categorias para filtrar aplicativos.", + "settings-show-categories-label": "Mostrar categorias", + "settings-show-icon-background-description": "Mostrar um fundo retangular arredondado atrás dos ícones.", + "settings-show-icon-background-label": "Mostrar fundo do ícone", + "settings-sort-by-usage-description": "Quando ativado, os aplicativos mais usados aparecem primeiro na lista.", + "settings-sort-by-usage-label": "Ordenar por mais usados", + "settings-terminal-command-description": "Comando para iniciar um terminal. P. ex. 'kitty -e' ou 'gnome-terminal --'.", + "settings-terminal-command-label": "Comando do terminal", + "settings-use-app2unit-description": "Usa um método de inicialização alternativo para gerenciar melhor os processos de aplicativos e evitar problemas.", + "settings-use-app2unit-label": "Usar App2Unit para iniciar aplicativos", + "settings-view-mode-description": "Escolha o layout para as entradas do Lançador.", + "settings-view-mode-label": "Modo de visualização", + "title": "Lançador" + }, + "location": { + "auto-locate-description": "Deteta automaticamente a sua localização periodicamente usando o seu endereço IP.", + "auto-locate-label": "Auto-Localização", + "calendar-cards-desc": "Organize e ative/desative cartões no painel do calendário.", + "calendar-cards-title": "Cartões de calendário", + "calendar-header-label": "Cabeçalho do calendário", + "calendar-month-label": "Mês do calendário", + "date-time-12hour-format-description": "Exibe a hora no formato de 12 horas na tela de bloqueio e no calendário. O relógio da barra tem suas próprias configurações.", + "date-time-12hour-format-label": "Use o formato de hora de 12 horas", + "date-time-desc": "Personalize como a data e a hora aparecem.", + "date-time-first-day-of-week-automatic": "Automático (usar localização do sistema)", + "date-time-first-day-of-week-description": "Escolha qual dia começa a semana no calendário.", + "date-time-first-day-of-week-label": "Primeiro dia da semana", + "date-time-show-events-description": "Exibir eventos no painel do calendário.", + "date-time-show-events-label": "Mostrar eventos do calendário", + "date-time-title": "Data e hora", + "date-time-use-analog-description": "Mostrar um relógio de estilo analógico na janela do calendário.", + "date-time-use-analog-label": "Use um relógio de estilo analógico", + "date-time-week-numbers-description": "Exibe a semana do ano (p. ex. Semana 38) no calendário.", + "date-time-week-numbers-label": "Mostrar números da semana", + "geolocate-now-button": "Localizar agora", + "location-desc": "Obtenha previsão do tempo e agendamento de luz noturna precisos definindo sua localização.", + "location-search-description": "p. ex. São Paulo, SP", + "location-search-label": "Procurar por uma localização", + "location-search-placeholder": "Digite o nome da localização", + "location-title": "Sua localização", + "weather-desc": "Escolha sua unidade de temperatura preferida.", + "weather-enabled-description": "Mostrar informações meteorológicas em toda a interface e buscar dados do clima. Quando desativado, todos os elementos do clima serão ocultados e nenhuma solicitação de rede será feita.", + "weather-enabled-label": "Ativar clima", + "weather-fahrenheit-description": "Exibe a temperatura em Fahrenheit em vez de Celsius.", + "weather-fahrenheit-label": "Exibir temperatura em Fahrenheit (°F)", + "weather-hide-city-description": "Oculte o nome da cidade das exibições de clima em toda a interface.", + "weather-hide-city-label": "Ocultar nome da cidade", + "weather-hide-timezone-description": "Oculte a abreviação do fuso horário das exibições de clima em toda a interface.", + "weather-hide-timezone-label": "Ocultar fuso horário", + "weather-show-effects-description": "Mostra efeitos visuais adicionais (como chuva, neve ou relâmpagos) no cartão de clima.", + "weather-show-effects-label": "Exibir efeitos climáticos", + "weather-show-in-calendar-description": "Mostre a previsão do tempo diária diretamente na sua visualização de calendário.", + "weather-show-in-calendar-label": "Exibir clima no calendário", + "weather-talia-mascot-always-description": "Sempre use Talia emojis em vez de tabler icons.", + "weather-talia-mascot-always-label": "Sempre mostrar os emojis de clima da Talia" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "Quando o fprintd (autenticação por impressão digital) está ativo, esta opção permite-lhe iniciar sessão com a sua palavra-passe em vez de uma impressão digital.", + "allow-password-with-fprintd-label": "Permitir login por senha com fprintd", + "auto-start-auth-description": "Inicia automaticamente a autenticação de impressão digital sem exigir uma pressão de tecla ou um clique de botão.", + "auto-start-auth-label": "Autenticação de início automático", + "clock-format-description": "Personalize o formato do relógio usando tokens de sintaxe de data/hora.", + "clock-format-label": "Formato do relógio", + "clock-style-analog": "Analógico", + "clock-style-custom": "Personalizado", + "clock-style-description": "Escolha o estilo visual do relógio na Tela de Bloqueio.", + "clock-style-digital": "Digital", + "clock-style-label": "Estilo do Relógio", + "compact-lockscreen-description": "Mostrar apenas a entrada de login e os controles do sistema, ocultando os widgets de clima e mídia.", + "compact-lockscreen-label": "Tela de bloqueio compacta", + "enable-lockscreen-media-controls-description": "Mostrar controles interativos de reprodução de mídia na tela de bloqueio.", + "enable-lockscreen-media-controls-label": "Controles de Mídia do Ecrã de Bloqueio", + "lock-on-suspend-description": "Bloquear a tela automaticamente ao suspender o sistema.", + "lock-on-suspend-label": "Bloquear ao suspender", + "lock-screen-animations-description": "Ativar ou desativar as animações do ecrã de bloqueio.", + "lock-screen-animations-label": "Animações da Tela de Bloqueio", + "lock-screen-blur-strength-description": "Aplica um efeito de desfoque ao papel de parede da tela de bloqueio.", + "lock-screen-blur-strength-label": "Intensidade do desfoque da tela de bloqueio", + "lock-screen-tint-strength-description": "Aplica uma sobreposição de tonalidade ao papel de parede da tela de bloqueio.", + "lock-screen-tint-strength-label": "Intensidade do matiz da tela de bloqueio", + "monitors-desc": "Mostrar tela de bloqueio em monitores específicos. O padrão é em todos, se nenhum for escolhido.", + "password-chars-description": "Ícones fofos usados para ocultar sua senha.", + "password-chars-label": "Ícones de senha aleatórios", + "show-hibernate-description": "Mostrar a opção 'hibernar' nos controles de energia.", + "show-hibernate-label": "Mostrar hibernar", + "show-session-buttons-description": "Permitir acesso às configurações de energia a partir da tela de bloqueio.", + "show-session-buttons-label": "Controles de energia", + "title": "Tela de bloqueio" + }, + "network": { + "bluetooth-description": "Ativar gerenciamento de Bluetooth.", + "bluetooth-rssi-polling-description": "Amostre periodicamente o RSSI para dispositivos conectados via bluetoothctl. Pode não estar disponível para todos os dispositivos; usa recursos mínimos quando ativado.", + "bluetooth-rssi-polling-label": "Sondagem de sinal Bluetooth", + "desc": "Gerencie conexões Wi-Fi e Bluetooth.", + "wifi-description": "Gerenciar redes sem fio (requer NetworkManager)." + }, + "notifications": { + "duration-critical-urgency-description": "Por quanto tempo as notificações de prioridade crítica permanecem visíveis.", + "duration-critical-urgency-label": "Urgência crítica", + "duration-desc": "Configure por quanto tempo as notificações permanecem visíveis com base no nível de urgência.", + "duration-low-urgency-description": "Por quanto tempo as notificações de baixa prioridade permanecem visíveis.", + "duration-low-urgency-label": "Urgência baixa", + "duration-normal-urgency-description": "Por quanto tempo as notificações de prioridade normal permanecem visíveis.", + "duration-normal-urgency-label": "Urgência normal", + "duration-respect-expire-description": "Usa o tempo de expiração definido na notificação.", + "duration-respect-expire-label": "Respeitar tempo de expiração", + "duration-title": "Duração da notificação", + "history-clear-dismiss-description": "Limpar notificação do histórico ao descartar.", + "history-clear-dismiss-label": "Limpar ao dispensar", + "history-critical-urgency-description": "Salvar notificações de prioridade crítica no histórico.", + "history-critical-urgency-label": "Salvar urgência crítica no histórico", + "history-desc": "Controlar quais notificações são salvas no histórico com base no nível de urgência.", + "history-low-urgency-description": "Salvar notificações de prioridade baixa no histórico.", + "history-low-urgency-label": "Salvar urgência baixa no histórico", + "history-normal-urgency-description": "Salvar notificações de prioridade normal no histórico.", + "history-normal-urgency-label": "Salvar urgência normal no histórico", + "monitors-desc": "Mostra a notificação em monitores específicos. O padrão é todos, se nenhum for escolhido.", + "rules-action-block": "Bloco", + "rules-action-block-desc": "Ignora completamente.", + "rules-action-hide": "Ocultar", + "rules-action-hide-desc": "Sem pop-up, sem som, adiciona ao histórico.", + "rules-action-label": "Ação", + "rules-action-mute": "Silenciar", + "rules-action-mute-desc": "Sem som, ainda mostra o pop-up e no histórico.", + "rules-add": "Adicionar regra", + "rules-delete": "Excluir regra", + "rules-description": "Corresponder nome da aplicação ou conteúdo. As regras são verificadas por ordem, e a primeira correspondência é aplicada.", + "rules-edit": "Editar regra", + "rules-label": "Regras de Filtro", + "rules-pattern-label": "Padrão", + "rules-pattern-placeholder": "firefox, discord, ou /regex/", + "rules-tab": "Regras", + "settings-always-on-top-description": "Exibe notificações acima de janelas em tela cheia e outras camadas.", + "settings-background-opacity-description": "Ajustar a opacidade dos fundos das notificações.", + "settings-density-description": "Escolha a densidade do cartão de notificação.", + "settings-density-label": "Densidade", + "settings-desc": "Configure a aparência e o comportamento das notificações.", + "settings-do-not-disturb-description": "Desativa todos os pop-ups de notificação quando ativado.", + "settings-enabled-description": "Ativar ou desativar o daemon de notificações, requer reiniciar o Noctalia shell.", + "settings-enabled-label": "Ativar notificações", + "settings-location-description": "Onde as notificações aparecem na tela.", + "settings-markdown-description": "Renderizar o conteúdo da notificação usando formatação Markdown.", + "settings-markdown-label": "Ativar Markdown", + "sounds-desc": "Configurar efeitos sonoros e volume de notificações.", + "sounds-enabled-description": "Ativar efeitos sonoros para notificações recebidas.", + "sounds-enabled-label": "Ativar sons de notificação", + "sounds-excluded-apps-description": "Pular a reprodução do som de notificação configurado para aplicativos específicos que têm seus próprios sons integrados.", + "sounds-excluded-apps-label": "Aplicativos excluídos", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Caminho para o arquivo de som tocado para notificações de prioridade crítica.", + "sounds-files-critical-label": "Som de urgência crítica", + "sounds-files-critical-select-title": "Selecionar arquivo de som de urgência crítica", + "sounds-files-desc": "Configurar arquivos de som personalizados para diferentes níveis de urgência de notificação.", + "sounds-files-low-description": "Caminho para o arquivo de som tocado para notificações de prioridade baixa.", + "sounds-files-low-label": "Som de urgência baixa", + "sounds-files-low-select-title": "Selecionar arquivo de som de urgência baixa", + "sounds-files-normal-description": "Caminho para o arquivo de som tocado para notificações de prioridade normal.", + "sounds-files-normal-label": "Som de urgência normal", + "sounds-files-normal-select-title": "Selecionar arquivo de som de urgência normal", + "sounds-files-placeholder": "Digitar o caminho para o arquivo de som", + "sounds-files-select-file": "Selecionar arquivo de som", + "sounds-files-title": "Arquivos de som", + "sounds-files-unified-description": "Caminho para o arquivo de som tocado para notificações.", + "sounds-files-unified-label": "Som de notificação", + "sounds-files-unified-select-title": "Selecionar arquivo de som de notificação", + "sounds-separate-description": "Usar arquivos de som diferentes para notificações de prioridade baixa, normal e crítica.", + "sounds-separate-label": "Usar sons diferentes por prioridade", + "sounds-title": "Configurações de som", + "sounds-unavailable-description": "Instale o Qt6 Multimedia para habilitar sons de notificação.", + "sounds-unavailable-label": "Sons de notificação indisponíveis", + "sounds-volume-description": "Ajustar o nível de volume para sons de notificação.", + "sounds-volume-label": "Volume do som", + "toast-battery-description": "Mostrar um aviso quando o nível da bateria cair abaixo desta percentagem.", + "toast-battery-label": "Aviso de bateria", + "toast-desc": "Configurar a aparência e o comportamento do toast.", + "toast-keyboard-description": "Mostrar uma notificação quando o layout do teclado mudar.", + "toast-keyboard-label": "Layout do teclado", + "toast-media-description": "Mostrar um toast quando o estado de reprodução de mídia mudar.", + "toast-media-label": "Multimédia" + }, + "osd": { + "always-on-top-description": "Exibir OSD acima de janelas em tela cheia e outras camadas.", + "always-on-top-label": "Sempre no topo", + "background-opacity-description": "Controla a transparência do fundo do OSD.", + "background-opacity-label": "Opacidade do fundo", + "description": "Configure indicadores na tela, como volume e brilho.", + "duration-auto-hide-description": "Ajusta o tempo antes da OSD desaparecer.", + "duration-auto-hide-label": "Ocultar após", + "duration-desc": "Quanto tempo a OSD permanece visível antes de ocultar automaticamente.", + "duration-title": "Tempo para ocultar", + "enabled-description": "Mostrar alterações de volume e brilho em tempo real.", + "enabled-label": "Ativar exibição na tela (OSD)", + "events-desc": "Selecione quais eventos acionam a exibição na tela.", + "general-desc": "Configurar a visibilidade e o comportamento do OSD.", + "location-description": "Onde a exibição na tela aparece.", + "monitors-desc": "Mostrar a OSD em monitores específicos. Padrão é todos se nenhum for escolhido.", + "title": "Exibição na tela", + "types-brightness-description": "Mostrar o OSD quando o brilho da tela mudar.", + "types-brightness-label": "Brilho", + "types-custom-text-description": "Mostrar OSD para mensagens de texto personalizadas via IPC.", + "types-custom-text-label": "Texto personalizado", + "types-desc": "Selecione os eventos que acionam o OSD.", + "types-input-volume-description": "Mostrar o OSD quando o volume do microfone mudar.", + "types-input-volume-label": "Volume de entrada", + "types-lockkey-description": "Mostrar o OSD quando Bloq Caps, Bloq Num ou Bloq Rol forem alternadas.", + "types-lockkey-label": "Teclas de bloqueio", + "types-media-description": "Mostrar OSD quando o estado de reprodução de mídia mudar (reproduzir, pausar, pular).", + "types-media-label": "Reprodução de mídia", + "types-title": "Eventos de disparo OSD", + "types-volume-description": "Mostrar o OSD quando o volume de saída de áudio mudar.", + "types-volume-label": "Volume de saída" + }, + "plugins": { + "auto-update": "Atualizar plugins automaticamente", + "auto-update-description": "Atualizar automaticamente todos os plugins quando a shell iniciar.", + "available-description": "Navegue e instale plugins a partir de fontes configuradas.", + "available-label": "Plugins disponíveis", + "available-no-plugins-description": "Verifique as fontes do seu plugin ou atualize a lista.", + "available-no-plugins-label": "Nenhum plugin disponível", + "check-for-updates": "Verificar atualizações", + "checking-for-updates": "Verificando atualizações...", + "collision-already-installed": "Este plugin já está instalado", + "collision-custom-version-exists": "Uma versão personalizada de \"{source}\" já está instalada", + "collision-official-version-exists": "A versão oficial deste plugin já está instalada", + "development-disable": "Desativar o modo de desenvolvimento para este plugin", + "development-enable": "Ativar o modo de desenvolvimento para este plugin.", + "filter-downloaded": "Baixado", + "filter-not-downloaded": "Não baixado", + "filter-tags-description": "Filtrar plugins por categoria ou estado de download.", + "filter-tags-label": "Etiquetas", + "hot-reload-description": "Recarrega automaticamente os plugins quando seus arquivos são alterados. Útil para o desenvolvimento de plugins.", + "hot-reload-label": "Recarga a quente (modo de desenvolvimento)", + "hot-reloaded": "Plugin recarregado: {name}", + "install-error": "Falha ao instalar: {error}", + "install-incompatible": "{plugin} requer Noctalia v{version} ou superior", + "install-success": "{plugin} instalado com sucesso", + "installed-description": "Gerenciar e configurar todos os plugins instalados localmente.", + "installed-label": "Plugins instalados", + "installed-no-plugins-description": "Instale os plugins da seção \"Disponível\".", + "installed-no-plugins-label": "Nenhum plugin instalado", + "installing": "Instalando {plugin}...", + "notify-updates": "Notificações de atualização de plugins", + "notify-updates-description": "Mostrar um popup de notificação quando novas atualizações de plugins estiverem disponíveis.", + "open-plugin-page": "Abrir página do plugin", + "open-plugins-tab": "Abrir configurações de plugins", + "plugin-settings-title": "Configurações do {plugin}", + "refresh-refreshing": "Atualizando a lista de plugins...", + "refresh-tooltip": "Atualizar plugins disponíveis", + "settings-error-not-loaded": "Plugin não carregado", + "settings-saved": "Configurações do plugin salvas", + "settings-tooltip": "Configurações do plugin", + "source-custom": "Fonte personalizada", + "sources-add-custom": "Adicionar repositório personalizado", + "sources-add-dialog-description": "Adicionar um repositório GitHub como fonte de plugin.", + "sources-add-dialog-error": "Falha ao adicionar a fonte do plugin", + "sources-add-dialog-name": "Nome do repositório", + "sources-add-dialog-name-placeholder": "Meus plugins personalizados", + "sources-add-dialog-success": "Fonte do plugin adicionada com sucesso", + "sources-add-dialog-title": "Adicionar fonte do plugin", + "sources-add-dialog-url": "URL do repositório", + "sources-description": "Gerenciar repositórios de plugins.", + "sources-placeholder": "Meu repositório legal", + "sources-remove-tooltip": "Remover código fonte do plugin", + "title": "Plugins", + "translations-reloaded": "Traduções recarregadas: {name}", + "uninstall-dialog-description": "Tem certeza de que deseja desinstalar {plugin}? Isso removerá todos os dados do plugin.", + "uninstall-dialog-title": "Desinstalar plugin", + "uninstall-error": "Falha ao desinstalar: {error}", + "uninstall-success": "{plugin} desinstalado com sucesso", + "uninstalling": "Desinstalando {plugin}...", + "update-all": "Atualizar tudo ({count})", + "update-all-success": "Todos os plugins foram atualizados com sucesso", + "update-available": "Nova atualização de plugin disponível", + "update-available-plural": "Novas atualizações de plugin disponíveis ({count})", + "update-error": "Falha ao atualizar o plugin: {plugin}: {error}", + "update-pending": "v{current} → v{new} (requer Noctalia v{required})", + "update-success": "{plugin} atualizado para a versão {version}", + "update-version": "v{current} → v{new}", + "updating": "Atualizando..." + }, + "region": { + "title": "Região" + }, + "session-menu": { + "countdown-duration-description": "Definir por quanto tempo o temporizador de contagem regressiva dura antes de executar ações de energia.", + "countdown-duration-label": "Duração da contagem regressiva", + "enable-countdown-description": "Mostrar um temporizador de contagem regressiva antes de executar ações de energia.", + "enable-countdown-label": "Ativar temporizador de contagem regressiva", + "entries-desc": "Personalizar quais ações de energia aparecem no menu de sessão e em que ordem.", + "entries-title": "Ações de energia", + "entry-settings-command-description": "Comando personalizado para executar para esta ação. Deixe vazio para usar o comando padrão do sistema.", + "entry-settings-command-placeholder": "p. ex. systemctl poweroff", + "entry-settings-default-command-lock": "Tela de bloqueio interna (sem comando)", + "entry-settings-default-command-logout": "Logout interno (sem comando)", + "entry-settings-default-info-description": "Se nenhum comando personalizado for especificado, o comando padrão do sistema será usado.", + "entry-settings-default-info-label": "Comando padrão", + "entry-settings-keybind-description": "Atribua uma combinação de teclas para acionar esta ação quando o menu de sessão estiver aberto.", + "entry-settings-keybind-placeholder": "Pressione Gravar para atribuir uma tecla...", + "entry-settings-keybind-recording": "Gravando... Pressione qualquer combinação de teclas", + "entry-settings-title": "Configurar {entry}", + "entry-settings-tooltip": "Configurar comando", + "general-desc": "Configurar o comportamento e a aparência do painel do menu de sessão.", + "large-buttons-layout-description": "Escolha como os botões do menu de sessão são exibidos.", + "large-buttons-layout-label": "Layout de botões grandes", + "large-buttons-style-description": "Exibir o menu de sessão com botões grandes em um layout de grade.", + "large-buttons-style-label": "Estilo de botões grandes", + "position-description": "Escolha onde o painel do Menu de sessão aparece quando aberto.", + "show-header-description": "Exibir o título e o botão de fechar na parte superior do menu de sessão.", + "show-header-label": "Mostrar cabeçalho", + "show-keybinds-description": "Exibir dicas de atalhos de teclado nas opções de sessão.", + "show-keybinds-label": "Mostrar atalhos de teclado" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Manter os widgets da área de trabalho visíveis enquanto o modo de desempenho Noctalia estiver ativado.", + "noctalia-performance-disable-desktop-widgets-label": "Ativar widgets de ambiente de trabalho no modo de desempenho", + "noctalia-performance-disable-wallpaper-description": "Mantenha os papéis de parede da área de trabalho, da visão geral e da tela de bloqueio visíveis enquanto o modo de desempenho Noctalia estiver ativado.", + "noctalia-performance-disable-wallpaper-label": "Ativar renderização de papel de parede no modo de desempenho", + "title": "Sistema" + }, + "system-monitor": { + "critical-color-label": "Cor crítica", + "custom-highlight-colors-title-label": "Cores de destaque personalizadas", + "disk-available-label": "Espaço em disco disponível", + "disk-section-label": "Uso do disco", + "enable-dgpu-monitoring-description": "Atenção: Isto irá ativar sua GPU dedicada (NVIDIA/AMD), o que pode impactar significativamente a duração da bateria em laptops com gráficos híbridos.", + "enable-dgpu-monitoring-label": "Ativar o monitoramento da GPU dedicada", + "external-monitor-description": "Digite o comando ou caminho da aplicação para executar ao ativar a aplicação externa de monitoramento do sistema.", + "external-monitor-label": "Comando do monitor do sistema externo", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "Configure o comportamento e a aparência do monitor do sistema.", + "gpu-section-label": "Temperatura da GPU", + "highlight-colors-section-label": "Cores de destaque", + "polling-interval-label": "Intervalo de pesquisa", + "polling-section-description": "Configure a frequência com que cada métrica do sistema é atualizada.", + "polling-section-label": "Intervalos de pesquisa", + "threshold-critical": "Crítico", + "threshold-warning": "Aviso", + "thresholds-section-description": "Ajuste os limites de aviso/crítico e os intervalos de consulta para cada métrica do sistema.", + "use-custom-highlight-colors-description": "Quando desativado, são usadas as cores de destaque padrão do tema.", + "use-custom-highlight-colors-label": "Usar cores de destaque personalizadas", + "warning-color-label": "Cor de aviso" + }, + "user-interface": { + "allow-panels-without-bar-description": "Quando ativado, os painéis podem abrir em qualquer tela. Quando desativado, os painéis só abrirão em telas que tenham uma barra, o que pode reduzir o uso de memória.", + "allow-panels-without-bar-label": "Permitir painéis em telas sem uma barra", + "animation-disable-description": "Desative todas as animações para uma experiência mais rápida e responsiva.", + "animation-disable-label": "Desativar animações da interface do usuário", + "animation-speed-description": "Ajustar a velocidade global da animação.", + "animation-speed-label": "Velocidade da animação", + "animation-speed-reset": "Redefinir velocidade da animação", + "appearance-desc": "Personalize elementos visuais como dicas de ferramentas, bordas e sombras.", + "blur-behind-description": "Desfoca a área atrás das barras e painéis usando o protocolo de desfoque do compositor.", + "blur-behind-label": "Desfoque de fundo", + "box-border-description": "Exibe um contorno ao redor das áreas de conteúdo.", + "box-border-label": "Contorno do recipiente", + "box-border-radius-description": "Ajusta o arredondamento dos cantos das principais seções do layout, como barras laterais, cards e painéis de conteúdo.", + "box-border-radius-label": "Raio do contêiner", + "control-border-radius-description": "Controla a curvatura de elementos interativos, incluindo botões, alternâncias e campos de texto.", + "control-border-radius-label": "Raio de entrada", + "desc": "Personalize a aparência, a sensação e o comportamento da interface.", + "dim-desktop-description": "Escurecer a área de trabalho quando painéis ou menus estiverem abertos.", + "dim-desktop-label": "Dim área de trabalho", + "dimmer-opacity-description": "Definir o nível de opacidade para o escurecimento da área de trabalho.", + "dimmer-opacity-label": "Opacidade da área de trabalho escurecida", + "panel-background-opacity-description": "Definir a opacidade do fundo para todos os painéis (barra, iniciador, configurações, etc...).", + "panel-background-opacity-label": "Opacidade de fundo do painel", + "panels-attached-to-bar-description": "Os painéis travam na barra e nas bordas da tela, criando uma aparência perfeita com cantos invertidos elegantes.", + "panels-attached-to-bar-label": "Ajustar painéis às bordas", + "panels-overlay-description": "Garante que os painéis e a barra permaneçam visíveis, mesmo sobre aplicativos em tela cheia.", + "panels-overlay-label": "Manter painéis e barra no topo", + "scaling-description": "Altera o tamanho da interface geral do usuário, excluindo a barra.", + "scaling-label": "Escalonamento da interface", + "scrollbar-always-visible-description": "Manter as barras de rolagem visíveis sempre que o conteúdo for rolável, em vez de mostrá-las apenas ao passar o mouse.", + "scrollbar-always-visible-label": "Sempre mostrar barras de rolagem", + "settings-panel-header": "Painel de Definições", + "settings-panel-mode-description": "Escolha o layout das configurações (pode ser necessário reabrir).", + "settings-panel-mode-label": "Modo do painel de configurações", + "settings-panel-sidebar-card-style-description": "Envolve a barra lateral de configurações num fundo preenchido com cantos arredondados.", + "settings-panel-sidebar-card-style-label": "Estilo de Cartão da Barra Lateral", + "shadows-description": "Ativa sombras projetadas sob barras e painéis.", + "shadows-direction-description": "Escolha onde a sombra é projetada.", + "shadows-direction-label": "Direção da sombra", + "shadows-label": "Sombras projetadas", + "title": "Interface do usuário", + "tooltips-description": "Ativar ou desativar dicas de ferramentas em toda a interface.", + "tooltips-label": "Mostrar dicas de ferramenta", + "translucent-widgets-description": "Torne botões, abas e outros widgets dentro dos painéis semitransparentes.", + "translucent-widgets-label": "Widgets translúcidos" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Alfabético", + "automation-change-mode-description": "Escolha como os papéis de parede são selecionados ao alterar automaticamente.", + "automation-change-mode-label": "Modo de alteração", + "automation-custom-interval-description": "Digite o tempo como HH:MM (p. ex. 01:30).", + "automation-custom-interval-label": "Intervalo personalizado", + "automation-interval-description": "Com que frequência alterar os papéis de parede automaticamente.", + "automation-interval-label": "Intervalo do papel de parede", + "automation-random-wallpaper-description": "Agende mudanças aleatórias de papel de parede em intervalos regulares.", + "automation-scheduled-change-description": "Alterar automaticamente os papéis de parede em intervalos regulares.", + "automation-scheduled-change-label": "Alteração agendada", + "look-feel-edge-smoothness-description": "Aplica um efeito suave e esfumaçado à borda das transições.", + "look-feel-edge-smoothness-label": "Suavizar borda da transição", + "look-feel-fill-color-description": "Escolha uma cor de preenchimento que pode aparecer atrás do papel de parede.", + "look-feel-fill-mode-description": "Selecione como a imagem deve ser dimensionada para corresponder à resolução do seu monitor.", + "look-feel-fill-mode-label": "Modo de preenchimento", + "look-feel-skip-startup-transition-description": "Ignorar a animação do papel de parede ao iniciar a shell.", + "look-feel-skip-startup-transition-label": "Pular transição de inicialização", + "look-feel-title": "Aparência e estilo", + "look-feel-transition-duration-description": "Duração das animações de transição em segundos.", + "look-feel-transition-duration-label": "Duração da transição", + "look-feel-transition-type-description": "Tipo de animação ao alternar entre papéis de parede.", + "look-feel-transition-type-label": "Tipo de transição", + "settings-clear-cache-button": "Limpar cache", + "settings-clear-cache-description": "Limpar papéis de parede redimensionados em cache para liberar espaço em disco.", + "settings-clear-cache-label": "Cache de papéis de parede", + "settings-clear-cache-toast": "Cache de papel de parede limpa", + "settings-desc": "Controle como os papéis de parede são gerenciados e exibidos.", + "settings-enable-management-description": "Gerencie papéis de parede com o Noctalia. Desmarque se preferir usar outro aplicativo.", + "settings-enable-management-label": "Ativar gerenciamento de papel de parede", + "settings-enable-overview-description": "Aplica um papel de parede desfocado e escurecido à tela de visão geral.", + "settings-enable-overview-label": "Ativar papel de parede de visão geral", + "settings-folder-description": "Caminho para a sua pasta principal de papéis de parede.", + "settings-folder-label": "Pasta de papéis de parede", + "settings-hide-wallpaper-filenames-tooltip-hide": "Ocultar nomes de arquivo", + "settings-hide-wallpaper-filenames-tooltip-show": "Mostrar nomes de arquivo", + "settings-monitor-specific-description": "Defina uma pasta de papel de parede diferente para cada monitor.", + "settings-monitor-specific-label": "Diretórios específicos por monitor", + "settings-monitor-specific-tooltip": "Pasta de papéis de parede do monitor", + "settings-overview-blur-strength-description": "Aplica a intensidade do desfoque à Visão Geral.", + "settings-overview-blur-strength-label": "Intensidade do desfoque da visão geral", + "settings-overview-tint-description": "Aplica a intensidade da tonalidade à visão geral.", + "settings-overview-tint-label": "Intensidade da tonalidade da visão geral", + "settings-recursive-search-description": "Também buscar papéis de parede em subpastas do diretório de papéis de parede.", + "settings-recursive-search-label": "Buscar em subpastas", + "settings-select-monitor-folder": "Selecionar pasta de papéis de parede do monitor", + "settings-selector-description": "Escolha o seu papel de parede.", + "settings-selector-position-description": "Escolha onde o painel do seletor de papel de parede aparece.", + "settings-show-hidden-files-tooltip-hide": "Ocultar arquivos ocultos", + "settings-show-hidden-files-tooltip-show": "Mostrar ficheiros ocultos", + "settings-title": "Configurações de papel de parede", + "settings-use-original-images-description": "Ignorar o redimensionamento de papéis de parede antes da exibição. Economiza espaço em disco e reduz o uso da CPU durante as mudanças de papel de parede, mas pode usar mais memória para imagens muito grandes.", + "settings-use-original-images-label": "Usar imagens originais", + "settings-view-mode-description": "Escolha como os papéis de parede são exibidos a partir do seu diretório.", + "settings-view-mode-label": "Modo de visualização", + "view-mode-browse": "Navegar em diretórios", + "view-mode-cycle-tooltip": "Modo de visualização: {mode} (clique para alterar)", + "view-mode-recursive": "Subdiretórios achatados", + "view-mode-single": "Diretório raiz" + } + }, + "placeholders": { + "add-new-keybind": "Adicionar nova combinação de teclas", + "command-example": "echo \"Olá, mundo\"", + "enter-command": "Digite o comando para executar (app ou script personalizado)", + "enter-ipc-identifier": "Introduza um identificador exclusivo para comandos IPC", + "enter-path": "Digite o caminho...", + "enter-text-to-collapse": "p. ex. 'nada está sendo reproduzido'. Use /regex/ para padrões.", + "enter-tooltip": "Inserir dica de ferramenta", + "enter-width-pixels": "Digite a largura em pixels", + "keybind-recording": "Gravando atalho de teclado...", + "search": "Pesquisar...", + "search-icons": "p. ex. noctalia, niri, battery, cloud", + "search-launcher": "Pesquisar entradas... ou use > para comandos", + "search-wallhaven": "Pesquisar no Wallhaven...", + "search-wallpapers": "Digite para filtrar papéis de parede..." + }, + "positions": { + "bottom": "Inferior", + "bottom-center": "Centro inferior", + "bottom-left": "Inferior esquerda", + "bottom-right": "Inferior direita", + "center": "Centralizar", + "center-left": "Esquerda central", + "center-right": "Direita central", + "close-to-bar": "Perto do botão da barra", + "follow-bar": "Seguir barra", + "left": "Esquerda", + "right": "Direito/Certo/À direita", + "top": "Topo", + "top-center": "Centro superior", + "top-left": "Superior esquerda", + "top-right": "Superior direita" + }, + "session-menu": { + "action-in-seconds": "{action} em {seconds} segundos...", + "cancel-timer": "Cancelar temporizador", + "title": "Menu da sessão" + }, + "setup": { + "all-done": "Concluído!", + "appearance": { + "subheader": "Escolha o modo escuro e as fontes de cor (papel de parede ou predefinidas)." + }, + "customize": { + "header": "Personalizar a sua experiência", + "subheader": "Ajuste a posição da barra, densidade, escala e mais." + }, + "skip-setup": "Ignorar configuração", + "telemetry-wizard-done": "Entendi!", + "telemetry-wizard-note": "Você está no controle — ative ou desative isso a qualquer momento nas configurações", + "telemetry-wizard-subtitle": "Adicionámos análises anónimas para ajudar a melhorar o Noctalia", + "telemetry-wizard-title": "Atualização de privacidade", + "wallpaper": { + "choose-dir": "Escolha um diretório contendo seus papéis de parede", + "dir-browse": "Procurar pasta", + "dir-description": "Escolha a pasta que contém seus papéis de parede.", + "dir-label": "Diretório de papéis de parede", + "dir-select-title": "Selecionar pasta de papéis de parede", + "header": "Escolha o seu papel de parede", + "no-dir": "Nenhum diretório de papéis de parede selecionado", + "no-valid": "Nenhuma imagem válida encontrada em: {dir}", + "none-in-dir": "Nenhum papel de parede encontrado no diretório", + "preview-error": "Falha ao carregar a imagem", + "select-prompt": "Selecione um papel de parede abaixo", + "subheader": "Defina o ambiente com um belo fundo." + }, + "welcome": "Bem-vindo", + "welcome-note": "Apenas alguns ajustes básicos para começar — o restante está em configurações", + "welcome-subtitle": "Vamos tornar o seu ambiente de trabalho único", + "welcome-title": "Bem-vindo ao Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "Nenhum reprodutor de mídia detectado", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "Aplicativo desconhecido", + "uptime": "Atividade: {uptime}", + "user-requested": "Solicitado pelo usuário", + "welcome-back": "Bem-vindo(a) de volta,", + "widget-settings-title": "Configurações de {widget}" + }, + "system-monitor": { + "core-usage": "Uso do núcleo {id}", + "cpu-temp": "Temperatura da CPU", + "cpu-usage": "Uso da CPU", + "disk": "Disco", + "download-speed": "Velocidade de download", + "gpu-temp": "Temperatura da GPU", + "load-average": "Carga média", + "title": "Monitor do sistema", + "upload-speed": "Velocidade de upload" + }, + "toast": { + "airplane-mode": { + "description": "Desativar todas as comunicações sem fio.", + "title": "Modo avião" + }, + "battery": { + "critical": "Bateria crítica", + "critical-desc": "Bateria a {percent}% — por favor, conecte o carregador imediatamente", + "low": "Bateria fraca", + "low-desc": "Bateria está em {percent}% — por favor, conecte o carregador" + }, + "bluetooth": { + "address-copied": "Endereço copiado para a área de transferência", + "auto-connect-disabled": "Conexão automática desativada", + "auto-connect-enabled": "Conexão automática ativada", + "auto-connecting": "A conectar a {count} dispositivo(s)...", + "confirm-code": "Confirme o código {value} no outro dispositivo.", + "connect-failed": "Falha ao conectar ao dispositivo", + "disconnect-failed": "Falha ao desconectar do dispositivo", + "discoverable-change-failed": "Falha ao alterar o estado de visibilidade", + "discoverable-disabled": "Visível desativado", + "discoverable-enabled": "Visível ativado", + "display-code": "Insira o código {value} no outro dispositivo.", + "forget-failed": "Falha ao esquecer o dispositivo", + "pair-failed": "Falha ao emparelhar o dispositivo", + "passkey-required": "Senha necessária pelo dispositivo.", + "pincode-required": "Código PIN exigido pelo dispositivo.", + "state-change-failed": "Falha ao alterar o estado do Bluetooth" + }, + "clipboard": { + "long-text": "Texto longo", + "unavailable": "Histórico da área de transferência indisponível", + "unavailable-desc": "O aplicativo 'cliphist' não está instalado. Por favor, instale-o para usar os recursos do histórico da área de transferência" + }, + "custom-command-failed": { + "description": "Comando falhou: {command}\\nCódigo de saída: {code}", + "title": "Comando personalizado falhou" + }, + "do-not-disturb": { + "disabled": "'Não perturbe' desativado", + "disabled-desc": "Mostrando todas as notificações", + "enabled": "'Não perturbe' ativado", + "enabled-desc": "Você encontrará essas notificações no seu histórico" + }, + "donation-opened": "Página de doação aberta no seu navegador", + "internet-limited": "Conectado sem Internet", + "keyboard-layout": { + "changed": "Layout de teclado alterado para {layout}", + "title": "Teclado" + }, + "missing-control-center": { + "description": "O widget da central de controle foi removido da barra. Para acessá-lo novamente pela barra, adicione o widget novamente. Você também pode abri-lo clicando com o botão direito na barra", + "label": "Último widget da central de controle removido" + }, + "night-light": { + "forced": "Ativação forçada", + "normal": "Modo normal", + "not-installed": "wlsunset não está instalado" + }, + "noctalia-performance": { + "disabled": "Modo de desempenho desativado", + "enabled": "Modo de desempenho ativado", + "label": "Noctalia Desempenho" + }, + "power-profile": { + "changed": "Perfil de energia alterado", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "Ocorreu um erro ao processar os templates", + "title": "Falha ao processar o modelo" + }, + "vpn": { + "connected": "Conectado a '{name}'", + "disconnected": "Desconectado de '{name}'" + }, + "wallpaper-colors": { + "disabled": "Cores do papel de parede desativadas", + "enabled": "Cores do papel de parede ativadas", + "label": "Geração de cores para papel de parede" + }, + "wifi": { + "connected": "Conectado a '{ssid}'", + "connection-failed": "Falha na conexão", + "connection-timeout": "Tempo de conexão esgotado", + "disconnected": "Desconectado de '{ssid}'", + "incorrect-password": "Senha incorreta", + "network-not-found": "Rede não encontrada" + } + }, + "tooltips": { + "add-widget": "Adicionar widget", + "auto-connect": "Alternar conexão automática para este dispositivo", + "bluetooth-auto-connect-off": "Conexão automática desativada", + "bluetooth-auto-connect-on": "Conexão automática ativada", + "bluetooth-devices": "Dispositivos Bluetooth", + "brightness-at": "Brilho: {brightness}%", + "click-to-start-recording": "Gravador de tela (iniciar gravação)", + "click-to-stop-recording": "Gravador de tela (parar gravação)", + "collapse": "Recolher barra lateral", + "copy-address": "Copiar endereço", + "delete-notification": "Excluir notificação", + "dismiss-notification": "Descartar notificação", + "do-not-disturb-enabled": "Não perturbe", + "expand": "Expandir barra lateral", + "forget-network": "Esquecer rede", + "grid-view": "Visualização em grade", + "hidden-files-hide": "Arquivos ocultos", + "hide-unnamed-devices": "Ocultar dispositivos sem nome", + "home": "Diretório home", + "input-muted": "Alternar mudo de entrada", + "keep-awake": "Manter acordado", + "keyboard-layout": "Layout de teclado {layout}", + "list-view": "Visualização em lista", + "manage-vpn": "Conexões VPN", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "Número máximo de widgets atingido", + "microphone-volume-at": "Volume do microfone: {volume}%", + "move-to-section": "Mover para {section}", + "mute": "Silenciar", + "next-media": "Próxima faixa", + "next-month": "Próximo mês", + "night-light-not-installed": "Luz noturna (não disponível)", + "noctalia-performance-enabled": "Modo de desempenho Noctalia", + "open-annotation-tool": "Abrir com ferramenta de anotação", + "open-control-center": "Central de controle", + "open-notification-history-enable-dnd": "Histórico de notificações", + "open-settings": "Configurações", + "open-tray-dropdown": "Bandeja do sistema", + "output-muted": "Alternar mudo da saída", + "power-profile": "{profile} perfil de energia", + "previous-media": "Faixa anterior", + "previous-month": "Mês anterior", + "refresh-devices": "Atualizar dispositivos", + "refresh-wallhaven": "Atualizar resultados do Wallhaven", + "refresh-wallpaper-list": "Atualizar lista de papéis de parede", + "remove-widget": "Remover widget", + "search-close": "Fechar pesquisa", + "session-menu": "Menu da Sessão", + "show-all-devices": "Mostrar todos os dispositivos", + "switch-to-dark-mode": "Modo escuro", + "switch-to-light-mode": "Modo claro", + "unmute": "Desativar o mudo", + "up": "Diretório superior", + "volume-at": "Volume de saída: {volume}%", + "wallpaper-selector": "Seletor de papel de parede" + }, + "wallpaper": { + "browse": { + "empty-directory": "Este diretório está vazio.", + "go-root": "Ir para a raiz do papel de parede", + "go-up": "Ir para a pasta pai", + "go-up-hint": "Use o botão voltar para navegar para cima." + }, + "configure-directory": "Configure seu diretório de papéis de parede com imagens.", + "fill-modes": { + "crop": "Cortar (Preencher)", + "fit": "Ajustar (Conter)", + "repeat": "Repetir (Mosaico)", + "stretch": "Esticar" + }, + "no-match": "Nenhuma correspondência encontrada.", + "no-wallpaper": "Nenhum papel de parede encontrado.", + "panel": { + "apikey-help": "É necessária uma chave de API para acessar conteúdo NSFW.", + "apikey-label": "Chave de API", + "apikey-managed-by-env": "Gerenciado através da variável de ambiente NOCTALIA_WALLHAVEN_API_KEY.", + "apikey-placeholder": "Insira sua chave da API Wallhaven", + "appearance-dark-tab": "Escuro", + "appearance-light-tab": "Claro", + "categories-anime": "Anime", + "categories-label": "Categorias", + "categories-people": "Pessoas", + "color-extraction-disabled": "Usar extração de cores do papel de parede", + "color-extraction-enabled": "Usar esquemas de cores predefinidos", + "header-devices-apply-all-tooltip": "Escolher papel de parede por monitor", + "header-devices-per-monitor-tooltip": "Aplicar a todos os monitores", + "header-link-light-dark-tooltip": "Manter papel de parede ao mudar de tema", + "header-separate-light-dark-tooltip": "Trocar papel de parede automaticamente com o tema", + "header-sun-linked-tooltip": "Separar papéis de parede claros e escuros", + "header-sun-separate-tooltip": "Mesmo papel de parede para claro e escuro", + "order-asc": "Crescente", + "order-desc": "Decrescente", + "order-label": "Ordem", + "purity-label": "Filtro de conteúdo", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Sketchy", + "ratios-all-portrait": "Todos os retratos", + "ratios-all-wide": "Todas as paisagens", + "ratios-any": "Qualquer", + "ratios-label": "Proporção da tela", + "resolution-atleast": "Pelo menos", + "resolution-exact": "Exato", + "resolution-label": "Resolução", + "resolution-mode-label": "Modo", + "solid-color-tooltip": "Fundo de cor sólida", + "sort-date-asc": "Ordenar por mais antigo primeiro", + "sort-date-desc": "Ordenar por mais recente primeiro", + "sort-name-asc": "Ordenar por nome (A-Z)", + "sort-name-desc": "Ordenar por nome (Z-A)", + "sort-random": "Ordenar aleatoriamente", + "sorting-date-added": "Data de adição", + "sorting-favorites": "Favoritos", + "sorting-label": "Ordenar por", + "sorting-relevance": "Relevância", + "sorting-toplist": "Lista superior", + "sorting-views": "Visualizações", + "source-label": "Fonte", + "source-wallhaven": "Wallhaven", + "title": "Seletor de papel de parede", + "wallhaven-settings-title": "Configurações do Wallhaven" + }, + "transitions": { + "disc": "Disco", + "fade": "Esmaecer", + "honeycomb": "Favo de mel", + "pixelate": "Pixelizar", + "stripes": "Listras", + "wipe": "Limpar" + }, + "try-different-search": "Tente uma busca diferente.", + "wallhaven": { + "loading": "Carregando papéis de parede...", + "no-results": "Nenhum papel de parede encontrado. Tente uma busca diferente.", + "page": "{current} de {total}", + "page-prefix": "Página", + "page-suffix": "de {total}" + } + }, + "weather": { + "clear-sky": "Céu limpo", + "drizzle": "Garoa", + "fog": "Névoa", + "mainly-clear": "Predominantemente limpo", + "overcast": "Nublado", + "partly-cloudy": "Parcialmente nublado", + "rain-showers": "Pancadas de chuva", + "snow": "Neve", + "thunderstorm": "Tempestade" + }, + "widgets": { + "color-picker": { + "palette-description": "Escolha entre uma vasta gama de cores predefinidas.", + "palette-label": "Paleta", + "palette-theme-colors": "Acesso rápido à paleta de cores do seu tema.", + "title": "Seletor de cores" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm em minúsculas", + "ampm-uppercase": "AM/PM em maiúsculas", + "common-12hour-time-minutes": "Horário de 12 horas com minutos", + "common-24hour-time-minutes": "Horário de 24 horas com minutos", + "common-24hour-time-seconds": "Horário de 24 horas com segundos", + "common-european-date": "Formato de data europeu", + "common-iso-date": "Formato de data ISO", + "common-us-date": "Formato de data dos EUA", + "common-weekday-date": "Dia da semana com data", + "common-weekday-month-day": "Dia da semana, mês e dia", + "day-abbreviated": "Nome do dia abreviado", + "day-full": "Nome completo do dia", + "day-leading-zero": "Dia com zero à esquerda (01-31)", + "day-no-leading-zero": "Dia sem zero à esquerda (1-31)", + "hour-leading-zero": "Hora com zero à esquerda (00-23) — formato de 24 horas", + "hour-no-leading-zero": "Hora sem zero à esquerda (0-23) — formato de 24 horas", + "minute-leading-zero": "Minuto com zero à esquerda (00-59)", + "minute-no-leading-zero": "Minuto sem zero à esquerda (0-59)", + "month-abbreviated": "Nome do mês abreviado", + "month-full": "Nome completo do mês", + "month-number-leading-zero": "Mês como número com zero à esquerda (01-12)", + "month-number-no-zero": "Mês como número sem zero à esquerda (1-12)", + "second-leading-zero": "Segundo com zero à esquerda (00-59)", + "second-no-leading-zero": "Segundo sem zero à esquerda (0-59)", + "timezone-abbreviation": "Abreviação do fuso horário", + "year-four-digit": "Ano com quatro dígitos", + "year-two-digit": "Ano com dois dígitos (00-99)" + }, + "file-picker": { + "item": "item", + "items": "itens", + "search-placeholder": "Pesquisar arquivos e pastas...", + "select-current": "Selecionar atual", + "select-file": "Selecionar arquivo", + "select-folder": "Selecionar pasta", + "selected": "Selecionado:", + "title": "Seletor de arquivos" + }, + "file-picker-title": "Selecione um arquivo", + "icon-picker": { + "title": "Seletor de ícones" + }, + "tooltip-placeholder": "Espaço reservado" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "Identidade Anônima", + "ca-cert": "Certificado CA", + "eap-method": "Método EAP", + "password": "Senha do Usuário", + "phase2-auth": "Autenticação de Fase 2", + "username": "Identidade / Nome de Usuário" + }, + "panel": { + "action-required": "Ação requerida", + "add-hidden-network": "Adicionar rede oculta", + "add-network": "Adicionar rede", + "available-interfaces": "Interfaces disponíveis", + "available-networks": "Redes disponíveis", + "disabled": "O Wi-Fi está desativado", + "disconnecting": "Desconectando...", + "dns": "DNS", + "enable-message": "Ative o Wi-Fi para ver as redes disponíveis.", + "enter-password": "Digite a senha...", + "forget": "Esquecer", + "forget-network": "Esquecer esta rede?", + "forgetting": "Esquecendo...", + "hidden-network": "Rede oculta", + "interface": "Interface de rede", + "internet-connected": "Conectado à internet", + "internet-limited": "Sem internet", + "internet-status": "Status da internet", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "Redes conhecidas", + "link-speed": "Velocidade de ligação", + "network-name-ssid": "Nome da rede (SSID)", + "no-ethernet-devices": "Nenhum dispositivo Ethernet detectado", + "no-networks": "Nenhuma rede encontrada", + "saved": "Salva", + "scan-again": "Escanear novamente", + "searching": "Procurando redes próximas...", + "security-open": "Abrir", + "security-wep": "WEP", + "security-wpa": "WPA/WPA2/WPA3 Pessoal", + "security-wpa-ent": "WPA Enterprise", + "security-wpa2-ent": "WPA2 Enterprise", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 Enterprise", + "show-password": "Mostrar senha", + "title": "Wi-Fi" + }, + "signal": { + "excellent": "Excelente", + "fair": "Razoável", + "good": "Bom", + "poor": "Ruim", + "weak": "Fraco" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/ru.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/ru.json new file mode 100644 index 0000000..2267606 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/ru.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "Очистить историю", + "close-app": "Закрыть {app}", + "connect-vpn": "Подключиться к {name}", + "cycle-visualizer": "Визуализатор циклов", + "disable-bluetooth": "Отключить Bluetooth", + "disable-dnd": "Отключить режим 'Не беспокоить'", + "disable-wifi": "Отключить Wi-Fi", + "disconnect-vpn": "Отключить {name}", + "dock-settings": "Настройки дока", + "enable-bluetooth": "Включить Bluetooth", + "enable-dnd": "Не беспокоить", + "enable-wifi": "Включить Wi-Fi", + "launcher-settings": "Настройки лаунчера", + "lower-to-bottom": "Опустить до дна", + "open-calendar": "Открыть календарь", + "open-display-settings": "Настройки экрана", + "open-launcher": "Открыть лаунчер", + "open-settings": "Открыть настройки", + "raise-to-top": "Поднять наверх", + "random-wallpaper": "Случайные обои", + "run-custom-command": "Выполнить пользовательскую команду", + "show-active-only": "Показывать только активные", + "show-all": "Показать все", + "toggle-mute": "Включить/выключить звук", + "widget-settings": "Настройки виджета" + }, + "authentication": { + "error": "Ошибка аутентификации", + "failed": "Сбой аутентификации" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Применить цвета темы к иконке активного окна.", + "hide-mode-description": "Управляет поведением виджета, когда ни одно окно не активно.", + "scrolling-mode-description": "Управление прокруткой текста для длинных заголовков окон.", + "show-app-icon-description": "Отображать иконку приложения рядом с заголовком окна.", + "show-app-icon-label": "Показывать иконку приложения", + "show-app-text-description": "Отобразить заголовок активного окна.", + "show-app-text-label": "Показывать заголовок окна" + }, + "audio-visualizer": { + "color-name-description": "Выберите цвет для визуализатора.", + "color-name-label": "Цвет заливки", + "height-description": "Пользовательская высота компонента.", + "hide-when-idle-description": "Если включено, визуализатор скрыт, пока плеер не начнёт воспроизведение.", + "hide-when-idle-label": "Скрывать, когда медиа не воспроизводится", + "width-description": "Пользовательская ширина компонента." + }, + "battery": { + "device-default": "Устройство отображения по умолчанию", + "device-description": "Выберите устройство для отображения состояния батареи.", + "device-label": "Устройство с батареей", + "display-mode-description": "Выберите, как отображается батарея на панели.", + "display-mode-graphic": "Графическая батарея", + "display-mode-graphic-clean": "Графическая батарея (без %)", + "display-mode-icon-always": "Значок - Всегда показывать %", + "display-mode-icon-hover": "Иконка - Показать при наведении", + "display-mode-icon-only": "Только значок", + "hide-if-idle-description": "Скрыть виджет, когда батарея не заряжается и не разряжается.", + "hide-if-idle-label": "Скрывать в режиме простоя", + "hide-if-not-detected-description": "Скрыть виджет, если в системе не обнаружена батарея.", + "hide-if-not-detected-label": "Скрывать при отсутствии батареи", + "low-battery-threshold-label": "Порог предупреждения о низком заряде батареи", + "show-noctalia-performance-description": "Отображать переключатель режима производительности Noctalia в панели батареи.
Отключает тени и анимации в Noctalia для снижения использования ресурсов.", + "show-noctalia-performance-label": "Показывать переключатель Производительность Noctalia", + "show-power-profile-description": "Отображать выбор профиля питания в панели батареи.", + "show-power-profile-label": "Показывать элементы управления профилем питания" + }, + "brightness": { + "apply-all-description": "Изменять яркость для всех мониторов при использовании колеса прокрутки.", + "apply-all-label": "Применить изменения прокрутки ко всем мониторам" + }, + "clock": { + "clock-display-description": "Настройте отображение часов, добавив токены из списка ниже. Чтобы использовать 12-часовой формат, необходимо включить токен 'AP'.", + "clock-display-label": "Отображение часов", + "custom-font-description": "Выберите пользовательский шрифт для отображения часов.", + "custom-font-label": "Пользовательский шрифт", + "custom-font-placeholder": "Выбрать пользовательский шрифт...", + "custom-font-search-placeholder": "Поиск шрифтов...", + "horizontal-bar-description": "Совет: используйте \\n для создания разрыва строки.", + "horizontal-bar-label": "Горизонтальная панель", + "preview": "Предварительный просмотр", + "tooltip-format-description": "Строка формата для подсказки, отображаемой при наведении на часы. Оставьте пустым, чтобы использовать подсказку по умолчанию.", + "tooltip-format-label": "Формат подсказки", + "use-custom-font-description": "Переопределить выбор шрифта по умолчанию пользовательским шрифтом для часов.", + "use-custom-font-label": "Использовать пользовательский шрифт", + "use-monospaced-font-description": "Если включено, часы будут использовать моноширинный шрифт.", + "use-monospaced-font-label": "Использовать моноширинный шрифт", + "vertical-bar-description": "Используйте пробел, чтобы разделить каждую часть на новую строку.", + "vertical-bar-label": "Вертикальная панель" + }, + "control-center": { + "browse-file": "Выбор файла", + "browse-library": "Обзор библиотеки", + "color-selection-description": "Применяет цвета темы к значкам.", + "enable-colorization-description": "Включить раскрашивание значка с применением цветов темы.", + "icon-description": "Выберите иконку из библиотеки или пользовательский файл.", + "select-custom-icon": "Выбрать пользовательскую иконку", + "use-distro-logo-description": "Использовать логотип вашего дистрибутива вместо пользовательской иконки.", + "use-distro-logo-label": "Использовать логотип дистрибутива вместо иконки" + }, + "custom-button": { + "collapse-condition-description": "Если выходной текст соответствует этому значению, кнопка будет скрыта.", + "collapse-condition-label": "Условие скрытия", + "color-selection-description": "Применить цвета темы к значку и тексту.", + "default-tooltip": "Пользовательская кнопка, настроить в настройках", + "display-command-output-description": "Введите команду для регулярного выполнения. Первая строка её вывода будет отображаться как текст.", + "display-command-output-label": "Вывод команды", + "display-command-output-stream-description": "Введите команду для непрерывного выполнения.", + "dynamic-text": "Динамический текст", + "enable-colorization-description": "Включить раскраску значка и текста пользовательской кнопки, применяя цвета темы.", + "enable-colorization-label": "Включить раскраску", + "general-tooltip-text-description": "Пользовательский текст для отображения во всплывающей подсказке кнопки.", + "general-tooltip-text-label": "Пользовательский текст всплывающей подсказки", + "hide-mode-always-expanded": "Всегда раскрыт", + "hide-mode-description": "Управляет видимостью виджета, когда команда не имеет вывода.", + "hide-mode-expand-with-output": "Раскрывать при наличии вывода", + "hide-mode-label": "Режим скрытия", + "hide-mode-max-transparent": "Максимально раскрыт, но прозрачный", + "icon-description": "Выберите иконку из библиотеки.", + "ipc-identifier-description": "Уникальный идентификатор для команд IPC. Используйте этот идентификатор с 'qs -c noctalia-shell ipc call cb [действие] [идентификатор]', чтобы управлять этой кнопкой через IPC.", + "ipc-identifier-label": "Идентификатор IPC", + "left-click-description": "Команда для выполнения при нажатии левой кнопкой мыши.", + "left-click-label": "Клик левой кнопкой", + "left-click-update-text": "Обновить отображаемый текст по левому клику", + "max-text-length-horizontal-description": "Максимальное количество символов для отображения в горизонтальной панели (0 для скрытия текста).", + "max-text-length-horizontal-label": "Макс. длина текста (горизонтально)", + "max-text-length-vertical-description": "Максимальное количество символов для отображения в вертикальной панели (0 для скрытия текста).", + "max-text-length-vertical-label": "Макс. длина текста (вертикально)", + "middle-click-description": "Команда для выполнения при нажатии средней кнопкой мыши.", + "middle-click-label": "Клик средней кнопкой", + "middle-click-update-text": "Обновить отображаемый текст по среднему клику", + "parse-json-description": "Разобрать вывод команды как объект JSON для динамической установки текста и иконки.", + "parse-json-label": "Разобрать вывод как JSON", + "refresh-interval-description": "Интервал в миллисекундах.", + "refresh-interval-label": "Интервал обновления", + "right-click-description": "Команда для выполнения при нажатии правой кнопкой мыши.", + "right-click-label": "Клик правой кнопкой", + "right-click-update-text": "Обновить отображаемый текст по правому клику", + "show-exec-tooltip-description": "Показывать всплывающие подсказки с деталями команд (левый/правый/средний клик, колесо).", + "show-exec-tooltip-label": "Показывать всплывающие подсказки команд", + "show-icon-description": "Переключает видимость иконки виджета.", + "show-icon-label": "Показать иконку", + "show-text-tooltip-description": "Показывать всплывающие подсказки с выводом текстовой команды.", + "show-text-tooltip-label": "Показывать динамические текстовые подсказки", + "text-stream-description": "Потоковые строки из команды будут отображаться как текст на кнопке.", + "text-stream-label": "Поток", + "wheel-description": "Команда для выполнения при использовании колеса прокрутки.
Используйте $delta для дельты колеса прокрутки в команде.", + "wheel-down": "Колесо вниз", + "wheel-down-description": "Команда для выполнения при прокрутке колеса вниз.", + "wheel-down-label": "Команда прокрутки колеса вниз", + "wheel-label": "Колесо мыши", + "wheel-mode-separate-description": "Включить раздельные команды для колеса прокрутки вверх и вниз.", + "wheel-mode-separate-label": "Раздельные команды колеса прокрутки", + "wheel-up": "Колесо вверх", + "wheel-up-description": "Команда для выполнения при прокрутке колеса вверх.", + "wheel-up-label": "Команда прокрутки колеса вверх", + "wheel-update-text": "Обновить отображаемый текст при прокрутке" + }, + "keyboard-layout": { + "show-icon-description": "Отображать значок раскладки клавиатуры." + }, + "lock-keys": { + "hide-when-off-description": "Скрыть индикатор, когда клавиша неактивна.", + "hide-when-off-label": "Скрывать, когда отключено", + "show-caps-lock-description": "Отображать статус Caps Lock.", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "Отображать статус Num Lock.", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "Отображать статус Scroll Lock.", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "Включить компактный режим для панели медиаплеера.", + "compact-mode-label": "Компактный режим", + "hide-mode-description": "Управляет поведением виджета, когда медиа не воспроизводится.", + "max-width-description": "Устанавливает максимальный горизонтальный размер виджета. Виджет будет сжиматься, чтобы соответствовать более короткому содержимому.", + "no-active-player": "Нет активного плеера", + "panel-section-description": "Настроить внешний вид и поведение панели медиаплеера.", + "panel-section-label": "Панель медиаплеера", + "scrolling-mode-description": "Управление включением прокрутки текста для длинных названий треков.", + "scrolling-mode-label": "Режим прокрутки", + "show-album-art-description": "Отображать обложку альбома для текущего воспроизводимого трека.", + "show-album-art-label": "Показывать обложку альбома", + "show-artist-first-description": "Исполнитель - название вместо название - исполнитель.", + "show-artist-first-label": "Сначала исполнитель", + "show-progress-ring-description": "Отображать круговой индикатор прогресса воспроизведения трека.", + "show-progress-ring-label": "Показывать кольцо прогресса", + "show-visualizer-description": "Отображать аудиовизуализатор при воспроизведении музыки.", + "show-visualizer-label": "Показывать визуализатор", + "use-fixed-width-description": "Если включено, виджет всегда будет использовать максимальную ширину вместо динамической подстройки под содержимое.", + "use-fixed-width-label": "Использовать фиксированную ширину", + "visualizer-type-description": "Выберите стиль аудиовизуализатора для отображения.", + "visualizer-type-label": "Тип визуализатора" + }, + "notification-history": { + "hide-widget-when-zero-description": "Скрывать значок уведомлений, если уведомлений нет.", + "hide-widget-when-zero-label": "Скрывать значок без уведомлений", + "hide-widget-when-zero-unread-description": "Скрывать значок уведомлений, если нет непрочитанных.", + "hide-widget-when-zero-unread-label": "Скрывать значок без непрочитанных", + "show-unread-badge-description": "Отображать значок, показывающий количество непрочитанных уведомлений.", + "show-unread-badge-label": "Показывать значок непрочитанных", + "unread-badge-color-description": "Выберите цвет для значка непрочитанных уведомлений.", + "unread-badge-color-label": "Цвет значка непрочитанных" + }, + "section-editor": { + "placeholder": "Выберите виджет для добавления...", + "search-placeholder": "Поиск виджетов..." + }, + "spacer": { + "width-description": "Ширина интервала в пикселях." + }, + "system-monitor": { + "compact-mode-description": "Отображать статистику в виде мини-диаграмм вместо текстовых значений. Предотвращает сдвиг макета.", + "compact-mode-label": "Компактный режим", + "cpu-cores-description": "Отображать использование ядер CPU по отдельности.", + "cpu-cores-label": "Ядра CPU", + "cpu-frequency-description": "Отобразить текущую тактовую частоту CPU в ГГц.", + "cpu-frequency-label": "Показать частоту CPU", + "cpu-temperature-description": "Показывать показания температуры процессора, если доступны.", + "cpu-temperature-label": "Температура CPU", + "cpu-usage-description": "Отображать текущий процент использования CPU.", + "cpu-usage-label": "Использование CPU", + "disk-path-description": "Выберите точку монтирования диска для мониторинга.", + "disk-path-label": "Путь к диску", + "gpu-temperature-description": "Показать показания температуры GPU, если доступны.", + "load-average-description": "Показать среднюю нагрузку системы.", + "load-average-label": "Средняя нагрузка", + "memory-percentage-description": "Показывать использование памяти в процентах вместо абсолютных значений.", + "memory-percentage-label": "Память в процентах", + "memory-usage-description": "Отображать информацию о текущем использовании RAM.", + "memory-usage-label": "Использование памяти", + "network-traffic-description": "Отображать скорости загрузки и скачивания по сети.", + "network-traffic-label": "Сетевой трафик", + "storage-as-percentage-description": "Показывать место на диске в процентах вместо абсолютных значений.", + "storage-as-percentage-label": "Диск в процентах", + "storage-available-description": "Показывает, сколько места на диске доступно, вместо того, сколько используется.", + "storage-available-label": "Доступное место на диске", + "storage-usage-description": "Показывать информацию об использовании дискового пространства.", + "storage-usage-label": "Использование хранилища", + "swap-usage-description": "Показать использование swap-памяти.", + "swap-usage-label": "Использование swap", + "use-monospace-font-description": "Используйте моноширинный шрифт для единообразной ширины символов.", + "use-monospace-font-label": "Моноширинный шрифт", + "use-padding-description": "Добавляет к текстовым значениям начальные пробелы, чтобы предотвратить смещение макета.", + "use-padding-description-disabled-monospace-font": "Для этой функции необходим моноширинный шрифт.", + "use-padding-description-disabled-vertical": "Вертикальная панель задач не поддерживает текстовые отступы.", + "use-padding-label": "Отступ Текста" + }, + "taskbar": { + "colorize-icons-description": "Применить цвета темы к иконкам панели задач.", + "hide-mode-description": "Управляет поведением виджета, когда нет соответствующих окон.", + "hide-mode-label": "Режим скрытия", + "icon-scale-description": "Задаёт коэффициент масштабирования для значков панели задач.", + "icon-scale-label": "Масштабирование значков", + "max-width-description": "Максимальная ширина панели задач в процентах от ширины экрана.", + "max-width-label": "Максимальная ширина", + "only-active-workspaces-description": "Показывать только приложения из активных рабочих пространств.", + "only-active-workspaces-label": "Только из активных рабочих пространств", + "only-same-monitor-description": "Показывать только приложения с монитора, на котором находится панель.", + "only-same-monitor-label": "Только с того же монитора", + "show-pinned-apps-description": "Показывать закреплённые приложения из панели приложений на панели задач.", + "show-pinned-apps-label": "Показать закреплённые приложения", + "show-title-description": "Отображать заголовки окон на панели задач.", + "show-title-description-disabled": "Вертикальная панель задач не поддерживает отображение заголовков.", + "show-title-label": "Показать заголовок", + "smart-width-description": "Автоматически регулировать ширину записей в зависимости от их количества.", + "smart-width-label": "Умная ширина", + "title-width-description": "Установить ширину заголовков окон на панели задач (в пикселях).", + "title-width-label": "Ширина заголовка", + "title-width-reset-tooltip": "Сбросить ширину заголовка" + }, + "tray": { + "chevron-color-description": "Применить цвета темы к иконке шеврона выдвижной панели.", + "chevron-color-label": "Цвет стрелки", + "colorize-icons-description": "Применить цвета темы к иконкам трея.", + "colorize-icons-label": "Раскрасить иконки", + "drawer-enabled-description": "Если включено, незакрепленные элементы трея отображаются во всплывающей панели.
Если отключено, все элементы трея отображаются в строку.", + "drawer-enabled-label": "Включить всплывающую панель", + "hide-passive-description": "Если включено, элементы в трее со статусом 'Пассивный' будут скрыты.", + "hide-passive-label": "Скрыть пассивные предметы" + }, + "volume": { + "display-mode-description": "Выберите, как это значение должно отображаться." + }, + "workspace": { + "character-count-description": "Количество символов для отображения из имён рабочих пространств (1-10).", + "character-count-label": "Количество символов", + "empty-color-description": "Установить цвет фона для пустых рабочих пространств.", + "empty-color-label": "Цвет пустого рабочего пространства", + "enable-scrollwheel-description": "Переключайтесь между рабочими пространствами с помощью колеса прокрутки мыши.", + "enable-scrollwheel-label": "Прокрутите, чтобы переключить рабочие пространства", + "focused-color-description": "Установить цвет фона для активного рабочего пространства.", + "focused-color-label": "Цвет активного рабочего пространства", + "follow-focused-screen-description": "Отображать рабочие пространства с текущего активного экрана, а не с экрана, на котором расположена панель.", + "follow-focused-screen-label": "Следовать за активным экраном", + "font-weight-description": "Установить визуальный вес текста в рабочем пространстве.", + "font-weight-label": "Толщина шрифта", + "grouped-border-opacity-description": "Установить уровень прозрачности для границ контейнера рабочей области.", + "grouped-border-opacity-label": "Прозрачность границы", + "hide-unoccupied-description": "Не отображать рабочие пространства без окон.", + "hide-unoccupied-label": "Скрыть незанятые", + "label-mode-description": "Выберите, как отображаются метки рабочих пространств.", + "label-mode-label": "Режим метки", + "occupied-color-description": "Установить цвет фона для занятых рабочих пространств.", + "occupied-color-label": "Цвет занятого рабочего пространства", + "pill-size-description": "Отрегулируйте размер индикаторов рабочих пространств (50%-100%).", + "pill-size-label": "Размер капсулы", + "reverse-scrolling-description": "Изменить направление переключения рабочих пространств при прокрутке.", + "reverse-scrolling-label": "Обратная прокрутка", + "show-applications-description": "Отображать значки приложений внутри каждого рабочего пространства.", + "show-applications-hover-description": "Отображать значки приложений внутри каждого рабочего пространства при наведении на него.", + "show-applications-hover-label": "Показывать приложения при наведении", + "show-applications-label": "Показать приложения", + "show-badge-description": "Показывать значок номера рабочего пространства в сгруппированном режиме.", + "show-badge-label": "Показывать значок рабочего стола", + "show-labels-only-when-occupied-description": "Показывать метки рабочих пространств только тогда, когда они содержат окна.", + "show-labels-only-when-occupied-label": "Показывать метки только при наличии содержимого", + "unfocused-icons-opacity-description": "Установить уровень прозрачности для неактивных значков приложений.", + "unfocused-icons-opacity-label": "Неактивные значки, прозрачность" + } + }, + "battery": { + "all-batteries": "Все батареи (объединенные)", + "battery-health": "Состояние батареи", + "battery-level": "Уровень заряда батареи", + "capacity-level": "Ёмкость: {level}", + "charging-rate": "Скорость зарядки: {rate} Вт", + "discharging-rate": "Скорость разрядки: {rate} Вт", + "health": "Здоровье: {percent}%", + "inhibit-idle-description": "Не даёт системе уснуть.", + "no-battery-detected": "Батарея не обнаружена", + "plugged-in": "Подключено", + "power-profile": "Профиль питания", + "time-left": "Осталось времени: {time}", + "time-until-full": "Время до полной зарядки: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Доступные устройства", + "blocked": "Заблокировано", + "connected-devices": "Подключенные устройства", + "device-address": "Адрес устройства", + "disabled": "Bluetooth отключен", + "discoverable": "Обнаруживаемый", + "enable-message": "Включите Bluetooth, чтобы увидеть доступные устройства.", + "known-devices": "Известные устройства", + "no-devices": "Нет доступных устройств", + "paired-devices": "Сопряжённые устройства", + "pairing-mode": "Убедитесь, что ваше устройство находится в режиме сопряжения.", + "scanning": "Поиск устройств...", + "signal-text-excellent": "Сигнал: Отличный", + "signal-text-fair": "Сигнал: Средний", + "signal-text-good": "Сигнал: Хороший", + "signal-text-poor": "Сигнал: Плохой", + "signal-text-unknown": "Сигнал: Неизвестен", + "signal-text-very-poor": "Сигнал: Очень слабый" + } + }, + "changelog": { + "error": { + "fetch-failed": "Не удалось загрузить данные журнала изменений. Пожалуйста, попробуйте позже.", + "rate-limit": "Превышен лимит GitHub. Попробуйте снова через несколько минут." + }, + "panel": { + "buttons-discord": "Присоединиться к нашему Discord", + "buttons-dismiss": "ОК", + "buttons-feedback": "Оставьте отзыв", + "empty": "Примечания к выпуску пока недоступны.", + "highlight-title": "Основные изменения", + "released": "Выпущено {date}", + "subtitle-fresh": "Спасибо за установку Noctalia! Вот что входит в эту сборку.", + "subtitle-updated": "Обновлено с {previousVersion}", + "title": "Что нового в {version}", + "version": "Версия {version}", + "version-new-user": "Новая установка" + } + }, + "common": { + "actions": "Действия", + "active": "Активный", + "add": "Добавить", + "appearance": "Внешний вид", + "apply": "Применить", + "auto-connect": "Автоматическое подключение", + "automation": "Автоматизация", + "available": "Доступно", + "back": "Назад", + "battery": "Батарея", + "behavior": "Поведение", + "bluetooth": "Bluetooth", + "brightness": "Яркость", + "browse": "Обзор", + "calendar": "Календарь", + "calendar-panel": "Панель календаря", + "cancel": "Отменить", + "cards": "Карточки", + "charging": "Зарядка", + "clear": "Очистить", + "clipboard": "Буфер обмена", + "close": "Закрыть", + "color-muted": "Выключено", + "colors": "Цвета", + "command": "Команда", + "confirm": "Подтвердить", + "connect": "Соединить", + "connected": "Подключено", + "connecting": "Соединение...", + "continue": "Продолжить", + "contributors": "Участники", + "copied-to-clipboard": "Скопировано в буфер обмена", + "countdown": "Обратный отсчёт", + "customize": "Настроить", + "date": "Дата", + "default": "По умолчанию", + "delete": "Удалить", + "devices": "Устройства", + "disabled": "Отключено", + "discharging": "Разрядка", + "disconnect": "Отключить", + "disconnected": "Отключено", + "disconnecting": "Отключение...", + "display-mode": "Режим отображения", + "documentation": "Документация", + "download": "Скачать", + "duration": "Продолжительность", + "dysfunctional": "Дисфункциональный", + "edit": "Редактировать", + "enabled": "Включено", + "error": "Ошибка", + "ethernet": "Ethernet", + "events": "События", + "execute": "Выполнить", + "faithful": "Верный", + "focus": "Фокус", + "font-weight-bold": "Жирный", + "font-weight-light": "Тонкий", + "font-weight-medium": "Средний", + "font-weight-regular": "Обычный", + "font-weight-semibold": "Полужирный", + "frequency": "Частота", + "gateway": "Шлюз", + "general": "Общее", + "height": "Высота", + "hibernate": "Гибернация", + "history": "История", + "icon": "Иконка", + "idle": "Простой", + "info": "Информация", + "input": "Ввод", + "install": "Установить", + "installed": "Установлено", + "interface": "Интерфейс", + "internet": "Интернет", + "keybind": "Привязка клавиш", + "language": "Язык", + "loading": "Загрузка...", + "local": "Местный", + "location": "Местоположение", + "lock": "Заблокировать", + "logout": "Выйти", + "look": "Внешний вид", + "margins": "Отступы", + "media": "Плеер", + "media-player": "Медиаплеер", + "memory": "Память", + "monitors": "Мониторы", + "move-down": "Переместить вниз", + "move-up": "Переместить вверх", + "network": "Сеть", + "next": "Следующий", + "night-light": "Ночной свет", + "no": "Нет", + "no-results": "Нет результатов", + "no-summary": "Нет сводки", + "none": "Ничего", + "not-found": "Не найдено", + "notifications": "Уведомления", + "official": "Официальный", + "on-surface": "На поверхности", + "output": "Вывод", + "pair": "Спарить", + "paired": "Спарено", + "pairing": "Сопряжение...", + "panels": "Панели", + "password": "Пароль", + "pause": "Пауза", + "performance": "Производительность", + "pin": "Закрепить", + "play": "Воспроизвести", + "polling": "Опрашивание", + "position": "Позиция", + "previous": "Предыдущий", + "primary": "Основной", + "random": "Случайный", + "reboot": "Перезагрузить", + "reboot-to-uefi": "Перезагрузить в UEFI", + "record": "Начать запись", + "refresh": "Обновить", + "required": "(обязательно)", + "reset": "Сброс", + "reset-to-default": "Сбросить по Умолчанию", + "result-count": "{count} результат", + "result-count-plural": "{count} результаты", + "resume": "Продолжить", + "save": "Сохранить", + "scanning": "Сканирование...", + "screen-corners": "Углы экрана", + "search": "Поиск", + "second": "{count} секунда", + "second-plural": "{count} секунд", + "secondary": "Вторичный", + "security": "Безопасность", + "select": "Выбрать", + "select-color": "Выбрать цвет", + "select-color-description": "Применить цвета темы для выделения.", + "select-icon-color": "Выбрать цвет значка", + "settings": "Настройки", + "shortcuts": "Ярлыки", + "shutdown": "Выключить", + "signal": "Сигнал", + "sound": "Звук", + "sources": "Источники", + "start": "Начать", + "stop": "Стоп", + "supporters": "Поддерживающие", + "suspend": "Приостановить", + "templates": "Шаблоны", + "tertiary": "Третичный", + "test": "Тест", + "thresholds": "Пороги", + "title": "Название", + "toast": "Toast", + "trusted": "Надёжный", + "uninstall": "Удалить", + "unknown": "Неизвестно", + "unpair": "Разорвать пару", + "unpin": "Открепить", + "update": "Обновление", + "upload": "Загрузить", + "username": "Имя пользователя", + "userspace-reboot": "Перезагрузить пользовательское пространство", + "version": "Версия", + "vibrant": "Яркий", + "visualizer": "Визуализатор", + "volume": "Объём", + "volumes": "Тома", + "wallpaper": "Обои", + "weather": "Погода", + "weather-loading": "Загрузка погоды...", + "weather-no-location": "Установите местоположение в Настройках, чтобы видеть погоду", + "week": "Неделя", + "widgets": "Виджеты", + "width": "Ширина", + "wifi": "Wi-Fi", + "windows": "Окна ", + "yes": "Да" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Профиль питания", + "tooltip-disabled": "Установите power-profiles-daemon, чтобы использовать профили питания" + }, + "wifi": { + "label-disconnected": "Wi-Fi отключен", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Всегда скрывать", + "always-show": "Всегда показывать", + "force-open": "Принудительно открыть", + "on-hover": "При наведении" + }, + "hide-modes": { + "auto-hide": "Автоматическое скрытие", + "hidden": "Скрывать, когда пусто", + "idle": "Скрывать в режиме простоя", + "non-exclusive": "Неисключительный", + "transparent": "Прозрачный, когда пустой", + "visible": "Всегда виден" + }, + "launcher": { + "categories": { + "all": "Всё", + "audiovideo": "Аудио и видео", + "chat": "Чат", + "development": "Разработка", + "education": "Образование", + "emoji-activity": "Активность", + "emoji-animals": "Животные", + "emoji-flags": "Флаги", + "emoji-food": "Еда и напитки", + "emoji-nature": "Природа", + "emoji-objects": "Объекты", + "emoji-people": "Люди и тело", + "emoji-recent": "Недавние", + "emoji-symbols": "Символы", + "emoji-travel": "Путешествия и места", + "game": "Игры", + "graphics": "Графика", + "misc": "Разное", + "office": "Офис", + "pinned": "Закреплено", + "system": "Система", + "webbrowser": "Веб-браузер" + }, + "providers": { + "applications": "Приложения", + "calculator": "Калькулятор", + "calculator-deprecated": "Команда >calc устарела и скоро будет удалена. Пожалуйста, вводите математические выражения прямо в поиск.", + "calculator-description": "Калькулятор — вычисление математических выражений", + "calculator-enter-expression": "Введите математическое выражение", + "calculator-press-enter-to-copy": "Нажмите Enter, чтобы скопировать результат", + "clipboard": "История буфера обмена", + "clipboard-clear-description": "Очистить всю историю буфера обмена", + "clipboard-clear-description-full": "Удалить все элементы из истории буфера обмена", + "clipboard-clear-history": "Очистить историю буфера обмена", + "clipboard-delete": "Удалить запись из буфера обмена", + "clipboard-history-disabled": "История буфера обмена отключена", + "clipboard-history-disabled-description": "Включите историю буфера обмена в настройках или установите cliphist", + "clipboard-loading": "Загрузка истории буфера обмена...", + "clipboard-search-description": "Поиск в истории буфера обмена", + "command-description": "Выполнять команды оболочки", + "emoji": "Выбор эмодзи", + "emoji-loading": "Загрузка эмодзи...", + "emoji-loading-description": "Пожалуйста, подождите", + "emoji-no-recent": "Недавних эмодзи пока нет", + "emoji-search-description": "Поиск и копирование эмодзи", + "settings-search-description": "Поиск и переход к настройкам", + "windows-search-description": "Поиск и фокусировка открытых окон" + } + }, + "lock-screen": { + "authenticating": "Аутентификация...", + "password": "Введите ваш пароль...", + "restart": "Перезагрузить", + "shut-down": "Выключить" + }, + "notifications": { + "panel": { + "click-to-collapse": "Нажмите, чтобы свернуть", + "click-to-expand": "Нажмите, чтобы развернуть", + "description": "Ваши уведомления будут появляться здесь по мере их поступления.", + "no-notifications": "Нет уведомлений" + }, + "range": { + "earlier": "Ранее", + "today": "Сегодня", + "yesterday": "Вчера" + }, + "time": { + "diff-d": "1 день назад", + "diff-dd": "{diff} дней назад", + "diff-h": "1 час назад", + "diff-hh": "{diff} часов назад", + "diff-m": "1 минуту назад", + "diff-mm": "{diff} минут назад", + "now": "сейчас" + } + }, + "options": { + "bar": { + "density-comfortable": "Комфортная", + "density-compact": "Компактная", + "density-default": "По умолчанию", + "density-mini": "Мини", + "density-spacious": "Просторный", + "display-mode-always-visible": "Всегда видны", + "type-floating": "Плавающий", + "type-framed": "В рамке", + "type-simple": "Простой" + }, + "control-center": { + "quick-settings-style-classic": "Классический", + "quick-settings-style-modern": "Современный" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "Комфортный", + "compact": "Компактный", + "default": "По умолчанию" + }, + "launcher-view-mode": { + "grid": "Сетка", + "list": "Список" + }, + "notification-density": { + "compact": "Компактный", + "default": "По умолчанию" + }, + "scrolling-modes": { + "always": "Всегда прокручивать", + "hover": "Прокручивать при наведении", + "never": "Никогда не прокручивать" + }, + "session-menu-grid-layout": { + "grid": "Сетка", + "single-row": "Один ряд" + }, + "settings-panel-mode": { + "attached": "Панель прикреплена к панели", + "centered": "Центрированная панель", + "window": "Отдельное окно" + }, + "visualizer-types": { + "linear": "Линейный", + "mirrored": "Зеркальный", + "wave": "Волна" + }, + "visualizer-visibility": { + "with-background": "Только с фоном" + }, + "workspace-labels": { + "index": "Индекс", + "index-and-name": "Индекс и название", + "name": "Имя" + } + }, + "panels": { + "about": { + "become-supporter": "Стать спонсором", + "changelog": "Посмотреть список изменений", + "changelog-on-startup": "Показывать журнал изменений при обновлении", + "changelog-on-startup-desc": "Автоматически показывать журнал изменений при обновлении Noctalia.", + "contributors-description": "Благодарим нашего замечательного участника: {count}!", + "contributors-description-plural": "Благодарим наших замечательных участников: {count}!", + "copy-info": "Копировать информацию", + "debug-disabled": "Режим отладки отключен", + "debug-enabled": "Режим отладки включен", + "info-copied": "Информация скопирована в буфер обмена", + "noctalia-available": "Доступно:", + "noctalia-desc": "Изящная и минималистичная оболочка рабочего стола, тщательно разработанная для Wayland, созданная с помощью Quickshell.", + "noctalia-git-commit": "Git коммит:", + "noctalia-installed-version": "Установленная версия:", + "noctalia-latest-version": "Последняя версия:", + "noctalia-qs-version": "Noctalia QS версия:", + "noctalia-title": "Оболочка Noctalia", + "privacy-policy": "Политика конфиденциальности", + "support": "Поддержать нас", + "supporter-badge": "Сторонник", + "supporters-desc": "Огромное спасибо нашему замечательному стороннику!", + "supporters-desc-plural": "Огромное спасибо нашим {count} замечательным сторонникам!", + "supporters-loading": "Загрузка спонсоров...", + "system-board": "Материнская плата:", + "system-cpu": "ЦП:", + "system-disk": "Диск:", + "system-gpu": "Графический процессор", + "system-host": "хост:", + "system-install-hint": "Установите fastfetch для просмотра информации о системе", + "system-kernel": "Ядро:", + "system-loading": "Загрузка системной информации...", + "system-memory": "Память:", + "system-monitor": "Монитор:", + "system-not-installed": "fastfetch не установлен", + "system-os": "ОС:", + "system-packages": "Пакеты:", + "system-product": "Продукт:", + "system-title": "Системная информация", + "system-uptime": "Время работы:", + "system-wm": "ВМ:", + "telemetry-data-copied": "Данные телеметрии скопированы в буфер обмена.", + "telemetry-desc": "Помогите улучшить Noctalia, поделившись анонимной информацией о системе (разрешение экрана, композитор, дистрибутив). Отправляется один раз при запуске, без отслеживания, данные автоматически удаляются через 30 дней.", + "telemetry-enabled": "Отправить анонимную системную информацию", + "telemetry-show-data": "Просмотр данных", + "telemetry-title": "Конфиденциальность", + "title": "О программе", + "up-to-date": "У вас всё актуально!", + "update-available": "Доступно обновление", + "view-commit": "Посмотреть коммит на GitHub" + }, + "audio": { + "devices-desc": "Настройка доступных устройств ввода и вывода аудио.", + "devices-input-device-description": "Выберите желаемое устройство ввода аудио.", + "devices-input-device-label": "Устройство ввода", + "devices-output-device-description": "Выберите желаемое устройство вывода аудио.", + "devices-output-device-label": "Устройство вывода", + "devices-title": "Аудиоустройства", + "external-mixer-description": "Введите команду или путь к приложению для запуска при активации функции внешнего аудиомикшера.", + "external-mixer-label": "Команда внешнего аудиомикшера", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Установите предпочитаемые и игнорируемые медиа-приложения.", + "media-excluded-player-description": "Добавьте ключевые слова для плееров, которые система должна игнорировать. Каждое ключевое слово должно быть на новой строке.", + "media-excluded-player-label": "Исключенный плеер", + "media-excluded-player-placeholder": "введите подстроку и нажмите +", + "media-frame-rate-description": "Более высокие частоты кадров обеспечивают более плавное изображение, но используют больше ресурсов.", + "media-frame-rate-label": "Частота кадров", + "media-primary-player-description": "Введите ключевое слово для идентификации вашего основного плеера.", + "media-primary-player-label": "Основной плеер", + "media-primary-player-placeholder": "напр. spotify, vlc, mpv", + "media-scrolling-speed-description": "Время в секундах, за которое заголовок прокручивается от начала до конца.", + "media-scrolling-speed-label": "Скорость прокрутки", + "media-scrolling-title-description": "Включить непрерывную прокрутку для длинных названий медиа.", + "media-scrolling-title-label": "Прокрутка названия", + "media-title": "Медиаплееры", + "on-middle-clicked-description": "Команда для выполнения при нажатии средней кнопки мыши.", + "panel-applications-empty": "В данный момент нет приложений, воспроизводящих звук", + "spectrum-mirrored-description": "Зеркально отобразите спектр, чтобы низкие частоты были в центре. Когда выключено, низкие частоты находятся слева, а высокие — справа.", + "spectrum-mirrored-label": "Зеркальный спектр", + "title": "Аудио", + "visualizer-type-description": "Выберите тип визуализации для воспроизведения медиа.", + "visualizer-type-label": "Тип визуализации", + "volumes-desc": "Настройка регуляторов громкости и уровней аудио.", + "volumes-feedback-sound-file-description": "Путь к звуковому файлу, воспроизводимому при изменении громкости.", + "volumes-feedback-sound-file-label": "Звуковой файл обратной связи громкости", + "volumes-feedback-sound-file-select-title": "Выбрать звуковой файл обратной связи громкости", + "volumes-input-volume-description": "Уровень громкости ввода с микрофона.", + "volumes-mute-input-description": "Отключить стандартный аудиовход (микрофон).", + "volumes-mute-input-label": "Отключить аудиовход", + "volumes-mute-output-description": "Отключить основной системный аудиовыход.", + "volumes-mute-output-label": "Отключить аудиовыход", + "volumes-output-volume-description": "Общесистемный уровень громкости.", + "volumes-step-size-description": "Настройка шага изменения громкости (колесо прокрутки, сочетания клавиш).", + "volumes-step-size-label": "Шаг изменения громкости", + "volumes-volume-feedback-description": "Воспроизводить звук при регулировке громкости.", + "volumes-volume-feedback-label": "Воспроизвести звук обратной связи громкости", + "volumes-volume-overdrive-description": "Разрешить повышение громкости выше 100%. Может не поддерживаться всем оборудованием.", + "volumes-volume-overdrive-label": "Разрешить перегрузку громкости" + }, + "bar": { + "appearance-auto-hide-delay-description": "Время, через которое Bar скроется после того, как курсор мыши покинет его.", + "appearance-auto-hide-delay-label": "Задержка скрытия", + "appearance-auto-hide-exclusive-note": "Примечание: Эксклюзивная зона автоматически отключается, когда автоскрытие активно.", + "appearance-auto-show-delay-description": "Время до появления панели, когда мышь входит в край.", + "appearance-auto-show-delay-label": "Задержка отображения", + "appearance-background-opacity-description": "Установите прозрачность фона специально для панели.", + "appearance-background-opacity-label": "Прозрачность фона бара", + "appearance-capsule-color-description": "Выберите цвет для капсул панели или не используйте никакой для цвета поверхности по умолчанию.", + "appearance-capsule-color-label": "Цвет капсулы", + "appearance-capsule-opacity-description": "Установить уровень непрозрачности для фона виджетов, когда капсула отображается.", + "appearance-capsule-opacity-label": "Непрозрачность капсулы", + "appearance-content-padding-description": "Отрегулируйте отступ между краями панели и виджетами.", + "appearance-content-padding-label": "Отступ содержимого", + "appearance-density-description": "Настройка отступов панели для компактного или просторного вида.", + "appearance-density-label": "Плотность панели", + "appearance-desc": "Настройка внешнего вида и положения панели.", + "appearance-display-mode-description": "Выберите, когда панель видна.", + "appearance-enable-exclusion-zone-inset-description": "Уменьшите зону исключения на 1 физический пиксель, чтобы окна вплотную идеально заходили под край панели.", + "appearance-enable-exclusion-zone-inset-label": "Внутренняя зона исключения", + "appearance-floating-description": "Отображает панель как плавающую 'таблетку'.", + "appearance-floating-label": "Плавающая панель", + "appearance-font-scale-description": "Настройте масштаб размера шрифта для текста, отображаемого на панели.", + "appearance-font-scale-label": "Масштаб шрифта", + "appearance-frame-radius": "Внутренний радиус", + "appearance-frame-settings-description": "Настройте толщину рамки и внутренний радиус углов", + "appearance-frame-settings-label": "Настройки рамки", + "appearance-frame-thickness": "Толщина", + "appearance-hide-on-overview-description": "Скрыть панель и закрыть панели, когда активен обзор компоновщика.", + "appearance-hide-on-overview-label": "Скрыть панель в обзоре", + "appearance-margins-description": "Настройка отступов вокруг плавающей панели.", + "appearance-margins-horizontal": "Горизонтальный отступ", + "appearance-margins-vertical": "Вертикальный отступ", + "appearance-outer-corners-description": "Отображает внешне изогнутые углы на панели.", + "appearance-outer-corners-label": "Внешние углы", + "appearance-position-description": "Выберите, где разместить панель на экране.", + "appearance-position-label": "Положение панели", + "appearance-show-capsule-description": "Показывать фон виджетов.", + "appearance-show-capsule-label": "Показывать капсулу", + "appearance-show-on-workspace-switch-description": "Автоматически кратко показывать Bar при смене рабочего пространства.", + "appearance-show-on-workspace-switch-label": "Показывать панель при переключении рабочего пространства", + "appearance-show-outline-description": "Отображает видимую границу вокруг каждого виджета.", + "appearance-show-outline-label": "Показать контуры виджетов", + "appearance-type-description": "Выберите стиль панели: Простой, Плавающий или В рамке.", + "appearance-type-label": "Тип панели", + "appearance-use-separate-opacity-description": "Включить использование отдельного значения прозрачности для фона полосы.", + "appearance-use-separate-opacity-label": "Использовать раздельную прозрачность столбцов", + "appearance-widget-spacing-description": "Отрегулируйте интервал между каждым виджетом на панели.", + "appearance-widget-spacing-label": "Расстояние между виджетами", + "behavior-middle-click-command-description": "Команда для выполнения по среднему клику.", + "behavior-middle-click-command-label": "Команда Среднего Клика", + "behavior-middle-click-command-placeholder": "niri msg action переключить обзор", + "behavior-middle-click-description": "Выберите действие средней кнопки мыши на пустых областях Панели.", + "behavior-middle-click-follow-mouse-description": "Открыть выбранную панель по среднему клику в позиции курсора.", + "behavior-middle-click-follow-mouse-label": "Средний клик следует за курсором", + "behavior-middle-click-label": "Действие по среднему клику на Панели", + "behavior-right-click-command-description": "Команда для выполнения по правому клику.", + "behavior-right-click-command-label": "Команда Правого Клика", + "behavior-right-click-command-placeholder": "notify-send \"Правый клик\"", + "behavior-right-click-description": "Выберите действие правого щелчка мыши на пустых областях панели.", + "behavior-right-click-follow-mouse-description": "Открыть выбранную панель по правому клику в позиции курсора.", + "behavior-right-click-follow-mouse-label": "Правый клик следует за курсором", + "behavior-right-click-label": "Действие по правому клику на Панели", + "behavior-wheel-wrap-description": "Если включено, прокрутка продолжается с последнего элемента к первому.", + "behavior-wheel-wrap-label": "По кругу", + "behavior-workspace-scroll-description": "Выберите, что делает колесо мыши на пустых областях панели.", + "behavior-workspace-scroll-label": "Действие колеса мыши Панели", + "behavior-workspace-scroll-option-content": "Содержимое", + "behavior-workspace-scroll-option-workspace": "Рабочая область", + "monitor-configure-widgets": "Настроить виджеты", + "monitor-override-settings": "Переопределить глобальные настройки", + "monitor-override-settings-description": "Использовать пользовательские настройки для этого монитора.", + "monitor-reset-all": "Сбросить все", + "monitor-widgets-title": "Настройка виджета - {monitor}", + "monitors-desc": "Показывать панель на определённых мониторах. По умолчанию на всех, если ни один не выбран.", + "monitors-desc-new": "Настройте, на каких мониторах отображать панель, и настройте параметры для каждого монитора.", + "monitors-title": "Отображение на мониторах", + "title": "Панель", + "tray-blacklist-description": "Добавьте правила исключения трея, поддерживает подстановочные знаки (*).", + "tray-blacklist-label": "Чёрный список", + "tray-blacklist-placeholder": "напр. nm-applet, Fcitx*", + "tray-pin-application": "Закрепить приложение", + "tray-unpin-application": "Открепить приложение", + "use-global-widgets": "Использовать глобальные виджеты", + "widgets-desc": "Перетаскивайте виджеты, чтобы изменить их порядок. Используйте контекстное меню (правой кнопкой мыши), чтобы перемещать виджеты между разделами или удалять их.", + "widgets-title": "Расположение виджетов" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Создавайте цветовые схемы на основе ваших обоев. Автоматически извлекает цвета для создания целостной темы.", + "color-source-use-wallpaper-colors-label": "Использовать цвета обоев", + "dark-mode-mode-description": "Включает автоматическое переключение между светлым и тёмным режимами.", + "dark-mode-mode-label": "Расписание тёмного режима", + "dark-mode-mode-manual": "Вручную", + "dark-mode-mode-off": "Выкл", + "dark-mode-switch-description": "Переключается на более тёмную тему для удобства просмотра ночью.", + "delete-error-description": "Не удалось удалить {scheme}", + "delete-error-title": "Ошибка удаления", + "delete-success-description": "{scheme} успешно удалена", + "delete-success-title": "Цветовая схема удалена", + "download-button": "Загрузить ещё", + "download-downloading": "Загрузка...", + "download-empty": "Нет доступных цветовых схем", + "download-error-api-error": "Ошибка API: {status}", + "download-error-description": "Не удалось загрузить {scheme}", + "download-error-download-failed": "Ошибка загрузки с кодом выхода: {code}", + "download-error-invalid-response": "Неверный формат ответа API", + "download-error-no-files": "Файлы для схемы не найдены", + "download-error-parse-failed": "Ошибка обработки ответа API: {error}", + "download-error-rate-limit": "Превышен лимит запросов GitHub API", + "download-error-title": "Ошибка загрузки", + "download-fetching": "Получение доступных цветовых схем...", + "download-success-description": "{scheme} успешно загружена", + "download-success-title": "Цветовая схема загружена", + "download-title": "Загрузить цветовые схемы", + "method-description": { + "content": "Схема Material Design с высокоточной экстракцией цветов, которая точно соответствует фактическим цветам исходного контента.", + "dysfunctional": "Как Faithful, но выбирает второе по доминантности цветовое семейство в качестве основного.", + "faithful": "Пытается оставаться близким к исходному цвету, создавая при этом гармоничную палитру.", + "fruit-salad": "Схема Material Design, создающая игривую, яркую палитру с разнообразными оттенками.", + "monochrome": "Схема Material Design, использующая однотонную шкалу серого с минимальным хроматическим содержанием.", + "muted": "Создает более мягкие, приглушенные тона для спокойного и сдержанного вида.", + "rainbow": "Схема Material Design, которая генерирует цвета по всему спектру для максимального разнообразия цветов.", + "tonal-spot": "Алгоритм Material Design по умолчанию. Создает сбалансированные палитры, используя тональные вариации исходного цвета.", + "vibrant": "Максимизирует насыщенность цвета для смелых, привлекательных палитр с яркими тонами." + }, + "predefined-desc": "Выберите из коллекции предопределённых цветовых схем.", + "predefined-generate-templates-label": "Генерировать шаблоны для предопределённых схем", + "predefined-title": "Предопределённые цветовые схемы", + "sync-gsettings-description": "Сопоставить системную тему с активным светлым или темным вариантом.", + "sync-gsettings-label": "Синхронизировать системную тему", + "templates-desc": "Применение цветов к внешним приложениям.", + "templates-filter-description": "Показать шаблоны из определённой категории.", + "templates-filter-label": "Фильтровать по категории", + "templates-misc-description": "Создайте свои собственные шаблоны.", + "templates-misc-label": "Дополнительно", + "templates-misc-user-templates-description": "Включайте только если вы знаете, что делаете — обратитесь к нашей онлайн-документации.", + "templates-misc-user-templates-label": "Включить пользовательские шаблоны", + "templates-none-detected": "Не обнаружено", + "templates-write-path": "Записывает: {filepath}", + "title": "Цветовая схема", + "wallpaper-method-description": "Выберите ваш любимый метод генерации палитры.", + "wallpaper-method-label": "Метод генерации палитры", + "wallpaper-monitor-source-description": "Выберите монитор, который будет использоваться для извлечения цветов обоев.", + "wallpaper-monitor-source-label": "Источник генерации цвета" + }, + "connections": { + "authentication-required": "Требуется аутентификация", + "bluetooth-auto-connect-description": "Автоматически подключаться к доверенным сопряженным устройствам при включении Bluetooth.", + "bluetooth-auto-connect-label": "Автоматически подключать сопряженные устройства", + "bluetooth-devices-unnamed": "Безымянные устройства не отображаются.", + "bluetooth-discoverable": "Это устройство обнаруживаемо как {hostName}, пока открыта эта вкладка настроек.", + "bluetooth-rssi-polling-description": "Периодически опрашивает RSSI для подключенных устройств через bluetoothctl. Может быть недоступно для всех устройств; при включении использует минимальные ресурсы.", + "bluetooth-rssi-polling-interval-description": "Настройте, как часто обновлять уровень сигнала для подключенных устройств.", + "bluetooth-rssi-polling-interval-label": "Интервал опроса", + "bluetooth-rssi-polling-label": "Опрос сигнала Bluetooth", + "disable-discoverability-description": "Скрыть ваше устройство от ближайших Bluetooth-устройств.", + "disable-discoverability-label": "Отключить видимость устройства", + "hide-unnamed-devices-description": "Скрыть устройства, которые отображаются только как Bluetooth-адреса.", + "hide-unnamed-devices-label": "Скрывать безымянные устройства", + "pin-instructions": "Пожалуйста, введите PIN-код, отображаемый на вашем устройстве.", + "title": "Подключения", + "wifi-header-text": "Используйте Wi-Fi, чтобы просматривать доступные сети и подключаться к интернету." + }, + "control-center": { + "cards-desc": "Настройте, какие элементы управления появляются в центре управления и в каком порядке.", + "desc": "Настройка позиционирования и поведения панели центра управления.", + "open-at-mouse-description": "При нажатии правой кнопкой мыши на пустую область Панели, откройте Центр управления рядом с курсором. Если отключено, вместо этого используется настройка положения выше.", + "open-at-mouse-label": "Открывать у курсора по правому клику", + "position-description": "Выберите, где появляется панель центра управления при открытии.", + "shortcuts-custom-button-command-description": "Команда для выполнения при нажатии на кнопку.", + "shortcuts-custom-button-enable-on-state-logic-description": "Включить вторую иконку и 'активное' состояние на основе команды проверки.", + "shortcuts-custom-button-enable-on-state-logic-label": "Включить логику активного состояния", + "shortcuts-custom-button-icon-description": "Выберите значок из библиотеки.", + "shortcuts-custom-button-on-clicked-label": "Команда для левого клика", + "shortcuts-custom-button-on-middle-clicked-description": "Команда для выполнения при нажатии средней кнопки мыши.", + "shortcuts-custom-button-on-state-command-description": "Команда для проверки, должна ли кнопка находиться в 'активном' состоянии. Возвращает 0 для 'включено', ненулевое для 'выключено'.", + "shortcuts-custom-button-on-state-command-label": "Команда проверки активного состояния", + "shortcuts-custom-button-on-state-icon-description": "Иконка для кнопки, когда она находится в 'активном' состоянии.", + "shortcuts-custom-button-on-state-icon-label": "Иконка активного состояния", + "shortcuts-custom-button-state-checks-add": "Добавить проверку состояния", + "shortcuts-custom-button-state-checks-command": "Команда для выполнения для этой проверки состояния", + "shortcuts-custom-button-state-checks-label": "Проверки состояния", + "shortcuts-custom-button-state-checks-remove": "Удалить", + "shortcuts-custom-button-tooltip-description": "Подсказка, которая будет отображаться при наведении на кнопку.", + "shortcuts-custom-button-tooltip-label": "Подсказка", + "shortcuts-title": "Виджеты ярлыков", + "system-monitor-disk-path-description": "Выберите точку монтирования диска, которую должна отслеживать карта системного монитора в центре управления.", + "system-monitor-disk-path-label": "Путь к диску системного монитора", + "title": "Центр управления" + }, + "desktop-widgets": { + "clock-enabled-description": "Показать виджет часов на рабочем столе.", + "clock-enabled-label": "Включить виджет часов", + "clock-format-label": "Формат", + "clock-height-description": "Высота виджета часов в пикселях.", + "clock-height-label": "Высота", + "clock-minimal-mode-description": "Использовать минимальное отображение часов с временем и датой.", + "clock-minimal-mode-label": "Минимальный режим", + "clock-rounded-corners-description": "Использовать скруглённые углы для фона виджета.", + "clock-rounded-corners-label": "Скруглённые углы", + "clock-show-background-description": "Показать фоновый контейнер для виджета часов.", + "clock-show-background-label": "Показать фон", + "clock-show-date-description": "Отобразите текущую дату под временем.", + "clock-show-date-label": "Показать дату", + "clock-show-seconds-description": "Отображать секунды во времени.", + "clock-show-seconds-label": "Показывать секунды", + "clock-style-analog": "Аналоговый", + "clock-style-binary": "Бинарный", + "clock-style-description": "Выберите стиль отображения часов.", + "clock-style-digital": "Цифровой", + "clock-style-label": "Стиль часов", + "clock-style-minimal": "Минимальный", + "clock-width-description": "Ширина виджета часов в пикселях.", + "cpu-intensive-note": "Виджеты, отмеченные знаком !, используют больше CPU и должны быть включены обдуманно.", + "edit-mode-button-label": "Войти в режим редактирования", + "edit-mode-controls-explanation": "Левый клик и перетаскивание: Переместить или изменить размер виджета.\nПравый клик: Открыть контекстное меню.", + "edit-mode-description": "Включите режим редактирования, чтобы перемещать и изменять положение виджетов рабочего стола. В этом режиме виджеты отображаются с контуром перетаскивания и могут быть перемещены.", + "edit-mode-exit-button": "Выйти из режима редактирования", + "edit-mode-grid-snap-label": "Привязка к сетке", + "edit-mode-grid-snap-scale-label": "Масштаб привязки к сетке", + "edit-mode-label": "Режим редактирования", + "enabled-description": "Включить или отключить виджеты рабочего стола полностью.", + "enabled-label": "Включить виджеты рабочего стола", + "general-desc": "Настройте виджеты, отображаемые на вашем рабочем столе.", + "general-title": "Настольные виджеты", + "media-player-enabled-description": "Отобразить виджет медиаплеера на рабочем столе.", + "media-player-enabled-label": "Включить виджет медиаплеера", + "media-player-rounded-corners-description": "Включить скругление углов на краях виджета.", + "media-player-show-album-art-description": "Показать обложку альбома и информацию о треках (название и исполнитель).", + "media-player-show-album-art-label": "Показывать обложку альбома и название", + "media-player-show-background-description": "Показать фоновый контейнер для виджета медиаплеера.", + "media-player-show-buttons-description": "Показать кнопки управления медиа (воспроизведение/пауза, предыдущий, следующий) на виджете медиаплеера.", + "media-player-show-buttons-label": "Показать кнопки", + "media-player-show-visualizer-description": "Показать наложение аудиовизуализатора.", + "media-player-visualizer-type-description": "Выберите тип визуализации для фона медиаплеера.", + "overview-enabled-description": "Показывать виджеты рабочего стола, пока обзор композитора открыт.", + "overview-enabled-label": "Показать в обзоре", + "system-stat-layout-bottom": "Низ", + "system-stat-layout-description": "Выберите, как отображается легенда относительно графика.", + "system-stat-layout-label": "Макет", + "system-stat-layout-side": "Сторона", + "system-stat-rounded-corners-description": "Использовать скругленные углы для фона виджета.", + "system-stat-rounded-corners-label": "Скругленные Углы", + "system-stat-show-background-description": "Показать фоновый контейнер для виджета системной статистики.", + "system-stat-show-background-label": "Показать фон", + "system-stat-stat-type-description": "Выберите, какую системную статистику отображать.", + "system-stat-stat-type-label": "Тип Статистики", + "title": "Настольные виджеты", + "weather-enabled-description": "Показать погодный виджет на рабочем столе.", + "weather-enabled-label": "Включить виджет погоды", + "weather-show-background-description": "Показать фоновый контейнер для погодного виджета." + }, + "display": { + "monitors-backlight-device-auto-option": "По умолчанию", + "monitors-backlight-device-description": "Выберите устройство подсветки для этого вывода.", + "monitors-backlight-device-label": "Устройство Подсветки", + "monitors-brightness-step-description": "Настройка шага изменения яркости (колесо прокрутки и сочетания клавиш).", + "monitors-brightness-step-label": "Шаг изменения яркости", + "monitors-brightness-unavailable-ddc-disabled": "Управление яркостью недоступно. Включите \"Поддержка внешней яркости\", чтобы управлять яркостью этого дисплея.", + "monitors-brightness-unavailable-generic": "Управление яркостью недоступно для этого дисплея.", + "monitors-desc": "Настройка яркости для каждого дисплея.", + "monitors-enforce-minimum-description": "Решает проблему полного отключения подсветки на некоторых дисплеях при 0% яркости.", + "monitors-enforce-minimum-label": "Обеспечить минимальную яркость (1%)", + "monitors-external-brightness-description": "Включить поддержку DDCUtil для управления яркостью на внешних дисплеях через протокол DDC/CI.", + "monitors-external-brightness-label": "Поддержка внешней яркости", + "monitors-global-brightness-description": "Настроить яркость всех мониторов одновременно.", + "monitors-global-brightness-label": "Все мониторы", + "monitors-title": "Настройки для каждого монитора", + "night-light-auto-schedule-description": "На основе времени заката и восхода солнца в {location} — рекомендуется.", + "night-light-auto-schedule-label": "Автоматическое расписание", + "night-light-desc": "Уменьшение излучения синего света, чтобы помочь вам лучше спать и уменьшить напряжение глаз.", + "night-light-enable-description": "Применить тёплый цветовой фильтр для уменьшения излучения синего света.", + "night-light-enable-label": "Включить ночной свет", + "night-light-force-activation-description": "Игнорирует расписание и немедленно применяет ночной фильтр.", + "night-light-force-activation-label": "Принудительная активация", + "night-light-manual-schedule-description": "Установить пользовательское время восхода и заката.", + "night-light-manual-schedule-label": "Ручное расписание", + "night-light-manual-schedule-select-start": "Выбрать время начала", + "night-light-manual-schedule-select-stop": "Выбрать время остановки", + "night-light-manual-schedule-sunrise": "Время восхода", + "night-light-manual-schedule-sunset": "Время заката", + "night-light-temperature-day": "День", + "night-light-temperature-day-description": "Управляет цветовой температурой в дневное время.", + "night-light-temperature-description": "Установите цветовую температуру для ночного и дневного времени.", + "night-light-temperature-label": "Цветовая температура", + "night-light-temperature-night": "Ночь", + "night-light-temperature-night-description": "Управляет цветовой температурой в ночное время.", + "title": "Дисплей" + }, + "dock": { + "appearance-background-opacity-description": "Настройка непрозрачности фона дока.", + "appearance-border-radius-description": "Измените радиус границы дока.", + "appearance-border-radius-label": "Радиус скругления границы", + "appearance-colorize-icons-description": "Применить цвета темы к иконкам приложений на доке (только для приложений не в фокусе).", + "appearance-colorize-icons-label": "Раскрасить иконки", + "appearance-dead-opacity-description": "Настройте прозрачность значков неактивных приложений.", + "appearance-dead-opacity-label": "Неактивные приложения", + "appearance-desc": "Настройка поведения и внешнего вида дока.", + "appearance-display-auto-hide": "Автоматически скрывать", + "appearance-display-description": "Выберите поведение дока.", + "appearance-display-exclusive": "Исключительно", + "appearance-dock-indicator-description": "Показывать небольшой индикатор, когда док скрыт.", + "appearance-dock-indicator-label": "Индикатор дока", + "appearance-floating-distance-description": "Установите расстояние между доком и краем экрана.", + "appearance-floating-distance-label": "Расстояние плавающего дока", + "appearance-group-apps-description": "Группировать несколько окон одного и того же приложения в одну запись Dock.", + "appearance-group-apps-label": "Группировать одинаковые приложения", + "appearance-group-click-action-cycle": "Переключение окон", + "appearance-group-click-action-description": "Выберите, что делает левый клик для сгруппированных приложений.", + "appearance-group-click-action-label": "Действие по клику сгруппированного приложения", + "appearance-group-click-action-list": "Открыть список окон", + "appearance-group-context-menu-mode-description": "Выберите, как контекстное меню ведет себя для сгруппированных приложений.", + "appearance-group-context-menu-mode-extended": "Расширенный", + "appearance-group-context-menu-mode-label": "Режим cгруппированного меню приложений", + "appearance-group-context-menu-mode-list": "Список окон", + "appearance-group-indicator-style-description": "Выберите, как сгруппированные индикаторы запущенных приложений отображают состояние активного окна.", + "appearance-group-indicator-style-dots": "Точки", + "appearance-group-indicator-style-label": "Стиль сгруппированного индикатора", + "appearance-group-indicator-style-number": "Число", + "appearance-hide-show-speed-description": "Настройка скорости анимации скрытия/открытия дока.", + "appearance-hide-show-speed-label": "Скорость анимации", + "appearance-icon-size-description": "Настройка общего размера дока.", + "appearance-icon-size-label": "Размер дока", + "appearance-inactive-indicators-description": "Отображать индикаторы для всех запущенных приложений, а не только для активного.", + "appearance-inactive-indicators-label": "Индикаторы открытых приложений", + "appearance-indicator-color-description": "Выберите цвет индикатора скрытого дока.", + "appearance-indicator-color-label": "Цвет индикатора", + "appearance-indicator-opacity-description": "Настройте непрозрачность индикатора скрытого дока.", + "appearance-indicator-opacity-label": "Непрозрачность индикатора", + "appearance-indicator-thickness-description": "Использовать более толстый индикатор скрытого дока (6 пк вместо 3 пк).", + "appearance-indicator-thickness-label": "Более толстый индикатор", + "appearance-launcher-icon-description": "Выберите значок, используемый для Dock Launcher.", + "appearance-launcher-icon-label": "Значок лаунчера", + "appearance-launcher-position-description": "Выберите, где значок запуска приложений отображается в доке.", + "appearance-launcher-position-end": "Конец", + "appearance-launcher-position-label": "Позиция запускателя", + "appearance-launcher-position-start": "Старт", + "appearance-launcher-use-distro-logo-description": "Используйте логотип вашего дистрибутива в качестве значка Dock Launcher.", + "appearance-launcher-use-distro-logo-label": "Использовать логотип дистрибутива вместо иконки", + "appearance-pinned-static-description": "Располагать закреплённые значки приложений свлева (сверху) в статичном порядке.", + "appearance-pinned-static-label": "Закреплённые приложения", + "appearance-position-description": "Выберите, где док будет отображаться на экране.", + "appearance-position-label": "Позиция", + "appearance-show-launcher-icon-description": "Показывать значок запуска приложений в доке.", + "appearance-show-launcher-icon-label": "Показать панель запуска приложений", + "appearance-sit-on-frame-description": "Выровнять док внутри границы рамки, а не поверх нее.", + "appearance-sit-on-frame-label": "док располагается на рамке", + "appearance-type-attached": "Закреплённая", + "appearance-type-description": "Выберите между плавающей пилюлей или статической панелью, прикрепленной к краю.", + "appearance-type-floating": "Плавающий", + "appearance-type-label": "Стиль дока", + "enabled-description": "Показать или скрыть док целиком.", + "enabled-label": "Включить док", + "monitors-desc": "Показывать док на определённых мониторах. По умолчанию на всех, если ни один не выбран.", + "monitors-only-same-monitor-description": "Показывать только приложения с монитора, где находится док.", + "monitors-only-same-monitor-label": "Только приложения с того же монитора", + "monitors-title": "Отображение на мониторах", + "title": "Док" + }, + "general": { + "copy-settings": "Копировать настройки", + "fonts-default-description": "Основной шрифт, используемый во всем интерфейсе.", + "fonts-default-label": "Шрифт по умолчанию", + "fonts-default-placeholder": "Выбрать шрифт по умолчанию...", + "fonts-default-scale-description": "Увеличение или уменьшение размера стандартного текста.", + "fonts-default-scale-label": "Размер шрифта по умолчанию", + "fonts-default-search-placeholder": "Поиск шрифта...", + "fonts-desc": "Выберите шрифты, используемые во всем интерфейсе.", + "fonts-monospace-description": "Моноширинный шрифт, используемый для чисел и отображения статистики.", + "fonts-monospace-label": "Моноширинный шрифт", + "fonts-monospace-placeholder": "Выбрать моноширинный шрифт...", + "fonts-monospace-scale-description": "Увеличение или уменьшение размера моноширинного текста.", + "fonts-monospace-scale-label": "Размер моноширинного шрифта", + "fonts-monospace-search-placeholder": "Поиск моноширинного шрифта...", + "fonts-title": "Шрифты", + "keybinds-conflict-description": "Комбинация клавиш уже назначена для {action}.", + "keybinds-conflict-title": "Конфликт горячих клавиш", + "keybinds-description": "Настройте глобальные клавиши навигации для панелей и лаунчера.", + "keybinds-down": "Переместить вниз", + "keybinds-enter": "Подтвердить / Действие", + "keybinds-escape": "Закрыть / Назад", + "keybinds-left": "Переместить влево", + "keybinds-modifier-description": "Сочетания клавиш должны включать клавишу-модификатор (Ctrl или Alt).", + "keybinds-modifier-title": "Требуется модификатор", + "keybinds-remove": "Удалить", + "keybinds-right": "Переместить вправо", + "keybinds-title": "Навигационные привязки клавиш", + "keybinds-up": "Переместить вверх", + "language-desc": "Выберите предпочитаемый язык для приложения.", + "language-select-auto-detect": "Автоматически", + "language-select-description": "Выберите язык, используемый в интерфейсе приложения.", + "language-select-label": "Язык приложения", + "launch-setup-wizard": "Мастер настройки", + "profile-desc": "Редактируйте данные пользователя и аватар.", + "profile-picture-description": "Ваша фотография профиля, которая появляется во всем интерфейсе.", + "profile-picture-label": "Фотография профиля", + "profile-select-avatar": "Выбрать изображение аватара", + "profile-title": "Профиль", + "profile-tooltip": "Фотография профиля", + "reverse-scrolling-description": "Инвертировать интерпретированное направление прокрутки.", + "reverse-scrolling-label": "Обратная прокрутка", + "screen-corners-desc": "Настройка скругления углов экрана и визуальных эффектов.", + "screen-corners-radius-description": "Настройка скругления углов экрана.", + "screen-corners-radius-label": "Радиус углов экрана", + "screen-corners-radius-reset": "Сбросить радиус углов экрана", + "screen-corners-show-corners-description": "Отображать скруглённые углы по краю экрана.", + "screen-corners-show-corners-label": "Показывать углы экрана", + "screen-corners-solid-black-description": "Использовать сплошной чёрный цвет вместо цвета фона панели.", + "screen-corners-solid-black-label": "Сплошные чёрные углы", + "screen-corners-title": "Углы экрана", + "settings-copied": "Настройки скопированы в буфер обмена", + "smooth-scrolling-description": "Анимировать прокрутку списков для более плавного использования колеса.", + "smooth-scrolling-label": "Плавная прокрутка", + "tab-basics": "Основное", + "tab-keybinds": "Горячие клавиши" + }, + "hooks": { + "color-generation-description": "Команда, которая будет выполнена после завершения генерации цвета (например, после применения темы на основе обоев).", + "color-generation-label": "Цвета сгенерированы", + "color-generation-placeholder": "напр. notify-send \"Цвета\" \"Сгенерировано\"", + "info-command-info-description": "• Команды выполняются через shell (sh -lc)
• Команды выполняются в фоновом режиме (отдельно)
• Кнопки 'Тест' выполняются с текущими значениями", + "info-parameters-description": "• Wallpaper hook: $1 = путь к обоям, $2 = имя экрана, $3 = тема (темная/светлая)
• Theme toggle hook: $1 = true/false (состояние тёмного режима)
• Screen lock/unlock hooks: $1 = lock/unlock (состояние блокировки экрана)
• Performance mode hooks: Без параметров
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "Доступные параметры", + "noctalia-started-description": "Команда для выполнения после завершения загрузки Noctalia.", + "noctalia-started-label": "Noctalia запущена", + "noctalia-started-placeholder": "напр. notify-send 'Noctalia загружена'", + "performance-mode-disabled-description": "Команда для выполнения при выключении режима производительности Noctalia.", + "performance-mode-disabled-label": "Режим производительности выключен", + "performance-mode-disabled-placeholder": "напр. notify-send \"Производительность\" \"Режим выключен\"", + "performance-mode-enabled-description": "Команда для выполнения при включении режима производительности Noctalia.", + "performance-mode-enabled-label": "Режим производительности включен", + "performance-mode-enabled-placeholder": "напр. notify-send \"Производительность\" \"Режим включен\"", + "screen-lock-description": "Команда для выполнения при блокировке экрана.", + "screen-lock-label": "Экран заблокирован", + "screen-lock-placeholder": "напр. notify-send \"Экран\" \"Заблокирован\"", + "screen-unlock-description": "Команда для выполнения при разблокировке экрана.", + "screen-unlock-label": "Экран разблокирован", + "screen-unlock-placeholder": "напр. notify-send \"Экран\" \"Разблокирован\"", + "session-description": "Команда, выполняемая перед выключением или перезагрузкой. Получает тип действия как $1 (выключением/перезагрузкой).", + "session-label": "Конец сессии", + "session-placeholder": "напр. notify-send \"Session\" \"$1\"", + "system-hooks-desc": "Настройка команд для выполнения при системных событиях.", + "system-hooks-enable-description": "Включить или отключить все команды хуков.", + "system-hooks-enable-label": "Включить хуки", + "system-hooks-title": "Системные хуки", + "theme-changed-description": "Команда для выполнения при переключении темы между тёмным и светлым режимами.", + "theme-changed-label": "Тема изменена", + "theme-changed-placeholder": "напр. notify-send \"Тема\" \"Переключена\"", + "title": "Хуки", + "wallpaper-changed-description": "Команда для выполнения при смене обоев.", + "wallpaper-changed-label": "Обои изменены", + "wallpaper-changed-placeholder": "напр. notify-send \"Обои\" \"Изменены\"" + }, + "idle": { + "custom-add": "Добавить команду", + "custom-description": "Выполнить команду оболочки после периода бездействия.", + "custom-entry-command": "Команда", + "custom-entry-delete": "Удалить", + "custom-entry-edit": "Редактировать Пользовательскую Команду", + "custom-entry-name": "Имя", + "custom-entry-name-placeholder": "напр. Выключить свет", + "custom-entry-new": "Новая Пользовательская Команда", + "custom-entry-timeout": "Время бездействия", + "custom-entry-timeout-format": "{count} секунда", + "custom-entry-timeout-format-plural": "{count} секунд", + "custom-entry-unnamed": "Безымянная команда", + "custom-label": "Пользовательские команды простоя", + "enable-description": "Автоматически выключать экран, блокировать или приостанавливать работу после периода бездействия.", + "enable-label": "Включить управление бездействием", + "fade-duration-description": "Секунды для анимации затемнения перед выполнением каждого действия. Любое движение мыши отменяет затемнение.", + "fade-duration-label": "Длительность затухания", + "lock-description": "Секунды бездействия до активации экрана блокировки.", + "lock-label": "Заблокировать экран", + "resume-command-label": "Возобновить команду", + "screen-off-description": "Секунды бездействия до выключения мониторов.", + "screen-off-label": "Выключить экран", + "status-description": "Время простоя, сообщаемое композитором.", + "status-label": "Время простоя", + "suspend-description": "Секунды бездействия до приостановки работы системы.", + "tab-behavior": "Поведение", + "tab-custom": "Пользовательский", + "timeouts-description": "Установите 0, чтобы отключить этап. Тайм-ауты приостанавливаются, пока 'Не засыпать' активен.", + "timeouts-label": "Тайм-ауты", + "title": "Бездействие", + "unavailable": "Нативный мониторинг бездействия недоступен на этом Compositorе." + }, + "indicator": { + "default-value": "По умолчанию: {value}", + "system-default": "По умолчанию" + }, + "launcher": { + "clipboard-desc": "Получайте доступ к истории буфера обмена и управляйте ею из лаунчера.", + "execute-desc": "Настройте способ запуска приложений.", + "execute-title": "Исполнение", + "settings-annotation-tool-description": "Команда для запуска при нажатии кнопки аннотирования в истории буфера обмена. Изображение будет передано в эту команду.", + "settings-annotation-tool-label": "Инструмент аннотирования", + "settings-annotation-tool-placeholder": "напр. 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Автоматически вставить выбранный элемент из буфера обмена. Требуется wtype.", + "settings-auto-paste-label": "Автоматическая вставка", + "settings-clip-chips-description": "Показать панель вкладок для фильтрации истории буфера обмена по типу (Изображения, Ссылки, Файлы, Код и т.д.).", + "settings-clip-chips-label": "Включить чипы категорий", + "settings-clip-preview-description": "Показывать предварительный просмотр содержимого буфера обмена при использовании команды >clip.", + "settings-clip-preview-label": "Включить предварительный просмотр буфера обмена", + "settings-clip-smart-icons-description": "Показывать специальные значки для ссылок, файлов, цветов и кода вместо общего значка буфера обмена.", + "settings-clip-smart-icons-label": "Включить умные значки", + "settings-clip-wrap-text-description": "Переносить текст в списке буфера обмена вместо обрезания.", + "settings-clip-wrap-text-label": "Обернуть текст из буфера обмена", + "settings-clipboard-history-description": "Доступ к ранее скопированным элементам из запуска.", + "settings-clipboard-history-label": "Включить историю буфера обмена", + "settings-clipboard-watch-image-description": "Полная строка команды, передаваемая в wl-paste для изменения изображений (требуется перезапуск).", + "settings-clipboard-watch-image-label": "Команда Отслеживания Изображений", + "settings-clipboard-watch-text-description": "Полная строка команды, переданная в wl-paste для изменений текста (требуется перезапуск).", + "settings-clipboard-watch-text-label": "Команда наблюдения за текстом", + "settings-custom-launch-prefix-description": "Добавлять префикс к командам с помощью пользовательского запуска (например, 'runapp' для интеграции с systemd).", + "settings-custom-launch-prefix-enabled-description": "Использовать пользовательский префикс для запуска приложений вместо метода по умолчанию.", + "settings-custom-launch-prefix-enabled-label": "Включить пользовательский префикс запуска", + "settings-custom-launch-prefix-label": "Пользовательский префикс запуска", + "settings-density-description": "Настройте размер значков приложений и плотность Launcher.", + "settings-density-label": "Плотность", + "settings-desc": "Настройка поведения и внешнего вида запуска.", + "settings-enable-session-search-description": "Показывать действия сеанса (блокировка, выключение, перезагрузка и т.д.) при поиске в Launcher.", + "settings-enable-session-search-label": "Включить поиск по сессии", + "settings-enable-settings-search-description": "Показывать результаты настроек при поиске в лаунчере.", + "settings-enable-settings-search-label": "Включить поиск в настройках", + "settings-enable-windows-search-description": "Поиск и фокусировка активных окон.", + "settings-enable-windows-search-label": "Включить поиск окон", + "settings-grid-view-description": "Показывать элементы в виде сетки вместо списка.", + "settings-icon-mode-description": "Использовать нативные системные иконки вместо иконок Tabler.", + "settings-icon-mode-label": "Использовать нативные иконки", + "settings-ignore-mouse-input-description": "Отключить взаимодействие с мышью и колесом прокрутки в лаунчере.", + "settings-ignore-mouse-input-label": "Игнорировать ввод мыши", + "settings-overlay-layer-description": "Отображать запускатель на слое наложения, над полноэкранными окнами. При включении запускатель не будет прикреплен к панели.", + "settings-overlay-layer-label": "Показывать поверх полноэкранного режима", + "settings-position-description": "Выберите, где появляется панель лаунчера.", + "settings-show-categories-description": "Показывать вкладки категорий для фильтрации приложений.", + "settings-show-categories-label": "Показывать категории", + "settings-show-icon-background-description": "Показывать закруглённый прямоугольник в качестве фона для иконок.", + "settings-show-icon-background-label": "Показать фон значка", + "settings-sort-by-usage-description": "Если включено, часто запускаемые приложения появляются в списке первыми.", + "settings-sort-by-usage-label": "Сортировать по частоте использования", + "settings-terminal-command-description": "Команда для запуска терминала. Например, 'kitty -e' или 'gnome-terminal --'.", + "settings-terminal-command-label": "Команда терминала", + "settings-use-app2unit-description": "Использует альтернативный метод запуска для лучшего управления процессами приложений и предотвращения проблем.", + "settings-use-app2unit-label": "Использовать App2Unit для запуска приложений", + "settings-view-mode-description": "Выберите макет для записей Запускателя.", + "settings-view-mode-label": "Режим просмотра", + "title": "Лаунчер" + }, + "location": { + "auto-locate-description": "Автоматически определять ваше местоположение периодически, используя ваш IP-адрес.", + "auto-locate-label": "Автоматическое Определение", + "calendar-cards-desc": "Организуйте и включайте/выключайте карточки в панели календаря.", + "calendar-cards-title": "Карточки календаря", + "calendar-header-label": "Заголовок календаря", + "calendar-month-label": "Календарный месяц", + "date-time-12hour-format-description": "Отображает время в 12-часовом формате на экране блокировки и в календаре. Часы на панели имеют собственные настройки.", + "date-time-12hour-format-label": "Использовать 12-часовой формат времени", + "date-time-desc": "Настройка отображения даты и времени.", + "date-time-first-day-of-week-automatic": "Автоматически (использовать системную локаль)", + "date-time-first-day-of-week-description": "Выберите, с какого дня начинается неделя в календаре.", + "date-time-first-day-of-week-label": "Первый день недели", + "date-time-show-events-description": "Отображать события на панели календаря.", + "date-time-show-events-label": "Показывать события календаря", + "date-time-title": "Дата и время", + "date-time-use-analog-description": "Показывать аналоговые часы в окне календаря.", + "date-time-use-analog-label": "Использовать аналоговый стиль часов", + "date-time-week-numbers-description": "Отображает номер недели в году (например, неделя 38) в календаре.", + "date-time-week-numbers-label": "Показывать номера недель", + "geolocate-now-button": "Найти сейчас", + "location-desc": "Получите точную погоду и расписание ночного света, установив своё местоположение.", + "location-search-description": "напр. Москва, RU", + "location-search-label": "Поиск местоположения", + "location-search-placeholder": "Введите название местоположения", + "location-title": "Ваше местоположение", + "weather-desc": "Выберите предпочитаемую единицу измерения температуры.", + "weather-enabled-description": "Показывать информацию о погоде во всем интерфейсе и получать данные о погоде. Если отключено, все элементы погоды будут скрыты, и сетевые запросы не будут выполняться.", + "weather-enabled-label": "Включить погоду", + "weather-fahrenheit-description": "Отображать температуру в Фаренгейтах вместо Цельсия.", + "weather-fahrenheit-label": "Отображать температуру в Фаренгейтах (°F)", + "weather-hide-city-description": "Скрыть название города из отображения погоды во всем интерфейсе.", + "weather-hide-city-label": "Скрыть название города", + "weather-hide-timezone-description": "Скрыть аббревиатуру часового пояса из отображения погоды во всем интерфейсе.", + "weather-hide-timezone-label": "Скрыть часовой пояс", + "weather-show-effects-description": "Отображает дополнительные визуальные эффекты (например, дождь, снег или молнию) на карточке погоды.", + "weather-show-effects-label": "Отображать погодные эффекты", + "weather-show-in-calendar-description": "Показывать ежедневный прогноз погоды непосредственно в вашем календаре.", + "weather-show-in-calendar-label": "Отображать погоду в календаре", + "weather-talia-mascot-always-description": "Всегда используйте Talia emojis вместо tabler icons.", + "weather-talia-mascot-always-label": "Всегда показывать погодные эмодзи Talia" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "Когда fprintd (аутентификация по отпечатку пальца) активен, эта опция позволяет вам входить в систему с помощью пароля вместо отпечатка пальца.", + "allow-password-with-fprintd-label": "Разрешить вход по паролю с помощью fprintd", + "auto-start-auth-description": "Автоматически запускает аутентификацию по отпечатку пальца без необходимости нажатия клавиши или щелчка кнопки.", + "auto-start-auth-label": "Автоматический запуск аутентификации", + "clock-format-description": "Настройте формат часов, используя токены синтаксиса даты/времени.", + "clock-format-label": "Формат часов", + "clock-style-analog": "Аналоговый", + "clock-style-custom": "Пользовательский", + "clock-style-description": "Выберите визуальный стиль часов на Экране блокировки.", + "clock-style-digital": "Цифровой", + "clock-style-label": "Стиль Часов", + "compact-lockscreen-description": "Показывать только поле для ввода логина и системные элементы управления, скрывая виджеты погоды и медиа.", + "compact-lockscreen-label": "Компактный экран блокировки", + "enable-lockscreen-media-controls-description": "Показывать интерактивные элементы управления воспроизведением медиа на экране блокировки.", + "enable-lockscreen-media-controls-label": "Элементы Управления Мультимедиа на Экране Блокировки", + "lock-on-suspend-description": "Автоматически блокировать экран при приостановке работы системы.", + "lock-on-suspend-label": "Блокировать при приостановке", + "lock-screen-animations-description": "Включить или отключить анимации экрана блокировки.", + "lock-screen-animations-label": "Анимации Экрана Блокировки", + "lock-screen-blur-strength-description": "Применяет эффект размытия к обоям экрана блокировки.", + "lock-screen-blur-strength-label": "Сила размытия экрана блокировки", + "lock-screen-tint-strength-description": "Применяет наложение оттенка к обоям экрана блокировки.", + "lock-screen-tint-strength-label": "Интенсивность тонировки экрана блокировки", + "monitors-desc": "Показать экран блокировки на определенных мониторах. По умолчанию на всех, если ни один не выбран.", + "password-chars-description": "Милые иконки, используемые для скрытия вашего пароля.", + "password-chars-label": "Случайные иконки паролей", + "show-hibernate-description": "Показывать опцию 'спящий режим' в элементах управления питанием.", + "show-hibernate-label": "Показывать спящий режим", + "show-session-buttons-description": "Разрешить доступ к настройкам питания с экрана блокировки.", + "show-session-buttons-label": "Элементы управления питанием", + "title": "Экран блокировки" + }, + "network": { + "bluetooth-description": "Включить управление Bluetooth.", + "bluetooth-rssi-polling-description": "Периодически опрашивать RSSI для подключённых устройств через bluetoothctl. Может быть недоступно для всех устройств; использует минимальные ресурсы при включении.", + "bluetooth-rssi-polling-label": "Опрос сигнала Bluetooth", + "desc": "Управление Wi-Fi и Bluetooth подключениями.", + "wifi-description": "Управление беспроводными сетями (требуется NetworkManager)." + }, + "notifications": { + "duration-critical-urgency-description": "Как долго остаются видимыми уведомления с критическим приоритетом.", + "duration-critical-urgency-label": "Критическая срочность", + "duration-desc": "Настройка времени видимости уведомлений в зависимости от их уровня срочности.", + "duration-low-urgency-description": "Как долго остаются видимыми уведомления с низким приоритетом.", + "duration-low-urgency-label": "Низкая срочность", + "duration-normal-urgency-description": "Как долго остаются видимыми уведомления с нормальным приоритетом.", + "duration-normal-urgency-label": "Нормальная срочность", + "duration-respect-expire-description": "Использовать таймаут истечения, установленный в уведомлении.", + "duration-respect-expire-label": "Учитывать таймаут истечения", + "duration-title": "Продолжительность уведомлений", + "history-clear-dismiss-description": "Удалять уведомление из истории при закрытии.", + "history-clear-dismiss-label": "Очищать при закрытии", + "history-critical-urgency-description": "Сохранять уведомления с критическим приоритетом в истории.", + "history-critical-urgency-label": "Сохранять критическую срочность в истории", + "history-desc": "Управление тем, какие уведомления сохраняются в истории в зависимости от их уровня срочности.", + "history-low-urgency-description": "Сохранять уведомления с низким приоритетом в истории.", + "history-low-urgency-label": "Сохранять низкую срочность в истории", + "history-normal-urgency-description": "Сохранять уведомления с нормальным приоритетом в истории.", + "history-normal-urgency-label": "Сохранять нормальную срочность в истории", + "monitors-desc": "Показывать уведомления на определённых мониторах. По умолчанию на всех, если ни один не выбран.", + "rules-action-block": "Блок", + "rules-action-block-desc": "Пропускает полностью.", + "rules-action-hide": "Скрыть", + "rules-action-hide-desc": "Без всплывающих окон, без звука, добавляет в историю.", + "rules-action-label": "Действие", + "rules-action-mute": "Без звука", + "rules-action-mute-desc": "Нет звука, всплывающее окно всё равно отображается и в истории.", + "rules-add": "Добавить правило", + "rules-delete": "Удалить правило", + "rules-description": "Сопоставить название приложения или содержимое. Правила проверяются по порядку, и первое совпадение применяется.", + "rules-edit": "Редактировать правило", + "rules-label": "Правила Фильтрации", + "rules-pattern-label": "Шаблон", + "rules-pattern-placeholder": "firefox, discord, или /regex/", + "rules-tab": "Правила", + "settings-always-on-top-description": "Отображать уведомления поверх полноэкранных окон и других слоёв.", + "settings-background-opacity-description": "Настройка непрозрачности фона уведомлений.", + "settings-density-description": "Выберите плотность карточки уведомлений.", + "settings-density-label": "Плотность", + "settings-desc": "Настройка внешнего вида и поведения уведомлений.", + "settings-do-not-disturb-description": "Отключить все всплывающие окна уведомлений, если включено.", + "settings-enabled-description": "Включить или отключить демон уведомлений, требуется перезапуск Noctalia shell.", + "settings-enabled-label": "Включить уведомления", + "settings-location-description": "Где уведомления появляются на экране.", + "settings-markdown-description": "Отображать содержимое уведомлений, используя форматирование Markdown.", + "settings-markdown-label": "Включить Markdown", + "sounds-desc": "Настроить звуковые эффекты и громкость уведомлений.", + "sounds-enabled-description": "Включить звуковые эффекты для входящих уведомлений.", + "sounds-enabled-label": "Включить звуки уведомлений", + "sounds-excluded-apps-description": "Пропускать воспроизведение настроенного звука уведомлений для определённых приложений, имеющих собственные встроенные звуки.", + "sounds-excluded-apps-label": "Исключённые приложения", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Путь к звуковому файлу, воспроизводимому для уведомлений с критическим приоритетом.", + "sounds-files-critical-label": "Звук критической важности", + "sounds-files-critical-select-title": "Выбрать файл звука критической важности", + "sounds-files-desc": "Настроить пользовательские звуковые файлы для разных уровней важности уведомлений.", + "sounds-files-low-description": "Путь к звуковому файлу, воспроизводимому для уведомлений с низким приоритетом.", + "sounds-files-low-label": "Звук низкой важности", + "sounds-files-low-select-title": "Выбрать файл звука низкой важности", + "sounds-files-normal-description": "Путь к звуковому файлу, воспроизводимому для уведомлений с нормальным приоритетом.", + "sounds-files-normal-label": "Звук нормальной важности", + "sounds-files-normal-select-title": "Выбрать файл звука нормальной важности", + "sounds-files-placeholder": "Введите путь к звуковому файлу", + "sounds-files-select-file": "Выбрать звуковой файл", + "sounds-files-title": "Звуковые файлы", + "sounds-files-unified-description": "Путь к звуковому файлу, воспроизводимому для уведомлений.", + "sounds-files-unified-label": "Звук уведомления", + "sounds-files-unified-select-title": "Выбрать файл звука уведомления", + "sounds-separate-description": "Использовать разные звуковые файлы для уведомлений с низким, нормальным и критическим приоритетом.", + "sounds-separate-label": "Использовать разные звуки для разных приоритетов", + "sounds-title": "Настройки звука", + "sounds-unavailable-description": "Установите Qt6 Multimedia для включения звуков уведомлений.", + "sounds-unavailable-label": "Звуки уведомлений недоступны", + "sounds-volume-description": "Настроить уровень громкости для звуков уведомлений.", + "sounds-volume-label": "Громкость звука", + "toast-battery-description": "Показывать предупреждение, когда уровень заряда батареи опускается ниже этого процента.", + "toast-battery-label": "Предупреждение о батарее", + "toast-desc": "Настроить внешний вид и поведение всплывающих уведомлений.", + "toast-keyboard-description": "Показывать всплывающее уведомление при смене раскладки клавиатуры.", + "toast-keyboard-label": "Раскладка клавиатуры", + "toast-media-description": "Показывать всплывающее уведомление при изменении состояния воспроизведения медиа.", + "toast-media-label": "Медиа" + }, + "osd": { + "always-on-top-description": "Отображать OSD поверх полноэкранных окон и других слоёв.", + "always-on-top-label": "Всегда сверху", + "background-opacity-description": "Управляет прозрачностью фона OSD.", + "background-opacity-label": "Прозрачность фона", + "description": "Настройка экранных индикаторов, таких как оверлеи громкости и яркости.", + "duration-auto-hide-description": "Настройка времени, прежде чем OSD исчезнет.", + "duration-auto-hide-label": "Автоматически скрывать через", + "duration-desc": "Как долго OSD остаётся видимым перед автоматическим скрытием.", + "duration-title": "Таймаут автоскрытия", + "enabled-description": "Показывать изменения громкости и яркости в реальном времени.", + "enabled-label": "Включить уведомления (OSD)", + "events-desc": "Выберите, по каким событиям выводить уведомления (OSD).", + "general-desc": "Настроить видимость и поведение уведомлений (OSD).", + "location-description": "Расположение экранных уведолений.", + "monitors-desc": "Показывать OSD на определённых мониторах. По умолчанию на всех, если ни один не выбран.", + "title": "Уведомления", + "types-brightness-description": "Показывать OSD при изменении яркости экрана.", + "types-brightness-label": "Яркость", + "types-custom-text-description": "Показывать OSD для пользовательских текстовых сообщений через IPC.", + "types-custom-text-label": "Пользовательский текст", + "types-desc": "Выберите события, которые должны запускать уведомления (OSD).", + "types-input-volume-description": "Показывать OSD при изменении громкости микрофона.", + "types-input-volume-label": "Входная громкость", + "types-lockkey-description": "Показывать OSD при переключении клавиш Caps Lock, Num Lock или Scroll Lock.", + "types-lockkey-label": "Клавиши блокировки", + "types-media-description": "Показывать OSD при изменении состояния воспроизведения медиа (воспроизведение, пауза, пропуск).", + "types-media-label": "Воспроизведение медиа", + "types-title": "События, запускающие уведомления (OSD)", + "types-volume-description": "Показывать OSD при изменении громкости аудиовыхода.", + "types-volume-label": "Выходная громкость" + }, + "plugins": { + "auto-update": "Автоматическое обновление плагинов", + "auto-update-description": "Автоматически обновлять все плагины при запуске оболочки.", + "available-description": "Просмотр и установка плагинов из настроенных источников.", + "available-label": "Доступные плагины", + "available-no-plugins-description": "Проверьте исходники вашего плагина или обновите список.", + "available-no-plugins-label": "Нет доступных плагинов", + "check-for-updates": "Проверить обновления", + "checking-for-updates": "Проверка обновлений...", + "collision-already-installed": "Этот плагин уже установлен", + "collision-custom-version-exists": "Пользовательская версия из \"{source}\" уже установлена", + "collision-official-version-exists": "Официальная версия этого плагина уже установлена", + "development-disable": "Отключить режим разработки для этого плагина", + "development-enable": "Включить режим разработки для этого плагина.", + "filter-downloaded": "Скачано", + "filter-not-downloaded": "Не загружено", + "filter-tags-description": "Фильтровать плагины по категории или статусу загрузки.", + "filter-tags-label": "Теги", + "hot-reload-description": "Автоматически перезагружать плагины при изменении их файлов. Полезно для разработки плагинов.", + "hot-reload-label": "Горячая перезагрузка (режим разработки)", + "hot-reloaded": "Перезагружен плагин: {name}", + "install-error": "Не удалось установить: {error}", + "install-incompatible": "{plugin} требует Noctalia версии {version} или выше", + "install-success": "Успешно установлен {plugin}", + "installed-description": "Управляйте и настраивайте все локально установленные плагины.", + "installed-label": "Установленные плагины", + "installed-no-plugins-description": "Установите плагины из раздела \"Доступно\".", + "installed-no-plugins-label": "Нет установленных плагинов", + "installing": "Установка {plugin}...", + "notify-updates": "Уведомления об обновлении плагинов", + "notify-updates-description": "Показывать всплывающее уведомление при наличии новых обновлений плагинов.", + "open-plugin-page": "Открыть страницу плагина", + "open-plugins-tab": "Открыть настройки плагинов", + "plugin-settings-title": "Настройки {plugin}", + "refresh-refreshing": "Обновление списка плагинов...", + "refresh-tooltip": "Обновить доступные плагины", + "settings-error-not-loaded": "Плагин не загружен", + "settings-saved": "Настройки плагина сохранены", + "settings-tooltip": "Настройки плагина", + "source-custom": "Пользовательский источник", + "sources-add-custom": "Добавить пользовательский репозиторий", + "sources-add-dialog-description": "Добавить репозиторий GitHub в качестве источника плагинов.", + "sources-add-dialog-error": "Не удалось добавить источник плагина", + "sources-add-dialog-name": "Имя репозитория", + "sources-add-dialog-name-placeholder": "Мои пользовательские плагины", + "sources-add-dialog-success": "Источник плагина успешно добавлен", + "sources-add-dialog-title": "Добавить исходный код плагина", + "sources-add-dialog-url": "URL репозитория", + "sources-description": "Управление репозиториями плагинов.", + "sources-placeholder": "Мой крутой репозиторий", + "sources-remove-tooltip": "Удалить исходный код плагина", + "title": "Плагины", + "translations-reloaded": "Переводы перезагружены: {name}", + "uninstall-dialog-description": "Вы уверены, что хотите удалить {plugin}? Это удалит все данные плагина.", + "uninstall-dialog-title": "Удалить плагин", + "uninstall-error": "Не удалось удалить: {error}", + "uninstall-success": "{plugin} успешно удалён", + "uninstalling": "Удаление {plugin}...", + "update-all": "Обновить все ({count})", + "update-all-success": "Все плагины успешно обновлены", + "update-available": "Доступно новое обновление плагина", + "update-available-plural": "Доступны новые обновления плагинов ({count})", + "update-error": "Не удалось обновить плагин: {plugin}: {error}", + "update-pending": "v{current} → v{new} (требуется Noctalia v{required})", + "update-success": "Плагин {plugin} обновлён до версии {version}", + "update-version": "v{current} → v{new}", + "updating": "Обновление..." + }, + "region": { + "title": "Регион" + }, + "session-menu": { + "countdown-duration-description": "Установить, как долго длится таймер обратного отсчёта перед выполнением действий питания.", + "countdown-duration-label": "Длительность обратного отсчёта", + "enable-countdown-description": "Показывать таймер обратного отсчёта перед выполнением действий питания.", + "enable-countdown-label": "Включить таймер обратного отсчёта", + "entries-desc": "Настроить, какие действия питания отображаются в меню сеанса и в каком порядке.", + "entries-title": "Действия питания", + "entry-settings-command-description": "Пользовательская команда для выполнения этого действия. Оставьте пустым, чтобы использовать системную команду по умолчанию.", + "entry-settings-command-placeholder": "напр. systemctl poweroff", + "entry-settings-default-command-lock": "Внутренний экран блокировки (без команды)", + "entry-settings-default-command-logout": "Внутренний выход из системы (без команды)", + "entry-settings-default-info-description": "Если не указана пользовательская команда, будет использована системная команда по умолчанию.", + "entry-settings-default-info-label": "Команда по умолчанию", + "entry-settings-keybind-description": "Назначьте комбинацию клавиш для вызова этого действия, когда меню сеанса открыто.", + "entry-settings-keybind-placeholder": "Нажмите Запись, чтобы назначить клавишу...", + "entry-settings-keybind-recording": "Запись... Нажмите любую комбинацию клавиш", + "entry-settings-title": "Настроить {entry}", + "entry-settings-tooltip": "Настроить команду", + "general-desc": "Настройка поведения и внешнего вида панели меню сеанса.", + "large-buttons-layout-description": "Выберите способ отображения кнопок меню сеанса.", + "large-buttons-layout-label": "Крупная раскладка кнопок", + "large-buttons-style-description": "Отобразить меню сеанса с большими кнопками в виде сетки.", + "large-buttons-style-label": "Стиль крупные кнопки", + "position-description": "Выберите, где появляется панель меню сеанса при открытии.", + "show-header-description": "Отображать заголовок и кнопку закрытия в верхней части меню сеанса.", + "show-header-label": "Показывать заголовок", + "show-keybinds-description": "Отображать подсказки по горячим клавишам в параметрах сеанса.", + "show-keybinds-label": "Показать сочетания клавиш" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Оставлять виджеты рабочего стола видимыми, пока режим производительности Noctalia включен.", + "noctalia-performance-disable-desktop-widgets-label": "Включить виджеты рабочего стола в режиме производительности", + "noctalia-performance-disable-wallpaper-description": "Сохраняйте видимыми обои рабочего стола, обзора и экрана блокировки, пока режим производительности Noctalia включен.", + "noctalia-performance-disable-wallpaper-label": "Включить рендеринг обоев в режиме производительности", + "title": "Система" + }, + "system-monitor": { + "critical-color-label": "Критический цвет", + "custom-highlight-colors-title-label": "Пользовательские цвета выделения", + "disk-available-label": "Доступно места на диске", + "disk-section-label": "Использование диска", + "enable-dgpu-monitoring-description": "Внимание: Это разбудит ваш дискретный графический процессор (NVIDIA/AMD), что может значительно повлиять на время работы от аккумулятора на ноутбуках с гибридной графикой.", + "enable-dgpu-monitoring-label": "Включить мониторинг дискретного GPU", + "external-monitor-description": "Введите команду или путь к приложению для запуска при активации внешнего приложения мониторинга системы.", + "external-monitor-label": "Команда внешнего монитора системы", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "Настроить поведение и внешний вид системного монитора.", + "gpu-section-label": "Температура GPU", + "highlight-colors-section-label": "Цвета выделения", + "polling-interval-label": "Интервал опроса", + "polling-section-description": "Настройте частоту обновления каждой системной метрики.", + "polling-section-label": "Интервалы опроса", + "threshold-critical": "Критический", + "threshold-warning": "Предупреждение", + "thresholds-section-description": "Настройте пороги предупреждения/критические пороги и интервалы опроса для каждой системной метрики.", + "use-custom-highlight-colors-description": "Если отключено, используются цвета выделения по умолчанию темы.", + "use-custom-highlight-colors-label": "Использовать пользовательские цвета выделения", + "warning-color-label": "Цвет предупреждения" + }, + "user-interface": { + "allow-panels-without-bar-description": "Когда включено, панели могут открываться на любом экране. Когда выключено, панели будут открываться только на экранах с панелью, что может снизить использование памяти.", + "allow-panels-without-bar-label": "Разрешить панели на экранах без панели задач", + "animation-disable-description": "Отключить все анимации для более быстрого и отзывчивого взаимодействия.", + "animation-disable-label": "Отключить анимации интерфейса", + "animation-speed-description": "Настройка глобальной скорости анимации.", + "animation-speed-label": "Скорость анимации", + "animation-speed-reset": "Сбросить скорость анимации", + "appearance-desc": "Настройте визуальные элементы, такие как всплывающие подсказки, границы и тени.", + "blur-behind-description": "Размывает область за панелями и всплывающими окнами, используя протокол размытия композитора.", + "blur-behind-label": "Размытие фона", + "box-border-description": "Отображает контур вокруг областей содержимого.", + "box-border-label": "Контур контейнера", + "box-border-radius-description": "Настраивает скругление углов основных разделов макета, таких как боковые панели, карточки и панели контента.", + "box-border-radius-label": "Радиус контейнера", + "control-border-radius-description": "Управляет скруглением интерактивных элементов, включая кнопки, переключатели и текстовые поля.", + "control-border-radius-label": "Радиус элементов ввода", + "desc": "Настройка внешнего вида, ощущений и поведения интерфейса.", + "dim-desktop-description": "Затемнять рабочий стол при открытии панелей или меню.", + "dim-desktop-label": "Затемнять рабочий стол", + "dimmer-opacity-description": "Установить уровень непрозрачности для затемнения рабочего стола.", + "dimmer-opacity-label": "Непрозрачность затемнённого рабочего стола", + "panel-background-opacity-description": "Установить прозрачность фона для всех панелей (верхней панели, панели запуска, настроек и т. д.).", + "panel-background-opacity-label": "Прозрачность фона панелей", + "panels-attached-to-bar-description": "Панели прикрепляются к панели и краям экрана, создавая цельный вид со стильными инвертированными углами.", + "panels-attached-to-bar-label": "Прикрепить панели к краям", + "panels-overlay-description": "Обеспечивает видимость панелей и панели задач даже поверх полноэкранных приложений.", + "panels-overlay-label": "Держать панели и панель задач сверху", + "scaling-description": "Изменяет размер общего пользовательского интерфейса, исключая панель.", + "scaling-label": "Масштабирование интерфейса", + "scrollbar-always-visible-description": "Всегда показывать полосы прокрутки, когда содержимое прокручивается, вместо того чтобы отображать их только при наведении.", + "scrollbar-always-visible-label": "Всегда показывать полосы прокрутки", + "settings-panel-header": "Панель настроек", + "settings-panel-mode-description": "Выберите раскладку настроек (может потребоваться перезапуск).", + "settings-panel-mode-label": "Режим панели настроек", + "settings-panel-sidebar-card-style-description": "Оборачивает боковую панель настроек в заполненный фон со скругленными углами.", + "settings-panel-sidebar-card-style-label": "Стиль карточек боковой панели", + "shadows-description": "Включает отбрасываемые тени под панелями и панелью задач.", + "shadows-direction-description": "Выберите, куда отбрасывается тень.", + "shadows-direction-label": "Направление тени", + "shadows-label": "Отбрасываемые тени", + "title": "Пользовательский интерфейс", + "tooltips-description": "Включить или отключить всплывающие подсказки во всем интерфейсе.", + "tooltips-label": "Показывать всплывающие подсказки", + "translucent-widgets-description": "Сделайте кнопки, вкладки и другие виджеты внутри панелей полупрозрачными.", + "translucent-widgets-label": "Полупрозрачные виджеты" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Алфавитный", + "automation-change-mode-description": "Выберите, как выбираются обои при автоматической смене.", + "automation-change-mode-label": "Режим смены", + "automation-custom-interval-description": "Введите время в формате ЧЧ:ММ (напр. 01:30).", + "automation-custom-interval-label": "Пользовательский интервал", + "automation-interval-description": "Частота автоматической смены обоев.", + "automation-interval-label": "Интервал смены обоев", + "automation-random-wallpaper-description": "Запланировать смену случайных обоев через регулярные интервалы.", + "automation-scheduled-change-description": "Автоматически менять обои через регулярные интервалы времени.", + "automation-scheduled-change-label": "Запланированная смена", + "look-feel-edge-smoothness-description": "Применяет мягкий, растушёванный эффект к краю переходов.", + "look-feel-edge-smoothness-label": "Смягчить край перехода", + "look-feel-fill-color-description": "Выберите цвет заливки, который может появиться за обоями.", + "look-feel-fill-mode-description": "Выберите, как изображение должно масштабироваться, чтобы соответствовать разрешению вашего монитора.", + "look-feel-fill-mode-label": "Режим заполнения", + "look-feel-skip-startup-transition-description": "Пропускать анимацию обоев при запуске оболочки.", + "look-feel-skip-startup-transition-label": "Пропустить стартовую анимацию", + "look-feel-title": "Внешний вид", + "look-feel-transition-duration-description": "Продолжительность анимации перехода в секундах.", + "look-feel-transition-duration-label": "Продолжительность перехода", + "look-feel-transition-type-description": "Тип анимации при переключении между обоями.", + "look-feel-transition-type-label": "Тип перехода", + "settings-clear-cache-button": "Очистить кэш", + "settings-clear-cache-description": "Очистить кэшированные масштабированные обои, чтобы освободить место на диске.", + "settings-clear-cache-label": "Кэш обоев", + "settings-clear-cache-toast": "Кэш обоев очищен", + "settings-desc": "Управление тем, как обои управляются и отображаются.", + "settings-enable-management-description": "Управлять обоями с помощью Noctalia. Снимите флажок, если предпочитаете использовать другое приложение.", + "settings-enable-management-label": "Включить управление обоями", + "settings-enable-overview-description": "Применяет размытые и затемнённые обои к экрану обзора.", + "settings-enable-overview-label": "Включить обои обзора", + "settings-folder-description": "Путь к вашей основной папке с обоями.", + "settings-folder-label": "Папка с обоями", + "settings-hide-wallpaper-filenames-tooltip-hide": "Скрыть имена файлов", + "settings-hide-wallpaper-filenames-tooltip-show": "Показать имена файлов", + "settings-monitor-specific-description": "Установить отдельную папку с обоями для каждого монитора.", + "settings-monitor-specific-label": "Каталоги для конкретного монитора", + "settings-monitor-specific-tooltip": "Папка обоев монитора", + "settings-overview-blur-strength-description": "Применяет силу размытия к обзору.", + "settings-overview-blur-strength-label": "Сила размытия обзора", + "settings-overview-tint-description": "Применяет интенсивность оттенка к обзору.", + "settings-overview-tint-label": "Интенсивность тонировки обзора", + "settings-recursive-search-description": "Также искать обои во вложенных папках каталога обоев.", + "settings-recursive-search-label": "Искать во вложенных папках", + "settings-select-monitor-folder": "Выбрать папку с обоями для монитора", + "settings-selector-description": "Перейти на панель выбора обоев.", + "settings-selector-position-description": "Выберите, где появляется панель выбора обоев.", + "settings-show-hidden-files-tooltip-hide": "Не показывать скрытые файлы", + "settings-show-hidden-files-tooltip-show": "Показать скрытые файлы", + "settings-title": "Настройки обоев", + "settings-use-original-images-description": "Пропускать изменение размера обоев перед отображением. Экономит место на диске и снижает нагрузку на CPU во время смены обоев, но может использовать больше памяти для очень больших изображений.", + "settings-use-original-images-label": "Использовать оригинальные изображения", + "settings-view-mode-description": "Выберите способ отображения обоев из вашей директории.", + "settings-view-mode-label": "Режим просмотра", + "view-mode-browse": "Просмотр каталогов", + "view-mode-cycle-tooltip": "Режим просмотра: {mode} (нажмите, чтобы изменить)", + "view-mode-recursive": "Объединить с подкаталогами", + "view-mode-single": "Корневой каталог" + } + }, + "placeholders": { + "add-new-keybind": "Добавить новую привязку клавиш", + "command-example": "например, echo \"Hello World\"", + "enter-command": "Введите команду для выполнения (приложение или пользовательский скрипт)", + "enter-ipc-identifier": "Введите уникальный идентификатор для команд IPC", + "enter-path": "Введите путь...", + "enter-text-to-collapse": "напр. 'ничего не играет'. Используйте /regex/ для шаблонов.", + "enter-tooltip": "Введите подсказку", + "enter-width-pixels": "Введите ширину в пикселях", + "keybind-recording": "Запись сочетания клавиш...", + "search": "Поиск...", + "search-icons": "напр. noctalia, niri, battery, cloud", + "search-launcher": "Поиск записей... или используйте > для команд", + "search-wallhaven": "Поиск в Wallhaven...", + "search-wallpapers": "Введите для фильтрации обоев..." + }, + "positions": { + "bottom": "Внизу", + "bottom-center": "Внизу по центру", + "bottom-left": "Внизу слева", + "bottom-right": "Внизу справа", + "center": "По центру", + "center-left": "По центру слева", + "center-right": "По центру справа", + "close-to-bar": "Рядом с кнопкой панели", + "follow-bar": "Следовать за панелью", + "left": "Слева", + "right": "Справа", + "top": "Вверху", + "top-center": "Вверху по центру", + "top-left": "Вверху слева", + "top-right": "Вверху справа" + }, + "session-menu": { + "action-in-seconds": "{action} через {seconds} секунд...", + "cancel-timer": "Отменить таймер", + "title": "Меню сеанса" + }, + "setup": { + "all-done": "Готово!", + "appearance": { + "subheader": "Выберите темный режим и источники цвета (обои или предопределенные)." + }, + "customize": { + "header": "Настройте свой опыт", + "subheader": "Настройте положение панели, плотность, масштабирование и многое другое." + }, + "skip-setup": "Пропустить настройку", + "telemetry-wizard-done": "Понял!", + "telemetry-wizard-note": "Вы всё контролируете — вы можете включить или отключить это в любое время в настройках", + "telemetry-wizard-subtitle": "Мы добавили анонимную аналитику, чтобы помочь улучшить Noctalia", + "telemetry-wizard-title": "Обновление конфиденциальности", + "wallpaper": { + "choose-dir": "Выберите каталог, содержащий ваши изображения обоев", + "dir-browse": "Обзор папки с обоями", + "dir-description": "Выберите папку, содержащую ваши обои.", + "dir-label": "Каталог обоев", + "dir-select-title": "Выбрать папку с обоями", + "header": "Выберите свои обои", + "no-dir": "Каталог обоев не выбран", + "no-valid": "Не найдено допустимых файлов изображений в: {dir}", + "none-in-dir": "Обои не найдены в каталоге", + "preview-error": "Не удалось загрузить изображение", + "select-prompt": "Выберите обои ниже", + "subheader": "Создайте настроение с красивым фоном." + }, + "welcome": "Добро пожаловать", + "welcome-note": "Всего несколько основ для начала — полные настройки находятся в настройках", + "welcome-subtitle": "Давайте сделаем ваш рабочий стол уникальным", + "welcome-title": "Добро пожаловать в Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "Медиаплеер не обнаружен", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "Неизвестное приложение", + "uptime": "Время работы: {uptime}", + "user-requested": "Запрошено пользователем", + "welcome-back": "С возвращением,", + "widget-settings-title": "Настройки виджета {widget}" + }, + "system-monitor": { + "core-usage": "Использование ядра {id}", + "cpu-temp": "Температура ЦП", + "cpu-usage": "Использование ЦП", + "disk": "Диск", + "download-speed": "Скорость загрузки", + "gpu-temp": "Температура GPU", + "load-average": "Средняя нагрузка", + "title": "Системный монитор", + "upload-speed": "Скорость загрузки" + }, + "toast": { + "airplane-mode": { + "description": "Отключить все беспроводные коммуникации.", + "title": "Режим полёта" + }, + "battery": { + "critical": "Критический заряд батареи", + "critical-desc": "Батарея заряжена на {percent}% — немедленно подключите зарядное устройство", + "low": "Низкий заряд батареи", + "low-desc": "Батарея заряжена на {percent}% — пожалуйста, подключите зарядное устройство" + }, + "bluetooth": { + "address-copied": "Адрес скопирован в буфер обмена", + "auto-connect-disabled": "Автоподключение отключено", + "auto-connect-enabled": "Автоподключение включено", + "auto-connecting": "Подключение к {count} устройству(ам)...", + "confirm-code": "Подтвердите код {value} на другом устройстве.", + "connect-failed": "Не удалось подключиться к устройству", + "disconnect-failed": "Не удалось отключиться от устройства", + "discoverable-change-failed": "Не удалось изменить состояние видимости", + "discoverable-disabled": "Обнаружение отключено", + "discoverable-enabled": "Обнаружение включено", + "display-code": "Введите код {value} на другом устройстве.", + "forget-failed": "Не удалось удалить устройство", + "pair-failed": "Не удалось выполнить сопряжение устройства", + "passkey-required": "Требуется ключ доступа устройства.", + "pincode-required": "PIN-код, требуемый устройством.", + "state-change-failed": "Не удалось изменить состояние Bluetooth" + }, + "clipboard": { + "long-text": "Длинный текст", + "unavailable": "История буфера обмена недоступна", + "unavailable-desc": "Приложение 'cliphist' не установлено. Пожалуйста, установите его, чтобы использовать функции истории буфера обмена" + }, + "custom-command-failed": { + "description": "Команда не выполнена: {command}\\nКод выхода: {code}", + "title": "Пользовательская команда не выполнена" + }, + "do-not-disturb": { + "disabled": "Режим 'Не беспокоить' отключен", + "disabled-desc": "Отображаются все уведомления", + "enabled": "Режим 'Не беспокоить' включен", + "enabled-desc": "Вы найдёте эти уведомления в своей истории" + }, + "donation-opened": "Страница пожертвований открыта в вашем браузере", + "internet-limited": "Подключено без доступа к интернету", + "keyboard-layout": { + "changed": "Раскладка клавиатуры изменена на {layout}", + "title": "Клавиатура" + }, + "missing-control-center": { + "description": "Виджет центра управления был удалён с панели. Чтобы снова получить к нему доступ с панели, вам нужно будет повторно добавить виджет. Вы также можете открыть его, нажав правой кнопкой мыши на панель", + "label": "Последний виджет центра управления удалён" + }, + "night-light": { + "forced": "Принудительная активация", + "normal": "Нормальный режим", + "not-installed": "wlsunset не установлен" + }, + "noctalia-performance": { + "disabled": "Режим производительности Noctalia отключен", + "enabled": "Режим производительности Noctalia включен", + "label": "Производительность Noctalia" + }, + "power-profile": { + "changed": "Профиль питания изменён", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "Произошла ошибка при обработке шаблонов", + "title": "Ошибка обработки шаблона" + }, + "vpn": { + "connected": "Подключено к '{name}'", + "disconnected": "Отключено от '{name}'" + }, + "wallpaper-colors": { + "disabled": "Цвета обоев отключены", + "enabled": "Цвета обоев включены", + "label": "Генерация цветов обоев" + }, + "wifi": { + "connected": "Подключено к '{ssid}'", + "connection-failed": "Соединение не установлено", + "connection-timeout": "Превышено время ожидания соединения", + "disconnected": "Отключено от '{ssid}'", + "incorrect-password": "Неверный пароль", + "network-not-found": "Сеть не найдена" + } + }, + "tooltips": { + "add-widget": "Добавить виджет", + "auto-connect": "Переключить автоподключение для этого устройства", + "bluetooth-auto-connect-off": "Автоподключение отключено", + "bluetooth-auto-connect-on": "Автоподключение включено", + "bluetooth-devices": "Устройства Bluetooth", + "brightness-at": "Яркость: {brightness}%", + "click-to-start-recording": "Запись экрана (начать запись)", + "click-to-stop-recording": "Запись экрана (остановить запись)", + "collapse": "Свернуть боковую панель", + "copy-address": "Копировать адрес", + "delete-notification": "Удалить уведомление", + "dismiss-notification": "Отклонить уведомление", + "do-not-disturb-enabled": "Не беспокоить", + "expand": "Развернуть боковую панель", + "forget-network": "Забыть сеть", + "grid-view": "Вид сеткой", + "hidden-files-hide": "Скрытые файлы", + "hide-unnamed-devices": "Скрыть безымянные устройства", + "home": "Домашний каталог", + "input-muted": "Включить/выключить отключение микрофона", + "keep-awake": "Не засыпать", + "keyboard-layout": "Раскладка клавиатуры {layout}", + "list-view": "Вид списком", + "manage-vpn": "VPN-подключения", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "Достигнуто максимальное количество виджетов", + "microphone-volume-at": "Громкость микрофона: {volume}%", + "move-to-section": "Перейти к {section}", + "mute": "Отключить звук", + "next-media": "Следующий трек", + "next-month": "Следующий месяц", + "night-light-not-installed": "Ночной свет (недоступен)", + "noctalia-performance-enabled": "Режим производительности Noctalia", + "open-annotation-tool": "Открыть с помощью инструмента аннотирования", + "open-control-center": "Центр управления", + "open-notification-history-enable-dnd": "История уведомлений", + "open-settings": "Настройки", + "open-tray-dropdown": "Системный трей", + "output-muted": "Переключить отключение звука", + "power-profile": "{profile} профиль питания", + "previous-media": "Предыдущий трек", + "previous-month": "Предыдущий месяц", + "refresh-devices": "Обновить устройства", + "refresh-wallhaven": "Обновить результаты Wallhaven", + "refresh-wallpaper-list": "Обновить список обоев", + "remove-widget": "Удалить виджет", + "search-close": "Закрыть поиск", + "session-menu": "Меню сеанса", + "show-all-devices": "Показать все устройства", + "switch-to-dark-mode": "Тёмный режим", + "switch-to-light-mode": "Светлый режим", + "unmute": "Включить звук", + "up": "Родительский каталог", + "volume-at": "Громкость вывода: {volume}%", + "wallpaper-selector": "Выбор обоев" + }, + "wallpaper": { + "browse": { + "empty-directory": "Этот каталог пуст.", + "go-root": "Перейти в корневую папку обоев", + "go-up": "Перейти в родительскую папку", + "go-up-hint": "Используйте кнопку «Назад» для навигации вверх." + }, + "configure-directory": "Настройте свой каталог обоев с изображениями.", + "fill-modes": { + "crop": "Обрезать (Заполнить)", + "fit": "Подогнать (Вместить)", + "repeat": "Повторить (Замостить)", + "stretch": "Растянуть" + }, + "no-match": "Совпадений не найдено.", + "no-wallpaper": "Обои не найдены.", + "panel": { + "apikey-help": "Для доступа к контенту NSFW требуется ключ API.", + "apikey-label": "Ключ API", + "apikey-managed-by-env": "Управляется через переменную окружения NOCTALIA_WALLHAVEN_API_KEY.", + "apikey-placeholder": "Введите свой API-ключ Wallhaven", + "appearance-dark-tab": "Темный", + "appearance-light-tab": "Светлый", + "categories-anime": "Аниме", + "categories-label": "Категории", + "categories-people": "Люди", + "color-extraction-disabled": "Использовать извлечение цветов обоев", + "color-extraction-enabled": "Использовать предопределенные цветовые схемы", + "header-devices-apply-all-tooltip": "Выбрать обои для каждого монитора", + "header-devices-per-monitor-tooltip": "Применить ко всем мониторам", + "header-link-light-dark-tooltip": "Сохранять обои при смене темы", + "header-separate-light-dark-tooltip": "Автоматическая смена обоев с темой", + "header-sun-linked-tooltip": "Раздельные светлые и темные обои", + "header-sun-separate-tooltip": "Одни и те же обои для светлой и темной темы", + "order-asc": "По возрастанию", + "order-desc": "По убыванию", + "order-label": "Порядок", + "purity-label": "Фильтр контента", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Sketchy", + "ratios-all-portrait": "Все портретные", + "ratios-all-wide": "Все широкие", + "ratios-any": "Любой", + "ratios-label": "Соотношение сторон", + "resolution-atleast": "По крайней мере", + "resolution-exact": "Точно", + "resolution-label": "Разрешение", + "resolution-mode-label": "Режим", + "solid-color-tooltip": "Однотонный фон", + "sort-date-asc": "Сортировать от старых к новым", + "sort-date-desc": "Сортировать от новых к старым", + "sort-name-asc": "Сортировать по имени (А-Я)", + "sort-name-desc": "Сортировать по имени (Я-А)", + "sort-random": "Сортировать случайно", + "sorting-date-added": "Дата добавления", + "sorting-favorites": "Избранное", + "sorting-label": "Сортировать по", + "sorting-relevance": "Релевантность", + "sorting-toplist": "Топ-лист", + "sorting-views": "Просмотры", + "source-label": "Источник", + "source-wallhaven": "Wallhaven", + "title": "Выбор обоев", + "wallhaven-settings-title": "Настройки Wallhaven" + }, + "transitions": { + "disc": "Диск", + "fade": "Затухание", + "honeycomb": "Соты", + "pixelate": "Пикселизировать", + "stripes": "Полосы", + "wipe": "Стирание" + }, + "try-different-search": "Попробуйте другой поисковый запрос.", + "wallhaven": { + "loading": "Загрузка обоев...", + "no-results": "Обои не найдены. Попробуйте другой поисковый запрос.", + "page": "{current} из {total}", + "page-prefix": "Страница", + "page-suffix": "из {total}" + } + }, + "weather": { + "clear-sky": "Ясное небо", + "drizzle": "Морось", + "fog": "Туман", + "mainly-clear": "Преимущественно ясно", + "overcast": "Пасмурно", + "partly-cloudy": "Переменная облачность", + "rain-showers": "Ливни", + "snow": "Снег", + "thunderstorm": "Гроза" + }, + "widgets": { + "color-picker": { + "palette-description": "Выберите из широкого диапазона предопределённых цветов.", + "palette-label": "Палитра", + "palette-theme-colors": "Быстрый доступ к цветовой палитре вашей темы.", + "title": "Выбор цвета" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm в нижнем регистре", + "ampm-uppercase": "AM/PM в верхнем регистре", + "common-12hour-time-minutes": "12-часовое время с минутами", + "common-24hour-time-minutes": "24-часовое время с минутами", + "common-24hour-time-seconds": "24-часовое время с секундами", + "common-european-date": "Европейский формат даты", + "common-iso-date": "Формат даты ISO", + "common-us-date": "Формат даты США", + "common-weekday-date": "День недели с датой", + "common-weekday-month-day": "День недели, месяц и число", + "day-abbreviated": "Сокращённое название дня", + "day-full": "Полное название дня", + "day-leading-zero": "День с ведущим нулём (01-31)", + "day-no-leading-zero": "День без ведущего нуля (1-31)", + "hour-leading-zero": "Час с ведущим нулём (00-23) — 24-часовой формат", + "hour-no-leading-zero": "Час без ведущего нуля (0-23) — 24-часовой формат", + "minute-leading-zero": "Минута с ведущим нулём (00-59)", + "minute-no-leading-zero": "Минута без ведущего нуля (0-59)", + "month-abbreviated": "Сокращённое название месяца", + "month-full": "Полное название месяца", + "month-number-leading-zero": "Месяц как число с ведущим нулём (01-12)", + "month-number-no-zero": "Месяц как число без ведущего нуля (1-12)", + "second-leading-zero": "Секунда с ведущим нулём (00-59)", + "second-no-leading-zero": "Секунда без ведущего нуля (0-59)", + "timezone-abbreviation": "Сокращение часового пояса", + "year-four-digit": "Год как четырёхзначное число", + "year-two-digit": "Год как двухзначное число (00-99)" + }, + "file-picker": { + "item": "элемент", + "items": "элементы", + "search-placeholder": "Поиск файлов и папок...", + "select-current": "Выбрать текущий", + "select-file": "Выбрать файл", + "select-folder": "Выбрать папку", + "selected": "Выбрано:", + "title": "Выбор файла" + }, + "file-picker-title": "Выберите файл", + "icon-picker": { + "title": "Выбор иконки" + }, + "tooltip-placeholder": "Заполнитель" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "Анонимная Личность", + "ca-cert": "Сертификат CA", + "eap-method": "Метод EAP", + "password": "Пароль пользователя", + "phase2-auth": "Аутентификация Фазы 2", + "username": "Идентификатор / Имя Пользователя" + }, + "panel": { + "action-required": "Требуется действие", + "add-hidden-network": "Добавить скрытую сеть", + "add-network": "Добавить сеть", + "available-interfaces": "Доступные интерфейсы", + "available-networks": "Доступные сети", + "disabled": "Wi-Fi отключен", + "disconnecting": "Отключение...", + "dns": "DNS", + "enable-message": "Включите Wi-Fi, чтобы увидеть доступные сети.", + "enter-password": "Введите пароль...", + "forget": "Забыть", + "forget-network": "Забыть эту сеть?", + "forgetting": "Забывание...", + "hidden-network": "Скрытая сеть", + "interface": "Сетевой интерфейс", + "internet-connected": "Подключено к интернету", + "internet-limited": "Нет интернета", + "internet-status": "Статус подключения к интернету", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "Известные сети", + "link-speed": "Скорость соединения", + "network-name-ssid": "Имя сети (SSID)", + "no-ethernet-devices": "Ethernet-устройства не обнаружены", + "no-networks": "Сети не найдены", + "saved": "Сохранено", + "scan-again": "Сканировать снова", + "searching": "Поиск ближайших сетей...", + "security-open": "Открытая", + "security-wep": "WEP", + "security-wpa": "WPA/WPA2/WPA3 Персональный", + "security-wpa-ent": "WPA Enterprise", + "security-wpa2-ent": "WPA2 Enterprise", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 Энтерпрайз", + "show-password": "Показать пароль", + "title": "Wi-Fi" + }, + "signal": { + "excellent": "Отличный", + "fair": "Средний", + "good": "Хороший", + "poor": "Плохой", + "weak": "Слабый" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/sv.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/sv.json new file mode 100644 index 0000000..5e8c8c4 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/sv.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "Töm historik", + "close-app": "Stäng {app}", + "connect-vpn": "Anslut till {name}", + "cycle-visualizer": "Växla visualiserare", + "disable-bluetooth": "Inaktivera Bluetooth", + "disable-dnd": "Inaktivera 'Stör inte'", + "disable-wifi": "Inaktivera Wi-Fi", + "disconnect-vpn": "Koppla från {name}", + "dock-settings": "Dockinställningar", + "enable-bluetooth": "Aktivera Bluetooth", + "enable-dnd": "Aktivera 'Stör inte'", + "enable-wifi": "Aktivera Wi-Fi", + "launcher-settings": "Startinställningar", + "lower-to-bottom": "Sänk till botten", + "open-calendar": "Öppna kalender", + "open-display-settings": "Skärminställningar", + "open-launcher": "Öppna startprogram", + "open-settings": "Öppna inställningar", + "raise-to-top": "Flytta till toppen", + "random-wallpaper": "Slumpmässig bakgrundsbild", + "run-custom-command": "Kör anpassat kommando", + "show-active-only": "Visa endast aktiva", + "show-all": "Visa alla", + "toggle-mute": "Växla mellan ljud av", + "widget-settings": "Widgetinställningar" + }, + "authentication": { + "error": "Autentiseringsfel", + "failed": "Autentisering misslyckades" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Tillämpa temafärger på ikonen för det aktiva fönstret.", + "hide-mode-description": "Styr hur widgeten beter sig när inget fönster är aktivt.", + "scrolling-mode-description": "Styr när textrullning är aktiverad för långa fönstertitlar. ", + "show-app-icon-description": "Visa applikationsikonen bredvid fönstertiteln.", + "show-app-icon-label": "Visa appikon", + "show-app-text-description": "Visa titeln på det aktiva fönstret.", + "show-app-text-label": "Visa fönstertitel" + }, + "audio-visualizer": { + "color-name-description": "Välj färg för visualiseraren.", + "color-name-label": "Fyllningsfärg", + "height-description": "Anpassad komponentbredd.", + "hide-when-idle-description": "När denna funktion är aktiverad döljs visualiseraren om ingen spelare är aktiv.", + "hide-when-idle-label": "Dölj när ingen media spelas upp", + "width-description": "Anpassad komponentbredd." + }, + "battery": { + "device-default": "Standard (visningsenhet)", + "device-description": "Välj vilken batterienhet som ska visas.", + "device-label": "Batterienhet", + "display-mode-description": "Välj hur batteriet visas i fältet.", + "display-mode-graphic": "Grafiskt batteri", + "display-mode-graphic-clean": "Grafiskt batteri (ingen %)", + "display-mode-icon-always": "Ikon - Visa alltid %", + "display-mode-icon-hover": "Ikon - Visa vid hovring", + "display-mode-icon-only": "Endast ikon", + "hide-if-idle-description": "Dölj widgeten när batteriet inte laddas eller laddas ur.", + "hide-if-idle-label": "Dölj när inaktiv", + "hide-if-not-detected-description": "Dölj widgeten när inget batteri upptäcks i systemet.", + "hide-if-not-detected-label": "Dölj när inte upptäckt", + "low-battery-threshold-label": "Varningsgräns för lågt batteri", + "show-noctalia-performance-description": "Visa växeln för Noctalia prestandaläget i batteripanelen.
Inaktiverar skuggor och animationer i Noctalia för att minska resursanvändningen.", + "show-noctalia-performance-label": "Visa Noctalia Prestandaväxeln", + "show-power-profile-description": "Visa valet av strömprofil i batteripanelen.", + "show-power-profile-label": "Visa strömprofilkontroller" + }, + "brightness": { + "apply-all-description": "Ändra ljusstyrka för alla bildskärmar när du använder skrollhjulet.", + "apply-all-label": "Tillämpa rullningsändringar på alla bildskärmar" + }, + "clock": { + "clock-display-description": "Anpassa klockans visning genom att lägga till symboler från listan nedan. För att använda 12-timmarsformatet måste du inkludera symbolen 'AP'.", + "clock-display-label": "Klockvisning", + "custom-font-description": "Välj ett anpassat teckensnitt för klockvisningen.", + "custom-font-label": "Anpassat teckensnitt", + "custom-font-placeholder": "Välj anpassat teckensnitt...", + "custom-font-search-placeholder": "Sök teckensnitt...", + "horizontal-bar-description": "Tips: Använd \\n för att skapa en radbrytning.", + "horizontal-bar-label": "Horisontell stapel", + "preview": "Förhandsgranska", + "tooltip-format-description": "Formateringssträng för verktygstips som visas när du håller muspekaren över klockan. Lämna tomt för att använda standardverktygstipset.", + "tooltip-format-label": "Verktygstipsformat", + "use-custom-font-description": "Åsidosätt standardvalet av teckensnitt med ett anpassat teckensnitt för klockan.", + "use-custom-font-label": "Använd anpassat teckensnitt", + "use-monospaced-font-description": "När denna funktion är aktiverad använder klockan ett monospaced-teckensnitt.", + "use-monospaced-font-label": "Använd monospaced-teckensnitt", + "vertical-bar-description": "Använd ett mellanslag för att separera varje del till en ny rad.", + "vertical-bar-label": "Vertikal stapel" + }, + "control-center": { + "browse-file": "Bläddra bland filer", + "browse-library": "Bläddra bland bibliotek", + "color-selection-description": "Tillämpa temafärger på ikoner.", + "enable-colorization-description": "Aktivera färgläggning för ikon, tillämpa temafärger.", + "icon-description": "Välj en ikon från biblioteket eller en anpassad fil.", + "select-custom-icon": "Välj en anpassad ikon", + "use-distro-logo-description": "Använd din distributions logotyp istället för en anpassad ikon.", + "use-distro-logo-label": "Använd distributionslogotyp istället för ikon" + }, + "custom-button": { + "collapse-condition-description": "Om utdatatexten matchar detta värde kommer knappen att döljas.", + "collapse-condition-label": "Dölj villkor", + "color-selection-description": "Tillämpa temafärger på ikon och text.", + "default-tooltip": "Anpassad knapp, konfigurera i inställningarna", + "display-command-output-description": "Ange ett kommando som ska köras med jämna mellanrum. Den första raden i utdata visas som text.", + "display-command-output-label": "Visa kommandoutdata", + "display-command-output-stream-description": "Ange ett kommando som ska köras kontinuerligt.", + "dynamic-text": "Dynamisk text", + "enable-colorization-description": "Aktivera färgläggning för den anpassade knappikonen och texten, tillämpa temafärger.", + "enable-colorization-label": "Aktivera färgläggning", + "general-tooltip-text-description": "Anpassad text att visa i knappens verktygstips.", + "general-tooltip-text-label": "Anpassad verktygstipsstext", + "hide-mode-always-expanded": "Alltid expanderad", + "hide-mode-description": "Styr widgetens synlighet när kommandot inte har någon utdata.", + "hide-mode-expand-with-output": "Expandera när det finns utdata", + "hide-mode-label": "Dölj läge", + "hide-mode-max-transparent": "Maximalt expanderad men transparent", + "icon-description": "Välj en ikon från biblioteket.", + "ipc-identifier-description": "Unik identifierare för IPC-kommandon. Använd denna identifierare med 'qs -c noctalia-shell ipc call cb [action] [identifier]' för att styra denna knapp via IPC.", + "ipc-identifier-label": "IPC-identifierare", + "left-click-description": "Kommando som ska utföras när knappen vänsterklickas.", + "left-click-label": "Vänsterklick", + "left-click-update-text": "Uppdatera visad text vid vänsterklick", + "max-text-length-horizontal-description": "Maximalt antal tecken som ska visas i horisontell stapel (0 för att dölja text).", + "max-text-length-horizontal-label": "Max textlängd (horisontell)", + "max-text-length-vertical-description": "Maximalt antal tecken som ska visas i vertikal stapel (0 för att dölja text).", + "max-text-length-vertical-label": "Max textlängd (vertikal)", + "middle-click-description": "Kommando som ska utföras när knappen klickas med mittknappen.", + "middle-click-label": "Mittklick", + "middle-click-update-text": "Uppdatera visad text vid mittklick", + "parse-json-description": "Analysera kommandoutmatningen som ett JSON-objekt för att dynamiskt ställa in text och ikon.", + "parse-json-label": "Analysera utmatning som JSON", + "refresh-interval-description": "Intervall i millisekunder.", + "refresh-interval-label": "Uppdateringsintervall", + "right-click-description": "Kommando som ska utföras när knappen högerklickas.", + "right-click-label": "Högerklicka", + "right-click-update-text": "Uppdatera visad text vid högerklick", + "show-exec-tooltip-description": "Visa verktygstips med kommandodetaljer (vänster-/höger-/mittenklick, rullhjul).", + "show-exec-tooltip-label": "Visa kommandoverktygstips", + "show-icon-description": "Växlar mellan att visa och dölja widgetens ikon.", + "show-icon-label": "Visa ikon", + "show-text-tooltip-description": "Visa verktygstips med utdata från textkommandot.", + "show-text-tooltip-label": "Visa dynamiska textverktygstips", + "text-stream-description": "Strömmade rader från kommandot visas som text på knappen.", + "text-stream-label": "Ström", + "wheel-description": "Kommando som ska utföras när rullhjulet används.
Använd $delta för rullhjulets delta i kommandot.", + "wheel-down": "Skrolla ned", + "wheel-down-description": "Kommando som ska utföras när rullhjulet rullas nedåt.", + "wheel-down-label": "Kommando för att rulla nedåt", + "wheel-label": "Rullhjul", + "wheel-mode-separate-description": "Aktivera separata kommandon för att rulla uppåt och nedåt.", + "wheel-mode-separate-label": "Separata hjulkommandon", + "wheel-up": "Rulla upp", + "wheel-up-description": "Kommando som ska utföras när rullhjulet rullas uppåt.", + "wheel-up-label": "Kommando för att rulla uppåt", + "wheel-update-text": "Uppdatera visad text vid rullning" + }, + "keyboard-layout": { + "show-icon-description": "Visa ikonen för tangentbordslayout." + }, + "lock-keys": { + "hide-when-off-description": "Dölj indikatorn när tangenten inte är aktiv.", + "hide-when-off-label": "Dölj när av", + "show-caps-lock-description": "Visa status för Caps Lock.", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "Visa status för Num Lock.", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "Visa status för Scroll Lock.", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "Aktivera en utrymmesbesparande layout för mediaspelarpanelen.", + "compact-mode-label": "Kompakt läge", + "hide-mode-description": "Styr hur widgeten beter sig när ingen media spelas upp.", + "max-width-description": "Ställer in widgetens maximala horisontella storlek. Widgeten krymper för att passa kortare innehåll.", + "no-active-player": "Ingen aktiv spelare", + "panel-section-description": "Konfigurera utseendet och beteendet för mediaspelarpanelen.", + "panel-section-label": "Mediaspelarpanel", + "scrolling-mode-description": "Styr när textrullning är aktiverad för långa spårtitlar.", + "scrolling-mode-label": "Rullningsläge", + "show-album-art-description": "Visa albumomslaget för det spår som spelas upp för närvarande.", + "show-album-art-label": "Visa albumomslag", + "show-artist-first-description": "Visa artist - titel istället för titel - artist.", + "show-artist-first-label": "Visa artist först", + "show-progress-ring-description": "Visa en cirkulär förloppsindikator som visar spårets förlopp.", + "show-progress-ring-label": "Visa förloppsindikator", + "show-visualizer-description": "Visa en ljudvisualiserare när musik spelas.", + "show-visualizer-label": "Visa visualiserare", + "use-fixed-width-description": "När denna funktion är aktiverad använder widgeten alltid maximal bredd istället för att dynamiskt anpassa sig efter innehållet.", + "use-fixed-width-label": "Använd fast bredd", + "visualizer-type-description": "Välj stil för ljudvisualiseraren som ska visas.", + "visualizer-type-label": "Visualiseringstyp" + }, + "notification-history": { + "hide-widget-when-zero-description": "Dölj meddelandeikonen när det inte finns några meddelanden.", + "hide-widget-when-zero-label": "Dölj ikonen när det inte finns några meddelanden", + "hide-widget-when-zero-unread-description": "Dölj meddelandeikonen när det inte finns några olästa meddelanden.", + "hide-widget-when-zero-unread-label": "Dölj ikonen när det inte finns några olästa meddelanden", + "show-unread-badge-description": "Visa ett märke som visar antalet olästa aviseringar.", + "show-unread-badge-label": "Visa märke för olästa", + "unread-badge-color-description": "Välj färg för märket för olästa aviseringar.", + "unread-badge-color-label": "Färg på oläst märke" + }, + "section-editor": { + "placeholder": "Välj en widget att lägga till...", + "search-placeholder": "Sök widget..." + }, + "spacer": { + "width-description": "Avståndsbredd i bildpunkter." + }, + "system-monitor": { + "compact-mode-description": "Visa statistik som minibar-diagram istället för textvärden. Förhindrar layoutförskjutning. ", + "compact-mode-label": "Kompakt läge", + "cpu-cores-description": "Visa CPU-kärnornas användning individuellt.", + "cpu-cores-label": "CPU-kärnor", + "cpu-frequency-description": "Visa aktuell CPU-klockhastighet i GHz.", + "cpu-frequency-label": "Visa CPU-frekvens", + "cpu-temperature-description": "Visa CPU-temperaturvärden om tillgängliga.", + "cpu-temperature-label": "CPU-temperatur", + "cpu-usage-description": "Visa aktuell CPU-användningsprocent.", + "cpu-usage-label": "CPU-användning", + "disk-path-description": "Välj vilken diskmonteringspunkt som ska övervakas.", + "disk-path-label": "Disksökväg", + "gpu-temperature-description": "Visa GPU-temperaturvärden om tillgängliga.", + "load-average-description": "Visa systemets genomsnittliga belastning.", + "load-average-label": "Genomsnittlig belastning", + "memory-percentage-description": "Visa minnesanvändning som procentandel istället för absoluta värden.", + "memory-percentage-label": "Minne i procent", + "memory-usage-description": "Visa aktuell information om RAM-användning.", + "memory-usage-label": "Minnesanvändning", + "network-traffic-description": "Visa nätverkets uppladdnings- och nedladdningshastigheter.", + "network-traffic-label": "Nätverkstrafik", + "storage-as-percentage-description": "Visa diskutrymme som procentandel istället för absoluta värden.", + "storage-as-percentage-label": "Disk i procent", + "storage-available-description": "Visar hur mycket diskutrymme som är tillgängligt istället för hur mycket som används.", + "storage-available-label": "Tillgängligt diskutrymme", + "storage-usage-description": "Visa information om diskutrymmesanvändning.", + "storage-usage-label": "Lagringsanvändning", + "swap-usage-description": "Visa användning av växlingsminne.", + "swap-usage-label": "Växlingsanvändning", + "use-monospace-font-description": "Använd monospace-teckensnitt för enhetlig teckenbredd. ", + "use-monospace-font-label": "Teckensnitt för fast bredd", + "use-padding-description": "Fyller ut textvärdena med inledande mellanslag för att förhindra layoutförskjutning.", + "use-padding-description-disabled-monospace-font": "Ett monospace-typsnitt behövs för den här funktionen.", + "use-padding-description-disabled-vertical": "Vertikal aktivitetsfält stöder inte textutfyllnad.", + "use-padding-label": "Textutfyllnad" + }, + "taskbar": { + "colorize-icons-description": "Tillämpa temafärger på ikoner i aktivitetsfältet.", + "hide-mode-description": "Styr hur widgeten beter sig när det inte finns några matchande fönster.", + "hide-mode-label": "Döljningsläge", + "icon-scale-description": "Ställer in skalningsfaktorn för ikoner i aktivitetsfältet.", + "icon-scale-label": "Ikonskalning", + "max-width-description": "Maximal bredd för aktivitetsfältet som en procentandel av skärmens bredd.", + "max-width-label": "Maximal bredd", + "only-active-workspaces-description": "Visa endast appar från aktiva arbetsytor.", + "only-active-workspaces-label": "Endast från aktiva arbetsytor", + "only-same-monitor-description": "Visa endast appar från den skärm där fältet finns.", + "only-same-monitor-label": "Endast från samma skärm", + "show-pinned-apps-description": "Visa fästa appar från dockningen i aktivitetsfältet.", + "show-pinned-apps-label": "Visa fästa appar", + "show-title-description": "Visa fönstertitlar i aktivitetsfältet.", + "show-title-description-disabled": "Vertikalt aktivitetsfält stöder inte visning av titlar.", + "show-title-label": "Visa titel", + "smart-width-description": "Justera automatiskt bredden på poster baserat på antalet poster.", + "smart-width-label": "Smart bredd", + "title-width-description": "Ställ in bredden på fönstertitlar i aktivitetsfältet (i pixlar).", + "title-width-label": "Titelbredd", + "title-width-reset-tooltip": "Återställ titelbredd" + }, + "tray": { + "chevron-color-description": "Tillämpa temafärger på lådans pil-ikon.", + "chevron-color-label": "Pilfärg", + "colorize-icons-description": "Tillämpa temafärger på ikoner i aktivitetsfältet.", + "colorize-icons-label": "Färglägg ikoner", + "drawer-enabled-description": "När funktionen är aktiverad visas fackikoner som inte är fästa i en låda.
När funktionen är inaktiverad visas alla fackikoner i rad.", + "drawer-enabled-label": "Aktivera låda", + "hide-passive-description": "När funktionen är aktiverad döljs fackikoner med statusen 'Passiv'.", + "hide-passive-label": "Dölj passiva ikoner" + }, + "volume": { + "display-mode-description": "Välj hur du vill att detta värde ska visas." + }, + "workspace": { + "character-count-description": "Antal tecken som ska visas från arbetsytans namn (1–10). ", + "character-count-label": "Teckenantal", + "empty-color-description": "Ställ in bakgrundsfärgen för tomma arbetsytor.", + "empty-color-label": "Färg för tomma arbetsytor", + "enable-scrollwheel-description": "Växla mellan arbetsytor med musens rullhjul.", + "enable-scrollwheel-label": "Rulla för att växla mellan arbetsytor", + "focused-color-description": "Ställ in bakgrundsfärgen för det fokuserade arbetsområdet.", + "focused-color-label": "Fokuserad arbetsområdesfärg", + "follow-focused-screen-description": "Visa arbetsområden från den skärm som för närvarande är fokuserad, snarare än den skärm där fältet finns.", + "follow-focused-screen-label": "Följ fokuserad skärm", + "font-weight-description": "Ställ in den visuella vikten för text inom arbetsytan.", + "font-weight-label": "Teckensnittsvikt", + "grouped-border-opacity-description": "Ställ in opacitetsnivån för arbetsytans behållargränser.", + "grouped-border-opacity-label": "Ramopacitet", + "hide-unoccupied-description": "Visa inte arbetsytor utan fönster.", + "hide-unoccupied-label": "Dölj oanvända", + "label-mode-description": "Välj hur arbetsytans etiketter ska visas.", + "label-mode-label": "Etikettläge", + "occupied-color-description": "Ställ in bakgrundsfärgen för använda arbetsytor.", + "occupied-color-label": "Färg för använda arbetsytor", + "pill-size-description": "Justera storleken på arbetsytepillerna (50%-100%).", + "pill-size-label": "Pillerstorlek", + "reverse-scrolling-description": "Vänd riktningen för arbetsyteväxling vid rullning.", + "reverse-scrolling-label": "Omvänd rullning", + "show-applications-description": "Visa applikationsikoner i varje arbetsyta.", + "show-applications-hover-description": "Visa programikoner i varje arbetsyta när arbetsytan hovras över.", + "show-applications-hover-label": "Visa applikationer vid hovring", + "show-applications-label": "Visa applikationer", + "show-badge-description": "Visa arbetsytans nummerskylt i grupperat läge.", + "show-badge-label": "Visa arbetsytans skylt", + "show-labels-only-when-occupied-description": "Visa endast arbetsytans etiketter när de innehåller fönster. ", + "show-labels-only-when-occupied-label": "Visa etiketter endast när de är upptagna", + "unfocused-icons-opacity-description": "Ställ in opacitetsnivån för appikoner som inte är i fokus.", + "unfocused-icons-opacity-label": "Opacitet för ikoner som inte är i fokus" + } + }, + "battery": { + "all-batteries": "Alla batterier (kombinerat)", + "battery-health": "Batteriets hälsa", + "battery-level": "Batterinivå", + "capacity-level": "Kapacitet: {level}", + "charging-rate": "Laddningshastighet: {rate}W", + "discharging-rate": "Urladdningshastighet: {rate}W", + "health": "Hälsa: {percent}%", + "inhibit-idle-description": "Håller systemet vaket.", + "no-battery-detected": "Inget batteri upptäckt", + "plugged-in": "Ansluten", + "power-profile": "Strömprofil", + "time-left": "Återstående tid: {time}", + "time-until-full": "Tid till full laddning: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Tillgängliga enheter", + "blocked": "Blockerad", + "connected-devices": "Anslutna enheter", + "device-address": "Enhetsadress", + "disabled": "Bluetooth är inaktiverat", + "discoverable": "Upptäckbar", + "enable-message": "Aktivera Bluetooth för att se tillgängliga enheter.", + "known-devices": "Kända enheter", + "no-devices": "Inga enheter tillgängliga", + "paired-devices": "Parkopplade enheter", + "pairing-mode": "Se till att din enhet är i parkopplingsläge.", + "scanning": "Söker efter enheter...", + "signal-text-excellent": "Signal: Utmärkt", + "signal-text-fair": "Signal: God", + "signal-text-good": "Signal: Bra", + "signal-text-poor": "Signal: Dålig", + "signal-text-unknown": "Signal: Okänd", + "signal-text-very-poor": "Signal: Mycket dålig" + } + }, + "changelog": { + "error": { + "fetch-failed": "Det går inte att läsa in ändringsloggdata. Försök igen senare.", + "rate-limit": "GitHub-hastighetsgränsen överskreds. Försök igen om några minuter." + }, + "panel": { + "buttons-discord": "Gå med i vår Discord", + "buttons-dismiss": "OK", + "buttons-feedback": "Ge feedback", + "empty": "Versionsfakta är ännu inte tillgängliga.", + "highlight-title": "Höjdpunkter", + "released": "Släpptes den {date}", + "subtitle-fresh": "Tack för att du installerade Noctalia! Här är vad som ingår i den här versionen.", + "subtitle-updated": "Uppdaterade från {previousVersion}", + "title": "Vad är nytt i {version}", + "version": "Version {version}", + "version-new-user": "Ny installation" + } + }, + "common": { + "actions": "Åtgärder", + "active": "Aktiv", + "add": "Lägg till", + "appearance": "Utseende", + "apply": "Tillämpa", + "auto-connect": "Automatisk anslutning", + "automation": "Automation", + "available": "Tillgänglig", + "back": "Tillbaka", + "battery": "Batteri", + "behavior": "Beteende", + "bluetooth": "Bluetooth", + "brightness": "Ljusstyrka", + "browse": "Bläddra", + "calendar": "Kalender", + "calendar-panel": "Kalenderpanel", + "cancel": "Avbryt", + "cards": "Kort", + "charging": "Laddning", + "clear": "Töm", + "clipboard": "Urklipp", + "close": "Stäng", + "color-muted": "Dämpad", + "colors": "Färger", + "command": "Kommando", + "confirm": "Bekräfta", + "connect": "Anslut", + "connected": "Ansluten", + "connecting": "Ansluter...", + "continue": "Fortsätt", + "contributors": "Bidragsgivare", + "copied-to-clipboard": "Kopierat till urklipp", + "countdown": "Nedräkning", + "customize": "Anpassa", + "date": "Datum", + "default": "Standard", + "delete": "Ta bort", + "devices": "Enheter", + "disabled": "Inaktiverad", + "discharging": "Urladdning", + "disconnect": "Koppla från", + "disconnected": "Frånkopplad", + "disconnecting": "Kopplar från...", + "display-mode": "Visningsläge", + "documentation": "Dokumentation", + "download": "Hämta ner", + "duration": "Varaktighet", + "dysfunctional": "Dysfunktionell", + "edit": "Redigera", + "enabled": "Aktiverad", + "error": "Fel", + "ethernet": "Ethernet", + "events": "Händelser", + "execute": "Exekvera", + "faithful": "Trogen", + "focus": "Fokus", + "font-weight-bold": "Fet", + "font-weight-light": "Lätt", + "font-weight-medium": "Mellan", + "font-weight-regular": "Vanlig", + "font-weight-semibold": "Halvfet", + "frequency": "Frekvens", + "gateway": "Gateway", + "general": "Allmänt", + "height": "Höjd", + "hibernate": "Viloläge", + "history": "Historik", + "icon": "Ikon", + "idle": "Overksam", + "info": "Info", + "input": "Inmatning", + "install": "Installera", + "installed": "Installerad", + "interface": "Gränssnitt", + "internet": "Internet", + "keybind": "Tangentbindning", + "language": "Språk", + "loading": "Läser in...", + "local": "Lokal", + "location": "Plats", + "lock": "Lås", + "logout": "Logga ut", + "look": "Titta", + "margins": "Marginaler", + "media": "Media", + "media-player": "Mediaspelare", + "memory": "Minne", + "monitors": "Skärmar", + "move-down": "Flytta ned", + "move-up": "Flytta upp", + "network": "Nätverk", + "next": "Nästa", + "night-light": "Nattlampa", + "no": "Nej", + "no-results": "Inga resultat", + "no-summary": "Ingen sammanfattning", + "none": "Ingen", + "not-found": "Hittades inte", + "notifications": "Meddelanden", + "official": "Officiellt", + "on-surface": "På ytan", + "output": "Utgång", + "pair": "Para ihop", + "paired": "Ihopparad", + "pairing": "Parar ihop...", + "panels": "Paneler", + "password": "Lösenord", + "pause": "Paus", + "performance": "Prestanda", + "pin": "Fäst", + "play": "Spela upp", + "polling": "Pollning", + "position": "Position", + "previous": "Föregående", + "primary": "Primär", + "random": "Slumpmässig", + "reboot": "Starta om", + "reboot-to-uefi": "Starta om till UEFI", + "record": "Starta inspelning", + "refresh": "Uppdatera", + "required": "(krävs)", + "reset": "Återställ", + "reset-to-default": "Återställ till Standard", + "result-count": "{count} resultat", + "result-count-plural": "{count} resultat", + "resume": "Återuppta", + "save": "Spara", + "scanning": "Söker igenom...", + "screen-corners": "Skärmhörn", + "search": "Sök", + "second": "{count} sekund", + "second-plural": "{count} sekunder", + "secondary": "Sekundär", + "security": "Säkerhet", + "select": "Välj", + "select-color": "Välj färg", + "select-color-description": "Tillämpa temafärger för betoning.", + "select-icon-color": "Välj ikonfärg", + "settings": "Inställningar", + "shortcuts": "Genvägar", + "shutdown": "Stäng av", + "signal": "Signal", + "sound": "Ljud", + "sources": "Källor", + "start": "Starta", + "stop": "Stoppa", + "supporters": "Stödjare", + "suspend": "Strömsparläge", + "templates": "Mallar", + "tertiary": "Tertiär", + "test": "Testa", + "thresholds": "Tröskelvärden", + "title": "Titel", + "toast": "Toast", + "trusted": "Betrodd", + "uninstall": "Avinstallera", + "unknown": "Okänd", + "unpair": "Avpara", + "unpin": "Avfäst", + "update": "Uppdatera", + "upload": "Skicka upp", + "username": "Användarnamn", + "userspace-reboot": "Starta om användarutrymme", + "version": "Version", + "vibrant": "Levande", + "visualizer": "Visualiserare", + "volume": "Volym", + "volumes": "Volymer", + "wallpaper": "Bakgrundsbild", + "weather": "Väder", + "weather-loading": "Läser in väder...", + "weather-no-location": "Ange en plats i Inställningarna för att se vädret", + "week": "Vecka", + "widgets": "Widgetar", + "width": "Bredd", + "wifi": "Wi-Fi", + "windows": "Fönster", + "yes": "Ja" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Strömprofil", + "tooltip-disabled": "Installera power-profiles-daemon för att använda strömprofiler" + }, + "wifi": { + "label-disconnected": "Wi-Fi frånkopplat", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Dölj alltid", + "always-show": "Visa alltid", + "force-open": "Tvinga öppning", + "on-hover": "Vid muspekning" + }, + "hide-modes": { + "auto-hide": "Dölj automatiskt", + "hidden": "Dölj när tom", + "idle": "Dölj när inaktiv", + "non-exclusive": "Icke-exklusiv", + "transparent": "Transparent när tom", + "visible": "Alltid synlig" + }, + "launcher": { + "categories": { + "all": "Alla", + "audiovideo": "Ljud och video", + "chat": "Chatta", + "development": "Utveckling", + "education": "Utbildning", + "emoji-activity": "Aktivitet", + "emoji-animals": "Djur", + "emoji-flags": "Flaggor", + "emoji-food": "Mat och dryck", + "emoji-nature": "Natur", + "emoji-objects": "Objekt", + "emoji-people": "Människor och kropp", + "emoji-recent": "Senaste", + "emoji-symbols": "Symboler", + "emoji-travel": "Resor och platser", + "game": "Spel", + "graphics": "Grafik", + "misc": "Övrigt", + "office": "Kontor", + "pinned": "Fäst", + "system": "System", + "webbrowser": "Webbläsare" + }, + "providers": { + "applications": "Applikationer", + "calculator": "Kalkylator", + "calculator-deprecated": "Kommandot >calc är föråldrat och kommer snart att tas bort. Skriv istället matematiska uttryck direkt i sökfältet.", + "calculator-description": "Kalkylator — beräkna matematiska uttryck", + "calculator-enter-expression": "Ange ett matematiskt uttryck", + "calculator-press-enter-to-copy": "Tryck på Enter för att kopiera resultatet", + "clipboard": "Urklippshistorik", + "clipboard-clear-description": "Töm all urklippshistorik", + "clipboard-clear-description-full": "Ta bort alla objekt från urklippshistoriken", + "clipboard-clear-history": "Töm urklippshistoriken", + "clipboard-delete": "Ta bort urklippspost", + "clipboard-history-disabled": "Urklippshistorik inaktiverad", + "clipboard-history-disabled-description": "Aktivera urklippshistorik i inställningarna eller installera cliphist", + "clipboard-loading": "Läser in urklippshistorik...", + "clipboard-search-description": "Sök i urklippshistorik", + "command-description": "Kör skalkommandon", + "emoji": "Emoji-väljare", + "emoji-loading": "Läser in emojis...", + "emoji-loading-description": "Vänta", + "emoji-no-recent": "Inga nya emojis ännu", + "emoji-search-description": "Sök och kopiera emojis", + "settings-search-description": "Sök och navigera till inställningar", + "windows-search-description": "Sök och fokusera öppna fönster" + } + }, + "lock-screen": { + "authenticating": "Autentiserar...", + "password": "Ange ditt lösenord...", + "restart": "Starta om", + "shut-down": "Stäng av" + }, + "notifications": { + "panel": { + "click-to-collapse": "Klicka för att fälla ihop", + "click-to-expand": "Klicka för att expandera", + "description": "Dina aviseringar visas här när de kommer in. ", + "no-notifications": "Inga meddelanden" + }, + "range": { + "earlier": "Tidigare", + "today": "Idag", + "yesterday": "Igår" + }, + "time": { + "diff-d": "1 dag sedan", + "diff-dd": "{diff} dagar sedan", + "diff-h": "1 timme sedan", + "diff-hh": "{diff} timmar sedan", + "diff-m": "1 minut sedan", + "diff-mm": "{diff} minuter sedan", + "now": "nu" + } + }, + "options": { + "bar": { + "density-comfortable": "Bekväm", + "density-compact": "Kompakt", + "density-default": "Standard", + "density-mini": "Mini", + "density-spacious": "Rymlig", + "display-mode-always-visible": "Alltid synlig", + "type-floating": "Flytande", + "type-framed": "Inramad", + "type-simple": "Enkel" + }, + "control-center": { + "quick-settings-style-classic": "Klassisk", + "quick-settings-style-modern": "Modern" + }, + "frame-rates-fps": "{fps} bilder/s", + "launcher-density": { + "comfortable": "Bekväm", + "compact": "Kompakt", + "default": "Standard" + }, + "launcher-view-mode": { + "grid": "Rutnät", + "list": "Lista" + }, + "notification-density": { + "compact": "Kompakt", + "default": "Standard" + }, + "scrolling-modes": { + "always": "Rulla alltid", + "hover": "Rulla vid muspekning", + "never": "Rulla aldrig" + }, + "session-menu-grid-layout": { + "grid": "Rutnät", + "single-row": "En rad" + }, + "settings-panel-mode": { + "attached": "Panel fäst vid fältet", + "centered": "Centrerad panel", + "window": "Separat fönster" + }, + "visualizer-types": { + "linear": "Linjär", + "mirrored": "Speglad", + "wave": "Våg" + }, + "visualizer-visibility": { + "with-background": "Endast med bakgrund" + }, + "workspace-labels": { + "index": "Index", + "index-and-name": "Index och namn", + "name": "Namn" + } + }, + "panels": { + "about": { + "become-supporter": "Bli supporter", + "changelog": "Visa ändringslogg", + "changelog-on-startup": "Visa ändringslogg vid uppdatering", + "changelog-on-startup-desc": "Visa ändringsloggen automatiskt när Noctalia uppdateras.", + "contributors-description": "Tack till våra {count} fantastiska bidragsgivare!", + "contributors-description-plural": "Tack till våra {count} fantastiska bidragsgivare!", + "copy-info": "Kopiera info", + "debug-disabled": "Felsökningsläge inaktiverat", + "debug-enabled": "Felsökningsläge aktiverat", + "info-copied": "Info kopierad till urklipp", + "noctalia-available": "Tillgänglig:", + "noctalia-desc": "Ett elegant och minimalistiskt skrivbordsskal som är noggrant utformat för Wayland och byggt med Quickshell.", + "noctalia-git-commit": "Git-commit:", + "noctalia-installed-version": "Installerad version:", + "noctalia-latest-version": "Senaste version:", + "noctalia-qs-version": "Noctalia QS version:", + "noctalia-title": "Noctalia-skal", + "privacy-policy": "Sekretesspolicy", + "support": "Stöd oss", + "supporter-badge": "Supporter", + "supporters-desc": "Ett stort tack till vår fantastiska supporter!", + "supporters-desc-plural": "Ett stort tack till våra {count} fantastiska supportrar!", + "supporters-loading": "Laddar supportrar...", + "system-board": "Moderkort:", + "system-cpu": "CPU:", + "system-disk": "Disk:", + "system-gpu": "GPU:", + "system-host": "Värd:", + "system-install-hint": "Installera fastfetch för att visa systeminformation", + "system-kernel": "Kärna:", + "system-loading": "Läser in systeminformation...", + "system-memory": "Minne:", + "system-monitor": "Skärm:", + "system-not-installed": "fastfetch är inte installerat", + "system-os": "Operativsystem:", + "system-packages": "Paket:", + "system-product": "Produkt:", + "system-title": "Systeminformation", + "system-uptime": "Driftstid:", + "system-wm": "WM:", + "telemetry-data-copied": "Telemetridata kopierad till urklipp", + "telemetry-desc": "Hjälp till att förbättra Noctalia genom att dela anonym systeminformation (skärmupplösning, kompositör, distro). Skickas en gång vid start, ingen spårning, data raderas automatiskt efter 30 dagar.", + "telemetry-enabled": "Skicka anonym systeminformation", + "telemetry-show-data": "Visa data", + "telemetry-title": "Integritet", + "title": "Om", + "up-to-date": "Du är uppdaterad!", + "update-available": "Uppdatering tillgänglig", + "view-commit": "Visa commit på GitHub" + }, + "audio": { + "devices-desc": "Konfigurera tillgängliga ljudinmatnings- och ljudutmatningsenheter.", + "devices-input-device-description": "Välj önskad ljudingångsenhet.", + "devices-input-device-label": "Ingångsenhet", + "devices-output-device-description": "Välj önskad ljudutgångsenhet.", + "devices-output-device-label": "Utgångsenhet", + "devices-title": "Ljudenheter", + "external-mixer-description": "Ange kommandot eller applikationssökvägen som ska startas när funktionen för extern ljudmixer aktiveras.", + "external-mixer-label": "Kommando för extern ljudmixer", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Ställ in dina föredragna och ignorerade medieapplikationer.", + "media-excluded-player-description": "Lägg till nyckelord för uppspelare som du vill att systemet ska ignorera. Varje nyckelord ska stå på en ny rad.", + "media-excluded-player-label": "Exkluderad spelare", + "media-excluded-player-placeholder": "skriv in delsträng och tryck på +", + "media-frame-rate-description": "Högre hastigheter är smidigare men använder mer resurser.", + "media-frame-rate-label": "Bildfrekvens", + "media-primary-player-description": "Ange ett nyckelord för att identifiera din huvudspelare.", + "media-primary-player-label": "Primär spelare", + "media-primary-player-placeholder": "t.ex. spotify, vlc, mpv", + "media-scrolling-speed-description": "Tid i sekunder för titeln att rulla från början till slut.", + "media-scrolling-speed-label": "Rullningshastighet", + "media-scrolling-title-description": "Aktivera kontinuerlig rullning för långa medietitlar.", + "media-scrolling-title-label": "Rullningstitel", + "media-title": "Mediaspelare", + "on-middle-clicked-description": "Kommando som ska utföras när knappen klickas på med mittknappen.", + "panel-applications-empty": "Inga applikationer spelar upp ljud för närvarande", + "spectrum-mirrored-description": "Spegla spektrumet så att låga frekvenser är i mitten. När avstängt är låga frekvenser till vänster och höga till höger.", + "spectrum-mirrored-label": "Speglad spektrum", + "title": "Ljud", + "visualizer-type-description": "Välj en visualiseringstyp för medieuppspelning.", + "visualizer-type-label": "Visualiseringstyp", + "volumes-desc": "Justera volymkontroller och ljudnivåer.", + "volumes-feedback-sound-file-description": "Sökväg till ljudfilen som spelas upp när volymen ändras.", + "volumes-feedback-sound-file-label": "Ljudfil för volymfeedback", + "volumes-feedback-sound-file-select-title": "Välj ljudfil för volymfeedback", + "volumes-input-volume-description": "Mikrofonens ingångsvolymnivå.", + "volumes-mute-input-description": "Stäng av standardljudingången (mikrofon).", + "volumes-mute-input-label": "Stäng av ljudingång", + "volumes-mute-output-description": "Stäng av systemets huvudsakliga ljudutgång.", + "volumes-mute-output-label": "Stäng av ljudutgång", + "volumes-output-volume-description": "Systemomfattande volymnivå.", + "volumes-step-size-description": "Justera stegstorleken för volymändringar (rullhjul, kortkommandon).", + "volumes-step-size-label": "Volymstegstorlek", + "volumes-volume-feedback-description": "Spela upp ett ljud när volymen justeras.", + "volumes-volume-feedback-label": "Spela upp ljud vid volymjustering", + "volumes-volume-overdrive-description": "Tillåt att volymen höjs över 100 %. Stöds kanske inte av all hårdvara.", + "volumes-volume-overdrive-label": "Tillåt volymöverstyrning" + }, + "bar": { + "appearance-auto-hide-delay-description": "Tid innan Bar döljs efter att musen lämnar den.", + "appearance-auto-hide-delay-label": "Dölj fördröjning", + "appearance-auto-hide-exclusive-note": "Obs: Exklusiv zon inaktiveras automatiskt när automatisk döljning är aktiv.", + "appearance-auto-show-delay-description": "Tid innan fältet visas när musen når kanten.", + "appearance-auto-show-delay-label": "Visningsfördröjning", + "appearance-background-opacity-description": "Ställ in bakgrundens opacitet specifikt för fältet.", + "appearance-background-opacity-label": "Fältets bakgrundsopacitet", + "appearance-capsule-color-description": "Välj en färg för fältkapslar, eller använd ingen för standardytfärgen.", + "appearance-capsule-color-label": "Kapselfärg", + "appearance-capsule-opacity-description": "Ställ in opacitetsnivån för widgetbakgrunder när kapseln visas.", + "appearance-capsule-opacity-label": "Kapselns opacitet", + "appearance-content-padding-description": "Justera avståndet mellan fältets kanter och widgetar.", + "appearance-content-padding-label": "Innehållsavstånd", + "appearance-density-description": "Justera stapelns avstånd för ett kompakt eller rymligt utseende.", + "appearance-density-label": "Stapeldensitet", + "appearance-desc": "Anpassa stapelns utseende och position.", + "appearance-display-mode-description": "Välj när listen är synlig.", + "appearance-enable-exclusion-zone-inset-description": "Minska exklusionszonen med 1 fysisk pixel så att fönster som ligger kant i kant perfekt överlappar under stapelkanten.", + "appearance-enable-exclusion-zone-inset-label": "Infälld uteslutningszon", + "appearance-floating-description": "Visa stapeln som en flytande 'piller'.", + "appearance-floating-label": "Flytande stapel", + "appearance-font-scale-description": "Justera skalningen av teckenstorleken för text som visas i fältet.", + "appearance-font-scale-label": "Teckensnittsskala", + "appearance-frame-radius": "Inre radie", + "appearance-frame-settings-description": "Justera ramens tjocklek och inre hörnradie", + "appearance-frame-settings-label": "Raminställningar", + "appearance-frame-thickness": "Tjocklek", + "appearance-hide-on-overview-description": "Dölj fältet och stäng panelerna när kompositörsöversikten är aktiv.", + "appearance-hide-on-overview-label": "Dölj fältet på översikten", + "appearance-margins-description": "Justera marginalerna runt det flytande fältet.", + "appearance-margins-horizontal": "Horisontell marginal", + "appearance-margins-vertical": "Vertikal marginal", + "appearance-outer-corners-description": "Visa utåtböjda hörn på stapeln.", + "appearance-outer-corners-label": "Ytterhörn", + "appearance-position-description": "Välj var fältet ska placeras på skärmen.", + "appearance-position-label": "Stapelposition", + "appearance-show-capsule-description": "Visa widgetbakgrunder.", + "appearance-show-capsule-label": "Visa kapsel", + "appearance-show-on-workspace-switch-description": "Visa Bar automatiskt en kort stund när arbetsytan ändras.", + "appearance-show-on-workspace-switch-label": "Visa fält vid arbetsytebyte", + "appearance-show-outline-description": "Visar en synlig kant runt varje widget.", + "appearance-show-outline-label": "Visa widgetkonturer", + "appearance-type-description": "Välj stil för fältet: Enkel, Flytande eller Inramad.", + "appearance-type-label": "Fälttyp", + "appearance-use-separate-opacity-description": "Aktivera för att använda ett separat opacitetsvärde för fältets bakgrund.", + "appearance-use-separate-opacity-label": "Använd separat opacitet för fältet", + "appearance-widget-spacing-description": "Justera avståndet mellan varje widget i fältet.", + "appearance-widget-spacing-label": "Widgetavstånd", + "behavior-middle-click-command-description": "Kommando att köra vid mellanklick.", + "behavior-middle-click-command-label": "Mellanklick-kommando", + "behavior-middle-click-command-placeholder": "niri msg action växla översikt", + "behavior-middle-click-description": "Välj vad ett mellanklick gör på tomma områden av Fältet.", + "behavior-middle-click-follow-mouse-description": "Öppna den valda mellanklickspanelen vid muspekarens position.", + "behavior-middle-click-follow-mouse-label": "Mellanklick följer mus", + "behavior-middle-click-label": "Fältets mittenklicksåtgärd", + "behavior-right-click-command-description": "Kommando att köra vid högerklick.", + "behavior-right-click-command-label": "Högerklickskommando", + "behavior-right-click-command-placeholder": "notify-send \"Högerklick\"", + "behavior-right-click-description": "Välj vad högerklick gör på tomma områden av fältet.", + "behavior-right-click-follow-mouse-description": "Öppna den valda högerklickspanelen vid markörpositionen.", + "behavior-right-click-follow-mouse-label": "Högerklick följer musen", + "behavior-right-click-label": "Åtgärd vid högerklick på Fältet", + "behavior-wheel-wrap-description": "När aktiverat fortsätter rullningen från det sista objektet till det första.", + "behavior-wheel-wrap-label": "Cykla", + "behavior-workspace-scroll-description": "Välj vad mushjulet gör på tomma områden i fältet.", + "behavior-workspace-scroll-label": "Musrullningsåtgärd för Fältet", + "behavior-workspace-scroll-option-content": "Innehåll", + "behavior-workspace-scroll-option-workspace": "Arbetsyta", + "monitor-configure-widgets": "Konfigurera widgetar", + "monitor-override-settings": "Åsidosätt globala inställningar", + "monitor-override-settings-description": "Använd anpassade inställningar för denna skärm.", + "monitor-reset-all": "Återställ alla", + "monitor-widgets-title": "Widgetkonfiguration - {monitor}", + "monitors-desc": "Visa stapel på specifika skärmar. Standardinställningen är alla om ingen är vald.", + "monitors-desc-new": "Konfigurera vilka skärmar som ska visa stapeln och anpassa inställningarna per skärm.", + "monitors-title": "Skärmar visar", + "title": "Stapel", + "tray-blacklist-description": "Lägg till uteslutningsregler för facket, stöder jokertecken (*).", + "tray-blacklist-label": "Svartlista", + "tray-blacklist-placeholder": "t.ex. nm-applet, Fcitx*", + "tray-pin-application": "Fäst applikation", + "tray-unpin-application": "Ta bort fästning av applikation", + "use-global-widgets": "Använd globala widgetar", + "widgets-desc": "Dra widgetar för att ändra deras ordning. Använd högerklickmenyn för att flytta widgetar mellan sektioner eller ta bort dem.", + "widgets-title": "Position för widgetar" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Skapa färgscheman från din bakgrundsbild. Extraherar automatiskt färger för att skapa ett sammanhängande tema.", + "color-source-use-wallpaper-colors-label": "Använd bakgrundsbildernas färger", + "dark-mode-mode-description": "Aktiverar automatisk växling mellan ljust och mörkt läge.", + "dark-mode-mode-label": "Schema för mörkt läge", + "dark-mode-mode-manual": "Manuellt", + "dark-mode-mode-off": "Av", + "dark-mode-switch-description": "Växlar till ett mörkare tema för att underlätta visning på natten.", + "delete-error-description": "Det gick inte att ta bort {scheme}", + "delete-error-title": "Borttagning misslyckades", + "delete-success-description": "{scheme} har tagits bort", + "delete-success-title": "Färgschemat togs bort", + "download-button": "Hämta ner fler", + "download-downloading": "Hämtar...", + "download-empty": "Inga färgscheman tillgängliga", + "download-error-api-error": "API-fel: {status}", + "download-error-description": "Det gick inte att hämta ner {scheme}", + "download-error-download-failed": "Nedladdningen misslyckades med avslutskod: {code}", + "download-error-invalid-response": "Ogiltigt API-svarformat", + "download-error-no-files": "Inga filer hittades för schemat", + "download-error-parse-failed": "Det gick inte att tolka API-svaret: {error}", + "download-error-rate-limit": "GitHub API-hastighetsgränsen överskriden", + "download-error-title": "Hämtningen misslyckades", + "download-fetching": "Hämtar tillgängliga färgscheman...", + "download-success-description": "{schema} har hämtats ner", + "download-success-title": "Färgschema har hämtats ner", + "download-title": "Hämta ner färgscheman", + "method-description": { + "content": "Material Design-schema med högkvalitativ färgutvinning som nära matchar källinnehållets faktiska färger.", + "dysfunctional": "Som Faithful, men väljer den näst mest dominanta färgfamiljen som primär.", + "faithful": "Försöker hålla sig nära källfärgen samtidigt som en harmonisk palett genereras.", + "fruit-salad": "Material Design-schema som ger en lekfull, livfull palett med olika och varierande nyanser.", + "monochrome": "Material Design-schema som använder en enda nyans i gråskala med minimalt kromatiskt innehåll.", + "muted": "Skapar mjukare, desaturerade toner för ett lugnt och diskret utseende.", + "rainbow": "Material Design-schema som skapar färger över hela spektrumet för maximal färgmångfald. ", + "tonal-spot": "Material Design-standardalgoritm. Skapar balanserade paletter med tonvariationer av källfärgen.", + "vibrant": "Maximerar färgmättnaden för djärva, iögonfallande paletter med levande toner." + }, + "predefined-desc": "Välj från en samling fördefinierade färgscheman.", + "predefined-generate-templates-label": "Generera mallar för fördefinierade scheman", + "predefined-title": "Fördefinierade färgscheman", + "sync-gsettings-description": "Matcha systemtemat med den aktiva ljusa eller mörka varianten.", + "sync-gsettings-label": "Synkronisera systemtema", + "templates-desc": "Tillämpa färger på externa applikationer.", + "templates-filter-description": "Visa mallar från en specifik kategori.", + "templates-filter-label": "Filtrera efter kategori", + "templates-misc-description": "Skapa dina egna mallar.", + "templates-misc-label": "Avancerat", + "templates-misc-user-templates-description": "Aktivera endast om du vet vad du gör — se vår online-dokumentation.", + "templates-misc-user-templates-label": "Aktivera användarmallar", + "templates-none-detected": "Inga upptäckta", + "templates-write-path": "Skriver: {filepath}", + "title": "Färgschema", + "wallpaper-method-description": "Välj din favoritmetod för att generera paletter.", + "wallpaper-method-label": "Metod för att generera palett", + "wallpaper-monitor-source-description": "Välj vilken bildskärm som ska användas för att extrahera bakgrundsbildernas färger.", + "wallpaper-monitor-source-label": "Källa för färggenerering" + }, + "connections": { + "authentication-required": "Autentisering krävs", + "bluetooth-auto-connect-description": "Anslut automatiskt till betrodda parkopplade enheter när Bluetooth är aktiverat.", + "bluetooth-auto-connect-label": "Anslut parade enheter automatiskt", + "bluetooth-devices-unnamed": "Namnlösa enheter visas inte.", + "bluetooth-discoverable": "Denna enhet är upptäckbar som {hostName} medan denna inställningsflik är öppen.", + "bluetooth-rssi-polling-description": "Samplar periodiskt RSSI för anslutna enheter via bluetoothctl. Kanske inte tillgängligt för alla enheter; använder minimala resurser när det är aktiverat.", + "bluetooth-rssi-polling-interval-description": "Konfigurera hur ofta signalstyrkan för anslutna enheter ska uppdateras.", + "bluetooth-rssi-polling-interval-label": "Avsökningsintervall", + "bluetooth-rssi-polling-label": "Bluetooth-signalavsökning", + "disable-discoverability-description": "Dölj din enhet från närliggande Bluetooth-enheter.", + "disable-discoverability-label": "Inaktivera enhetssynlighet", + "hide-unnamed-devices-description": "Dölj enheter som endast visas som Bluetooth-adresser.", + "hide-unnamed-devices-label": "Dölj namnlösa enheter", + "pin-instructions": "Ange PIN-koden som visas på din enhet.", + "title": "Anslutningar", + "wifi-header-text": "Använd Wi-Fi för att se tillgängliga nätverk och ansluta till internet." + }, + "control-center": { + "cards-desc": "Anpassa vilka kontroller som ska visas i kontrollcentret och i vilken ordning.", + "desc": "Konfigurera kontrollpanelens placering och beteende.", + "open-at-mouse-description": "När du högerklickar på ett tomt område av Fältet, öppna Kontrollcentret nära markören. Om inaktiverat används istället positioneringsinställningen ovan.", + "open-at-mouse-label": "Öppna vid markören med högerklick", + "position-description": "Välj var kontrollpanelen ska visas när den öppnas.", + "shortcuts-custom-button-command-description": "Kommandot som ska utföras när knappen klickas på.", + "shortcuts-custom-button-enable-on-state-logic-description": "Aktivera en andra ikon och 'aktivt' tillstånd baserat på ett kontrollkommando.", + "shortcuts-custom-button-enable-on-state-logic-label": "Aktivera logik för aktivt tillstånd", + "shortcuts-custom-button-icon-description": "Välj en ikon från biblioteket.", + "shortcuts-custom-button-on-clicked-label": "Kommando vid vänsterklick", + "shortcuts-custom-button-on-middle-clicked-description": "Kommando som ska köras när knappen mittklickas.", + "shortcuts-custom-button-on-state-command-description": "Kommando som ska utföras för att kontrollera om knappen ska vara i läge 'på'. Returnerar 0 för på, icke-noll för av.", + "shortcuts-custom-button-on-state-command-label": "Kommando för att kontrollera om knappen är på", + "shortcuts-custom-button-on-state-icon-description": "Ikonen för knappen när den är 'på'.", + "shortcuts-custom-button-on-state-icon-label": "Ikon för på-läge", + "shortcuts-custom-button-state-checks-add": "Lägg till tillståndskontroll", + "shortcuts-custom-button-state-checks-command": "Kommando att utföra för denna tillståndskontroll", + "shortcuts-custom-button-state-checks-label": "Tillståndskontroller", + "shortcuts-custom-button-state-checks-remove": "Ta bort", + "shortcuts-custom-button-tooltip-description": "Verktygstips som visas när man håller muspekaren över knappen.", + "shortcuts-custom-button-tooltip-label": "Verktygstips", + "shortcuts-title": "Genvägswidgetar", + "system-monitor-disk-path-description": "Välj vilken diskmonteringspunkt som systemövervakningskortet i kontrollcentret ska övervaka.", + "system-monitor-disk-path-label": "Disksökväg för systemövervakning", + "title": "Kontrollcenter" + }, + "desktop-widgets": { + "clock-enabled-description": "Visa en klockwidget på skrivbordet.", + "clock-enabled-label": "Aktivera klockwidget", + "clock-format-label": "Format", + "clock-height-description": "Klockwidgetens höjd i bildpunkter.", + "clock-height-label": "Höjd", + "clock-minimal-mode-description": "Använd en minimal klockskärm med tid och datum.", + "clock-minimal-mode-label": "Minimalt läge", + "clock-rounded-corners-description": "Använd rundade hörn för widgetens bakgrund.", + "clock-rounded-corners-label": "Rundade hörn", + "clock-show-background-description": "Visa bakgrundsbehållaren för klockwidgeten.", + "clock-show-background-label": "Visa bakgrund", + "clock-show-date-description": "Visa aktuellt datum under tiden.", + "clock-show-date-label": "Visa datum", + "clock-show-seconds-description": "Visa sekunder i tiden.", + "clock-show-seconds-label": "Visa sekunder", + "clock-style-analog": "Analog", + "clock-style-binary": "Binär", + "clock-style-description": "Välj klockans visningsstil.", + "clock-style-digital": "Digital", + "clock-style-label": "Klockstil", + "clock-style-minimal": "Minimal", + "clock-width-description": "Klockwidgets bredd i bildpunkter.", + "cpu-intensive-note": "Widgets markerade med ! använder mer CPU och bör aktiveras med eftertanke.", + "edit-mode-button-label": "Gå till redigeringsläge", + "edit-mode-controls-explanation": "Vänsterklicka och dra: Flytta eller ändra storlek på widgeten.\nHögerklicka: Öppna kontextmenyns alternativ.", + "edit-mode-description": "Aktivera redigeringsläge för att flytta och omplacera skrivbordswidgets. När det är aktiverat visas en dragram runt widgets och de kan omplaceras.", + "edit-mode-exit-button": "Avsluta redigeringsläge", + "edit-mode-grid-snap-label": "Rutnätsfästning", + "edit-mode-grid-snap-scale-label": "Rutnätsfästningsskala", + "edit-mode-label": "Redigeringsläge", + "enabled-description": "Aktivera eller inaktivera skrivbordswidgets helt.", + "enabled-label": "Aktivera skrivbordswidgetar", + "general-desc": "Konfigurera widgetar som visas på skrivbordet.", + "general-title": "Skrivbordswidgetar", + "media-player-enabled-description": "Visa en mediaspelarwidget på skrivbordet.", + "media-player-enabled-label": "Aktivera mediaspelarwidget", + "media-player-rounded-corners-description": "Aktivera rundade hörn på widgetens kanter.", + "media-player-show-album-art-description": "Visa albumomslag och spårinformation (titel och artist).", + "media-player-show-album-art-label": "Visa albumomslag och titel", + "media-player-show-background-description": "Visa bakgrundsbehållaren.", + "media-player-show-buttons-description": "Visa mediekontrollknappar (spela/pausa, föregående, nästa).", + "media-player-show-buttons-label": "Visa knappar", + "media-player-show-visualizer-description": "Visa ljudvisualiseringsöverlägget.", + "media-player-visualizer-type-description": "Välj en visualiseringstyp.", + "overview-enabled-description": "Visa skrivbordswidgetar medan kompositorns översikt är öppen.", + "overview-enabled-label": "Visa i översikt", + "system-stat-layout-bottom": "Botten", + "system-stat-layout-description": "Välj hur förklaringen visas i förhållande till diagrammet.", + "system-stat-layout-label": "Layout", + "system-stat-layout-side": "Sida", + "system-stat-rounded-corners-description": "Använd rundade hörn för widgetbakgrunden.", + "system-stat-rounded-corners-label": "Rundade Hörn", + "system-stat-show-background-description": "Visa bakgrundsbehållaren för systemstatistik-widgeten.", + "system-stat-show-background-label": "Visa bakgrund", + "system-stat-stat-type-description": "Välj vilken systemstatistik som ska visas.", + "system-stat-stat-type-label": "Statistiktyp", + "title": "Skrivbordswidgetar", + "weather-enabled-description": "Visa en väderwidget på skrivbordet.", + "weather-enabled-label": "Aktivera väderwidget", + "weather-show-background-description": "Visa bakgrundsbehållaren för väderwidgeten." + }, + "display": { + "monitors-backlight-device-auto-option": "Standard", + "monitors-backlight-device-description": "Välj en bakgrundsbelysningsenhet för denna utgång.", + "monitors-backlight-device-label": "Bakgrundsbelysningsenhet", + "monitors-brightness-step-description": "Justera stegstorleken för ljusstyrkeändringar (rullhjul och kortkommandon).", + "monitors-brightness-step-label": "Ljusstyrkestegstorlek", + "monitors-brightness-unavailable-ddc-disabled": "Ljusstyrkekontroll otillgänglig. Aktivera \"Extern ljusstyrkestöd\"för att kontrollera ljusstyrkan på denna skärm.", + "monitors-brightness-unavailable-generic": "Ljusstyrkekontroll är inte tillgänglig för denna skärm.", + "monitors-desc": "Justera inställningarna för ljusstyrka för varje skärm.", + "monitors-enforce-minimum-description": "Löser problemet med att bakgrundsbelysningen stängs av helt på vissa skärmar vid 0 % ljusstyrka.", + "monitors-enforce-minimum-label": "Tvinga fram minsta ljusstyrka (1 %)", + "monitors-external-brightness-description": "Aktivera DDCUtil-stöd för att kontrollera ljusstyrkan på externa skärmar via DDC/CI-protokollet.", + "monitors-external-brightness-label": "Externt ljusstyrkestöd", + "monitors-global-brightness-description": "Justera ljusstyrkan för alla bildskärmar samtidigt.", + "monitors-global-brightness-label": "Alla bildskärmar", + "monitors-title": "Inställningar per skärm", + "night-light-auto-schedule-description": "Baserat på solnedgångs- och soluppgångstiden i {location} — rekommenderas. ", + "night-light-auto-schedule-label": "Automatisk schemaläggning", + "night-light-desc": "Minska utsläppet av blått ljus för att hjälpa dig sova bättre och minska belastningen på ögonen.", + "night-light-enable-description": "Använd ett varmt färgfilter för att minska utsläppet av blått ljus.", + "night-light-enable-label": "Aktivera nattbelysning", + "night-light-force-activation-description": "Ignorerar schemaläggningen och tillämpar nattfiltret omedelbart.", + "night-light-force-activation-label": "Tvinga aktivering", + "night-light-manual-schedule-description": "Ställ in anpassade tider för soluppgång och solnedgång.", + "night-light-manual-schedule-label": "Manuell schemaläggning", + "night-light-manual-schedule-select-start": "Välj starttid", + "night-light-manual-schedule-select-stop": "Välj sluttid", + "night-light-manual-schedule-sunrise": "Soluppgångstid", + "night-light-manual-schedule-sunset": "Solnedgångstid", + "night-light-temperature-day": "Dag", + "night-light-temperature-day-description": "Styr temperaturen under dagtid.", + "night-light-temperature-description": "Ställ in färgvärmen för natt och dag.", + "night-light-temperature-label": "Färgtemperatur", + "night-light-temperature-night": "Natt", + "night-light-temperature-night-description": "Styr temperaturen under natten.", + "title": "Skärm" + }, + "dock": { + "appearance-background-opacity-description": "Justera dockans bakgrundsopacitet.", + "appearance-border-radius-description": "Justera dockans kantradie.", + "appearance-border-radius-label": "Kantradie", + "appearance-colorize-icons-description": "Tillämpa temafärger på dockappikoner (endast icke-fokuserade appar).", + "appearance-colorize-icons-label": "Färglägg ikoner", + "appearance-dead-opacity-description": "Justera opaciteten för appikoner som inte körs. ", + "appearance-dead-opacity-label": "Opacitet för inaktiva", + "appearance-desc": "Anpassa dockans beteende och utseende. ", + "appearance-display-auto-hide": "Dölj automatiskt", + "appearance-display-description": "Välj hur dockan ska fungera.", + "appearance-display-exclusive": "Exklusiv", + "appearance-dock-indicator-description": "Visa en liten indikator när dockan är dold.", + "appearance-dock-indicator-label": "Dockindikator", + "appearance-floating-distance-description": "Ställ in avståndet mellan dockan och skärmens kant.", + "appearance-floating-distance-label": "Dockens flytande avstånd", + "appearance-group-apps-description": "Gruppera flera fönster från samma app till en Dock-post.", + "appearance-group-apps-label": "Gruppera samma appar", + "appearance-group-click-action-cycle": "Växla fönster", + "appearance-group-click-action-description": "Välj vad vänsterklick gör för grupperade appar.", + "appearance-group-click-action-label": "Klickåtgärd för grupperad app", + "appearance-group-click-action-list": "Öppna fönsterlista", + "appearance-group-context-menu-mode-description": "Välj hur snabbmenyn fungerar för grupperade appar.", + "appearance-group-context-menu-mode-extended": "Utökad", + "appearance-group-context-menu-mode-label": "Grupperat appmenyläge", + "appearance-group-context-menu-mode-list": "Fönsterlista", + "appearance-group-indicator-style-description": "Välj hur grupperade körande indikatorer visar fokuserat fönstertillstånd.", + "appearance-group-indicator-style-dots": "Punkter", + "appearance-group-indicator-style-label": "Grupperad indikatorstil", + "appearance-group-indicator-style-number": "Nummer", + "appearance-hide-show-speed-description": "Justera hastigheten för dockens döljnings-/visningsanimation.", + "appearance-hide-show-speed-label": "Döljnings-/visningshastighet", + "appearance-icon-size-description": "Justera dockens totala storlek. ", + "appearance-icon-size-label": "Dockstorlek", + "appearance-inactive-indicators-description": "Visa indikatorer för alla appar, inte bara den som är aktiv för tillfället.", + "appearance-inactive-indicators-label": "Indikatorer som körs", + "appearance-indicator-color-description": "Välj färg för den dolda dockans indikator.", + "appearance-indicator-color-label": "Indikatorfärg", + "appearance-indicator-opacity-description": "Justera opaciteten för den dolda dockans indikator.", + "appearance-indicator-opacity-label": "Indikatorns opacitet", + "appearance-indicator-thickness-description": "Aktivera en tjockare indikator för den dolda dockan (6 px istället för 3 px).", + "appearance-indicator-thickness-label": "Tjockare indikator", + "appearance-launcher-icon-description": "Välj ikonen som används för Dock Launcher.", + "appearance-launcher-icon-label": "Launcher-ikon", + "appearance-launcher-position-description": "Välj var startprogrammets ikon visas i dockan.", + "appearance-launcher-position-end": "Slut", + "appearance-launcher-position-label": "Startarens position", + "appearance-launcher-position-start": "Starta", + "appearance-launcher-use-distro-logo-description": "Använd din distributions logotyp som Dock Launcher-ikon.", + "appearance-launcher-use-distro-logo-label": "Använd distrologotyp istället för ikon", + "appearance-pinned-static-description": "Flytta alltid fastnålade appikoner till vänster i statisk ordning. ", + "appearance-pinned-static-label": "Statiska fästa appar", + "appearance-position-description": "Välj var dockan ska visas på skärmen.", + "appearance-position-label": "Position", + "appearance-show-launcher-icon-description": "Visa ikonen för programstartaren i dockan.", + "appearance-show-launcher-icon-label": "Visa appstartare", + "appearance-sit-on-frame-description": "Justera dockan innanför ramkanten istället för att ligga ovanpå.", + "appearance-sit-on-frame-label": "Dockan sitter på ramen", + "appearance-type-attached": "Dockad", + "appearance-type-description": "Välj mellan en flytande pillerform eller en statisk stapel fäst vid kanten.", + "appearance-type-floating": "Flytande", + "appearance-type-label": "Dockstil", + "enabled-description": "Visa eller dölj dockningsstationen helt.", + "enabled-label": "Aktivera dockningsstation", + "monitors-desc": "Visa dockningsstationen på specifika bildskärmar. Standardinställningen är alla om ingen är vald.", + "monitors-only-same-monitor-description": "Visa endast appar från den bildskärm där dockningsstationen finns.", + "monitors-only-same-monitor-label": "Endast appar från samma skärm", + "monitors-title": "Skärmvisning", + "title": "Docka" + }, + "general": { + "copy-settings": "Kopiera inställningar", + "fonts-default-description": "Huvudtypsnitt som används i hela gränssnittet.", + "fonts-default-label": "Standardteckensnitt", + "fonts-default-placeholder": "Välj standardteckensnitt...", + "fonts-default-scale-description": "Öka eller minska storleken på standardtexten.", + "fonts-default-scale-label": "Standardteckensnittsstorlek", + "fonts-default-search-placeholder": "Sök teckensnitt.. .", + "fonts-desc": "Välj de teckensnitt som används i gränssnittet.", + "fonts-monospace-description": "Teckensnitt med fast bredd som används för visning av siffror och statistik.", + "fonts-monospace-label": "Teckensnitt med fast bredd", + "fonts-monospace-placeholder": "Välj teckensnitt med fast bredd...", + "fonts-monospace-scale-description": "Öka eller minska storleken på text med fast bredd.", + "fonts-monospace-scale-label": "Teckensnittsstorlek med fast bredd", + "fonts-monospace-search-placeholder": "Sök teckensnitt med fast bredd...", + "fonts-title": "Teckensnitt", + "keybinds-conflict-description": "Tangentkombinationen är redan tilldelad {action}.", + "keybinds-conflict-title": "Tangentbindningskonflikt", + "keybinds-description": "Konfigurera globala navigeringsknappar för paneler och startprogram.", + "keybinds-down": "Flytta nedåt", + "keybinds-enter": "Bekräfta / Åtgärd", + "keybinds-escape": "Stäng / Tillbaka", + "keybinds-left": "Flytta vänster", + "keybinds-modifier-description": "Genvägar måste inkludera en modifieringstangent (Ctrl eller Alt).", + "keybinds-modifier-title": "Modifierare krävs", + "keybinds-remove": "Ta bort", + "keybinds-right": "Flytta höger", + "keybinds-title": "Navigeringstangentbindningar", + "keybinds-up": "Flytta upp", + "language-desc": "Välj önskat språk för applikationen.", + "language-select-auto-detect": "Automatiskt", + "language-select-description": "Välj det språk som ska användas i programmets gränssnitt.", + "language-select-label": "Programspråk", + "launch-setup-wizard": "Installationsguide", + "profile-desc": "Redigera dina användaruppgifter och avatar.", + "profile-picture-description": "Din profilbild som visas i hela gränssnittet.", + "profile-picture-label": "Profilbild", + "profile-select-avatar": "Välj avatarbild", + "profile-title": "Profil", + "profile-tooltip": "Profilbild", + "reverse-scrolling-description": "Vänd den tolkade rullningsriktningen.", + "reverse-scrolling-label": "Omvänd rullning", + "screen-corners-desc": "Anpassa rundningen av skärmens hörn och visuella effekter.", + "screen-corners-radius-description": "Justera skärmens rundade hörn.", + "screen-corners-radius-label": "Skärmens hörnradie", + "screen-corners-radius-reset": "Återställ skärmens hörnradie", + "screen-corners-show-corners-description": "Visa rundade hörn på skärmens kanter. ", + "screen-corners-show-corners-label": "Visa skärmhörn", + "screen-corners-solid-black-description": "Använd helt svart istället för stapelns bakgrundsfärg.", + "screen-corners-solid-black-label": "Helt svarta hörn", + "screen-corners-title": "Skärmhörn", + "settings-copied": "Inställningar kopierade till urklipp", + "smooth-scrolling-description": "Animera listrullning för en smidigare hjulupplevelse.", + "smooth-scrolling-label": "Mjuk rullning", + "tab-basics": "Grundläggande", + "tab-keybinds": "Tangentbindningar" + }, + "hooks": { + "color-generation-description": "Kommando som ska köras när färggenereringen är klar (t.ex. efter tapetbaserad tematisering).", + "color-generation-label": "Färger genererade", + "color-generation-placeholder": "t.ex. notify-send \"Färger\" \"Genererade\"", + "info-command-info-description": "• Kommandon exekveras via shell (sh -lc)
• Kommandon körs i bakgrunden (fristående)
• Testknappar exekveras med aktuella värden", + "info-parameters-description": "• Wallpaper hook: $1 = bakgrundsbildens sökväg, $2 = skärmnamn, $3 = tema (mörkt/ljust)
• Theme toggle hook: $1 = true/false (mörkt lägestatus)
• Screen lock/unlock hooks: $1 = lock/unlock (skärmlåsstatus)
• Performance mode hooks: Inga parametrar
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "Tillgängliga parametrar", + "noctalia-started-description": "Kommando som ska utföras när Noctalia har laddats klart.", + "noctalia-started-label": "Noctalia startat", + "noctalia-started-placeholder": "t.ex. notify-send 'Noctalia laddat'", + "performance-mode-disabled-description": "Kommando som ska utföras när Noctalia prestandaläget är inaktiverat.", + "performance-mode-disabled-label": "Prestandaläget inaktiverat", + "performance-mode-disabled-placeholder": "t.ex. notify-send \"Prestanda\"\"Läget inaktiverat\"", + "performance-mode-enabled-description": "Kommando som ska utföras när Noctalia prestandaläget är aktiverat.", + "performance-mode-enabled-label": "Prestandaläget aktiverat", + "performance-mode-enabled-placeholder": "t.ex. notify-send \"Prestanda\"\"Läget aktiverat\"", + "screen-lock-description": "Kommando som ska utföras när skärmen är låst.", + "screen-lock-label": "Skärm låst", + "screen-lock-placeholder": "t.ex. notify-send \"Skärm\"\"Låst\"", + "screen-unlock-description": "Kommando som ska utföras när låsskärmen är upplåst.", + "screen-unlock-label": "Skärm upplåst", + "screen-unlock-placeholder": "t.ex. notify-send \"Skärm\"\"Upplåst\"", + "session-description": "Kommando som ska utföras före avstängning eller omstart. Tar emot åtgärdstyp som $1 (avstängning/omstart). ", + "session-label": "Session slut", + "session-placeholder": "t.ex. notify-send \"Session\"\"$1\"", + "system-hooks-desc": "Konfigurera kommandon som ska utföras när systemhändelser inträffar.", + "system-hooks-enable-description": "Aktivera eller inaktivera alla hook-kommandon.", + "system-hooks-enable-label": "Aktivera hooks", + "system-hooks-title": "Systemhooks", + "theme-changed-description": "Kommando som ska utföras när temat växlar mellan mörkt och ljust läge.", + "theme-changed-label": "Tema ändrat", + "theme-changed-placeholder": "t.ex. notify-send \"Tema\"\"Växlat\"", + "title": "Hooks", + "wallpaper-changed-description": "Kommando som ska utföras när bakgrundsbilden ändras.", + "wallpaper-changed-label": "Bakgrundsbild ändrad", + "wallpaper-changed-placeholder": "t.ex. notify-send \"Bakgrundsbild\"\"Ändrad\"" + }, + "idle": { + "custom-add": "Lägg till kommando", + "custom-description": "Kör ett skal-kommando efter en period av inaktivitet.", + "custom-entry-command": "Kommando", + "custom-entry-delete": "Ta bort", + "custom-entry-edit": "Redigera Anpassat Kommando", + "custom-entry-name": "Namn", + "custom-entry-name-placeholder": "t.ex. Släck lamporna", + "custom-entry-new": "Nytt Anpassat Kommando", + "custom-entry-timeout": "Inaktivitetstid", + "custom-entry-timeout-format": "{count} sekund", + "custom-entry-timeout-format-plural": "{count} sekunder", + "custom-entry-unnamed": "Namnlöst kommando", + "custom-label": "Anpassade inaktivitetskommandon", + "enable-description": "Stäng automatiskt av skärmen, lås eller försätt i viloläge efter en period av inaktivitet.", + "enable-label": "Aktivera inaktivitetshantering", + "fade-duration-description": "Sekunder för animationen som tonar till svart innan varje åtgärd utlöses. Alla musrörelser avbryter nedtoningen.", + "fade-duration-label": "Toningstid", + "lock-description": "Sekunder av inaktivitet innan låsskärmen aktiveras.", + "lock-label": "Lås skärm", + "resume-command-label": "Återuppta kommando", + "screen-off-description": "Sekunder av inaktivitet innan skärmarna stängs av.", + "screen-off-label": "Stäng av skärmen", + "status-description": "Inaktiv tid som rapporterats av compositorn.", + "status-label": "Inaktiv tid", + "suspend-description": "Sekunder av inaktivitet innan systemet försätts i viloläge.", + "tab-behavior": "Beteende", + "tab-custom": "Anpassad", + "timeouts-description": "Ställ in på 0 för att inaktivera ett steg. Tidsgränser pausas medan 'Håll vaken' är aktivt.", + "timeouts-label": "Tidsgränser", + "title": "Inaktivitet", + "unavailable": "Inbyggd inaktivitetsövervakning är inte tillgänglig på denna Compositor." + }, + "indicator": { + "default-value": "Standard: {värde}", + "system-default": "Systemstandard" + }, + "launcher": { + "clipboard-desc": "Öppna och hantera din urklippshistorik från startprogrammet.", + "execute-desc": "Konfigurera hur program startas.", + "execute-title": "Exekvering", + "settings-annotation-tool-description": "Kommando som körs när du klickar på anteckningsknappen i klippbordshistoriken. Bilden kommer att skickas till detta kommando.", + "settings-annotation-tool-label": "Anteckningsverktyg", + "settings-annotation-tool-placeholder": "t.ex. 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Klistra in det valda klippbordsinnehållet automatiskt. Kräver wtype. ", + "settings-auto-paste-label": "Automatisk klistra in", + "settings-clip-chips-description": "Visa en flikfält för att filtrera klippbordshistoriken efter typ (Bilder, Länkar, Filer, Kod, etc.).", + "settings-clip-chips-label": "Aktivera kategorichips", + "settings-clip-preview-description": "Visa en förhandsgranskning av klippbordets innehåll när du använder kommandot >clip.", + "settings-clip-preview-label": "Aktivera förhandsgranskning av klipp", + "settings-clip-smart-icons-description": "Visa specifika ikoner för länkar, filer, färger och kod istället för en generisk urklippsikon.", + "settings-clip-smart-icons-label": "Aktivera smarta ikoner", + "settings-clip-wrap-text-description": "Vik text i klippbordslistan istället för att avkorta den.", + "settings-clip-wrap-text-label": "Vik in text i urklipp", + "settings-clipboard-history-description": "Öppna tidigare kopierade objekt från startprogrammet.", + "settings-clipboard-history-label": "Aktivera klippbordshistorik", + "settings-clipboard-watch-image-description": "Fullständig kommandosträng som skickas till wl-paste för bildändringar (kräver omstart).", + "settings-clipboard-watch-image-label": "Bildövervakningskommando", + "settings-clipboard-watch-text-description": "Fullständig kommandosträng skickad till wl-paste för textändringar (kräver omstart).", + "settings-clipboard-watch-text-label": "Textbevakningskommando", + "settings-custom-launch-prefix-description": "Prefixkommandon med en anpassad startfunktion (t.ex. 'runapp' för systemd-integration).", + "settings-custom-launch-prefix-enabled-description": "Använd ett anpassat prefix för att starta applikationer istället för standardmetoden.", + "settings-custom-launch-prefix-enabled-label": "Aktivera anpassat startprefix", + "settings-custom-launch-prefix-label": "Anpassat startprefix", + "settings-density-description": "Justera storleken på applikationsikoner och Launcherns densitet.", + "settings-density-label": "Densitet", + "settings-desc": "Anpassa startprogrammets beteende och utseende.", + "settings-enable-session-search-description": "Visa sessionsåtgärder (lås, stäng av, starta om, etc.) vid sökning i Launcher.", + "settings-enable-session-search-label": "Aktivera sessionssökning", + "settings-enable-settings-search-description": "Visa inställningsresultat vid sökning i startprogrammet.", + "settings-enable-settings-search-label": "Aktivera inställningssökning", + "settings-enable-windows-search-description": "Sök och fokusera aktiva fönster.", + "settings-enable-windows-search-label": "Aktivera fönstersökning", + "settings-grid-view-description": "Visa objekt i ett rutnät istället för en lista.", + "settings-icon-mode-description": "Använd inbyggda systemikoner istället för Tabler-ikoner.", + "settings-icon-mode-label": "Använd inbyggda ikoner", + "settings-ignore-mouse-input-description": "Inaktivera musinteraktion och rullhjul i startprogrammet.", + "settings-ignore-mouse-input-label": "Ignorera musinmatning", + "settings-overlay-layer-description": "Visa startprogrammet på överlagringslagret, ovanför helskärmsfönster. När aktiverat fästs startprogrammet inte vid fältet.", + "settings-overlay-layer-label": "Visa ovanför helskärm", + "settings-position-description": "Välj var startprogrammet ska visas.", + "settings-show-categories-description": "Visa kategoriflikar för att filtrera applikationer.", + "settings-show-categories-label": "Visa kategorier", + "settings-show-icon-background-description": "Visa en rundad rektangulär bakgrund bakom ikonerna.", + "settings-show-icon-background-label": "Visa ikonbakgrund", + "settings-sort-by-usage-description": "När denna funktion är aktiverad visas de program som används oftast först i listan.", + "settings-sort-by-usage-label": "Sortera efter mest använda", + "settings-terminal-command-description": "Kommando för att starta en terminal. T.ex. 'kitty -e' eller 'gnome-terminal --'.", + "settings-terminal-command-label": "Terminalkommando", + "settings-use-app2unit-description": "Använder en alternativ startmetod för att bättre hantera app-processer och förhindra problem.", + "settings-use-app2unit-label": "Använd App2Unit för att starta applikationer", + "settings-view-mode-description": "Välj layout för startprogrammets poster.", + "settings-view-mode-label": "Visningsläge", + "title": "Startprogram" + }, + "location": { + "auto-locate-description": "Upptäck automatiskt din plats periodiskt med hjälp av din IP-adress.", + "auto-locate-label": "Automatisk Lokalisering", + "calendar-cards-desc": "Organisera och aktivera/inaktivera kort i kalenderpanelen.", + "calendar-cards-title": "Kalenderkort", + "calendar-header-label": "Kalenderrubrik", + "calendar-month-label": "Kalendermånad", + "date-time-12hour-format-description": "Visa tiden i 12-timmarsformat på låsskärmen och i kalendern. Bar-klockan har sina egna inställningar.", + "date-time-12hour-format-label": "Använd 12-timmarsformat", + "date-time-desc": "Anpassa hur datum och tid visas.", + "date-time-first-day-of-week-automatic": "Automatisk (använd systemets lokala inställningar)", + "date-time-first-day-of-week-description": "Välj vilken dag som är veckans första dag i kalendern.", + "date-time-first-day-of-week-label": "Veckans första dag", + "date-time-show-events-description": "Visa händelser i kalenderpanelen.", + "date-time-show-events-label": "Visa kalenderhändelser", + "date-time-title": "Datum och tid", + "date-time-use-analog-description": "Visa en analog klocka i kalenderfönstret.", + "date-time-use-analog-label": "Använd analog klocka", + "date-time-week-numbers-description": "Visa veckans nummer (t.ex. vecka 38) i kalendern.", + "date-time-week-numbers-label": "Visa veckans nummer", + "geolocate-now-button": "Lokalisera nu", + "location-desc": "Få exakt väderinformation och schemaläggning av nattbelysning genom att ange din plats.", + "location-search-description": "t.ex. Stockholm, SE", + "location-search-label": "Sök efter en plats", + "location-search-placeholder": "Ange platsens namn", + "location-title": "Din plats", + "weather-desc": "Välj önskad temperaturenhet.", + "weather-enabled-description": "Visa väderinformation i hela gränssnittet och hämta väderdata. När funktionen är inaktiverad döljs alla väderelement och inga nätverksförfrågningar görs.", + "weather-enabled-label": "Aktivera väder", + "weather-fahrenheit-description": "Visa temperaturen i Fahrenheit istället för Celsius.", + "weather-fahrenheit-label": "Visa temperaturen i Fahrenheit (°F)", + "weather-hide-city-description": "Dölj stadsnamnet från vädervisningarna i hela gränssnittet.", + "weather-hide-city-label": "Dölj stadsnamn", + "weather-hide-timezone-description": "Dölj tidszonförkortningen från vädervisningarna i hela gränssnittet.", + "weather-hide-timezone-label": "Dölj tidszon", + "weather-show-effects-description": "Visa ytterligare visuella effekter (som regn, snö eller blixtar) på väderkortet.", + "weather-show-effects-label": "Visa vädereffekter", + "weather-show-in-calendar-description": "Visa den dagliga väderprognosen direkt i din kalendervy.", + "weather-show-in-calendar-label": "Visa väder i kalendern", + "weather-talia-mascot-always-description": "Använd alltid Talia emojis istället för tabler icons.", + "weather-talia-mascot-always-label": "Visa alltid Talia väder-emojis" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "När fprintd (fingeravtrycksautentisering) är aktivt, låter det här alternativet dig fortfarande logga in med ditt lösenord istället för ett fingeravtryck", + "allow-password-with-fprintd-label": "Tillåt lösenordsinloggning med fprintd", + "auto-start-auth-description": "Startar automatiskt fingeravtrycksautentisering utan att du behöver trycka på en tangent eller knapp.", + "auto-start-auth-label": "Automatisk start av autentisering", + "clock-format-description": "Anpassa klockformatet med datum-/tidssyntax-tokens.", + "clock-format-label": "Klockformat", + "clock-style-analog": "Analog", + "clock-style-custom": "Anpassad", + "clock-style-description": "Välj den visuella stilen för klockan på låsskärmen.", + "clock-style-digital": "Digital", + "clock-style-label": "Klockstil", + "compact-lockscreen-description": "Visa endast inloggningsfältet och systemkontrollerna, dölj väder- och mediewidgets.", + "compact-lockscreen-label": "Kompakt låsskärm", + "enable-lockscreen-media-controls-description": "Visa interaktiva mediekontroller på låsskärmen.", + "enable-lockscreen-media-controls-label": "Mediekontroller på Låsskärmen", + "lock-on-suspend-description": "Lås skärmen automatiskt när systemet pausas.", + "lock-on-suspend-label": "Lås vid paus", + "lock-screen-animations-description": "Aktivera eller inaktivera låsskärmsanimeringar.", + "lock-screen-animations-label": "Låsskärmsanimationer", + "lock-screen-blur-strength-description": "Tillämpar en oskärpaeffekt på låsskärmens bakgrundsbild.", + "lock-screen-blur-strength-label": "Oskärpestyrka för låsskärmen", + "lock-screen-tint-strength-description": "Tillämpar en färgtoningsöverlagring på låsskärmens bakgrundsbild.", + "lock-screen-tint-strength-label": "Låsskärmens toningsstyrka", + "monitors-desc": "Visa låsskärm på specifika bildskärmar. Standard är alla om inga väljs.", + "password-chars-description": "Söta ikoner som används för att dölja ditt lösenord.", + "password-chars-label": "Slumpmässiga lösenordsikoner", + "show-hibernate-description": "Visa alternativet 'viloläge' i strömkontrollerna.", + "show-hibernate-label": "Visa viloläge", + "show-session-buttons-description": "Tillåt åtkomst till ströminställningar från låsskärmen. ", + "show-session-buttons-label": "Strömkontroller", + "title": "Låsskärm" + }, + "network": { + "bluetooth-description": "Aktivera Bluetooth-hantering.", + "bluetooth-rssi-polling-description": "Sampla RSSI regelbundet för anslutna enheter via bluetoothctl. Kan inte vara tillgängligt för alla enheter; använder minimala resurser när det är aktiverat.", + "bluetooth-rssi-polling-label": "Bluetooth-signalpollning", + "desc": "Hantera Wi-Fi- och Bluetooth-anslutningar.", + "wifi-description": "Hantera trådlösa nätverk (kräver NetworkManager)." + }, + "notifications": { + "duration-critical-urgency-description": "Hur länge meddelanden med kritisk prioritet förblir synliga.", + "duration-critical-urgency-label": "Kritisk brådskande", + "duration-desc": "Konfigurera hur länge meddelanden förblir synliga baserat på deras brådskande nivå.", + "duration-low-urgency-description": "Hur länge meddelanden med låg prioritet förblir synliga.", + "duration-low-urgency-label": "Låg brådskande", + "duration-normal-urgency-description": "Hur länge meddelanden med normal prioritet förblir synliga.", + "duration-normal-urgency-label": "Normal brådskande", + "duration-respect-expire-description": "Använd den tidsgräns som anges i meddelandet.", + "duration-respect-expire-label": "Respektera tidsgränsen för utgång", + "duration-title": "Meddelandets varaktighet", + "history-clear-dismiss-description": "Rensa avisering från historiken när den avfärdas.", + "history-clear-dismiss-label": "Rensa vid avfärdande", + "history-critical-urgency-description": "Spara meddelanden med kritisk prioritet i historiken.", + "history-critical-urgency-label": "Spara kritiska meddelanden i historiken", + "history-desc": "Kontrollera vilka meddelanden som sparas i historiken baserat på deras brådskande nivå.", + "history-low-urgency-description": "Spara meddelanden med låg prioritet i historiken.", + "history-low-urgency-label": "Spara låg prioritet i historiken", + "history-normal-urgency-description": "Spara meddelanden med normal prioritet i historiken.", + "history-normal-urgency-label": "Spara normal prioritet i historiken", + "monitors-desc": "Visa meddelanden på specifika skärmar. Standardinställningen är alla om ingen väljs.", + "rules-action-block": "Block", + "rules-action-block-desc": "Hoppar över helt.", + "rules-action-hide": "Dölj", + "rules-action-hide-desc": "Ingen popup, inget ljud, läggs till i historiken.", + "rules-action-label": "Åtgärd", + "rules-action-mute": "Tysta", + "rules-action-mute-desc": "Inget ljud, visar fortfarande popup och i historiken.", + "rules-add": "Lägg till regel", + "rules-delete": "Ta bort regel", + "rules-description": "Matcha appnamn eller innehåll. Regler kontrolleras i ordning, och den första matchningen tillämpas.", + "rules-edit": "Redigera regel", + "rules-label": "Filterregler", + "rules-pattern-label": "Mönster", + "rules-pattern-placeholder": "firefox, discord, eller /regex/", + "rules-tab": "Regler", + "settings-always-on-top-description": "Visa meddelanden ovanför helskärmsfönster och andra lager.", + "settings-background-opacity-description": "Justera opaciteten för meddelandenas bakgrund.", + "settings-density-description": "Välj densitet för aviseringskortet.", + "settings-density-label": "Densitet", + "settings-desc": "Konfigurera meddelandenas utseende och beteende.", + "settings-do-not-disturb-description": "Inaktivera alla popup-meddelanden när funktionen är aktiverad.", + "settings-enabled-description": "Aktivera eller inaktivera meddelandedemonen, kräver omstart av Noctalia-skalet.", + "settings-enabled-label": "Aktivera meddelanden", + "settings-location-description": "Var meddelanden visas på skärmen.", + "settings-markdown-description": "Rendera meddelandeinnehåll med Markdown-formatering.", + "settings-markdown-label": "Aktivera Markdown", + "sounds-desc": "Konfigurera ljud effekter och volym för meddelanden.", + "sounds-enabled-description": "Aktivera ljudeffekter för inkommande meddelanden.", + "sounds-enabled-label": "Aktivera meddelandeljud", + "sounds-excluded-apps-description": "Hoppa över att spela upp det konfigurerade meddelandeljudet för specifika applikationer som har egna inbyggda ljud.", + "sounds-excluded-apps-label": "Exkluderade applikationer", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Sökväg till ljudfilen som spelas upp för meddelanden med kritisk prioritet.", + "sounds-files-critical-label": "Ljud för kritisk brådskande händelse", + "sounds-files-critical-select-title": "Välj ljudfil för kritisk brådskande händelse", + "sounds-files-desc": "Konfigurera anpassade ljudfiler för olika nivåer av brådskande meddelanden.", + "sounds-files-low-description": "Sökväg till ljudfilen som spelas upp för meddelanden med låg prioritet.", + "sounds-files-low-label": "Ljud för låg prioritet", + "sounds-files-low-select-title": "Välj ljudfil för låg prioritet", + "sounds-files-normal-description": "Sökväg till ljudfilen som spelas upp för meddelanden med normal prioritet.", + "sounds-files-normal-label": "Ljud för normal prioritet", + "sounds-files-normal-select-title": "Välj ljudfil för normal prioritet", + "sounds-files-placeholder": "Ange sökväg till ljudfil", + "sounds-files-select-file": "Välj ljudfil", + "sounds-files-title": "Ljudfiler", + "sounds-files-unified-description": "Sökväg till ljudfilen som spelas upp för aviseringar.", + "sounds-files-unified-label": "Meddelandeljud", + "sounds-files-unified-select-title": "Välj meddelandeljudfil", + "sounds-separate-description": "Använd olika ljudfiler för meddelanden med låg, normal och kritisk prioritet.", + "sounds-separate-label": "Använd olika ljud per prioritet", + "sounds-title": "Ljudinställningar", + "sounds-unavailable-description": "Installera Qt6 Multimedia för att aktivera meddelandeljud.", + "sounds-unavailable-label": "Meddelandeljud otillgängliga", + "sounds-volume-description": "Justera volymnivån för meddelandeljud.", + "sounds-volume-label": "Ljudvolym", + "toast-battery-description": "Visa en varning när batterinivån sjunker under denna procentandel.", + "toast-battery-label": "Batterivarning", + "toast-desc": "Konfigurera utseende och beteende för toast.", + "toast-keyboard-description": "Visa en toast när tangentbordslayouten ändras.", + "toast-keyboard-label": "Tangentbordslayout", + "toast-media-description": "Visa en toast när mediaspelningsstatus ändras.", + "toast-media-label": "Media" + }, + "osd": { + "always-on-top-description": "Visa OSD ovanför helskärmsfönster och andra lager.", + "always-on-top-label": "Alltid överst", + "background-opacity-description": "Styr transparensen för OSD-bakgrunden.", + "background-opacity-label": "Bakgrundens opacitet", + "description": "Konfigurera indikatorer på skärmen, såsom volym- och ljusstyrkeöverlägg.", + "duration-auto-hide-description": "Justera tiden innan OSD försvinner.", + "duration-auto-hide-label": "Dölj automatiskt efter", + "duration-desc": "Hur länge OSD förblir synligt innan det döljs automatiskt. ", + "duration-title": "Automatisk döljningstid", + "enabled-description": "Visa volym- och ljusstyrkeändringar i realtid.", + "enabled-label": "Aktivera skärmdisplay", + "events-desc": "Välj vilka händelser som ska utlösa skärmdisplayen.", + "general-desc": "Konfigurera OSD:s synlighet och beteende.", + "location-description": "Var skärmdisplayen visas.", + "monitors-desc": "Visa OSD på specifika skärmar. Standardinställningen är alla om ingen är vald.", + "title": "Skärmdisplay", + "types-brightness-description": "Visa OSD när skärmens ljusstyrka ändras.", + "types-brightness-label": "Ljusstyrka", + "types-custom-text-description": "Visa OSD för anpassade textmeddelanden från IPC.", + "types-custom-text-label": "Anpassad text", + "types-desc": "Välj de händelser som aktiverar OSD.", + "types-input-volume-description": "Visa OSD när mikrofonvolymen ändras. ", + "types-input-volume-label": "Ingångsvolym", + "types-lockkey-description": "Visa OSD när Caps Lock, Num Lock eller Scroll Lock aktiveras.", + "types-lockkey-label": "Låsta tangenter", + "types-media-description": "Visa OSD när mediaspelningsstatus ändras (spela, pausa, hoppa över).", + "types-media-label": "Medieuppspelning", + "types-title": "OSD-utlösande händelser", + "types-volume-description": "Visa OSD när ljudutgångsvolymen ändras.", + "types-volume-label": "Utgångsvolym" + }, + "plugins": { + "auto-update": "Uppdatera insticksmoduler automatiskt", + "auto-update-description": "Uppdatera automatiskt alla plugins när skalet startar.", + "available-description": "Bläddra bland och installera insticksmoduler från konfigurerade källor.", + "available-label": "Tillgängliga insticksmoduler", + "available-no-plugins-description": "Kontrollera dina instickskällor eller uppdatera listan.", + "available-no-plugins-label": "Inga insticksmoduler tillgängliga", + "check-for-updates": "Sök efter uppdateringar", + "checking-for-updates": "Söker efter uppdateringar...", + "collision-already-installed": "Denna insticksmodul är redan installerad", + "collision-custom-version-exists": "En anpassad version från \"{source}\"är redan installerad", + "collision-official-version-exists": "Den officiella versionen av denna insticksmodul är redan installerad", + "development-disable": "Inaktivera utvecklingsläge för denna plugin", + "development-enable": "Aktivera utvecklingsläge för denna plugin.", + "filter-downloaded": "Hämtade", + "filter-not-downloaded": "Inte hämtade", + "filter-tags-description": "Filtrera insticksmoduler efter kategori eller hämtningsstatus.", + "filter-tags-label": "Taggar", + "hot-reload-description": "Ladda om insticksmoduler automatiskt när deras filer ändras. Användbart för insticksutveckling.", + "hot-reload-label": "Hot reload (dev mode)", + "hot-reloaded": "Startade om insticksmodul: {name}", + "install-error": "Installation misslyckades: {error}", + "install-incompatible": "{plugin} kräver Noctalia v{version} eller högre", + "install-success": "{plugin} har installerats", + "installed-description": "Hantera och konfigurera alla lokalt installerade insticksmoduler.", + "installed-label": "Installerade insticksmoduler", + "installed-no-plugins-description": "Installera insticksmoduler från avsnittet \"Tillgängliga\".", + "installed-no-plugins-label": "Inga insticksmoduler installerade", + "installing": "Installerar {plugin}...", + "notify-updates": "Meddelanden om plugin-uppdateringar", + "notify-updates-description": "Visa en notifikationspopup när nya plugin-uppdateringar är tillgängliga.", + "open-plugin-page": "Öppna insticksmodulsida", + "open-plugins-tab": "Öppna insticksmodulinställningar", + "plugin-settings-title": "Inställningar för {plugin}", + "refresh-refreshing": "Uppdaterar insticksmodullista...", + "refresh-tooltip": "Uppdatera tillgängliga insticksmoduler", + "settings-error-not-loaded": "Insticksmodul inte inläst", + "settings-saved": "Insticksmodulsinställningar sparade", + "settings-tooltip": "Insticksmodulinställningar", + "source-custom": "Anpassad källa", + "sources-add-custom": "Lägg till anpassat arkiv", + "sources-add-dialog-description": "Lägg till ett GitHub-arkiv som insticksmodulkälla.", + "sources-add-dialog-error": "Det gick inte att lägga till insticksmodulkälla", + "sources-add-dialog-name": "Arkivnamn", + "sources-add-dialog-name-placeholder": "Mina anpassade insticksmoduler", + "sources-add-dialog-success": "Insticksmodulkälla har lagts till", + "sources-add-dialog-title": "Lägg till insticksmodulkälla", + "sources-add-dialog-url": "Arkivets URL", + "sources-description": "Hantera insticksmodularkiv.", + "sources-placeholder": "Mitt coola arkiv", + "sources-remove-tooltip": "Ta bort insticksmodulkälla", + "title": "Insticksmoduler", + "translations-reloaded": "Översättningar omladdade: {name}", + "uninstall-dialog-description": "Är du säker på att du vill avinstallera {plugin}? Detta kommer att ta bort all insticksmoduldata.", + "uninstall-dialog-title": "Avinstallera insticksmodul", + "uninstall-error": "Avinstallationen misslyckades: {error}", + "uninstall-success": "{plugin} har avinstallerats", + "uninstalling": "Avinstallerar {plugin}...", + "update-all": "Uppdatera alla ({count})", + "update-all-success": "Alla insticksmoduler har uppdaterats", + "update-available": "Ny insticksuppdatering tillgänglig", + "update-available-plural": "Insticksuppdateringar tillgängliga ({count})", + "update-error": "Misslyckades med att uppdatera insticksmodul: {plugin}: {error}", + "update-pending": "v{current} → v{new} (kräver Noctalia v{required})", + "update-success": "Uppdaterade {plugin} till v{version}", + "update-version": "v{current} → v{new}", + "updating": "Uppdaterar..." + }, + "region": { + "title": "Region" + }, + "session-menu": { + "countdown-duration-description": "Ställ in hur länge nedräkningstimern ska pågå innan strömåtgärder utförs.", + "countdown-duration-label": "Nedräkningstid", + "enable-countdown-description": "Visa en nedräkningstimer innan strömåtgärder utförs. ", + "enable-countdown-label": "Aktivera nedräkning", + "entries-desc": "Anpassa vilka strömåtgärder som visas i sessionsmenyn och i vilken ordning.", + "entries-title": "Strömåtgärder", + "entry-settings-command-description": "Anpassat kommando som ska utföras för denna åtgärd. Lämna tomt för att använda standardkommandot i systemet.", + "entry-settings-command-placeholder": "t.ex. systemctl poweroff", + "entry-settings-default-command-lock": "Intern låsskärm (inget kommando)", + "entry-settings-default-command-logout": "Intern utloggning (inget kommando)", + "entry-settings-default-info-description": "Om inget anpassat kommando anges kommer standardkommandot för systemet att användas.", + "entry-settings-default-info-label": "Standardkommando", + "entry-settings-keybind-description": "Tilldela en tangentkombination för att utlösa denna åtgärd när sessionsmenyn är öppen.", + "entry-settings-keybind-placeholder": "Tryck på Spela in för att tilldela en tangent...", + "entry-settings-keybind-recording": "Inspelning... Tryck på valfri tangentkombination", + "entry-settings-title": "Konfigurera {entry}", + "entry-settings-tooltip": "Konfigurera kommando", + "general-desc": "Konfigurera sessionens menypanels beteende och utseende.", + "large-buttons-layout-description": "Välj hur sessionens menyknappar ska visas.", + "large-buttons-layout-label": "Stora knappar", + "large-buttons-style-description": "Visa sessionens meny med stora knappar i ett rutnät.", + "large-buttons-style-label": "Stor knappstil", + "position-description": "Välj var sessionsmenypanelen ska visas när den öppnas.", + "show-header-description": "Visa titeln och stängningsknappen högst upp i sessionsmenyn.", + "show-header-label": "Visa rubrik", + "show-keybinds-description": "Visa kortkommando-tips på sessionsalternativ.", + "show-keybinds-label": "Visa kortkommandon" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Håll skrivbordswidgets synliga medan Noctalia prestandaläget är aktiverat.", + "noctalia-performance-disable-desktop-widgets-label": "Aktivera skrivbordswidgetar i prestandaläge", + "noctalia-performance-disable-wallpaper-description": "Behåll skrivbords-, översikts- och låsskärmsbakgrundsbilder synliga när Noctalia prestandaläget är aktiverat.", + "noctalia-performance-disable-wallpaper-label": "Aktivera bakgrundsbildsåtergivning i prestandaläge", + "title": "System" + }, + "system-monitor": { + "critical-color-label": "Kritisk färg", + "custom-highlight-colors-title-label": "Anpassade markeringsfärger", + "disk-available-label": "Tillgängligt diskutrymme", + "disk-section-label": "Diskanvändning", + "enable-dgpu-monitoring-description": "Varning: Detta aktiverar din diskreta GPU (NVIDIA/AMD), vilket kan påverka batteritiden avsevärt på bärbara datorer med hybridgrafik.", + "enable-dgpu-monitoring-label": "Aktivera övervakning av diskret GPU", + "external-monitor-description": "Ange kommandot eller applikationsvägen som ska startas när den externa systemövervakningsapplikationen aktiveras.", + "external-monitor-label": "Kommando för extern systemmonitor", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor ", + "general-desc": "Konfigurera systemövervakningens beteende och utseende.", + "gpu-section-label": "GPU-temperatur", + "highlight-colors-section-label": "Markeringsfärger", + "polling-interval-label": "Pollningsintervall", + "polling-section-description": "Konfigurera hur ofta varje systemmetrik uppdateras.", + "polling-section-label": "Pollningsintervall", + "threshold-critical": "Kritisk", + "threshold-warning": "Varning", + "thresholds-section-description": "Justera varnings-/kritiska trösklar för varje systemmetrik.", + "use-custom-highlight-colors-description": "När denna funktion är inaktiverad används standardfärgerna för markeringar. ", + "use-custom-highlight-colors-label": "Använd anpassade markeringsfärger", + "warning-color-label": "Varningsfärg" + }, + "user-interface": { + "allow-panels-without-bar-description": "När denna funktion är aktiverad kan paneler öppnas på valfri skärm. När den är inaktiverad öppnas paneler endast på skärmar som har en bar, vilket kan minska minnesanvändningen.", + "allow-panels-without-bar-label": "Tillåt paneler på skärmar utan bar", + "animation-disable-description": "Inaktivera alla animationer för en snabbare och mer responsiv upplevelse.", + "animation-disable-label": "Inaktivera UI-animationer", + "animation-speed-description": "Justera global animationshastighet.", + "animation-speed-label": "Animationshastighet", + "animation-speed-reset": "Återställ animationshastighet", + "appearance-desc": "Anpassa visuella element som verktygstips, kantlinjer och skuggor.", + "blur-behind-description": "Suddar ut området bakom fält och paneler med hjälp av kompositorns suddighetsprotokoll.", + "blur-behind-label": "Bakgrundsoskärpa", + "box-border-description": "Visa en kontur runt innehållsområden.", + "box-border-label": "Behållarkontur", + "box-border-radius-description": "Justerar hörnrundningen för större layoutsektioner, såsom sidofält, kort och innehållspaneler.", + "box-border-radius-label": "Behållarradie", + "control-border-radius-description": "Styr kurvaturen för interaktiva element, inklusive knappar, växlar och textfält.", + "control-border-radius-label": "Inmatningsradie", + "desc": "Anpassa utseendet, känslan och beteendet hos gränssnittet.", + "dim-desktop-description": "Dämpa skrivbordet när paneler eller menyer är öppna.", + "dim-desktop-label": "Dämpa skrivbordet", + "dimmer-opacity-description": "Ställ in opacitetsnivån för dämpning av skrivbordet. ", + "dimmer-opacity-label": "Dimrad skrivbordsopacitet", + "panel-background-opacity-description": "Ställ in bakgrundsopaciteten för alla paneler (fält, startprogram, inställningar osv.). ", + "panel-background-opacity-label": "Panelens bakgrundsopacitet", + "panels-attached-to-bar-description": "Panelerna låses fast vid fältet och skärmens kanter, vilket skapar ett sömlöst utseende med snygga inverterade hörn.", + "panels-attached-to-bar-label": "Fäst paneler vid kanterna", + "panels-overlay-description": "Säkerställer att paneler och fältet förblir synliga, även över helskärmsapplikationer. ", + "panels-overlay-label": "Håll paneler och fält överst", + "scaling-description": "Ändrar storleken på det allmänna användargränssnittet, exklusive fältet.", + "scaling-label": "Gränssnittsskalning", + "scrollbar-always-visible-description": "Håll rullningslister synliga när innehåll är rullbart, istället för att bara visa dem vid hovring.", + "scrollbar-always-visible-label": "Visa alltid rullningslister", + "settings-panel-header": "Inställningspanel", + "settings-panel-mode-description": "Välj inställningslayout (kan kräva att du öppnar om).", + "settings-panel-mode-label": "Inställningspanelens läge", + "settings-panel-sidebar-card-style-description": "Omsluter sidofältet för inställningar i en fylld bakgrund med rundade hörn.", + "settings-panel-sidebar-card-style-label": "Sidopanelens kortstil", + "shadows-description": "Aktiverar skuggor under fält och paneler.", + "shadows-direction-description": "Välj var skuggan ska kastas.", + "shadows-direction-label": "Skuggans riktning", + "shadows-label": "Fallskugga", + "title": "Användargränssnitt", + "tooltips-description": "Aktivera eller inaktivera verktygstips i hela gränssnittet.", + "tooltips-label": "Visa verktygstips", + "translucent-widgets-description": "Gör knappar, flikar och andra widgets inuti paneler halvtransparenta.", + "translucent-widgets-label": "Genomskinliga widgetar" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Alfabetiskt", + "automation-change-mode-description": "Välj hur bakgrundsbilder ska väljas när de ändras automatiskt.", + "automation-change-mode-label": "Ändra läge", + "automation-custom-interval-description": "Ange tid i formatet TT:MM (t.ex. 01:30).", + "automation-custom-interval-label": "Anpassat intervall", + "automation-interval-description": "Hur ofta bakgrundsbilder ska ändras automatiskt. ", + "automation-interval-label": "Bakgrundsbildsintervall", + "automation-random-wallpaper-description": "Schemalägg slumpmässiga bakgrundsbildsbyten med jämna mellanrum.", + "automation-scheduled-change-description": "Byt bakgrundsbilder automatiskt med jämna mellanrum.", + "automation-scheduled-change-label": "Schemalagd ändring", + "look-feel-edge-smoothness-description": "Tillämpar en mjuk, uttonad effekt på övergångarnas kanter.", + "look-feel-edge-smoothness-label": "Mjuk övergångskant", + "look-feel-fill-color-description": "Välj en fyllningsfärg som kan visas bakom bakgrundsbilden.", + "look-feel-fill-mode-description": "Välj hur bilden ska skalas för att passa din skärms upplösning.", + "look-feel-fill-mode-label": "Fyllningsläge", + "look-feel-skip-startup-transition-description": "Hoppa över bakgrundsbildsanimationen när skalet startar.", + "look-feel-skip-startup-transition-label": "Hoppa över startövergång", + "look-feel-title": "Utseende och känsla", + "look-feel-transition-duration-description": "Övergångsanimationernas varaktighet i sekunder.", + "look-feel-transition-duration-label": "Övergångens varaktighet", + "look-feel-transition-type-description": "Animationstyp vid byte mellan bakgrundsbilder.", + "look-feel-transition-type-label": "Övergångstyp", + "settings-clear-cache-button": "Rensa cache", + "settings-clear-cache-description": "Rensa cachade, storleksändrade bakgrundsbilder för att frigöra diskutrymme.", + "settings-clear-cache-label": "Bakgrundscache", + "settings-clear-cache-toast": "Bakgrundscache rensad", + "settings-desc": "Styr hur bakgrundsbilder hanteras och visas.", + "settings-enable-management-description": "Hantera bakgrundsbilder med Noctalia. Avmarkera om du föredrar att använda ett annat program.", + "settings-enable-management-label": "Aktivera bakgrundsbildshantering", + "settings-enable-overview-description": "Tillämpar en suddig och nedtonad bakgrundsbild på översiktsskärmen.", + "settings-enable-overview-label": "Aktivera översiktsbakgrundsbild", + "settings-folder-description": "Sökväg till din huvudsakliga bakgrundsbildsmapp.", + "settings-folder-label": "Bakgrundsbildsmapp", + "settings-hide-wallpaper-filenames-tooltip-hide": "Dölj filnamn", + "settings-hide-wallpaper-filenames-tooltip-show": "Visa filnamn", + "settings-monitor-specific-description": "Ställ in en annan bakgrundsbildsmapp för varje skärm.", + "settings-monitor-specific-label": "Skärmspecifika kataloger", + "settings-monitor-specific-tooltip": "Skärmens bakgrundsbildsmapp", + "settings-overview-blur-strength-description": "Tillämpar oskärpans styrka på översikten.", + "settings-overview-blur-strength-label": "Översiktens oskärpestyrka", + "settings-overview-tint-description": "Tillämpar toningsstyrkan på översikten.", + "settings-overview-tint-label": "Översiktstoningsstyrka", + "settings-recursive-search-description": "Sök även efter bakgrundsbilder i undermappar i bakgrundsbildskatalogen.", + "settings-recursive-search-label": "Sök i undermappar", + "settings-select-monitor-folder": "Välj mapp för bakgrundsbilder", + "settings-selector-description": "Välj din bakgrundsbild.", + "settings-selector-position-description": "Välj var panelen för val av bakgrundsbild ska visas.", + "settings-show-hidden-files-tooltip-hide": "Dölj dolda filer", + "settings-show-hidden-files-tooltip-show": "Visa dolda filer", + "settings-title": "Bakgrundsinställningar", + "settings-use-original-images-description": "Hoppa över att ändra storlek på bakgrundsbilder före visning. Sparar diskutrymme och minskar CPU-användning vid bakgrundsbildsbyten, men kan använda mer minne för mycket stora bilder.", + "settings-use-original-images-label": "Använd originalbilder", + "settings-view-mode-description": "Välj hur bakgrundsbilder ska visas från din katalog.", + "settings-view-mode-label": "Visningsläge", + "view-mode-browse": "Bläddra i kataloger", + "view-mode-cycle-tooltip": "Visningsläge: {läge} (klicka för att ändra)", + "view-mode-recursive": "Platta underkataloger", + "view-mode-single": "Rotkatalog" + } + }, + "placeholders": { + "add-new-keybind": "Lägg till ny tangentbindning", + "command-example": "echo \"Hello World\"", + "enter-command": "Ange kommando att utföra (app eller anpassat skript)", + "enter-ipc-identifier": "Ange unik identifierare för IPC-kommandon", + "enter-path": "Ange sökväg...", + "enter-text-to-collapse": "t.ex. 'ingenting spelas upp'. Använd /regex/ för mönster.", + "enter-tooltip": "Ange verktygstips", + "enter-width-pixels": "Ange bredd i bildpunkter", + "keybind-recording": "Spelar in tangentbindning...", + "search": "Sök... ", + "search-icons": "t.ex. noctalia, niri, batteri, moln", + "search-launcher": "Sök poster... eller använd > för kommandon", + "search-wallhaven": "Sök på Wallhaven...", + "search-wallpapers": "Skriv för att filtrera bakgrundsbilder... " + }, + "positions": { + "bottom": "Nedre", + "bottom-center": "Nedre mitt", + "bottom-left": "Nedre vänster", + "bottom-right": "Nedre höger", + "center": "Mitten", + "center-left": "Vänster mitt", + "center-right": "Höger mitt", + "close-to-bar": "Stäng till stapelknapp", + "follow-bar": "Följ stapel", + "left": "Vänster", + "right": "Höger", + "top": "Överst", + "top-center": "Överst i mitten", + "top-left": "Överst till vänster", + "top-right": "Överst till höger" + }, + "session-menu": { + "action-in-seconds": "{action} på {sekunder} sekunder...", + "cancel-timer": "Avbryt timer", + "title": "Sessionsmeny" + }, + "setup": { + "all-done": "Allt klart!", + "appearance": { + "subheader": "Välj mörkt läge och färgkällor (bakgrundsbild eller fördefinierade)." + }, + "customize": { + "header": "Anpassa din upplevelse", + "subheader": "Justera fältets position, densitet, skalning och mer." + }, + "skip-setup": "Hoppa över inställningar", + "telemetry-wizard-done": "Jag fattar!", + "telemetry-wizard-note": "Du har kontrollen — aktivera eller inaktivera detta när som helst i inställningar", + "telemetry-wizard-subtitle": "Vi har lagt till anonym analys för att förbättra Noctalia", + "telemetry-wizard-title": "Uppdatering av integritet", + "wallpaper": { + "choose-dir": "Välj en katalog som innehåller dina bakgrundsbilder", + "dir-browse": "Bläddra efter bakgrundsbildsmapp", + "dir-description": "Välj mappen som innehåller dina bakgrundsbilder.", + "dir-label": "Bakgrundsbildskatalog", + "dir-select-title": "Välj bakgrundsbildsmapp", + "header": "Välj din bakgrundsbild", + "no-dir": "Ingen bakgrundsbildskatalog vald", + "no-valid": "Inga giltiga bildfiler hittades i: {dir}", + "none-in-dir": "Inga bakgrundsbilder hittades i katalogen", + "preview-error": "Det gick inte att läsa in bilden", + "select-prompt": "Välj en bakgrundsbild nedan", + "subheader": "Skapa stämning med en vacker bakgrund." + }, + "welcome": "Välkommen", + "welcome-note": "Bara några grundläggande tips för att komma igång — alla alternativ finns i inställningarna", + "welcome-subtitle": "Låt oss göra ditt skrivbord unikt för dig", + "welcome-title": "Välkommen till Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "Ingen mediaspelare upptäckt", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "Okänd app", + "uptime": "Driftstid: {uptime}", + "user-requested": "Begärt av användare", + "welcome-back": "Välkommen tillbaka", + "widget-settings-title": "Inställningar för {widget}" + }, + "system-monitor": { + "core-usage": "Kärna {id} användning", + "cpu-temp": "CPU-temperatur", + "cpu-usage": "CPU-användning", + "disk": "Disk", + "download-speed": "Hämtningshastighet", + "gpu-temp": "GPU-temperatur", + "load-average": "Genom. belastning", + "title": "Systemövervakare", + "upload-speed": "Sändningshastighet" + }, + "toast": { + "airplane-mode": { + "description": "Inaktivera all trådlös kommunikation.", + "title": "Flygplansläge" + }, + "battery": { + "critical": "Kritisk batterinivå", + "critical-desc": "Batteriet är på {percent}% — anslut laddaren omedelbart", + "low": "Lågt batteri", + "low-desc": "Batteriet är på {percent}% — anslut laddaren" + }, + "bluetooth": { + "address-copied": "Adress kopierad till urklipp", + "auto-connect-disabled": "Automatisk anslutning inaktiverad", + "auto-connect-enabled": "Automatisk anslutning aktiverad", + "auto-connecting": "Ansluter till {count} enhet(er)...", + "confirm-code": "Bekräfta koden {value} på den andra enheten.", + "connect-failed": "Det gick inte att ansluta till enheten", + "disconnect-failed": "Det gick inte att koppla bort från enheten", + "discoverable-change-failed": "Det gick inte att ändra synlighetsstatus", + "discoverable-disabled": "Upptäckbar inaktiverad", + "discoverable-enabled": "Upptäckbar aktiverad", + "display-code": "Ange koden {value} på den andra enheten.", + "forget-failed": "Det gick inte att glömma enheten", + "pair-failed": "Det gick inte att para ihop enheten", + "passkey-required": "Enheten kräver lösenord.", + "pincode-required": "Enheten kräver PIN-kod.", + "state-change-failed": "Det gick inte att ändra Bluetooth-status" + }, + "clipboard": { + "long-text": "Lång text", + "unavailable": "Urklippshistorik otillgänglig", + "unavailable-desc": "Applikationen 'cliphist' är inte installerad. Installera den för att kunna använda klippbordshistorikfunktionerna" + }, + "custom-command-failed": { + "description": "Kommando misslyckades: {command}\\nAvslutningskod: {code}", + "title": "Anpassat kommando misslyckades" + }, + "do-not-disturb": { + "disabled": "'Stör inte' inaktiverat", + "disabled-desc": "Visar alla aviseringar", + "enabled": "'Stör inte' aktiverat", + "enabled-desc": "Du hittar dessa aviseringar i din historik" + }, + "donation-opened": "Donationssidan öppnades i din webbläsare", + "internet-limited": "Ansluten utan internet", + "keyboard-layout": { + "changed": "Tangentbordslayouten ändrad till {layout}", + "title": "Tangentbord" + }, + "missing-control-center": { + "description": "Kontrollcenter-widgeten har tagits bort från fältet. För att komma åt den från fältet igen måste du lägga till widgeten på nytt. Du kan också öppna den genom att högerklicka på fältet", + "label": "Senaste kontrollcenter-widgeten borttagen" + }, + "night-light": { + "forced": "Tvingad aktivering", + "normal": "Normalt läge", + "not-installed": "wlsunset inte installerat" + }, + "noctalia-performance": { + "disabled": "Prestandaläget inaktiverat", + "enabled": "Prestandaläget aktiverat", + "label": "Noctalia Prestanda" + }, + "power-profile": { + "changed": "Strömprofil ändrad", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "Ett fel uppstod vid bearbetning av mallar", + "title": "Bearbetning av mallar misslyckades" + }, + "vpn": { + "connected": "Ansluten till '{name}'", + "disconnected": "Frånkopplad från '{name}'" + }, + "wallpaper-colors": { + "disabled": "Bakgrundsfärger inaktiverade", + "enabled": "Bakgrundsfärger aktiverade", + "label": "Bakgrundsfärger" + }, + "wifi": { + "connected": "Ansluten till '{ssid}'", + "connection-failed": "Anslutningen misslyckades", + "connection-timeout": "Anslutningstidsgräns", + "disconnected": "Kopplade från '{ssid}'", + "incorrect-password": "Felaktigt lösenord", + "network-not-found": "Nätverket hittades inte" + } + }, + "tooltips": { + "add-widget": "Lägg till widget", + "auto-connect": "Växla automatisk anslutning för denna enhet", + "bluetooth-auto-connect-off": "Automatisk anslutning inaktiverad", + "bluetooth-auto-connect-on": "Automatisk anslutning aktiverad", + "bluetooth-devices": "Bluetooth-enheter", + "brightness-at": "Ljusstyrka: {brightness}%", + "click-to-start-recording": "Skärminspelare (starta inspelning)", + "click-to-stop-recording": "Skärminspelare (stoppa inspelning)", + "collapse": "Dölj sidofält", + "copy-address": "Kopiera adress", + "delete-notification": "Ta bort avisering", + "dismiss-notification": "Avfärda avisering", + "do-not-disturb-enabled": "Stör inte", + "expand": "Expandera sidofält", + "forget-network": "Glöm nätverk", + "grid-view": "Rutnätsvy", + "hidden-files-hide": "Dolda filer", + "hide-unnamed-devices": "Dölj namnlösa enheter", + "home": "Hemkatalog", + "input-muted": "Växla mellan ljud avstängt och ljud på", + "keep-awake": "Håll vaken", + "keyboard-layout": "{layout} tangentbordslayout", + "list-view": "Listvy", + "manage-vpn": "VPN-anslutningar", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "Maximalt antal widgetar uppnått", + "microphone-volume-at": "Mikrofonvolym: {volume}%", + "move-to-section": "Flytta till {section}", + "mute": "Ljud av", + "next-media": "Nästa spår", + "next-month": "Nästa månad", + "night-light-not-installed": "Nattlampa (inte tillgänglig)", + "noctalia-performance-enabled": "Noctalia prestandaläget", + "open-annotation-tool": "Öppna med anteckningsverktyg", + "open-control-center": "Kontrollcenter", + "open-notification-history-enable-dnd": "Aviseringshistorik", + "open-settings": "Inställningar", + "open-tray-dropdown": "Systemfält", + "output-muted": "Växla mellan ljud avstängt", + "power-profile": "Energiprofilen {profil}", + "previous-media": "Föregående spår", + "previous-month": "Föregående månad", + "refresh-devices": "Uppdatera enheter", + "refresh-wallhaven": "Uppdatera Wallhaven-resultat", + "refresh-wallpaper-list": "Uppdatera bakgrundsbildslista", + "remove-widget": "Ta bort widget", + "search-close": "Stäng sökning", + "session-menu": "Sessionsmeny", + "show-all-devices": "Visa alla enheter", + "switch-to-dark-mode": "Mörkt läge", + "switch-to-light-mode": "Ljust läge", + "unmute": "Inaktivera ljud", + "up": "Överordnad katalog", + "volume-at": "Utgångsvolym: {volume}%", + "wallpaper-selector": "Bakgrundsbildsväljare" + }, + "wallpaper": { + "browse": { + "empty-directory": "Denna katalog är tom.", + "go-root": "Gå till bakgrundsbildens rotkatalog", + "go-up": "Gå till överordnad mapp", + "go-up-hint": "Använd bakåtknappen för att navigera uppåt." + }, + "configure-directory": "Konfigurera din bakgrundsbildskatalog med bilder.", + "fill-modes": { + "crop": "Beskär (Fyll)", + "fit": "Anpassa (Innehåll)", + "repeat": "Upprepa (Sida vid sida)", + "stretch": "Sträck ut" + }, + "no-match": "Ingen matchning hittades.", + "no-wallpaper": "Ingen bakgrundsbild hittades.", + "panel": { + "apikey-help": "En API-nyckel krävs för att få åtkomst till NSFW-innehåll.", + "apikey-label": "API-nyckel", + "apikey-managed-by-env": "Hanteras via miljövariabeln NOCTALIA_WALLHAVEN_API_KEY.", + "apikey-placeholder": "Ange din Wallhaven API-nyckel", + "appearance-dark-tab": "Mörk", + "appearance-light-tab": "Ljus", + "categories-anime": "Anime", + "categories-label": "Kategorier", + "categories-people": "Personer", + "color-extraction-disabled": "Använd bakgrundsfärgsutvinning", + "color-extraction-enabled": "Använd fördefinierade färgscheman", + "header-devices-apply-all-tooltip": "Välj bakgrundsbild per skärm", + "header-devices-per-monitor-tooltip": "Tillämpa på alla bildskärmar", + "header-link-light-dark-tooltip": "Behåll bakgrundsbild vid temabyte", + "header-separate-light-dark-tooltip": "Byt bakgrundsbild automatiskt med tema", + "header-sun-linked-tooltip": "Separata ljusa och mörka bakgrundsbilder", + "header-sun-separate-tooltip": "Samma bakgrundsbild för ljust och mörkt", + "order-asc": "Stigande", + "order-desc": "Fallande", + "order-label": "Ordning", + "purity-label": "Innehållsfilter", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Sketchy", + "ratios-all-portrait": "Alla porträtt", + "ratios-all-wide": "Alla breda", + "ratios-any": "Alla", + "ratios-label": "Bildförhållande", + "resolution-atleast": "Minst", + "resolution-exact": "Exakt", + "resolution-label": "Upplösning", + "resolution-mode-label": "Läge", + "solid-color-tooltip": "Enfärgad bakgrund", + "sort-date-asc": "Sortera efter äldst först", + "sort-date-desc": "Sortera efter nyast först", + "sort-name-asc": "Sortera efter namn (A-Z)", + "sort-name-desc": "Sortera efter namn (Z-A)", + "sort-random": "Sortera slumpmässigt", + "sorting-date-added": "Datum tillagt", + "sorting-favorites": "Favoriter", + "sorting-label": "Sortera efter", + "sorting-relevance": "Relevans", + "sorting-toplist": "Topplista", + "sorting-views": "Visningar", + "source-label": "Källa", + "source-wallhaven": "Wallhaven", + "title": "Bakgrundsbildsväljare", + "wallhaven-settings-title": "Wallhaven-inställningar" + }, + "transitions": { + "disc": "Skiva", + "fade": "Tonad", + "honeycomb": "Bikaka", + "pixelate": "Pixla", + "stripes": "Ränder", + "wipe": "Torkning" + }, + "try-different-search": "Prova en annan sökfråga.", + "wallhaven": { + "loading": "Läser in bakgrundsbilder...", + "no-results": "Inga bakgrundsbilder hittades. Prova en annan sökfråga.", + "page": "{current} av {total}", + "page-prefix": "Sida", + "page-suffix": "av {total}" + } + }, + "weather": { + "clear-sky": "Klar himmel", + "drizzle": "Duggregn", + "fog": "Dimma", + "mainly-clear": "Mestadels klart", + "overcast": "Molnigt", + "partly-cloudy": "Delvis molnigt", + "rain-showers": "Regnskurar", + "snow": "Snö", + "thunderstorm": "Åskväder" + }, + "widgets": { + "color-picker": { + "palette-description": "Välj bland ett stort utbud av fördefinierade färger.", + "palette-label": "Palett", + "palette-theme-colors": "Snabb åtkomst till ditt temas färgpalett. ", + "title": "Färgväljare" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm i gemener", + "ampm-uppercase": "AM/PM i versaler", + "common-12hour-time-minutes": "12-timmarsformat med minuter", + "common-24hour-time-minutes": "24-timmarsformat med minuter", + "common-24hour-time-seconds": "24-timmarsformat med sekunder", + "common-european-date": "Europeiskt datumformat", + "common-iso-date": "ISO-datumformat", + "common-us-date": "Amerikanskt datumformat", + "common-weekday-date": "Veckodag med datum", + "common-weekday-month-day": "Veckodag, månad och dag", + "day-abbreviated": "Förkortat dagnamn", + "day-full": "Fullständigt dagnamn", + "day-leading-zero": "Dag med inledande nolla (01-31)", + "day-no-leading-zero": "Dag utan inledande nolla (1-31)", + "hour-leading-zero": "Timme med inledande nolla (00-23) — 24-timmarsformat", + "hour-no-leading-zero": "Timme utan inledande nolla (0-23) — 24-timmarsformat", + "minute-leading-zero": "Minut med inledande nolla (00-59)", + "minute-no-leading-zero": "Minut utan inledande nolla (0-59)", + "month-abbreviated": "Förkortat månadsnamn", + "month-full": "Fullständigt månadsnamn", + "month-number-leading-zero": "Månad som nummer med inledande nolla (01-12)", + "month-number-no-zero": "Månad som nummer utan inledande nolla (1-12)", + "second-leading-zero": "Sekund med inledande nolla (00-59)", + "second-no-leading-zero": "Sekund utan inledande nolla (0-59)", + "timezone-abbreviation": "Förkortning av tidszon", + "year-four-digit": "År som fyrsiffrigt tal", + "year-two-digit": "År som tvåsiffrigt tal (00-99)" + }, + "file-picker": { + "item": "objekt", + "items": "objekt", + "search-placeholder": "Sök efter filer och mappar...", + "select-current": "Välj aktuell", + "select-file": "Välj fil", + "select-folder": "Välj mapp", + "selected": "Vald:", + "title": "Filväljare" + }, + "file-picker-title": "Välj en fil", + "icon-picker": { + "title": "Ikonväljare" + }, + "tooltip-placeholder": "Platshållare" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "Anonym Identitet", + "ca-cert": "CA-certifikat", + "eap-method": "EAP-metod", + "password": "Användarlösenord", + "phase2-auth": "Fas 2-autentisering", + "username": "Identitet / Användarnamn" + }, + "panel": { + "action-required": "Åtgärd krävs", + "add-hidden-network": "Lägg till dolt nätverk", + "add-network": "Lägg till nätverk", + "available-interfaces": "Tillgängliga gränssnitt", + "available-networks": "Tillgängliga nätverk", + "disabled": "Wi‑Fi är inaktiverat", + "disconnecting": "Kopplar från...", + "dns": "DNS", + "enable-message": "Aktivera Wi‑Fi för att se tillgängliga nätverk.", + "enter-password": "Ange Wi‑Fi-lösenord... ", + "forget": "Glöm", + "forget-network": "Glöm detta nätverk", + "forgetting": "Glömmer...", + "hidden-network": "Dolt nätverk", + "interface": "Nätverksgränssnitt", + "internet-connected": "Ansluten till internet", + "internet-limited": "Inget internet", + "internet-status": "Internetstatus", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "Kända nätverk", + "link-speed": "Länkhastighet", + "network-name-ssid": "Nätverksnamn (SSID)", + "no-ethernet-devices": "Inga Ethernet-enheter upptäckta", + "no-networks": "Inga Wi‑Fi-nätverk hittades", + "saved": "Sparat", + "scan-again": "Sök igen", + "searching": "Söker efter nätverk...", + "security-open": "Öppna", + "security-wep": "WEP", + "security-wpa": "WPA/WPA2/WPA3 Personligt", + "security-wpa-ent": "WPA Enterprise", + "security-wpa2-ent": "WPA2 Enterprise", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 Enterprise", + "show-password": "Visa lösenord", + "title": "Wi‑Fi" + }, + "signal": { + "excellent": "Utmärkt", + "fair": "Ok", + "good": "Bra", + "poor": "Dåligt", + "weak": "Svag" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/tr.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/tr.json new file mode 100644 index 0000000..71c8a2e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/tr.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "Geçmişi temizle", + "close-app": "{app}'i kapat", + "connect-vpn": "{name} bağlantısına bağlan", + "cycle-visualizer": "Döngü görselleştirici", + "disable-bluetooth": "Bluetooth'u kapat", + "disable-dnd": "'Rahatsız etmeyin'i kapat", + "disable-wifi": "Kablosuz bağlantıyı kapat", + "disconnect-vpn": "{name} bağlantısını kes", + "dock-settings": "Dock ayarları", + "enable-bluetooth": "Bluetooth'u etkinleştir", + "enable-dnd": "'Rahatsız etmeyin'i etkinleştir", + "enable-wifi": "Kablosuz bağlantıyı etkinleştir", + "launcher-settings": "Başlatıcı ayarları", + "lower-to-bottom": "Alta doğru alçalt", + "open-calendar": "Takvimi aç", + "open-display-settings": "Ekran ayarları", + "open-launcher": "Başlatıcıyı aç", + "open-settings": "Ayarları aç", + "raise-to-top": "En üste taşı", + "random-wallpaper": "Rastgele duvar kâğıdı", + "run-custom-command": "Özel komut çalıştır", + "show-active-only": "Sadece etkin olanları göster", + "show-all": "Tümünü göster", + "toggle-mute": "Sesi kapat/aç", + "widget-settings": "Araç takımı ayarları" + }, + "authentication": { + "error": "Kimlik doğrulama hatası", + "failed": "Kimlik doğrulama başarısız" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Aktif pencere simgelerine tema renklerini uygula.", + "hide-mode-description": "Pencere aktif değilken araç takımlarının nasıl davrandığını kontrol eder.", + "scrolling-mode-description": "Uzun pencere başlıkları için metin kaydırmanın ne zaman etkinleştirileceğini kontrol et.", + "show-app-icon-description": "Pencere başlığı yanında uygulama simgesini göster.", + "show-app-icon-label": "Uygulama simgesini göster", + "show-app-text-description": "Etkin pencerenin başlığını göster.", + "show-app-text-label": "Pencere başlığını göster" + }, + "audio-visualizer": { + "color-name-description": "Görselleştirici için renk seçin.", + "color-name-label": "Dolgu rengi", + "height-description": "Özel bileşen genişliği.", + "hide-when-idle-description": "Etkinleştirildiğinde, çalar aktif bir şekilde çalmadığı sürece görselleştirici gizlidir.", + "hide-when-idle-label": "Boşken gizle", + "width-description": "Özel bileşen genişliği." + }, + "battery": { + "device-default": "Varsayılan (Bileşik aygıt)", + "device-description": "Görüntülenecek pil cihazını seçin.", + "device-label": "Pil cihazı", + "display-mode-description": "Pilin çubukta nasıl görüntüleneceğini seçin.", + "display-mode-graphic": "Grafik pil", + "display-mode-graphic-clean": "Grafik pil (yüzde yok)", + "display-mode-icon-always": "Simge - Her zaman % göster", + "display-mode-icon-hover": "Simge - Üzerine gelindiğinde göster", + "display-mode-icon-only": "Yalnızca simge", + "hide-if-idle-description": "Pil şarj olmuyor veya deşarj olmuyorken aracı gizle.", + "hide-if-idle-label": "Boşta kalınca gizle", + "hide-if-not-detected-description": "Sistemde pil algılanmadığında araç çubuğunu gizle.", + "hide-if-not-detected-label": "Algılanmazsa gizle", + "low-battery-threshold-label": "Düşük batarya eşiği", + "show-noctalia-performance-description": "Pil panelinde Noctalia performans modu anahtarını göster.
Kaynak kullanımını azaltmak için Noctalia'da gölgeleri ve animasyonları devre dışı bırakır.", + "show-noctalia-performance-label": "Noctalia Performans anahtarını göster", + "show-power-profile-description": "Pil panelinde güç profili seçimlerini göster.", + "show-power-profile-label": "Güç profili denetimlerini göster" + }, + "brightness": { + "apply-all-description": "Kaydırma tekerleğini kullanırken tüm monitörlerin parlaklığını değiştir.", + "apply-all-label": "Kaydırma değişikliklerini tüm monitörlere uygula" + }, + "clock": { + "clock-display-description": "Saatinizin görüntüsünü özelleştirmek için aşağıdaki listeden jetonları ekleyin. 12 saat formatını kullanmak için 'AP' jetonunu eklemelisiniz.", + "clock-display-label": "Saat görüntüleme", + "custom-font-description": "Saat görüntüleme için özel bir yazı tipi seçin.", + "custom-font-label": "Özel yazı tipi", + "custom-font-placeholder": "Özel yazı tipi seçin...", + "custom-font-search-placeholder": "Yazı tiplerini ara...", + "horizontal-bar-description": "İpucu: Bir satır sonu oluşturmak için \\n kullanın.", + "horizontal-bar-label": "Yatay çubuk", + "preview": "Önizleme", + "tooltip-format-description": "Saat üzerine geldiğinizde gösterilen ipucu için format dizesi. Varsayılan ipucunu kullanmak için boş bırakın.", + "tooltip-format-label": "İpucu formatı", + "use-custom-font-description": "Saat için varsayılan yazı tipi seçimini özel bir yazı tipiyle geçersiz kılın.", + "use-custom-font-label": "Özel yazı tipi kullan", + "use-monospaced-font-description": "Etkinleştirildiğinde, saat tek aralıklı yazı tipini kullanır.", + "use-monospaced-font-label": "Tek aralıklı yazı tipi kullan", + "vertical-bar-description": "Her parçayı yeni bir satıra ayırmak için boşluk kullanın.", + "vertical-bar-label": "Dikey çubuk" + }, + "control-center": { + "browse-file": "Dosyaya göz at", + "browse-library": "Kütüphaneye göz at", + "color-selection-description": "Simgeye tema renklerini uygular.", + "enable-colorization-description": "Simge için renklendirmeyi etkinleştir, tema renklerini uygula.", + "icon-description": "Kütüphaneden veya özel bir dosyadan bir simge seçin.", + "select-custom-icon": "Özel bir simge seç", + "use-distro-logo-description": "Özel simge yerine dağıtımınızın logosunu kullanın.", + "use-distro-logo-label": "Dağıtım logosunu simge yerine kullan" + }, + "custom-button": { + "collapse-condition-description": "Çıktı metni bu değerle eşleşirse düğme daralacak.", + "collapse-condition-label": "Daralma durumu", + "color-selection-description": "Tema renklerini simgeye ve metne uygula.", + "default-tooltip": "Özel düğme, ayarlarda yapılandırın", + "display-command-output-description": "Düzenli aralıklarla çalıştırılacak bir komut girin. Çıktısının ilk satırı metin olarak gösterilecektir.", + "display-command-output-label": "Komut çıktısını göster", + "display-command-output-stream-description": "Sürekli çalıştırılacak bir komut girin.", + "dynamic-text": "Hareketli metin", + "enable-colorization-description": "Özel düğme simgesi ve metni için renklendirmeyi etkinleştir, tema renklerini uygular.", + "enable-colorization-label": "Renklendirmeyi etkinleştir", + "general-tooltip-text-description": "Düğmenin araç ipucunda görüntülenecek özel metin.", + "general-tooltip-text-label": "Özel ipucu metni", + "hide-mode-always-expanded": "Her zaman geniş", + "hide-mode-description": "Komutun çıktı vermemesi durumunda gerecin görünürlüğünü kontrol eder.", + "hide-mode-expand-with-output": "Çıktı olduğunda genişlet", + "hide-mode-label": "Gizleme modu", + "hide-mode-max-transparent": "En büyük genişlik ama saydam", + "icon-description": "Kütüphaneden bir simge seçin.", + "ipc-identifier-description": "IPC komutları için benzersiz tanımlayıcı. Bu tanımlayıcıyı 'qs -c noctalia-shell ipc call cb [eylem] [tanımlayıcı]' ile kullanarak bu düğmeyi IPC aracılığıyla kontrol edin.", + "ipc-identifier-label": "IPC Tanımlayıcısı", + "left-click-description": "Düğmeye sol tıklandığında yürütülecek komut.", + "left-click-label": "Sol tıklama", + "left-click-update-text": "Sol tıklamayla görüntülenen metni güncelle", + "max-text-length-horizontal-description": "Yatay çubukta gösterilecek en fazla karakter sayısı (metni gizlemek için 0).", + "max-text-length-horizontal-label": "En uzun metin uzunluğu (yatay)", + "max-text-length-vertical-description": "Dikey çubukta gösterilecek en fazla karakter sayısı (metni gizlemek için 0).", + "max-text-length-vertical-label": "En uzun metin uzunluğu (dikey)", + "middle-click-description": "Orta düğmeye tıklandığında yürütülecek komut.", + "middle-click-label": "Orta tıklama", + "middle-click-update-text": "Orta tıklamayla görüntülenen metni güncelle", + "parse-json-description": "Komut çıktısını hareketli metin ve simge olarak ayarlamak için bir JSON nesnesi olarak ayrıştırın.", + "parse-json-label": "Çıktıyı JSON olarak ayrıştır", + "refresh-interval-description": "milisaniye cinsinden aralık.", + "refresh-interval-label": "Yenileme aralığı", + "right-click-description": "Sağ düğmeye tıklandığında yürütülecek komut.", + "right-click-label": "Sağ tıklama", + "right-click-update-text": "Sağ tıklamada görüntülenen metni güncelle", + "show-exec-tooltip-description": "Komut ayrıntılarını içeren ipuçlarını göster (sol/sağ/orta tıklama, tekerlek).", + "show-exec-tooltip-label": "Komut ipuçlarını göster", + "show-icon-description": "Takım aracının simgesinin görünürlüğünü değiştirir.", + "show-icon-label": "Simgeyi göster", + "show-text-tooltip-description": "Metin komutunun çıktısıyla araç ipuçlarını göster.", + "show-text-tooltip-label": "Dinamik metin ipuçlarını göster", + "text-stream-description": "Komuttan gelen akış satırları düğmede metin olarak gösterilecektir.", + "text-stream-label": "Akış", + "wheel-description": "Kaydırma tekerleği kullanıldığında yürütülecek komut.
Komutta kaydırma tekerleği deltası için $delta kullanın.", + "wheel-down": "Aşağı kaydır", + "wheel-down-description": "Kaydırma tekerleği aşağı kaydırıldığında yürütülecek komut.", + "wheel-down-label": "Kaydırma tekerleği aşağı komutu", + "wheel-label": "Kaydırma tekerleği", + "wheel-mode-separate-description": "Kaydırma tekerleği yukarı ve aşağı için ayrı komutları etkinleştir.", + "wheel-mode-separate-label": "Ayrı kaydırma tekerleği komutları", + "wheel-up": "Yukarı kaydır", + "wheel-up-description": "Kaydırma tekerleği yukarı kaydırıldığında yürütülecek komut.", + "wheel-up-label": "Kaydırma tekerleği yukarı komutu", + "wheel-update-text": "Kaydırmada gösterilen metni güncelle" + }, + "keyboard-layout": { + "show-icon-description": "Klavye düzeni simgesini görüntüle." + }, + "lock-keys": { + "hide-when-off-description": "Tuş etkin olmadığında göstergeyi gizle.", + "hide-when-off-label": "Kapatıldığında gizle", + "show-caps-lock-description": "Caps Lock düğmesi durumunu göster.", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "Num Lock durumunu göster.", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "Scroll Lock durumunu göster.", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "Medya oynatıcı paneli için yerden tasarruf sağlayan bir düzeni etkinleştirin.", + "compact-mode-label": "Sıkıştırılmış mod", + "hide-mode-description": "Ortam çalınmadığında araç takımının nasıl davrandığını kontrol eder.", + "max-width-description": "Araç takımının en fazla yatay boyutunu ayarlar. Araç takımı daha kısa içerik için küçülecektir.", + "no-active-player": "Etkin çalar yok", + "panel-section-description": "Medya oynatıcı panelinin görünümünü ve davranışını yapılandırın.", + "panel-section-label": "Medya oynatıcı paneli", + "scrolling-mode-description": "Uzun parça başlıkları için metin kaydırmanın ne zaman etkinleştirileceğini kontrol et.", + "scrolling-mode-label": "Kaydırma modu", + "show-album-art-description": "Şu anda çalan parçanın albüm kapağını göster.", + "show-album-art-label": "Albüm kapağını göster", + "show-artist-first-description": "Sanatçı - başlık yerine başlık - sanatçı olarak göster.", + "show-artist-first-label": "Önce sanatçıyı göster", + "show-progress-ring-description": "Parça ilerlemesini gösteren dairesel bir ilerleme göstergesi gösterin.", + "show-progress-ring-label": "İlerleme halkası göster", + "show-visualizer-description": "Müzik çalarken bir ses görselleştirici göster.", + "show-visualizer-label": "Görselleştiriciyi göster", + "use-fixed-width-description": "Etkinleştirildiğinde, araç takımını hareketli olarak içeriğe göre ayarlamak yerine her zaman en fazla genişliği kullanır.", + "use-fixed-width-label": "Sabit genişlik kullan", + "visualizer-type-description": "Gösterilecek ses görselleştirici biçemini seçin.", + "visualizer-type-label": "Görselleştirici türü" + }, + "notification-history": { + "hide-widget-when-zero-description": "Bildirim yokken bildirim simgesini gizle.", + "hide-widget-when-zero-label": "Bildirim yokken simgeyi gizle", + "hide-widget-when-zero-unread-description": "Okunmamış bildirim yokken bildirim simgesini gizle.", + "hide-widget-when-zero-unread-label": "Okunmamış yokken simgeyi gizle", + "show-unread-badge-description": "Okunmamış bildirim sayısını gösteren bir rozet göster.", + "show-unread-badge-label": "Okunmamış bildirimleri göster", + "unread-badge-color-description": "Okunmamış bildirim rozeti için rengi seçin.", + "unread-badge-color-label": "Okunmamış rozet rengi" + }, + "section-editor": { + "placeholder": "Eklenecek bir araç takımı seçin...", + "search-placeholder": "Araç takımı ara..." + }, + "spacer": { + "width-description": "Piksel olarak aralık genişliği." + }, + "system-monitor": { + "compact-mode-description": "İstatistikleri metin değerleri yerine mini çubuk grafikler olarak görüntüleyin. Düzen kaymasını önler.", + "compact-mode-label": "Kompakt mod", + "cpu-cores-description": "CPU çekirdeklerinin kullanımını ayrı ayrı görüntüle.", + "cpu-cores-label": "CPU Çekirdekleri", + "cpu-frequency-description": "Mevcut CPU saat hızını GHz cinsinden görüntüle.", + "cpu-frequency-label": "CPU frekansını göster", + "cpu-temperature-description": "Mevcut işlemci sıcaklık okumalarını gösterir.", + "cpu-temperature-label": "İşlemci sıcaklığı", + "cpu-usage-description": "Mevcut işlemci kullanım yüzdesini göster.", + "cpu-usage-label": "İşlemci kullanımı", + "disk-path-description": "İzlenecek disk bağlama noktasını seçin.", + "disk-path-label": "Disk yolu", + "gpu-temperature-description": "GPU sıcaklık değerleri varsa göster.", + "load-average-description": "Sistem ortalama yükünü göster.", + "load-average-label": "Ortalama yük", + "memory-percentage-description": "Mutlak değerler yerine bellek kullanımını yüzde olarak göster.", + "memory-percentage-label": "Bellek yüzde olarak", + "memory-usage-description": "Mevcut bellek kullanım bilgilerini göster.", + "memory-usage-label": "Bellek kullanımı", + "network-traffic-description": "Ağ yükleme ve indirme hızlarını göster.", + "network-traffic-label": "Ağ trafiği", + "storage-as-percentage-description": "Disk alanını mutlak değerler yerine yüzde olarak göster.", + "storage-as-percentage-label": "Disk yüzde olarak", + "storage-available-description": "Kullanılan miktar yerine ne kadar disk alanı olduğunu gösterir.", + "storage-available-label": "Kullanılabilir disk alanı", + "storage-usage-description": "Disk alanı kullanım bilgilerini göster.", + "storage-usage-label": "Depolama kullanımı", + "swap-usage-description": "Takas belleği kullanımını göster.", + "swap-usage-label": "Takas kullanımı", + "use-monospace-font-description": "Tekdüze karakter genişliği için tek aralıklı yazı tipini kullanın.", + "use-monospace-font-label": "Tek aralıklı yazı tipi", + "use-padding-description": "Düzen kaymasını önlemek için metin değerlerini baştaki boşluklarla doldurur.", + "use-padding-description-disabled-monospace-font": "Bu özellik için tek aralıklı bir yazı tipi gereklidir.", + "use-padding-description-disabled-vertical": "Dikey görev çubuğu metin dolgusunu desteklemez.", + "use-padding-label": "Metin Dolgusu" + }, + "taskbar": { + "colorize-icons-description": "Görev çubuğu simgelerine tema renklerini uygula.", + "hide-mode-description": "Eşleşen pencere olmadığında araç takımının nasıl davrandığını kontrol eder.", + "hide-mode-label": "Gizleme modu", + "icon-scale-description": "Görev çubuğu simgeleri için ölçeklendirme faktörünü ayarlar.", + "icon-scale-label": "Simge ölçeklendirme", + "max-width-description": "Görev çubuğunun ekran genişliğine göre maksimum genişliği yüzdesi.", + "max-width-label": "Maksimum genişlik", + "only-active-workspaces-description": "Sadece aktif çalışma alanlarından uygulamaları göster.", + "only-active-workspaces-label": "Sadece aktif çalışma alanlarından", + "only-same-monitor-description": "Çubuğun bulunduğu monitördeki uygulamaları göster.", + "only-same-monitor-label": "Aynı monitörden sadece", + "show-pinned-apps-description": "Dock'taki sabitlenmiş uygulamaları görev çubuğunda göster.", + "show-pinned-apps-label": "Sabitlenmiş uygulamaları göster", + "show-title-description": "Görev çubuğunda pencere başlıklarını göster.", + "show-title-description-disabled": "Dikey görev çubuğu başlık göstermeyi desteklemiyor.", + "show-title-label": "Başlığı göster", + "smart-width-description": "Girdi sayısına göre girdi genişliğini otomatik olarak ayarla.", + "smart-width-label": "Akıllı genişlik", + "title-width-description": "Görev çubuğundaki pencere başlıklarının genişliğini ayarla (piksel olarak).", + "title-width-label": "Başlık genişliği", + "title-width-reset-tooltip": "Başlık genişliğini sıfırla" + }, + "tray": { + "chevron-color-description": "Çekmecenin ok simgesine tema renklerini uygula.", + "chevron-color-label": "Ok rengi", + "colorize-icons-description": "Tepsi simgelerine tema renklerini uygula.", + "colorize-icons-label": "Simgeleri renklendir", + "drawer-enabled-description": "Etkinleştirildiğinde, sabitlenmemiş tepsi öğeleri bir çekmece panelinde gösterilir.
Devre dışı bırakıldığında, tüm tepsi öğeleri satır içi gösterilir.", + "drawer-enabled-label": "Çekmeceyi etkinleştir", + "hide-passive-description": "Etkinleştirildiğinde, 'Pasif' durumdaki tepsi öğeleri gizlenir.", + "hide-passive-label": "Pasif öğeleri gizle" + }, + "volume": { + "display-mode-description": "Bu değerin nasıl görünmesini istediğinizi seçin." + }, + "workspace": { + "character-count-description": "Çalışma alanı adlarından gösterilecek karakter sayısı (1-10).", + "character-count-label": "Karakter sayısı", + "empty-color-description": "Boş Workspace'ler için arka plan rengini ayarla.", + "empty-color-label": "Boş çalışma alanı rengi", + "enable-scrollwheel-description": "Fare tekerleği ile çalışma alanları arasında geçiş yapın.", + "enable-scrollwheel-label": "Çalışma alanları arasında geçiş yapmak için kaydırın", + "focused-color-description": "Odaklanılan Workspace için arka plan rengini ayarla.", + "focused-color-label": "Odaklanılan çalışma alanı rengi", + "follow-focused-screen-description": "Çubuğun bulunduğu ekran yerine, şu anda odaklanmış ekrandaki çalışma alanlarını göster.", + "follow-focused-screen-label": "Odaklanmış ekranı takip et", + "font-weight-description": "Çalışma alanı içindeki metin için görsel ağırlığı ayarlayın.", + "font-weight-label": "Yazı tipi ağırlığı", + "grouped-border-opacity-description": "Çalışma alanı kapsayıcı kenarlıklarının opaklık düzeyini ayarlayın.", + "grouped-border-opacity-label": "Kenarlık opaklığı", + "hide-unoccupied-description": "Penceresi olmayan çalışma alanlarını gösterme.", + "hide-unoccupied-label": "Dolu olmayanları gizle", + "label-mode-description": "Çalışma alanı etiketlerinin nasıl gösterileceğini seçin.", + "label-mode-label": "Etiket Modu", + "occupied-color-description": "Dolu Workspaces için arka plan rengini ayarla.", + "occupied-color-label": "Dolu çalışma alanı rengi", + "pill-size-description": "Çalışma alanı haplarının boyutunu ayarlayın (50%-100%).", + "pill-size-label": "Kapsül boyutu", + "reverse-scrolling-description": "Kaydırırken çalışma alanı geçiş yönünü tersine çevir.", + "reverse-scrolling-label": "Ters kaydırma", + "show-applications-description": "Her çalışma alanının içinde uygulama simgelerini görüntüle.", + "show-applications-hover-description": "Çalışma alanının üzerine gelindiğinde, her çalışma alanının içinde uygulama simgelerini görüntüle.", + "show-applications-hover-label": "Üzerine gelindiğinde uygulamaları göster", + "show-applications-label": "Uygulamaları göster", + "show-badge-description": "Gruplandırılmış modda çalışma alanı numarası işaretini göster.", + "show-badge-label": "Çalışma alanı rozetini göster", + "show-labels-only-when-occupied-description": "Yalnızca pencere içeren çalışma alanı etiketlerini göster.", + "show-labels-only-when-occupied-label": "Yalnızca dolu olduğunda etiketleri göster", + "unfocused-icons-opacity-description": "Odaklanılmamış uygulama simgelerinin opaklık düzeyini ayarlayın.", + "unfocused-icons-opacity-label": "Odak dışı simgelerin opaklığı" + } + }, + "battery": { + "all-batteries": "Tüm bataryalar (birleşik)", + "battery-health": "Pil sağlığı", + "battery-level": "Pil seviyesi", + "capacity-level": "Kapasite: {level}", + "charging-rate": "Şarj oranı: {rate} W", + "discharging-rate": "Deşarj oranı: {rate} W", + "health": "Sağlık: {percent}%", + "inhibit-idle-description": "Sistemi uyanık tutar.", + "no-battery-detected": "Pil tespit edilmedi", + "plugged-in": "Prize takılı", + "power-profile": "Güç profili", + "time-left": "Kalan süre: {time}", + "time-until-full": "Dolma süresi: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Mevcut cihazlar", + "blocked": "Engellendi", + "connected-devices": "Bağlı cihazlar", + "device-address": "Cihaz adresi", + "disabled": "Bluetooth devre dışı", + "discoverable": "Keşfedilebilir", + "enable-message": "Mevcut cihazları görmek için Bluetooth'u etkinleştirin.", + "known-devices": "Bilinen cihazlar", + "no-devices": "Mevcut cihaz yok", + "paired-devices": "Eşleştirilmiş cihazlar", + "pairing-mode": "Cihazınızın eşleştirme modunda olduğundan emin olun.", + "scanning": "Cihazlar tarınıyor...", + "signal-text-excellent": "Sinyal: Mükemmel", + "signal-text-fair": "Sinyal: Adil", + "signal-text-good": "Sinyal: İyi", + "signal-text-poor": "Sinyal: Zayıf", + "signal-text-unknown": "Sinyal: Bilinmiyor", + "signal-text-very-poor": "Sinyal: Çok zayıf" + } + }, + "changelog": { + "error": { + "fetch-failed": "Değişiklik günlüğü verileri yüklenemedi. Lütfen daha sonra tekrar dene.", + "rate-limit": "GitHub sınırına ulaşıldı. Lütfen birkaç dakika sonra tekrar dene." + }, + "panel": { + "buttons-discord": "Discord sunucumuza katıl", + "buttons-dismiss": "Tamam", + "buttons-feedback": "Geri bildirim verin", + "empty": "Sürüm notları henüz hazır değil.", + "highlight-title": "Öne çıkanlar", + "released": "{date} tarihinde yayımlandı", + "subtitle-fresh": "Noctalia’yı kurduğun için teşekkürler! Bu sürümde gelenler bunlar.", + "subtitle-updated": "{previousVersion} sürümünden güncellendi", + "title": "{version} sürümünde neler yeni", + "version": "Sürüm {version}", + "version-new-user": "Yeni kurulum" + } + }, + "common": { + "actions": "Eylemler", + "active": "Aktif", + "add": "Ekle", + "appearance": "Görünüm", + "apply": "Uygula", + "auto-connect": "Otomatik bağlan", + "automation": "Otomasyon", + "available": "Mevcut", + "back": "Geri", + "battery": "Pil", + "behavior": "Davranış", + "bluetooth": "Bluetooth", + "brightness": "Parlaklık", + "browse": "Göz at", + "calendar": "Takvim", + "calendar-panel": "Takvim paneli", + "cancel": "İptal", + "cards": "Kartlar", + "charging": "Şarj etme", + "clear": "Temizle", + "clipboard": "Panoya", + "close": "Kapat", + "color-muted": "Sessiz", + "colors": "Renkler", + "command": "Komut", + "confirm": "Onayla", + "connect": "Bağlan", + "connected": "Bağlı", + "connecting": "Bağlanıyor...", + "continue": "Devam Et", + "contributors": "Katkıda bulunanlar", + "copied-to-clipboard": "Panoya kopyalandı", + "countdown": "Geri sayım", + "customize": "Özelleştir", + "date": "Tarih", + "default": "Varsayılan", + "delete": "Sil", + "devices": "Cihazlar", + "disabled": "Devre dışı", + "discharging": "Taburcu etme", + "disconnect": "Bağlantıyı kes", + "disconnected": "Bağlantı kesildi", + "disconnecting": "Bağlantı kesiliyor...", + "display-mode": "Görüntü modu", + "documentation": "Dokümantasyon", + "download": "İndir", + "duration": "Süre", + "dysfunctional": "İşlevsiz", + "edit": "Düzenle", + "enabled": "Etkinleştirildi", + "error": "Hata", + "ethernet": "Ethernet", + "events": "Etkinlikler", + "execute": "Yürüt", + "faithful": "Sadık", + "focus": "Odaklanma", + "font-weight-bold": "Kalın", + "font-weight-light": "İnce", + "font-weight-medium": "Orta", + "font-weight-regular": "Normal", + "font-weight-semibold": "Yarı kalın", + "frequency": "Sıklık", + "gateway": "Geçit", + "general": "Genel", + "height": "Yükseklik", + "hibernate": "Hazırda beklet", + "history": "Tarih", + "icon": "Simge", + "idle": "Boşta", + "info": "Bilgi", + "input": "Giriş", + "install": "Kurulum", + "installed": "Yüklendi", + "interface": "Arayüz", + "internet": "İnternet", + "keybind": "Tuş Ataması", + "language": "Dil", + "loading": "Yükleniyor...", + "local": "Yerel", + "location": "Konum", + "lock": "Kilitle", + "logout": "Çıkış yap", + "look": "Görünüm", + "margins": "Kenar boşlukları", + "media": "Medya", + "media-player": "Medya oynatıcı", + "memory": "Hafıza", + "monitors": "Monitörler", + "move-down": "Aşağı Taşı", + "move-up": "Yukarı taşı", + "network": "Ağ", + "next": "Sonraki", + "night-light": "Gece ışığını", + "no": "Hayır", + "no-results": "Sonuç bulunamadı", + "no-summary": "Özet yok", + "none": "Hiçbiri", + "not-found": "Bulunamadı", + "notifications": "Bildirimler", + "official": "Resmi", + "on-surface": "Yüzeyde", + "output": "Çıktı", + "pair": "Eşleştir", + "paired": "Eşleştirildi", + "pairing": "Eşleştiriliyor...", + "panels": "Paneller", + "password": "Şifre", + "pause": "Duraklat", + "performance": "Performans", + "pin": "Sabitle", + "play": "Oynat", + "polling": "Yoklama", + "position": "Pozisyon", + "previous": "Önceki", + "primary": "Birincil", + "random": "Rastgele", + "reboot": "Yeniden başlat", + "reboot-to-uefi": "UEFI'ye yeniden başlat", + "record": "Kaydı Başlat", + "refresh": "Yenile", + "required": "(gerekli)", + "reset": "Sıfırla", + "reset-to-default": "Varsayılanlara Sıfırla", + "result-count": "{count} sonuç", + "result-count-plural": "{count} sonuçlar", + "resume": "Sürdür", + "save": "Kaydet", + "scanning": "Taranıyor...", + "screen-corners": "Ekran köşeleri", + "search": "Arama", + "second": "{count} saniye", + "second-plural": "{count} saniye", + "secondary": "İkincil", + "security": "Güvenlik", + "select": "Seç", + "select-color": "Renk Seç", + "select-color-description": "Vurgu için tema renklerini uygula.", + "select-icon-color": "Simge rengini seçin", + "settings": "Ayarlar", + "shortcuts": "Kısayollar", + "shutdown": "Kapat", + "signal": "Sinyal", + "sound": "Ses", + "sources": "Kaynaklar", + "start": "Başlat", + "stop": "Dur", + "supporters": "Destekçiler", + "suspend": "Askıya al", + "templates": "Şablonlar", + "tertiary": "Üçüncül", + "test": "Test", + "thresholds": "Eşikler", + "title": "Başlık", + "toast": "Toast", + "trusted": "Güvenilir", + "uninstall": "Kaldır", + "unknown": "Bilinmiyor", + "unpair": "Eşleştirmeyi kaldır", + "unpin": "Sabitlemeyi kaldır", + "update": "Güncelleme", + "upload": "Yükle", + "username": "Kullanıcı Adı", + "userspace-reboot": "Kullanıcı alanını yeniden başlat", + "version": "Sürüm", + "vibrant": "Canlı", + "visualizer": "Görselleştirici", + "volume": "Hacim", + "volumes": "Hacimler", + "wallpaper": "Duvar kağıdı", + "weather": "Hava durumu", + "weather-loading": "Hava durumu yükleniyor...", + "weather-no-location": "Hava durumunu görmek için Ayarlar'da bir konum belirleyin", + "week": "Hafta", + "widgets": "Araçlar", + "width": "Genişlik", + "wifi": "Wi-Fi", + "windows": "Pencereler", + "yes": "Evet" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Güç profili", + "tooltip-disabled": "Güç profillerini kullanmak için power-profiles-daemon kur" + }, + "wifi": { + "label-disconnected": "Kablosuz bağlantı kesik", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Her zaman saklan", + "always-show": "Daima göster", + "force-open": "Zorla açmak", + "on-hover": "Üzerine gelindiğinde" + }, + "hide-modes": { + "auto-hide": "Otomatik gizleme", + "hidden": "Boş olduğunda gizle", + "idle": "Boşta kalınca gizle", + "non-exclusive": "Münhasır olmayan", + "transparent": "Boşken şeffaf", + "visible": "Her zaman görünür" + }, + "launcher": { + "categories": { + "all": "Tümü", + "audiovideo": "Ses ve görüntü", + "chat": "Sohbet", + "development": "Geliştirme", + "education": "Eğitim", + "emoji-activity": "Aktivite", + "emoji-animals": "Hayvanlar", + "emoji-flags": "Bayraklar", + "emoji-food": "Yiyecek ve içecek", + "emoji-nature": "Doğa", + "emoji-objects": "Nesneler", + "emoji-people": "İnsanlar ve vücut", + "emoji-recent": "Son kullanılanlar", + "emoji-symbols": "Semboller", + "emoji-travel": "Seyahat ve yerler", + "game": "Oyunlar", + "graphics": "Grafikler", + "misc": "Çeşitli", + "office": "Ofis", + "pinned": "Sabitlendi", + "system": "Sistem", + "webbrowser": "Tarayıcı" + }, + "providers": { + "applications": "Uygulamalar", + "calculator": "Hesap makinesi", + "calculator-deprecated": ">calc komutu kullanımdan kaldırılmıştır ve yakında kaldırılacaktır. Lütfen matematiksel ifadeleri doğrudan aramaya yazın.", + "calculator-description": "Hesap makinesi — matematiksel ifadeleri hesapla", + "calculator-enter-expression": "Matematiksel bir ifade girin", + "calculator-press-enter-to-copy": "Sonucu kopyalamak için Enter'a basın", + "clipboard": "Panoya geçmiş", + "clipboard-clear-description": "Tüm panoya geçmişini temizle", + "clipboard-clear-description-full": "Tüm öğeleri panoya geçmişinden kaldır", + "clipboard-clear-history": "Panoya geçmişini temizle", + "clipboard-delete": "Panodaki öğeyi sil", + "clipboard-history-disabled": "Panoya geçmişi devre dışı", + "clipboard-history-disabled-description": "Panoya geçmişini ayarlarda etkinleştir veya cliphist kur", + "clipboard-loading": "Panoya geçmişi yükleniyor...", + "clipboard-search-description": "Panoya geçmişini ara", + "command-description": "Kabuk komutlarını çalıştır", + "emoji": "Emoji seçici", + "emoji-loading": "Emojiler yükleniyor...", + "emoji-loading-description": "Lütfen bekleyin", + "emoji-no-recent": "Henüz yeni emoji yok", + "emoji-search-description": "Emoji arama ve kopyalama", + "settings-search-description": "Ayarları ara ve git", + "windows-search-description": "Açık pencereleri ara ve odakla" + } + }, + "lock-screen": { + "authenticating": "Doğrulanıyor...", + "password": "Parolanızı girin...", + "restart": "Yeniden başlat", + "shut-down": "Kapat" + }, + "notifications": { + "panel": { + "click-to-collapse": "Daraltmak için tıklayın", + "click-to-expand": "Genişletmek için tıklayın", + "description": "Bildirimler geldikçe burada görünecek.", + "no-notifications": "Bildirim yok" + }, + "range": { + "earlier": "Öncekiler", + "today": "Bugün", + "yesterday": "Dün" + }, + "time": { + "diff-d": "1 gün önce", + "diff-dd": "{diff} gün önce", + "diff-h": "1 saat önce", + "diff-hh": "{diff} saat önce", + "diff-m": "1 dakika önce", + "diff-mm": "{diff} dakika önce", + "now": "şimdi" + } + }, + "options": { + "bar": { + "density-comfortable": "Rahat", + "density-compact": "Sıkı", + "density-default": "Varsayılan", + "density-mini": "Ufak", + "density-spacious": "Geniş", + "display-mode-always-visible": "Her Zaman Görünür", + "type-floating": "Yüzen", + "type-framed": "Çerçeveli", + "type-simple": "Basit" + }, + "control-center": { + "quick-settings-style-classic": "Klasik", + "quick-settings-style-modern": "Modern" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "Konforlu", + "compact": "Kompakt", + "default": "Varsayılan" + }, + "launcher-view-mode": { + "grid": "Izgara", + "list": "Liste" + }, + "notification-density": { + "compact": "Kompakt", + "default": "Varsayılan" + }, + "scrolling-modes": { + "always": "Her zaman kaydır", + "hover": "Üzerine gelince kaydır", + "never": "Asla kaydırma" + }, + "session-menu-grid-layout": { + "grid": "Izgara", + "single-row": "Tek sıra" + }, + "settings-panel-mode": { + "attached": "Panel çubuğa takılı", + "centered": "Ortalanmış panel", + "window": "Ayrı pencere" + }, + "visualizer-types": { + "linear": "Doğrusal", + "mirrored": "Yansıtılmış", + "wave": "Dalga" + }, + "visualizer-visibility": { + "with-background": "Sadece arka planla" + }, + "workspace-labels": { + "index": "İndeks", + "index-and-name": "Dizin ve ad", + "name": "İsim" + } + }, + "panels": { + "about": { + "become-supporter": "Destekçi ol", + "changelog": "Değişiklik günlüğünü görüntüle", + "changelog-on-startup": "Güncellemede değişiklik günlüğünü göster", + "changelog-on-startup-desc": "Noctalia güncellendiğinde değişiklik günlüğünü otomatik olarak göster.", + "contributors-description": "{count} harika katılımcımıza teşekkürler!", + "contributors-description-plural": "{count} harika katılımcımıza teşekkürler!", + "copy-info": "Bilgileri kopyala", + "debug-disabled": "Hata ayıklama modu devre dışı bırakıldı", + "debug-enabled": "Hata ayıklama modu etkinleştirildi", + "info-copied": "Bilgi panoya kopyalandı", + "noctalia-available": "Mevcut:", + "noctalia-desc": "Wayland için düşünülmüş, Quickshell ile inşa edilmiş şık ve minimal masaüstü kabuğu.", + "noctalia-git-commit": "Git commit:", + "noctalia-installed-version": "Yüklü sürüm:", + "noctalia-latest-version": "En son sürüm:", + "noctalia-qs-version": "Noctalia QS sürümü:", + "noctalia-title": "Noctalia kabuğu", + "privacy-policy": "Gizlilik politikası", + "support": "Bizi destekleyin", + "supporter-badge": "Destekçi", + "supporters-desc": "Harika destekçimize kocaman bir teşekkür!", + "supporters-desc-plural": "{count} harika destekçimize kocaman teşekkürler!", + "supporters-loading": "Destekçiler yükleniyor...", + "system-board": "Anakart:", + "system-cpu": "İşlemci:", + "system-disk": "Disk:", + "system-gpu": "GPU:", + "system-host": "Host:", + "system-install-hint": "Sistem bilgilerini görüntülemek için fastfetch'i kurun", + "system-kernel": "Çekirdek:", + "system-loading": "Sistem bilgileri yükleniyor...", + "system-memory": "Hafıza:", + "system-monitor": "Monitör:", + "system-not-installed": "fastfetch kurulu değil", + "system-os": "İşletim sistemi:", + "system-packages": "Paketler:", + "system-product": "Ürün:", + "system-title": "Sistem Bilgisi", + "system-uptime": "Çalışma süresi:", + "system-wm": "WM:", + "telemetry-data-copied": "Telemetri verileri panoya kopyalandı.", + "telemetry-desc": "Anonim sistem bilgisi (ekran çözünürlüğü, kompozitör, dağıtım) paylaşarak Noctalia'yı geliştirmeye yardımcı olun. Başlangıçta bir kez gönderilir, izleme yapılmaz, veriler 30 gün sonra otomatik olarak silinir.", + "telemetry-enabled": "Anonim sistem bilgisi gönder.", + "telemetry-show-data": "Verileri görüntüle", + "telemetry-title": "Gizlilik", + "title": "Hakkında", + "up-to-date": "Güncelsiniz!", + "update-available": "Güncelleme mevcut", + "view-commit": "GitHub'da taahhüdü görüntüle" + }, + "audio": { + "devices-desc": "Mevcut ses giriş ve çıkış cihazlarını yapılandırın.", + "devices-input-device-description": "İstenilen ses giriş cihazını seçin.", + "devices-input-device-label": "Giriş cihazı", + "devices-output-device-description": "İstenilen ses çıkış cihazını seçin.", + "devices-output-device-label": "Çıkış cihazı", + "devices-title": "Ses cihazları", + "external-mixer-description": "Harici ses karıştırıcısı özelliği etkinleştirilirken başlatılacak komutu veya uygulama yolunu girin.", + "external-mixer-label": "Harici ses karıştırıcısı komutu", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Tercih ettiğiniz ve yoksayacağınız ortam uygulamalarını ayarlayın.", + "media-excluded-player-description": "Sistemin yoksaymasını istediğiniz oynatıcılar için anahtar kelimeler ekleyin. Her anahtar kelime ayrı bir satırda olmalıdır.", + "media-excluded-player-label": "Dışlanan oynatıcı", + "media-excluded-player-placeholder": "alt dize girin ve + tuşuna basın", + "media-frame-rate-description": "Daha yüksek hızlar daha yumuşaktır ancak daha fazla kaynak kullanır.", + "media-frame-rate-label": "Kare hızı", + "media-primary-player-description": "Ana oynatıcınızı belirlemek için bir anahtar kelime girin.", + "media-primary-player-label": "Ana oynatıcı", + "media-primary-player-placeholder": "örn. spotify, vlc, mpv", + "media-scrolling-speed-description": "Başlığın baştan sona kayması için saniye cinsinden süre.", + "media-scrolling-speed-label": "Kaydırma hızı", + "media-scrolling-title-description": "Uzun ortam başlıkları için sürekli kaydırmayı etkinleştirin.", + "media-scrolling-title-label": "Kayan başlık", + "media-title": "Ortam oynatıcıları", + "on-middle-clicked-description": "Düğmeye orta tıklama yapıldığında yürütülecek komut.", + "panel-applications-empty": "Şu anda ses çalan uygulama yok", + "spectrum-mirrored-description": "Spektrumu yansıt, böylece düşük frekanslar merkezde olur. Kapalıyken, düşük frekanslar solda ve yüksek frekanslar sağda olur.", + "spectrum-mirrored-label": "Spektrum aynalama", + "title": "Ses", + "visualizer-type-description": "Medya oynatma için bir görselleştirme türü seçin.", + "visualizer-type-label": "Görselleştirme türü", + "volumes-desc": "Ses kontrollerini ve ses seviyelerini ayarlayın.", + "volumes-feedback-sound-file-description": "Ses seviyesi değiştirilirken çalınan ses dosyasının yolu.", + "volumes-feedback-sound-file-label": "Ses geri bildirim ses dosyası", + "volumes-feedback-sound-file-select-title": "Ses geri bildirimi ses dosyası seç", + "volumes-input-volume-description": "Mikrofon giriş ses seviyesi.", + "volumes-mute-input-description": "Varsayılan ses girişini (mikrofon) sessize al.", + "volumes-mute-input-label": "Ses girişini sessize al", + "volumes-mute-output-description": "Sistemin ana ses çıkışını sessize al.", + "volumes-mute-output-label": "Ses çıkışını sessize al", + "volumes-output-volume-description": "Sistem genelindeki ses seviyesi.", + "volumes-step-size-description": "Ses değişimleri için adım boyutunu ayarlayın (tekerlek, klavye kısayolları).", + "volumes-step-size-label": "Ses adım boyutu", + "volumes-volume-feedback-description": "Ses seviyesi ayarlanırken geri bildirim sesi çal.", + "volumes-volume-feedback-label": "Ses düzeyi geri bildirim sesini çal", + "volumes-volume-overdrive-description": "Sesi %100'ün üzerine çıkarmaya izin ver. Bazı donanımlar desteklemeyebilir.", + "volumes-volume-overdrive-label": "Ses aşımına izin ver" + }, + "bar": { + "appearance-auto-hide-delay-description": "Fare ayrıldıktan sonra Bar'ın gizlenmeden önceki süre.", + "appearance-auto-hide-delay-label": "Gizleme Gecikmesi", + "appearance-auto-hide-exclusive-note": "Not: Otomatik gizleme etkin olduğunda özel bölge otomatik olarak devre dışı bırakılır.", + "appearance-auto-show-delay-description": "Fare kenara girdiğinde çubuğun görünmesinden önceki süre.", + "appearance-auto-show-delay-label": "Gösterme Gecikmesi", + "appearance-background-opacity-description": "Çubuğun arka plan opaklığını özel olarak ayarlayın.", + "appearance-background-opacity-label": "Çubuk arka plan opaklığı", + "appearance-capsule-color-description": "Çubuk kapsülleri için bir renk seçin veya varsayılan yüzey rengi için hiçbirini kullanmayın.", + "appearance-capsule-color-label": "Kapsül rengi", + "appearance-capsule-opacity-description": "Kapsül gösterildiğinde araç takımı arka planlarının saydamlık seviyesini ayarla.", + "appearance-capsule-opacity-label": "Kapsül saydamlığı", + "appearance-content-padding-description": "Çubuk kenarları ile widget'lar arasındaki dolguyu ayarlayın.", + "appearance-content-padding-label": "İçerik dolgusu", + "appearance-density-description": "Sıkı veya geniş bir görünüm için araç çubuğu dolgusunu ayarlayın.", + "appearance-density-label": "Araç Çubuğu yoğunluğu", + "appearance-desc": "Araç çubuğunun görünümünü ve konumunu özelleştirin.", + "appearance-display-mode-description": "Çubuğun ne zaman görünür olacağını seçin.", + "appearance-enable-exclusion-zone-inset-description": "Hariç tutma bölgesini 1 fiziksel piksel azaltın, böylece araç çubuğu kenarının altına tam oturan pencereler kusursuzca taşar.", + "appearance-enable-exclusion-zone-inset-label": "Girintili dışlama bölgesi", + "appearance-floating-description": "Araç çubuğunu yüzen bir 'kapsül' olarak görüntüler. Not: Bu, ekran köşelerini kenarlara taşıyacaktır.", + "appearance-floating-label": "Yüzen araç çubuğu", + "appearance-font-scale-description": "Çubukta görüntülenen metin için yazı tipi boyutu ölçeğini ayarlayın.", + "appearance-font-scale-label": "Yazı tipi ölçeği", + "appearance-frame-radius": "İç Yarıçap", + "appearance-frame-settings-description": "Çerçeve kalınlığını ve iç köşe yarıçapını ayarlayın", + "appearance-frame-settings-label": "Çerçeve Ayarları", + "appearance-frame-thickness": "Kalınlık", + "appearance-hide-on-overview-description": "Kompozitör önizlemesi aktif olduğunda çubuğu gizle ve panelleri kapat.", + "appearance-hide-on-overview-label": "Genel bakışta çubuğu gizle", + "appearance-margins-description": "Yüzen araç çubuğunun etrafındaki kenar boşluklarını ayarlayın.", + "appearance-margins-horizontal": "Yatay kenar boşluğu", + "appearance-margins-vertical": "Dikey kenar boşluğu", + "appearance-outer-corners-description": "Çubuğun dışa doğru kavisli köşelerini görüntüler.", + "appearance-outer-corners-label": "Dış köşeler", + "appearance-position-description": "Araç çubuğunu ekranda nereye yerleştireceğinizi seçin.", + "appearance-position-label": "Araç çubuğu konumu", + "appearance-show-capsule-description": "Araç takımı arka planlarını göster.", + "appearance-show-capsule-label": "Kapsülü göster", + "appearance-show-on-workspace-switch-description": "Çalışma alanı değiştiğinde Bar'ı otomatik olarak kısa bir süre göster.", + "appearance-show-on-workspace-switch-label": "Çalışma alanı değiştirildiğinde çubuğu göster", + "appearance-show-outline-description": "Her widget'ın etrafında görünür bir kenarlık görüntüler.", + "appearance-show-outline-label": "Araç kutusu ana hatlarını göster", + "appearance-type-description": "Çubuk stilini seçin: Basit, Yüzen veya Çerçeveli.", + "appearance-type-label": "Çubuk türü", + "appearance-use-separate-opacity-description": "Çubuk arka planı için ayrı bir opaklık değeri kullanmayı etkinleştir.", + "appearance-use-separate-opacity-label": "Ayrı çubuk opaklığı kullan", + "appearance-widget-spacing-description": "Çubuktaki her bir widget arasındaki boşluğu ayarlayın.", + "appearance-widget-spacing-label": "Widget aralığı", + "behavior-middle-click-command-description": "Orta tıklamada çalıştırılacak komut.", + "behavior-middle-click-command-label": "Orta Tıklama Komutu", + "behavior-middle-click-command-placeholder": "niri msg action genel bakışı değiştir", + "behavior-middle-click-description": "Çubuğun boş alanlarında orta tıklamanın ne yapacağını seçin.", + "behavior-middle-click-follow-mouse-description": "Seçilen orta tıklama panelini imleç konumunda aç.", + "behavior-middle-click-follow-mouse-label": "Orta tıklama fareyi takip etsin", + "behavior-middle-click-label": "Çubuk orta tıklama eylemi", + "behavior-right-click-command-description": "Sağ tıklamada çalıştırılacak komut.", + "behavior-right-click-command-label": "Sağ Tıklama Komutu", + "behavior-right-click-command-placeholder": "notify-send \"Sağ tıklama\"", + "behavior-right-click-description": "Çubuğun boş alanlarında sağ tıklamanın ne yapacağını seçin.", + "behavior-right-click-follow-mouse-description": "Seçili sağ tık panelini imleç konumunda aç.", + "behavior-right-click-follow-mouse-label": "Sağ tıklama fareyi takip etsin", + "behavior-right-click-label": "Çubuğa sağ tıklama eylemi", + "behavior-wheel-wrap-description": "Etkinleştirildiğinde, kaydırma son öğeden ilk öğeye doğru devam eder.", + "behavior-wheel-wrap-label": "Döngüsel", + "behavior-workspace-scroll-description": "Fare tekerleğinin çubuğun boş alanlarında ne yapacağını seçin.", + "behavior-workspace-scroll-label": "Çubuk fare tekerleği eylemi", + "behavior-workspace-scroll-option-content": "İçerik", + "behavior-workspace-scroll-option-workspace": "Çalışma Alanı", + "monitor-configure-widgets": "Araçları yapılandır", + "monitor-override-settings": "Genel ayarları geçersiz kıl", + "monitor-override-settings-description": "Bu monitör için özel ayarları kullan.", + "monitor-reset-all": "Hepsini sıfırla", + "monitor-widgets-title": "Widget Yapılandırması - {monitor}", + "monitors-desc": "Araç çubuğunu belirli ekranlarda gösterin. Hiçbiri seçilmezse varsayılan olarak tümünde gösterilir.", + "monitors-desc-new": "Çubuğun hangi monitörlerde görüntüleneceğini yapılandırın ve monitör başına ayarları özelleştirin.", + "monitors-title": "Ekranlar", + "title": "Araç çubuğu", + "tray-blacklist-description": "Tepsi hariç tutma kuralları ekleyin, joker karakterleri destekler (*).", + "tray-blacklist-label": "Kara liste", + "tray-blacklist-placeholder": "örn. nm-applet, Fcitx*", + "tray-pin-application": "Uygulamayı sabitle", + "tray-unpin-application": "Sabitlemeyi kaldır", + "use-global-widgets": "Genel araçları kullan", + "widgets-desc": "Araç takımları sıralarını değiştirmek için sürükleyin. Araç takımlarını bölümler arasında aktarmak veya kaldırmak için sağ tıklama menüsünü kullanın.", + "widgets-title": "Araç takımlarını konumlandırma" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Duvar kağıdınızdan renk şemaları oluşturun. Uyumlu bir tema oluşturmak için renkleri otomatik olarak çıkarır.", + "color-source-use-wallpaper-colors-label": "Duvar kâğıdı renklerini kullan", + "dark-mode-mode-description": "Işık ve koyu mod arasında otomatik geçişi etkinleştirir.", + "dark-mode-mode-label": "Koyu mod zamanlaması", + "dark-mode-mode-manual": "El ile", + "dark-mode-mode-off": "Kapalı", + "dark-mode-switch-description": "Gece daha kolay görüntülemek için daha koyu bir tema geçiş yapar.", + "delete-error-description": "{scheme} silinemedi", + "delete-error-title": "Silme başarısız", + "delete-success-description": "{scheme} başarıyla silindi", + "delete-success-title": "Renk şeması silindi", + "download-button": "Daha fazla indir", + "download-downloading": "İndiriliyor...", + "download-empty": "Mevcut renk şeması yok", + "download-error-api-error": "API hatası: {status}", + "download-error-description": "{scheme} indirilemedi", + "download-error-download-failed": "İndirme başarısız, çıkış kodu: {code}", + "download-error-invalid-response": "Geçersiz API yanıt formatı", + "download-error-no-files": "Şema için dosya bulunamadı", + "download-error-parse-failed": "API yanıtı ayrıştırılamadı: {error}", + "download-error-rate-limit": "GitHub API hız sınırı aşıldı", + "download-error-title": "İndirme başarısız", + "download-fetching": "Mevcut renk şemaları alınıyor...", + "download-success-description": "{scheme} başarıyla indirildi", + "download-success-title": "Renk şeması indirildi", + "download-title": "Renk şemalarını indir", + "method-description": { + "content": "Kaynak içeriğin gerçek renkleriyle yakından eşleşen, yüksek doğruluklu renk çıkarma özelliğine sahip Material Design şeması.", + "dysfunctional": "Faithful gibi, ancak ikinci en baskın renk ailesini birincil olarak seçer.", + "faithful": "Uyumlu bir palet oluştururken kaynak rengine yakın kalmaya çalışır.", + "fruit-salad": "Çeşitli ve farklı tonlara sahip, eğlenceli, canlı bir palet üreten Material Design şeması.", + "monochrome": "Minimal kromatik içerikli tek tonlu gri tonlamalı Material Design şeması.", + "muted": "Sakin ve mütevazı bir görünüm için daha yumuşak, doygunluğu azaltılmış tonlar oluşturur.", + "rainbow": "Maksimum renk çeşitliliği için tüm spektrumda renkler üreten Materyal Tasarımı şeması.", + "tonal-spot": "Varsayılan Material Design algoritması. Kaynak rengin ton varyasyonlarını kullanarak dengeli paletler oluşturur.", + "vibrant": "Canlı tonlara sahip, cesur, göz alıcı paletler için renk doygunluğunu en üst düzeye çıkarır." + }, + "predefined-desc": "Önceden tanımlanmış renk şemaları koleksiyonundan seçim yapın.", + "predefined-generate-templates-label": "Önceden tanımlanmış şemalar için şablonlar oluşturun", + "predefined-title": "Önceden tanımlanmış renk şemaları", + "sync-gsettings-description": "Sistem temasını aktif açık veya koyu varyantla eşleştir.", + "sync-gsettings-label": "Sistem temasını senkronize et", + "templates-desc": "Renkleri harici uygulamalara uygula.", + "templates-filter-description": "Belirli bir kategoriden şablonları göster.", + "templates-filter-label": "Kategoriye göre filtrele", + "templates-misc-description": "Kendi şablonlarınızı oluşturun.", + "templates-misc-label": "Gelişmiş", + "templates-misc-user-templates-description": "Yalnızca ne yaptığınızı biliyorsanız etkinleştirin — çevrimiçi belgelerimize bakın.", + "templates-misc-user-templates-label": "Kullanıcı şablonlarını etkinleştir", + "templates-none-detected": "Hiçbiri algılanmadı", + "templates-write-path": "Yazıyor: {filepath}", + "title": "Renk şeması", + "wallpaper-method-description": "Favori palet oluşturma yönteminizi seçin.", + "wallpaper-method-label": "Palet oluşturma yöntemi", + "wallpaper-monitor-source-description": "Duvar kağıdı renklerini çıkarmak için hangi monitörün kullanılacağını seçin.", + "wallpaper-monitor-source-label": "Renk oluşturma kaynağı" + }, + "connections": { + "authentication-required": "Kimlik doğrulama gerekli", + "bluetooth-auto-connect-description": "Bluetooth etkinleştirildiğinde güvenilir eşleştirilmiş cihazlara otomatik olarak bağlan.", + "bluetooth-auto-connect-label": "Eşleştirilmiş cihazları otomatik bağla", + "bluetooth-devices-unnamed": "Adlandırılmamış cihazlar gösterilmez.", + "bluetooth-discoverable": "Bu ayarlar sekmesi açıkken bu cihaz {hostName} olarak keşfedilebilir.", + "bluetooth-rssi-polling-description": "bluetoothctl aracılığıyla bağlı cihazların RSSI değerini periyodik olarak örnekler. Tüm cihazlar için mevcut olmayabilir; etkinleştirildiğinde minimum kaynak kullanır.", + "bluetooth-rssi-polling-interval-description": "Bağlı cihazlar için sinyal gücünün ne sıklıkta güncelleneceğini yapılandırın.", + "bluetooth-rssi-polling-interval-label": "Yoklama Aralığı", + "bluetooth-rssi-polling-label": "Bluetooth sinyal yoklaması", + "disable-discoverability-description": "Cihazınızı yakındaki Bluetooth cihazlarından gizleyin.", + "disable-discoverability-label": "Cihaz görünürlüğünü devre dışı bırak", + "hide-unnamed-devices-description": "Yalnızca Bluetooth adresleri olarak görünen cihazları gizle.", + "hide-unnamed-devices-label": "İsimsiz cihazları gizle", + "pin-instructions": "Lütfen cihazınızda gösterilen PIN kodunu girin.", + "title": "Bağlantılar", + "wifi-header-text": "Mevcut ağları görmek ve internete bağlanmak için Wi-Fi kullanın." + }, + "control-center": { + "cards-desc": "Kontrol merkezinde hangi kontrollerin ve hangi sırada görüneceğini özelleştirin.", + "desc": "Kontrol merkezi paneli konumunu ve davranışını yapılandırın.", + "open-at-mouse-description": "Bar'ın boş bir alanına sağ tıkladığınızda, Kontrol Merkezi'ni imlecin yakınında açın. Devre dışı bırakıldığında, bunun yerine yukarıdaki konum ayarı kullanılır.", + "open-at-mouse-label": "Sağ tıklamada imleç konumunda aç", + "position-description": "Kontrol merkezi paneli açıldığında nerede görüneceğini seçin.", + "shortcuts-custom-button-command-description": "Düğmeye tıklandığında çalıştırılacak komut.", + "shortcuts-custom-button-enable-on-state-logic-description": "Bir kontrol komutuna bağlı olarak ikinci bir simgeyi ve 'etkin' durumunu etkinleştir.", + "shortcuts-custom-button-enable-on-state-logic-label": "Açık durum mantığını etkinleştir", + "shortcuts-custom-button-icon-description": "Kütüphaneden bir simge seçin.", + "shortcuts-custom-button-on-clicked-label": "Sol tık komutu", + "shortcuts-custom-button-on-middle-clicked-description": "Düğmeye orta tıklama yapıldığında çalıştırılacak komut.", + "shortcuts-custom-button-on-state-command-description": "Düğmenin 'açık' durumda olup olmadığını kontrol etmek için çalıştırılacak komut. Açık için 0, kapalı için sıfır olmayan bir değer döndürür.", + "shortcuts-custom-button-on-state-command-label": "Durum kontrol komutu", + "shortcuts-custom-button-on-state-icon-description": "Düğme 'açık' durumdayken kullanılan simge.", + "shortcuts-custom-button-on-state-icon-label": "Durum simgesinde", + "shortcuts-custom-button-state-checks-add": "Durum kontrolü ekle", + "shortcuts-custom-button-state-checks-command": "Bu durum kontrolü için yürütülecek komut", + "shortcuts-custom-button-state-checks-label": "Durum kontrolleri", + "shortcuts-custom-button-state-checks-remove": "Kaldır", + "shortcuts-custom-button-tooltip-description": "Düğme üzerine gelindiğinde gösterilecek araç ipucu.", + "shortcuts-custom-button-tooltip-label": "Araç ipucu", + "shortcuts-title": "Kısayol araç takımları", + "system-monitor-disk-path-description": "Kontrol merkezindeki sistem izleyici kartının izlemesi gereken disk bağlama noktasını seçin.", + "system-monitor-disk-path-label": "Sistem izleyici disk yolu", + "title": "Kontrol merkezi" + }, + "desktop-widgets": { + "clock-enabled-description": "Masaüstünde bir saat aracı göster.", + "clock-enabled-label": "Saat widget'ını etkinleştir", + "clock-format-label": "Biçim", + "clock-height-description": "Saat aracı yüksekliği piksel cinsinden.", + "clock-height-label": "Yükseklik", + "clock-minimal-mode-description": "Zaman ve tarih içeren minimal bir saat görüntüsü kullanın.", + "clock-minimal-mode-label": "Minimal mod", + "clock-rounded-corners-description": "Widget arka planı için yuvarlatılmış köşeler kullanın.", + "clock-rounded-corners-label": "Yuvarlatılmış köşeler", + "clock-show-background-description": "Saat widget'ı için arka plan kabını göster.", + "clock-show-background-label": "Arka planı göster", + "clock-show-date-description": "Saati gösterdikten sonra, mevcut tarihi de göster.", + "clock-show-date-label": "Tarihi göster", + "clock-show-seconds-description": "Saniyeleri saatte göster.", + "clock-show-seconds-label": "Saniyeleri göster", + "clock-style-analog": "Analog", + "clock-style-binary": "İkili", + "clock-style-description": "Saat görüntüleme stilini seçin.", + "clock-style-digital": "Dijital", + "clock-style-label": "Saat stili", + "clock-style-minimal": "Minimal", + "clock-width-description": "Saat widget'ının piksel cinsinden genişliği.", + "cpu-intensive-note": "! ile işaretlenmiş widgetlar daha fazla CPU kullanır ve dikkatli bir şekilde etkinleştirilmelidir.", + "edit-mode-button-label": "Düzenleme moduna gir", + "edit-mode-controls-explanation": "Sol tıklayıp sürükleyin: Bileşeni taşıyın veya yeniden boyutlandırın.\nSağ tıklayın: Bağlam menüsü seçeneklerini açın.", + "edit-mode-description": "Masaüstü araçlarını taşımak ve yeniden konumlandırmak için düzenleme modunu etkinleştirin. Etkinleştirildiğinde, araçlar bir sürükleme ana hattı gösterir ve yeniden konumlandırılabilir.", + "edit-mode-exit-button": "Düzenleme modundan çık", + "edit-mode-grid-snap-label": "Izgaraya hizala", + "edit-mode-grid-snap-scale-label": "Izgara hizalama ölçeği", + "edit-mode-label": "Düzenleme modu", + "enabled-description": "Masaüstü araçlarını tamamen etkinleştir veya devre dışı bırak.", + "enabled-label": "Masaüstü araçlarını etkinleştir", + "general-desc": "Masaüstünüzde görünen araçları yapılandırın.", + "general-title": "Masaüstü araçları", + "media-player-enabled-description": "Masaüstünde bir medya oynatıcı aracı göster.", + "media-player-enabled-label": "Medya oynatıcı widget'ını etkinleştir", + "media-player-rounded-corners-description": "Widget kenarlarında yuvarlak köşeleri etkinleştir.", + "media-player-show-album-art-description": "Albüm kapağını ve parça bilgilerini (başlık ve sanatçı) göster.", + "media-player-show-album-art-label": "Albüm kapağını ve başlığını göster", + "media-player-show-background-description": "Medya oynatıcı widget'ı için arka plan konteynerini göster.", + "media-player-show-buttons-description": "Medya oynatıcı widget'ında medya kontrol düğmelerini (oynat/durdur, önceki, sonraki) göster.", + "media-player-show-buttons-label": "Düğmeleri göster", + "media-player-show-visualizer-description": "Ses görselleştirici katmanını göster.", + "media-player-visualizer-type-description": "Masaüstü medya oynatıcı arka planı için bir görselleştirme türü seçin.", + "overview-enabled-description": "Kompozitör genel görünümü açıkken masaüstü araç takımlarını göster.", + "overview-enabled-label": "Genel bakışta göster", + "system-stat-layout-bottom": "Alt", + "system-stat-layout-description": "Efsanenin grafiğe göre nasıl görüntüleneceğini seçin.", + "system-stat-layout-label": "Düzen", + "system-stat-layout-side": "Taraf", + "system-stat-rounded-corners-description": "Widget arka planı için yuvarlatılmış köşeler kullan.", + "system-stat-rounded-corners-label": "Yuvarlatılmış Köşeler", + "system-stat-show-background-description": "Sistem istatistik Widget'ı için arka plan kapsayıcısını göster.", + "system-stat-show-background-label": "Arka planı göster", + "system-stat-stat-type-description": "Hangi sistem istatistiğini görüntüleyeceğini seç.", + "system-stat-stat-type-label": "İstatistik Türü", + "title": "Masaüstü araçları", + "weather-enabled-description": "Masaüstünde bir hava durumu aracı göster.", + "weather-enabled-label": "Hava durumu araç kutusunu etkinleştir", + "weather-show-background-description": "Hava durumu widget'ı için arka plan konteynerini göster." + }, + "display": { + "monitors-backlight-device-auto-option": "Varsayılan", + "monitors-backlight-device-description": "Bu çıkış için bir arka ışık cihazı seçin.", + "monitors-backlight-device-label": "Arka Işık Cihazı", + "monitors-brightness-step-description": "Parlaklık değişimleri için adım boyutunu ayarlayın (tekerlek ve klavye kısayolları).", + "monitors-brightness-step-label": "Parlaklık adım boyutu", + "monitors-brightness-unavailable-ddc-disabled": "Parlaklık kontrolü kullanılamıyor. Bu ekranın parlaklığını kontrol etmek için \"Harici parlaklık desteği\"ni etkinleştirin.", + "monitors-brightness-unavailable-generic": "Bu ekran için parlaklık kontrolü mevcut değil.", + "monitors-desc": "Her ekran için parlaklık ayarlarını yapın.", + "monitors-enforce-minimum-description": "Bazı ekranlarda %0 parlaklıkta arka ışığın tamamen kapanması sorununu çözer.", + "monitors-enforce-minimum-label": "Minimum parlaklığı zorla (%1)", + "monitors-external-brightness-description": "Harici ekranlarda parlaklığı DDC/CI protokolü üzerinden kontrol etmek için DDCUtil desteğini etkinleştir.", + "monitors-external-brightness-label": "Harici parlaklık desteği", + "monitors-global-brightness-description": "Tüm monitörlerin parlaklığını aynı anda ayarla.", + "monitors-global-brightness-label": "Tüm monitörler", + "monitors-title": "Ekran başına ayarlar", + "night-light-auto-schedule-description": "{location} konumunda gün batımı ve gün doğumu zamanına göre — önerilen.", + "night-light-auto-schedule-label": "Otomatik zamanlama", + "night-light-desc": "Daha iyi uyumanıza ve göz yorgunluğunu azaltmanıza yardımcı olmak için mavi ışık yayılımını azaltın.", + "night-light-enable-description": "Mavi ışık yayılımını azaltmak için sıcak renk filtresi uygulayın.", + "night-light-enable-label": "Gece ışığını etkinleştir", + "night-light-force-activation-description": "Zamanlamayı yoksayar ve gece filtresini hemen uygular.", + "night-light-force-activation-label": "Zorla etkinleştir", + "night-light-manual-schedule-description": "Gün doğumu ve gün batımı için özel zamanlar ayarlayın.", + "night-light-manual-schedule-label": "Manuel zamanlama", + "night-light-manual-schedule-select-start": "Başlangıç zamanını seç", + "night-light-manual-schedule-select-stop": "Bitiş zamanını seç", + "night-light-manual-schedule-sunrise": "Gün doğumu zamanı", + "night-light-manual-schedule-sunset": "Gün batımı zamanı", + "night-light-temperature-day": "Gündüz", + "night-light-temperature-day-description": "Gündüz saatlerindeki renk sıcaklığını kontrol eder.", + "night-light-temperature-description": "Gece ve gündüz için renk sıcaklığını ayarlayın.", + "night-light-temperature-label": "Renk sıcaklığı", + "night-light-temperature-night": "Gece", + "night-light-temperature-night-description": "Gece saatlerindeki renk sıcaklığını kontrol eder.", + "title": "Görüntü" + }, + "dock": { + "appearance-background-opacity-description": "Dock’un arka plan opaklığını ayarlayın.", + "appearance-border-radius-description": "Dock’un kenar yarıçapını ayarlayın.", + "appearance-border-radius-label": "Kenar yarıçapı", + "appearance-colorize-icons-description": "Dock uygulama simgelerine tema renklerini uygulayın (sadece odaklanılmamış uygulamalar).", + "appearance-colorize-icons-label": "Simgeleri renklendir", + "appearance-dead-opacity-description": "Çalışmayan uygulama simgelerinin opaklığını ayarlayın.", + "appearance-dead-opacity-label": "Ölü opaklık", + "appearance-desc": "Dock’un davranışını ve görünümünü özelleştirin.", + "appearance-display-auto-hide": "Otomatik gizle", + "appearance-display-description": "Dock’un nasıl davranacağını seçin.", + "appearance-display-exclusive": "Özel", + "appearance-dock-indicator-description": "Dock gizlendiğinde küçük bir gösterge göster.", + "appearance-dock-indicator-label": "Dock göstergesi", + "appearance-floating-distance-description": "Dock ile ekran kenarı arasındaki mesafeyi ayarlayın.", + "appearance-floating-distance-label": "Dock yüzen mesafesi", + "appearance-group-apps-description": "Aynı uygulamanın birden fazla penceresini tek bir Dock girişinde grupla.", + "appearance-group-apps-label": "Aynı uygulamaları grupla", + "appearance-group-click-action-cycle": "Pencereler arasında geçiş yap", + "appearance-group-click-action-description": "Gruplandırılmış uygulamalar için sol tıklamanın ne yapacağını seçin.", + "appearance-group-click-action-label": "Gruplandırılmış Uygulama Tıklama Eylemi", + "appearance-group-click-action-list": "Pencere listesini aç", + "appearance-group-context-menu-mode-description": "Gruplandırılmış uygulamalar için bağlam menüsünün nasıl davranacağını seçin.", + "appearance-group-context-menu-mode-extended": "Genişletilmiş", + "appearance-group-context-menu-mode-label": "Gruplandırılmış uygulama menüsü modu", + "appearance-group-context-menu-mode-list": "Pencere listesi", + "appearance-group-indicator-style-description": "Gruplandırılmış çalışan göstergelerin odaklanmış pencere durumunu nasıl görüntüleyeceğini seçin.", + "appearance-group-indicator-style-dots": "Noktalar", + "appearance-group-indicator-style-label": "Gruplandırılmış gösterge stili", + "appearance-group-indicator-style-number": "Sayı", + "appearance-hide-show-speed-description": "Dock’un gizle/göster animasyonunun hızını ayarlayın.", + "appearance-hide-show-speed-label": "Gizle/göster hızı", + "appearance-icon-size-description": "Dock’un genel boyutunu ayarlayın.", + "appearance-icon-size-label": "Dock boyutu", + "appearance-inactive-indicators-description": "Tüm uygulamalar için, sadece etkin olan için değil, gösterge haplarını göster.", + "appearance-inactive-indicators-label": "Çalışan göstergeler", + "appearance-indicator-color-description": "Gizli dock göstergesinin rengini seçin.", + "appearance-indicator-color-label": "Gösterge rengi", + "appearance-indicator-opacity-description": "Gizli dock göstergesinin opaklığını ayarlayın.", + "appearance-indicator-opacity-label": "Gösterge opaklığı", + "appearance-indicator-thickness-description": "Daha kalın bir gizli dock göstergesi kullanın (3 px yerine 6 px).", + "appearance-indicator-thickness-label": "Daha kalın gösterge", + "appearance-launcher-icon-description": "Dock Launcher için kullanılan simgeyi seçin.", + "appearance-launcher-icon-label": "Başlatıcı Simgesi", + "appearance-launcher-position-description": "Başlatıcı simgesinin dock'ta nerede görüneceğini seçin.", + "appearance-launcher-position-end": "Son", + "appearance-launcher-position-label": "Başlatıcı konumu", + "appearance-launcher-position-start": "Başlat", + "appearance-launcher-use-distro-logo-description": "Dağıtımınızın logosunu Dock Launcher simgesi olarak kullanın.", + "appearance-launcher-use-distro-logo-label": "Simge yerine dağıtım logosunu kullan", + "appearance-pinned-static-description": "Sabit sırada, sabitlenmiş uygulama simgelerini her zaman sola itin.", + "appearance-pinned-static-label": "Sabitlenmiş statik uygulamalar", + "appearance-position-description": "Dock’un ekranda nerede görüneceğini seçin.", + "appearance-position-label": "Pozisyon", + "appearance-show-launcher-icon-description": "Uygulama başlatıcı simgesini dock'ta göster.", + "appearance-show-launcher-icon-label": "Uygulama başlatıcıyı göster", + "appearance-sit-on-frame-description": "Dock'u çerçevenin kenar boşluğunun içine hizalayın, üstüne oturmak yerine.", + "appearance-sit-on-frame-label": "Dock çerçevede yer alır", + "appearance-type-attached": "Sabitlenmiş", + "appearance-type-description": "Yüzen bir hap veya kenara bağlı sabit bir çubuk arasından seçim yapın.", + "appearance-type-floating": "Yüzen", + "appearance-type-label": "Dock stili", + "enabled-description": "Dock’u tamamen gösterin veya gizleyin.", + "enabled-label": "Dock’u etkinleştir", + "monitors-desc": "Dock’u belirli ekranlarda gösterin. Hiçbiri seçilmezse varsayılan olarak tümünde gösterilir.", + "monitors-only-same-monitor-description": "Yalnızca dock’un bulunduğu monitördeki uygulamaları göster.", + "monitors-only-same-monitor-label": "Yalnızca aynı monitördeki uygulamalar", + "monitors-title": "Ekranlar", + "title": "Dock" + }, + "general": { + "copy-settings": "Ayarları kopyala", + "fonts-default-description": "Arayüz boyunca kullanılan ana yazı tipi.", + "fonts-default-label": "Varsayılan yazı tipi", + "fonts-default-placeholder": "Varsayılan yazı tipi seçin...", + "fonts-default-scale-description": "Standart metin boyutunu artırın veya azaltın.", + "fonts-default-scale-label": "Varsayılan yazı tipi boyutu", + "fonts-default-search-placeholder": "Yazı tipi ara...", + "fonts-desc": "Arayüz boyunca kullanılacak yazı tiplerini seçin.", + "fonts-monospace-description": "Sayılar ve istatistikler için kullanılan eş aralıklı yazı tipi.", + "fonts-monospace-label": "Eş aralıklı yazı tipi", + "fonts-monospace-placeholder": "Eş aralıklı yazı tipi seçin...", + "fonts-monospace-scale-description": "Eş aralıklı metin boyutunu artırın veya azaltın.", + "fonts-monospace-scale-label": "Eş aralıklı yazı tipi boyutu", + "fonts-monospace-search-placeholder": "Eş aralıklı yazı tipi ara...", + "fonts-title": "Yazı tipleri", + "keybinds-conflict-description": "Tuş kombinasyonu zaten {action} için atanmış.", + "keybinds-conflict-title": "Tuş ataması çakışması", + "keybinds-description": "Paneller ve başlatıcı için genel navigasyon tuşlarını yapılandırın.", + "keybinds-down": "Aşağı taşı", + "keybinds-enter": "Onayla / Eylem", + "keybinds-escape": "Kapat / Geri", + "keybinds-left": "Sola taşı", + "keybinds-modifier-description": "Kısayollar bir değiştirici tuş (Ctrl veya Alt) içermelidir.", + "keybinds-modifier-title": "Değiştirici gerekli", + "keybinds-remove": "Kaldır / Sil", + "keybinds-right": "Sağa taşı", + "keybinds-title": "Gezinme tuş atamaları", + "keybinds-up": "Yukarı taşı", + "language-desc": "Uygulama için tercih ettiğiniz dili seçin.", + "language-select-auto-detect": "Otomatik", + "language-select-description": "Uygulama arayüzünde kullanılacak dili seçin.", + "language-select-label": "Uygulama dili", + "launch-setup-wizard": "Kurulum sihirbazı", + "profile-desc": "Kullanıcı detaylarınızı ve avatarınızı düzenleyin.", + "profile-picture-description": "Arayüzün tamamında görünen profil fotoğrafınız.", + "profile-picture-label": "Profil resmi", + "profile-select-avatar": "Avatar resmini seç", + "profile-title": "Avatar", + "profile-tooltip": "Avatar resmi", + "reverse-scrolling-description": "Yorumlanan kaydırma yönünü tersine çevir.", + "reverse-scrolling-label": "Kaydırmayı tersine çevir", + "screen-corners-desc": "Ekran köşesi yuvarlatma ve görsel efektleri özelleştirin.", + "screen-corners-radius-description": "Ekranın yuvarlatılmış köşelerini ayarlayın.", + "screen-corners-radius-label": "Ekran köşesi yarıçapı", + "screen-corners-radius-reset": "Ekran köşesi yarıçapını sıfırla", + "screen-corners-show-corners-description": "Ekran kenarlarında yuvarlatılmış köşeler görüntüle.", + "screen-corners-show-corners-label": "Ekran köşelerini göster", + "screen-corners-solid-black-description": "Araç çubuğu arka plan rengi yerine tam siyah kullan.", + "screen-corners-solid-black-label": "Siyah köşeler", + "screen-corners-title": "Ekran köşeleri", + "settings-copied": "Ayarlar panoya kopyalandı", + "smooth-scrolling-description": "Daha akıcı bir tekerlek deneyimi için liste kaydırmayı canlandırın.", + "smooth-scrolling-label": "Akıcı kaydırma", + "tab-basics": "Temeller", + "tab-keybinds": "Tuş atamaları" + }, + "hooks": { + "color-generation-description": "Renk oluşturma tamamlandığında (örn. duvar kağıdı tabanlı tema uygulandıktan sonra) çalıştırılacak komut.", + "color-generation-label": "Renkler oluşturuldu", + "color-generation-placeholder": "örn. notify-send \"Renkler\" \"Oluşturuldu\"", + "info-command-info-description": "• Komutlar kabuk (sh -lc) ile yürütülür
• Komutlar arka planda yürütülür (ayrık)
• Test düğmeleri mevcut değerlerle yürütülür", + "info-parameters-description": "• Wallpaper hook: $1 = duvar kağıdı yolu, $2 = ekran adı, $3 = tema (koyu/açık)
• Theme toggle hook: $1 = true/false (koyu mod durumu)
• Screen lock/unlock hooks: $1 = lock/unlock (ekran kilidi durumu)
• Performance mode hooks: Parametre yok
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "Kullanılabilir parametreler", + "noctalia-started-description": "Noctalia yüklendiğinde çalıştırılacak komut.", + "noctalia-started-label": "Noctalia başladı", + "noctalia-started-placeholder": "örn. notify-send 'Noctalia Yüklendi'", + "performance-mode-disabled-description": "Noctalia performans modu devre dışı bırakıldığında çalıştırılacak komut.", + "performance-mode-disabled-label": "Performans modu devre dışı bırakıldı", + "performance-mode-disabled-placeholder": "örn. notify-send \"Performans\" \"Mod devre dışı\"", + "performance-mode-enabled-description": "Noctalia performans modu etkinleştirildiğinde çalıştırılacak komut.", + "performance-mode-enabled-label": "Performans modu etkinleştirildi", + "performance-mode-enabled-placeholder": "örn. notify-send \"Performans\" \"Mod etkin\"", + "screen-lock-description": "Ekran kilitlendiğinde çalıştırılacak komut.", + "screen-lock-label": "Ekran kilitlendi", + "screen-lock-placeholder": "örn. notify-send \"Ekran\" \"Kilitlendi\"", + "screen-unlock-description": "Ekran kilidi açıldığında çalıştırılacak komut.", + "screen-unlock-label": "Ekran kilidi açıldı", + "screen-unlock-placeholder": "örn. notify-send \"Ekran\" \"Açıldı\"", + "session-description": "Kapatma veya yeniden başlatma öncesinde çalıştırılacak komut. Eylem türünü $1 olarak alır (kapatma/yeniden başlatma).", + "session-label": "Oturum sonu", + "session-placeholder": "örn. notify-send \"Session\" \"$1\"", + "system-hooks-desc": "Sistem olayları gerçekleştiğinde çalıştırılacak komutları yapılandırın.", + "system-hooks-enable-description": "Tüm kanca komutlarını etkinleştirin veya devre dışı bırakın.", + "system-hooks-enable-label": "Hooks etkinleştir", + "system-hooks-title": "Sistem hooks", + "theme-changed-description": "Tema koyu ve aydınlık mod arasında geçişte çalıştırılacak komut.", + "theme-changed-label": "Tema değişti", + "theme-changed-placeholder": "örn. notify-send \"Tema\" \"Geçiş yapıldı\"", + "title": "Kancalar", + "wallpaper-changed-description": "Duvar kâğıdı değiştiğinde çalıştırılacak komut.", + "wallpaper-changed-label": "Duvar kâğıdı değişti", + "wallpaper-changed-placeholder": "örn. notify-send \"Duvar Kâğıdı\" \"Değişti\"" + }, + "idle": { + "custom-add": "Komut Ekle", + "custom-description": "Bir süre hareketsizlikten sonra bir kabuk komutu çalıştır.", + "custom-entry-command": "Komut", + "custom-entry-delete": "Sil", + "custom-entry-edit": "Özel Komutu Düzenle", + "custom-entry-name": "Ad", + "custom-entry-name-placeholder": "örn. Işıkları kapat", + "custom-entry-new": "Yeni Özel Komut", + "custom-entry-timeout": "Boşta kalma süresi", + "custom-entry-timeout-format": "{count} saniye", + "custom-entry-timeout-format-plural": "{count} saniye", + "custom-entry-unnamed": "İsimsiz komut", + "custom-label": "Özel boşta kalma komutları", + "enable-description": "Belirli bir süre işlem yapılmadığında ekranı otomatik olarak kapat, kilitle veya askıya al.", + "enable-label": "Boşta kalma yönetimini etkinleştir", + "fade-duration-description": "Her eylem tetiklenmeden önce kararan animasyonun saniye cinsinden süresi. Herhangi bir fare hareketi kararmayı iptal eder.", + "fade-duration-label": "Solma süresi", + "lock-description": "Kilit ekranı etkinleşmeden önceki hareketsizlik süresi (saniye).", + "lock-label": "Ekranı Kilitle", + "resume-command-label": "Komutu devam ettir", + "screen-off-description": "Monitörler kapanmadan önceki hareketsizlik süresi (saniye).", + "screen-off-label": "Ekranı kapat", + "status-description": "Compositor tarafından bildirilen boşta kalma süresi.", + "status-label": "Boşta kalma süresi", + "suspend-description": "Sistem askıya alınmadan önceki hareketsizlik süresi (saniye).", + "tab-behavior": "Davranış", + "tab-custom": "Özel", + "timeouts-description": "Bir aşamayı devre dışı bırakmak için 0 olarak ayarlayın. 'Uyanık kal' aktifken zaman aşımları duraklatılır.", + "timeouts-label": "Zaman aşımları", + "title": "Boşta", + "unavailable": "Yerel boşta kalma izleme bu Compositor'da mevcut değil." + }, + "indicator": { + "default-value": "Varsayılan: {value}", + "system-default": "Sistem varsayılanı" + }, + "launcher": { + "clipboard-desc": "Başlatıcıdan panonuzun geçmişine erişin ve yönetin.", + "execute-desc": "Uygulamaların nasıl başlatılacağını yapılandırın.", + "execute-title": "İcra", + "settings-annotation-tool-description": "Panodaki geçmişte açıklama ekle düğmesine tıklandığında çalıştırılacak komut. Görüntü bu komuta yönlendirilecektir.", + "settings-annotation-tool-label": "Açıklama aracı", + "settings-annotation-tool-placeholder": "örn. 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Seçili panodaki öğeyi otomatik olarak yapıştır. Wtype gerektirir.", + "settings-auto-paste-label": "Otomatik yapıştırma", + "settings-clip-chips-description": "Panoya geçmişini türe göre filtrelemek için bir sekme çubuğu göster (Görseller, Bağlantılar, Dosyalar, Kod vb.).", + "settings-clip-chips-label": "Kategori Çiplerini Etkinleştir", + "settings-clip-preview-description": ">clip komutu kullanılırken panodaki içeriğin önizlemesini gösterir.", + "settings-clip-preview-label": "Panoyu önizlemeyi etkinleştir", + "settings-clip-smart-icons-description": "Genel bir pano simgesi yerine bağlantılar, dosyalar, renkler ve kod için belirli simgeler göster.", + "settings-clip-smart-icons-label": "Akıllı Simgeleri Etkinleştir", + "settings-clip-wrap-text-description": "Panodaki metni kırpmak yerine listeye sığdır.", + "settings-clip-wrap-text-label": "Panoyu metni kaydır", + "settings-clipboard-history-description": "Başlatıcıdan daha önce kopyalanan öğelere erişin.", + "settings-clipboard-history-label": "Pano geçmişini etkinleştir", + "settings-clipboard-watch-image-description": "Görüntü değişiklikleri için wl-paste'e iletilen tam komut dizesi (yeniden başlatma gerektirir).", + "settings-clipboard-watch-image-label": "Görüntü İzleme Komutu", + "settings-clipboard-watch-text-description": "Görüntü değişiklikleri için wl-paste'e iletilen tam komut dizesi (yeniden başlatma gerektirir).", + "settings-clipboard-watch-text-label": "Metin izleme komutu", + "settings-custom-launch-prefix-description": "Komutlara özel bir başlatıcı ile ön ek ekleyin (örn. systemd entegrasyonu için 'runapp').", + "settings-custom-launch-prefix-enabled-description": "Uygulamaları başlatmak için varsayılan yöntem yerine özel bir ön ek kullanın.", + "settings-custom-launch-prefix-enabled-label": "Özel başlatma ön ekini etkinleştir", + "settings-custom-launch-prefix-label": "Özel başlatma ön eki", + "settings-density-description": "Uygulama simgelerinin boyutunu ve Launcher yoğunluğunu ayarlayın.", + "settings-density-label": "Yoğunluk", + "settings-desc": "Başlatıcının davranışını ve görünümünü özelleştirin.", + "settings-enable-session-search-description": "Launcher'da arama yaparken oturum eylemlerini (kilitle, kapat, yeniden başlat vb.) göster.", + "settings-enable-session-search-label": "Oturum aramasını etkinleştir", + "settings-enable-settings-search-description": "Başlatıcıda arama yaparken ayarlar sonuçlarını göster.", + "settings-enable-settings-search-label": "Ayarlar aramasını etkinleştir", + "settings-enable-windows-search-description": "Etkin pencereleri ara ve odakla.", + "settings-enable-windows-search-label": "Pencere aramasını etkinleştir", + "settings-grid-view-description": "Öğeleri liste yerine ızgara düzeninde görüntüle.", + "settings-icon-mode-description": "Tabler simgeleri yerine yerel sistem simgelerini kullanın.", + "settings-icon-mode-label": "Yerel simgeleri kullan", + "settings-ignore-mouse-input-description": "Başlatıcıda fare etkileşimini ve kaydırma tekerleğini devre dışı bırak.", + "settings-ignore-mouse-input-label": "Fare girişini yoksay", + "settings-overlay-layer-description": "Başlatıcıyı tam ekran pencerelerinin üzerinde, yer paylaşımı katmanında görüntüle. Etkinleştirildiğinde, başlatıcı çubuğa takılmaz.", + "settings-overlay-layer-label": "Tam ekranın üzerinde göster", + "settings-position-description": "Başlatıcı panelinin nerede görüneceğini seçin.", + "settings-show-categories-description": "Uygulamaları filtrelemek için kategori sekmelerini göster.", + "settings-show-categories-label": "Kategorileri göster", + "settings-show-icon-background-description": "Simgelerin arkasında yuvarlak köşeli dikdörtgen bir arka plan göster.", + "settings-show-icon-background-label": "Simge arka planını göster", + "settings-sort-by-usage-description": "Etkinleştirildiğinde, sıkça başlatılan uygulamalar listede ilk olarak görünür.", + "settings-sort-by-usage-label": "En çok kullanılana göre sırala", + "settings-terminal-command-description": "Bir terminal başlatmak için komut. Örn. 'kitty -e' veya 'gnome-terminal --'.", + "settings-terminal-command-label": "Terminal komutu", + "settings-use-app2unit-description": "Uygulama süreçlerini daha iyi yönetmek ve sorunları önlemek için alternatif bir başlatma yöntemi kullanır.", + "settings-use-app2unit-label": "Uygulamaları başlatmak için App2Unit kullan", + "settings-view-mode-description": "Başlatıcı girişleri için düzeni seçin.", + "settings-view-mode-label": "Görünüm modu", + "title": "Başlatıcı" + }, + "location": { + "auto-locate-description": "IP adresinizi kullanarak konumunuzu periyodik olarak otomatik olarak algılayın.", + "auto-locate-label": "Otomatik Konumlandırma", + "calendar-cards-desc": "Takvim panelinde kartları düzenleyin ve etkinleştirin/devre dışı bırakın.", + "calendar-cards-title": "Takvim kartları", + "calendar-header-label": "Takvim başlığı", + "calendar-month-label": "Takvim ayı", + "date-time-12hour-format-description": "Zamanı kilit ekranında ve takvimde 12 saatlik formatta gösterir. Araç çubuğu saatinin kendi ayarları vardır.", + "date-time-12hour-format-label": "12 saatlik zaman formatını kullan", + "date-time-desc": "Tarih ve saatin nasıl görüneceğini özelleştirin.", + "date-time-first-day-of-week-automatic": "Otomatik (sistem yerel ayarını kullan)", + "date-time-first-day-of-week-description": "Takvimde haftanın hangi günü başladığını seçin.", + "date-time-first-day-of-week-label": "Haftanın ilk günü", + "date-time-show-events-description": "Takvim panelindeki etkinlikleri göster.", + "date-time-show-events-label": "Takvim etkinliklerini göster", + "date-time-title": "Tarih & Saat", + "date-time-use-analog-description": "Takvim penceresinde analog tarzda bir saat göster.", + "date-time-use-analog-label": "Analog tarzı saat kullan", + "date-time-week-numbers-description": "Takvimde yılın haftasını (örn. 38. Hafta) gösterir.", + "date-time-week-numbers-label": "Hafta numaralarını göster", + "geolocate-now-button": "Şimdi konumlandır", + "location-desc": "Konumunuzu ayarlayarak hassas hava durumu ve gece ışığı zamanlaması alın.", + "location-search-description": "örn. İstanbul, TR", + "location-search-label": "Konum ara", + "location-search-placeholder": "Konum adını girin", + "location-title": "Konumunuz", + "weather-desc": "Tercih ettiğiniz sıcaklık birimini seçin.", + "weather-enabled-description": "Arayüz boyunca hava durumu bilgilerini gösterin ve hava durumu verileri alın. Devre dışı bırakıldığında, tüm hava durumu öğeleri gizlenecek ve ağ istekleri yapılmayacaktır.", + "weather-enabled-label": "Hava durumunu etkinleştir", + "weather-fahrenheit-description": "Sıcaklığı santigrat yerine fahrenhayt cinsinden gösterin.", + "weather-fahrenheit-label": "Sıcaklığı fahrenhayt (°F) cinsinden göster", + "weather-hide-city-description": "Arayüzün tamamında hava durumu ekranlarında şehir adını gizle.", + "weather-hide-city-label": "Şehir adını gizle", + "weather-hide-timezone-description": "Arayüzün tamamındaki hava durumu ekranlarından saat dilimi kısaltmasını gizle.", + "weather-hide-timezone-label": "Saat dilimini gizle", + "weather-show-effects-description": "Hava durumu kartında ek görsel efektler (yağmur, kar veya şimşek gibi) gösterir.", + "weather-show-effects-label": "Hava efektlerini göster", + "weather-show-in-calendar-description": "Günlük hava durumu tahminini doğrudan takvim görünümünüzde gösterin.", + "weather-show-in-calendar-label": "Takvimde hava durumunu göster", + "weather-talia-mascot-always-description": "tabler icons yerine her zaman Talia emojis kullanın.", + "weather-talia-mascot-always-label": "Talia hava durumu emojilerini her zaman göster" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "fprintd (parmak izi kimlik doğrulaması) etkin olduğunda, bu seçenek parmak izi yerine parolanızı kullanarak oturum açmanızı sağlar.", + "allow-password-with-fprintd-label": "fprintd ile parola girişine izin ver", + "auto-start-auth-description": "Bir tuşa basmaya veya düğmeye tıklamaya gerek kalmadan parmak izi kimlik doğrulamasını otomatik olarak başlatır.", + "auto-start-auth-label": "Otomatik kimlik doğrulama başlatma", + "clock-format-description": "Tarih/saat sözdizimi belirteçlerini kullanarak saat biçimini özelleştirin.", + "clock-format-label": "Saat Biçimi", + "clock-style-analog": "Analog", + "clock-style-custom": "Özel", + "clock-style-description": "Kilit Ekranı'ndaki saatin görsel stilini seçin.", + "clock-style-digital": "Dijital", + "clock-style-label": "Saat Stili", + "compact-lockscreen-description": "Sadece giriş girdisini ve sistem kontrollerini göster, hava durumu ve ortam araç takımlarını gizle.", + "compact-lockscreen-label": "Kompakt ekran kilidi", + "enable-lockscreen-media-controls-description": "Kilit ekranında etkileşimli medya oynatma kontrollerini göster.", + "enable-lockscreen-media-controls-label": "Kilit Ekranı Medya Kontrolleri", + "lock-on-suspend-description": "Sistemi askıya alırken otomatik olarak ekranı kilitler.", + "lock-on-suspend-label": "Askıya alırken kilitle", + "lock-screen-animations-description": "Kilit ekranı animasyonlarını etkinleştir veya devre dışı bırak.", + "lock-screen-animations-label": "Kilit Ekranı Animasyonları", + "lock-screen-blur-strength-description": "Kilit ekranı duvar kağıdına bulanıklık efekti uygular.", + "lock-screen-blur-strength-label": "Kilit ekranı bulanıklık gücü", + "lock-screen-tint-strength-description": "Kilit ekranı duvar kağıdına bir renk tonu katmanı uygular.", + "lock-screen-tint-strength-label": "Kilit ekranı ton gücü", + "monitors-desc": "Kilit ekranını belirli monitörlerde göster. Hiçbiri seçilmezse varsayılan olarak tüm monitörlerde gösterilir.", + "password-chars-description": "Şifrenizi gizlemek için kullanılan sevimli simgeler.", + "password-chars-label": "Rastgele şifre simgeleri", + "show-hibernate-description": "Güç kontrollerinde 'hazırda beklet' seçeneğini göster.", + "show-hibernate-label": "Hazırda beklet seçeneğini göster", + "show-session-buttons-description": "Kilit ekranından güç ayarlarına erişime izin ver.", + "show-session-buttons-label": "Güç kontrolleri", + "title": "Ekran kilit" + }, + "network": { + "bluetooth-description": "Bluetooth yönetimini etkinleştir.", + "bluetooth-rssi-polling-description": "Bağlı cihazlar için RSSI'yı bluetoothctl aracılığıyla periyodik olarak örnekleyin. Tüm cihazlar için mevcut olmayabilir; etkinleştirildiğinde minimum kaynak kullanır.", + "bluetooth-rssi-polling-label": "Bluetooth sinyal yoklaması", + "desc": "Kablosuz ve Bluetooth bağlantılarını yönetin.", + "wifi-description": "Kablosuz ağları yönet (NetworkManager gerektirir)." + }, + "notifications": { + "duration-critical-urgency-description": "Kritik öncelikli bildirimlerin ne kadar süreyle görüneceği.", + "duration-critical-urgency-label": "Kritik öncelikli", + "duration-desc": "Bildirimlerin aciliyet seviyelerine göre ne kadar süreyle görüneceğini yapılandırın.", + "duration-low-urgency-description": "Düşük öncelikli bildirimlerin ne kadar süreyle görüneceği.", + "duration-low-urgency-label": "Düşük öncelikli", + "duration-normal-urgency-description": "Normal öncelikli bildirimlerin ne kadar süreyle görüneceği.", + "duration-normal-urgency-label": "Normal öncelikli", + "duration-respect-expire-description": "Bildirimde ayarlanan sona erme zaman aşımını kullanın.", + "duration-respect-expire-label": "Sona erme zaman aşımına uyar", + "duration-title": "Bildirim süresi", + "history-clear-dismiss-description": "Bildirimi kapatıldığında geçmişten sil.", + "history-clear-dismiss-label": "Kapatıldığında temizle", + "history-critical-urgency-description": "Kritik öncelikli bildirimleri geçmişe kaydet.", + "history-critical-urgency-label": "Kritik aciliyeti geçmişe kaydet", + "history-desc": "Aciliyet seviyelerine göre hangi bildirimlerin geçmişe kaydedileceğini kontrol edin.", + "history-low-urgency-description": "Düşük öncelikli bildirimleri geçmişe kaydet.", + "history-low-urgency-label": "Düşük aciliyeti geçmişe kaydet", + "history-normal-urgency-description": "Normal öncelikli bildirimleri geçmişe kaydet.", + "history-normal-urgency-label": "Normal aciliyeti geçmişe kaydet", + "monitors-desc": "Bildirimleri belirli ekranlarda gösterin. Hiçbiri seçilmezse varsayılan olarak tümünde gösterilir.", + "rules-action-block": "Blok", + "rules-action-block-desc": "Tamamen atlar.", + "rules-action-hide": "Gizle", + "rules-action-hide-desc": "Açılır pencere yok, ses yok, geçmişe eklenir.", + "rules-action-label": "Eylem", + "rules-action-mute": "Sesi Kapat", + "rules-action-mute-desc": "Ses yok, yine de açılır pencere gösteriyor ve geçmişte.", + "rules-add": "Kural Ekle", + "rules-delete": "Kuralı Sil", + "rules-description": "Uygulama adını veya içeriği eşleştir. Kurallar sırayla kontrol edilir ve ilk eşleşen uygulanır.", + "rules-edit": "Kuralı Düzenle", + "rules-label": "Filtre Kuralları", + "rules-pattern-label": "Desen", + "rules-pattern-placeholder": "firefox, discord, veya /regex/", + "rules-tab": "Kurallar", + "settings-always-on-top-description": "Bildirimleri tam ekran pencerelerinin ve diğer katmanların üzerinde görüntüle.", + "settings-background-opacity-description": "Bildirim arka planlarının opaklığını ayarlayın.", + "settings-density-description": "Bildirim kartı yoğunluğunu seçin.", + "settings-density-label": "Yoğunluk", + "settings-desc": "Bildirimlerin görünümünü ve davranışını yapılandırın.", + "settings-do-not-disturb-description": "Etkinleştirildiğinde tüm bildirim açılır pencerelerini devre dışı bırakır.", + "settings-enabled-description": "Bildirim arka plan programını etkinleştir veya devre dışı bırak, Noctalia shell'in yeniden başlatılmasını gerektirir.", + "settings-enabled-label": "Bildirimleri etkinleştir", + "settings-location-description": "Bildirimlerin ekranda nerede görüneceği.", + "settings-markdown-description": "Bildirim içeriğini Markdown biçimlendirmesi kullanarak oluştur.", + "settings-markdown-label": "Markdown'ı Etkinleştir", + "sounds-desc": "Bildirim ses efektlerini ve ses seviyesini yapılandırın.", + "sounds-enabled-description": "Gelen bildirimler için ses efektlerini etkinleştir.", + "sounds-enabled-label": "Bildirim seslerini etkinleştir", + "sounds-excluded-apps-description": "Kendi dahili seslerine sahip belirli uygulamalar için ayarlanan bildirim sesini çalmayı es geçin.", + "sounds-excluded-apps-label": "Hariç tutulan uygulamalar", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Kritik öncelikli bildirimler için çalınan ses dosyasının yolu.", + "sounds-files-critical-label": "Kritik aciliyet sesi", + "sounds-files-critical-select-title": "Kritik aciliyet ses dosyası seç", + "sounds-files-desc": "Farklı bildirim aciliyet seviyeleri için özel ses dosyaları yapılandırın.", + "sounds-files-low-description": "Düşük öncelikli bildirimler için çalınan ses dosyasının yolu.", + "sounds-files-low-label": "Düşük aciliyet sesi", + "sounds-files-low-select-title": "Düşük aciliyet ses dosyası seç", + "sounds-files-normal-description": "Normal öncelikli bildirimler için çalınan ses dosyasının yolu.", + "sounds-files-normal-label": "Normal aciliyet sesi", + "sounds-files-normal-select-title": "Normal aciliyet ses dosyası seç", + "sounds-files-placeholder": "Ses dosyası yolunu girin", + "sounds-files-select-file": "Ses dosyası seç", + "sounds-files-title": "Ses dosyaları", + "sounds-files-unified-description": "Bildirimler için çalınan ses dosyasının yolu.", + "sounds-files-unified-label": "Bildirim sesi", + "sounds-files-unified-select-title": "Bildirim ses dosyası seç", + "sounds-separate-description": "Düşük, normal ve kritik öncelikli bildirimler için farklı ses dosyaları kullanın.", + "sounds-separate-label": "Öncelik başına farklı sesler kullan", + "sounds-title": "Ses ayarları", + "sounds-unavailable-description": "Bildirim seslerini etkinleştirmek için Qt6 Multimedia'yı kurun.", + "sounds-unavailable-label": "Bildirim sesleri kullanılamıyor", + "sounds-volume-description": "Bildirim sesleri için ses seviyesini ayarlayın.", + "sounds-volume-label": "Ses seviyesi", + "toast-battery-description": "Pil seviyesi bu yüzdenin altına düştüğünde bir uyarı göster.", + "toast-battery-label": "Pil uyarısı", + "toast-desc": "Bildirim penceresi görünümünü ve davranışını yapılandırın.", + "toast-keyboard-description": "Klavye düzeni değiştiğinde bir bildirim göster.", + "toast-keyboard-label": "Klavye düzeni", + "toast-media-description": "Medya oynatma durumu değiştiğinde bir bildirim göster.", + "toast-media-label": "Medya" + }, + "osd": { + "always-on-top-description": "OSD'yi tam ekran pencerelerinin ve diğer katmanların üzerinde görüntüle.", + "always-on-top-label": "Her zaman üstte", + "background-opacity-description": "OSD arka planının şeffaflığını kontrol eder.", + "background-opacity-label": "Arka plan opaklığı", + "description": "Ses ve parlaklık kaplamaları gibi ekran göstergelerini yapılandırın.", + "duration-auto-hide-description": "OSD'nin kaybolmadan önceki süreyi ayarlayın.", + "duration-auto-hide-label": "Şu kadar sonra otomatik gizle", + "duration-desc": "OSD'nin otomatik olarak gizlenmeden önce ne kadar süreyle görüneceği.", + "duration-title": "Otomatik gizleme zaman aşımı", + "enabled-description": "Ses ve parlaklık değişikliklerini gerçek zamanlı olarak gösterin.", + "enabled-label": "Ekran üstü gösterim (OSD) etkinleştir", + "events-desc": "Ekran üstü gösterimi tetikleyecek olayları seçin.", + "general-desc": "OSD'nin görünürlüğünü ve davranışını yapılandırın.", + "location-description": "Ekran üstü gösterimlerin görüneceği konum.", + "monitors-desc": "OSD'yi belirli ekranlarda gösterin. Hiçbiri seçilmezse varsayılan olarak tümünde gösterilir.", + "title": "Ekran üstü gösterim", + "types-brightness-description": "Ekran parlaklığı değiştiğinde OSD'yi göster.", + "types-brightness-label": "Parlaklık", + "types-custom-text-description": "IPC üzerinden özel metin iletileri için OSD göster.", + "types-custom-text-label": "Özel metin", + "types-desc": "OSD'yi tetikleyecek olayları seçin.", + "types-input-volume-description": "Mikrofon ses düzeyi değiştiğinde OSD'yi göster.", + "types-input-volume-label": "Giriş sesi", + "types-lockkey-description": "Caps Lock, Num Lock veya Scroll Lock değiştirildiğinde OSD'yi göster.", + "types-lockkey-label": "Kilit tuşları", + "types-media-description": "Medya oynatma durumu değiştiğinde OSD'yi göster (oynat, duraklat, atla).", + "types-media-label": "Medya oynatma", + "types-title": "OSD tetikleme olayları", + "types-volume-description": "Ses çıkış düzeyi değiştiğinde OSD'yi göster.", + "types-volume-label": "Çıkış sesi" + }, + "plugins": { + "auto-update": "Eklentileri otomatik güncelle", + "auto-update-description": "Shell başladığında tüm eklentileri otomatik olarak güncelle.", + "available-description": "Yapılandırılmış kaynaklardan eklentilere göz atın ve yükleyin.", + "available-label": "Mevcut eklentiler", + "available-no-plugins-description": "Eklenti kaynaklarınızı kontrol edin veya listeyi yenileyin.", + "available-no-plugins-label": "Kullanılabilir eklenti yok", + "check-for-updates": "Güncellemeleri kontrol et", + "checking-for-updates": "Güncellemeler kontrol ediliyor...", + "collision-already-installed": "Bu eklenti zaten yüklü", + "collision-custom-version-exists": "\"{source}\" kaynağından özel bir sürüm zaten yüklü", + "collision-official-version-exists": "Bu eklentinin resmi sürümü zaten yüklü", + "development-disable": "Bu eklenti için geliştirme modunu devre dışı bırak", + "development-enable": "Bu eklenti için geliştirme modunu etkinleştir.", + "filter-downloaded": "İndirildi", + "filter-not-downloaded": "İndirilmedi", + "filter-tags-description": "Eklentileri kategoriye veya indirme durumuna göre filtrele.", + "filter-tags-label": "Etiketler", + "hot-reload-description": "Eklenti dosyaları değiştiğinde eklentileri otomatik olarak yeniden yükle. Eklenti geliştirme için kullanışlıdır.", + "hot-reload-label": "Hızlı yeniden yükleme (geliştirme modu)", + "hot-reloaded": "Yeniden yüklenen eklenti: {name}", + "install-error": "Yükleme başarısız oldu: {error}", + "install-incompatible": "{plugin} Noctalia v{version} veya daha üstünü gerektiriyor", + "install-success": "{plugin} başarıyla kuruldu", + "installed-description": "Yerel olarak yüklenmiş tüm eklentileri yönetin ve yapılandırın.", + "installed-label": "Yüklü eklentiler", + "installed-no-plugins-description": "\"Mevcut\" bölümünden eklentileri kurun.", + "installed-no-plugins-label": "Eklenti kurulu değil", + "installing": "{eklenti} kuruluyor...", + "notify-updates": "Eklenti güncelleme bildirimleri", + "notify-updates-description": "Yeni eklenti güncellemeleri olduğunda bir bildirim penceresi göster.", + "open-plugin-page": "Eklenti sayfasını aç", + "open-plugins-tab": "Eklenti ayarlarına git", + "plugin-settings-title": "{plugin} ayarları", + "refresh-refreshing": "Eklenti listesi yenileniyor...", + "refresh-tooltip": "Mevcut eklentileri yenile", + "settings-error-not-loaded": "Eklenti yüklenmedi", + "settings-saved": "Eklenti ayarları kaydedildi", + "settings-tooltip": "Eklenti ayarları", + "source-custom": "Özel kaynak", + "sources-add-custom": "Özel depo ekle", + "sources-add-dialog-description": "Bir GitHub deposunu eklenti kaynağı olarak ekle.", + "sources-add-dialog-error": "Eklenti kaynağı eklenemedi", + "sources-add-dialog-name": "Depo adı", + "sources-add-dialog-name-placeholder": "Özel eklentilerim", + "sources-add-dialog-success": "Eklenti kaynağı başarıyla eklendi", + "sources-add-dialog-title": "Eklenti kaynağı ekle", + "sources-add-dialog-url": "Depo URL'si", + "sources-description": "Eklenti depolarını yönetin.", + "sources-placeholder": "Havalı depom", + "sources-remove-tooltip": "Eklenti kaynağını kaldır", + "title": "Eklentiler", + "translations-reloaded": "Çeviriler yeniden yüklendi: {name}", + "uninstall-dialog-description": "{plugin} eklentisini kaldırmak istediğinizden emin misiniz? Bu, tüm eklenti verilerini kaldıracaktır.", + "uninstall-dialog-title": "Eklentiyi kaldır", + "uninstall-error": "Kaldırma başarısız oldu: {error}", + "uninstall-success": "{plugin} başarıyla kaldırıldı", + "uninstalling": "{plugin} kaldırılıyor...", + "update-all": "Tümünü güncelle ({count})", + "update-all-success": "Tüm eklentiler başarıyla güncellendi", + "update-available": "Yeni eklenti güncellemesi mevcut", + "update-available-plural": "Yeni eklenti güncellemeleri mevcut ({count})", + "update-error": "Eklenti güncellenemedi: {plugin}: {error}", + "update-pending": "v{current} → v{new} (Noctalia v{required} gerektirir)", + "update-success": "{plugin} v{version} sürümüne güncellendi", + "update-version": "v{current} → v{new}", + "updating": "Güncelleniyor..." + }, + "region": { + "title": "Bölge" + }, + "session-menu": { + "countdown-duration-description": "Güç eylemlerini çalıştırmadan önce geri sayım zamanlayıcısının ne kadar süreceğini ayarlayın.", + "countdown-duration-label": "Geri sayım süresi", + "enable-countdown-description": "Güç eylemlerini çalıştırmadan önce bir geri sayım zamanlayıcısı göster.", + "enable-countdown-label": "Geri sayım zamanlayıcısını etkinleştir", + "entries-desc": "Oturum menüsünde hangi güç eylemlerinin görüneceğini ve hangi sırada görüneceğini özelleştirin.", + "entries-title": "Güç eylemleri", + "entry-settings-command-description": "Bu işlem için yürütülecek özel komut. Varsayılan sistem komutunu kullanmak için boş bırakın.", + "entry-settings-command-placeholder": "örn. systemctl poweroff", + "entry-settings-default-command-lock": "Dahili kilit ekranı (komut yok)", + "entry-settings-default-command-logout": "Dahili oturum kapatma (komut yok)", + "entry-settings-default-info-description": "Özel bir komut belirtilmezse, varsayılan sistem komutu kullanılır.", + "entry-settings-default-info-label": "Varsayılan komut", + "entry-settings-keybind-description": "Oturum menüsü açıkken bu eylemi tetiklemek için bir tuş kombinasyonu atayın.", + "entry-settings-keybind-placeholder": "Bir tuş atamak için Kaydet'e basın...", + "entry-settings-keybind-recording": "Kaydediliyor... Herhangi bir tuş kombinasyonuna basın", + "entry-settings-title": "{entry} yapılandır", + "entry-settings-tooltip": "Yapılandırma komutu", + "general-desc": "Oturum menüsü panelinin davranışını ve görünümünü yapılandırın.", + "large-buttons-layout-description": "Oturum menüsü düğmelerinin nasıl görüntüleneceğini seçin.", + "large-buttons-layout-label": "Büyük düğme düzeni", + "large-buttons-style-description": "Oturum menüsünü büyük butonlarla ızgara düzeninde görüntüle.", + "large-buttons-style-label": "Büyük düğme stili", + "position-description": "Oturum menüsü panelinin açıldığında nerede görüneceğini seçin.", + "show-header-description": "Oturum menüsünün üst kısmında başlığı ve kapat düğmesini göster.", + "show-header-label": "Başlığı göster", + "show-keybinds-description": "Oturum seçeneklerinde tuş kombinasyonu ipuçlarını görüntüle.", + "show-keybinds-label": "Klavye kısayollarını göster" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Noctalia performans modu etkinleştirildiğinde masaüstü widget'larını görünür tut.", + "noctalia-performance-disable-desktop-widgets-label": "Performans modunda masaüstü widget'larını etkinleştir", + "noctalia-performance-disable-wallpaper-description": "Noctalia performans modu etkinleştirildiğinde masaüstü, genel bakış ve kilit ekranı duvar kağıtlarını görünür tutun.", + "noctalia-performance-disable-wallpaper-label": "Performans modunda duvar kağıdı oluşturmayı etkinleştir", + "title": "Sistem" + }, + "system-monitor": { + "critical-color-label": "Kritik renk", + "custom-highlight-colors-title-label": "Özel vurgulama renkleri", + "disk-available-label": "Kullanılabilir disk alanı", + "disk-section-label": "Disk kullanımı", + "enable-dgpu-monitoring-description": "Uyarı: Bu, ayrık GPU'nuzu (NVIDIA/AMD) uyandıracak ve bu da hibrit grafiklere sahip dizüstü bilgisayarlarda pil ömrünü önemli ölçüde etkileyebilir.", + "enable-dgpu-monitoring-label": "Ayrık GPU izlemeyi etkinleştir", + "external-monitor-description": "Harici sistem izleyici uygulamasını etkinleştirirken başlatılacak komut veya uygulama yolunu girin.", + "external-monitor-label": "Harici sistem izleyici komutu", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "Sistem İzleyicisinin davranışını ve görünümünü yapılandırın.", + "gpu-section-label": "GPU sıcaklığı", + "highlight-colors-section-label": "Vurgulama renkleri", + "polling-interval-label": "Yoklama aralığı", + "polling-section-description": "Her bir sistem metriğinin ne sıklıkta güncelleneceğini yapılandırın.", + "polling-section-label": "Yoklama aralıkları", + "threshold-critical": "Kritik", + "threshold-warning": "Uyarı", + "thresholds-section-description": "Her sistem ölçüsü için uyarı/kritik eşiklerini ve yoklama aralıklarını ayarlayın.", + "use-custom-highlight-colors-description": "Devre dışı bırakıldığında, tema varsayılan vurgulama renkleri kullanılır.", + "use-custom-highlight-colors-label": "Özel vurgulama renklerini kullan", + "warning-color-label": "Uyarı rengi" + }, + "user-interface": { + "allow-panels-without-bar-description": "Etkinleştirildiğinde, paneller herhangi bir ekranda açılabilir. Devre dışı bırakıldığında, paneller yalnızca bir çubuğu olan ekranlarda açılır, bu da bellek kullanımını azaltabilir.", + "allow-panels-without-bar-label": "Çubuksuz ekranlardaki panellere izin ver", + "animation-disable-description": "Daha hızlı ve daha duyarlı bir deneyim için tüm animasyonları devre dışı bırakın.", + "animation-disable-label": "Kullanıcı arayüzü animasyonlarını devre dışı bırak", + "animation-speed-description": "Genel animasyon hızını ayarlar.", + "animation-speed-label": "Animasyon hızı", + "animation-speed-reset": "Animasyon hızını sıfırla", + "appearance-desc": "Araç ipuçları, kenarlıklar ve gölgeler gibi görsel öğeleri özelleştirin.", + "blur-behind-description": "Çubukların ve panellerin arkasındaki alanı kompozitör bulanıklık protokolünü kullanarak bulanıklaştırır.", + "blur-behind-label": "Arka plan bulanıklığı", + "box-border-description": "İçerik alanlarının etrafında bir çerçeve görüntüler.", + "box-border-label": "Konteyner ana hattı", + "box-border-radius-description": "Kenar çubukları, kartlar ve içerik panelleri gibi ana düzen bölümlerinin köşe yuvarlaklığını ayarlar.", + "box-border-radius-label": "Konteyner yarıçapı", + "control-border-radius-description": "İnteraktif öğelerin, düğmeler, geçişler ve metin alanları dahil olmak üzere, eğriliğini kontrol eder.", + "control-border-radius-label": "Giriş yarıçapı", + "desc": "Arayüzün görünümünü, hissini ve davranışını özelleştirin.", + "dim-desktop-description": "Paneller veya menüler açıkken masaüstünü karart.", + "dim-desktop-label": "Masaüstünü karart", + "dimmer-opacity-description": "Masaüstü karartması için opaklık seviyesini ayarlayın.", + "dimmer-opacity-label": "Karartılmış masaüstü opaklığı", + "panel-background-opacity-description": "Tüm panellerin (çubuk, başlatıcı, ayarlar vb.) arka plan opaklığını ayarlayın.", + "panel-background-opacity-label": "Panel arka plan opaklığı", + "panels-attached-to-bar-description": "Paneller çubuğa ve ekran kenarlarına kilitlenerek şık ters köşelerle kusursuz bir görünüm oluşturur.", + "panels-attached-to-bar-label": "Kenarları yakala", + "panels-overlay-description": "Panellerin ve çubuğun, tam ekran uygulamalarda bile görünür kalmasını sağlar.", + "panels-overlay-label": "Panelleri ve çubuğu üstte tut", + "scaling-description": "Çubuğu hariç tutarak genel kullanıcı arayüzünün boyutunu değiştirir.", + "scaling-label": "Arayüz ölçekleme", + "scrollbar-always-visible-description": "İçerik kaydırılabilir olduğunda kaydırma çubuklarını her zaman görünür tut, yalnızca üzerine gelindiğinde göstermek yerine.", + "scrollbar-always-visible-label": "Kaydırma çubuklarını her zaman göster", + "settings-panel-header": "Ayarlar Paneli", + "settings-panel-mode-description": "Ayarlar düzenini seçin (yeniden açılması gerekebilir).", + "settings-panel-mode-label": "Ayarlar paneli modu", + "settings-panel-sidebar-card-style-description": "Ayarlar kenar çubuğunu yuvarlatılmış köşeli dolgulu bir arka planla sarar.", + "settings-panel-sidebar-card-style-label": "Kenar çubuğu kart stili", + "shadows-description": "Çubukların ve panellerin altında gölgelerin etkinleştirilmesini sağlar.", + "shadows-direction-description": "Gölgenin nereye düşeceğini seçin.", + "shadows-direction-label": "Gölge yönü", + "shadows-label": "Gölge efektleri", + "title": "Kullanıcı arayüzü", + "tooltips-description": "Arayüz genelindeki ipuçlarını etkinleştirin veya devre dışı bırakın.", + "tooltips-label": "İpuçlarını göster", + "translucent-widgets-description": "Panellerin içindeki düğmeleri, sekmeleri ve diğer widget'ları yarı saydam yapın.", + "translucent-widgets-label": "Yarı saydam widget'lar" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Alfabetik", + "automation-change-mode-description": "Otomatik değiştirirken duvar kâğıtlarının nasıl seçileceğini seçin.", + "automation-change-mode-label": "Değiştirme modu", + "automation-custom-interval-description": "Zamanı SS:DD olarak girin (örn. 01:30).", + "automation-custom-interval-label": "Özel aralık", + "automation-interval-description": "Duvar kâğıtlarının otomatik değiştirilme sıklığını belirleyin.", + "automation-interval-label": "Duvar kâğıdı aralığı", + "automation-random-wallpaper-description": "Düzenli aralıklarla rastgele duvar kâğıdı değişiklikleri planlayın.", + "automation-scheduled-change-description": "Düzenli aralıklarla duvar kâğıtlarını otomatik olarak değiştirin.", + "automation-scheduled-change-label": "Zamanlanmış değişiklik", + "look-feel-edge-smoothness-description": "Geçişlerin kenarına yumuşak, tüylü bir efekt uygular.", + "look-feel-edge-smoothness-label": "Geçiş kenarını yumuşat", + "look-feel-fill-color-description": "Duvar kâğıdının arkasında görünebilecek bir dolgu rengi seçin.", + "look-feel-fill-mode-description": "Görüntünün ekran çözünürlüğünüzle eşleşmesi için nasıl ölçekleneceğini seçin.", + "look-feel-fill-mode-label": "Doldurma modu", + "look-feel-skip-startup-transition-description": "Kabuk başladığında duvar kağıdı animasyonunu atla.", + "look-feel-skip-startup-transition-label": "Başlangıç geçişini atla", + "look-feel-title": "Görünüm ve his", + "look-feel-transition-duration-description": "Geçiş animasyonlarının saniye cinsinden süresi.", + "look-feel-transition-duration-label": "Geçiş süresi", + "look-feel-transition-type-description": "Duvar kâğıtları arasında geçiş yaparken animasyon türü.", + "look-feel-transition-type-label": "Geçiş türü", + "settings-clear-cache-button": "Önbelleği temizle", + "settings-clear-cache-description": "Önbelleğe alınmış yeniden boyutlandırılmış duvar kağıtlarını temizleyerek disk alanını boşaltın.", + "settings-clear-cache-label": "Duvar kağıdı önbelleği", + "settings-clear-cache-toast": "Duvar kağıdı önbelleği temizlendi", + "settings-desc": "Duvar kâğıtlarının nasıl yönetileceğini ve görüntüleneceğini kontrol edin.", + "settings-enable-management-description": "Duvar kâğıtlarını Noctalia ile yönetin. Başka bir uygulamayı kullanmayı tercih ediyorsanız işareti kaldırın.", + "settings-enable-management-label": "Duvar kâğıdı yönetimini etkinleştir", + "settings-enable-overview-description": "Genel bakış ekranına bulanık ve karartılmış bir duvar kâğıdı uygular.", + "settings-enable-overview-label": "Genel bakış duvar kâğıdını etkinleştir", + "settings-folder-description": "Ana duvar kâğıdı klasörünüzün yolu.", + "settings-folder-label": "Duvar kâğıdı klasörü", + "settings-hide-wallpaper-filenames-tooltip-hide": "Dosya adlarını gizleme", + "settings-hide-wallpaper-filenames-tooltip-show": "Dosya adlarını göster", + "settings-monitor-specific-description": "Her ekran için farklı bir duvar kâğıdı klasörü ayarlayın.", + "settings-monitor-specific-label": "Ekrana özgü dizinler", + "settings-monitor-specific-tooltip": "Ekrana özgü duvar kâğıdı klasörü", + "settings-overview-blur-strength-description": "Genel bakışa bulanıklık gücünü uygular.", + "settings-overview-blur-strength-label": "Genel bakış bulanıklık gücü", + "settings-overview-tint-description": "Ton gücünü genel bakışa uygular.", + "settings-overview-tint-label": "Genel Bakış ton gücü", + "settings-recursive-search-description": "Duvar kâğıdı dizinin alt klasörlerinde de duvar kâğıdı arayın.", + "settings-recursive-search-label": "Alt klasörleri ara", + "settings-select-monitor-folder": "Ekran duvar kâğıdı klasörünü seç", + "settings-selector-description": "Duvar kâğıdınızı seçin.", + "settings-selector-position-description": "Duvar kâğıdı seçici panelinin nerede görüneceğini seçin.", + "settings-show-hidden-files-tooltip-hide": "Gizli dosyaları gizle", + "settings-show-hidden-files-tooltip-show": "Gizli dosyaları göster", + "settings-title": "Duvar kâğıdı ayarları", + "settings-use-original-images-description": "Duvar kağıtlarını görüntülemeden önce yeniden boyutlandırmayı atla. Disk alanından tasarruf sağlar ve duvar kağıdı değişiklikleri sırasında CPU kullanımını azaltır, ancak çok büyük görüntüler için daha fazla bellek kullanabilir.", + "settings-use-original-images-label": "Orijinal görselleri kullan", + "settings-view-mode-description": "Duvar kağıtlarının dizininizden nasıl görüntüleneceğini seçin.", + "settings-view-mode-label": "Görüntüleme modu", + "view-mode-browse": "Dizinlere göz at", + "view-mode-cycle-tooltip": "Görünüm modu: {mode} (değiştirmek için tıklayın)", + "view-mode-recursive": "Düzleştirilmiş alt dizinler", + "view-mode-single": "Kök dizin" + } + }, + "placeholders": { + "add-new-keybind": "Yeni tuş ataması ekle", + "command-example": "echo \"Hello World\"", + "enter-command": "Çalıştırılacak komutu girin (uygulama veya özel komut)", + "enter-ipc-identifier": "IPC komutları için benzersiz bir tanımlayıcı girin", + "enter-path": "Yolu girin...", + "enter-text-to-collapse": "örn. 'nothing is playing'. Desenler için /regex/ kullanın.", + "enter-tooltip": "Araç ipucunu girin", + "enter-width-pixels": "Genişliği piksel olarak girin", + "keybind-recording": "Tuş kombinasyonu kaydediliyor...", + "search": "Ara...", + "search-icons": "örn. noctalia, niri, battery, cloud", + "search-launcher": "Girişleri arayın... veya > komutları için kullanın", + "search-wallhaven": "Wallhaven'da ara...", + "search-wallpapers": "Duvar kâğıtlarını filtrelemek için yazın..." + }, + "positions": { + "bottom": "Alt", + "bottom-center": "Alt merkez", + "bottom-left": "Sol alt", + "bottom-right": "Sağ alt", + "center": "Merkez", + "center-left": "Sol merkez", + "center-right": "Sağ merkez", + "close-to-bar": "Çubuğa yakın kapat", + "follow-bar": "Çubuğu takip et", + "left": "Sol", + "right": "Sağ", + "top": "Üst", + "top-center": "Üst merkez", + "top-left": "Sol üst", + "top-right": "Sağ üst" + }, + "session-menu": { + "action-in-seconds": "{seconds} saniyede {action}...", + "cancel-timer": "Zamanlayıcıyı iptal et", + "title": "Oturum menüsü" + }, + "setup": { + "all-done": "Tamamlandı!", + "appearance": { + "subheader": "Koyu modu ve renk kaynaklarını (duvar kağıdı veya önceden tanımlanmış) seçin." + }, + "customize": { + "header": "Deneyiminizi özelleştirin", + "subheader": "Araç çubuğu konumunu, yoğunluğunu, ölçeğini ve daha fazlasını ayarlayın." + }, + "skip-setup": "Kurulumu Atla", + "telemetry-wizard-done": "Anlaşıldı!", + "telemetry-wizard-note": "Kontrol sizde — istediğiniz zaman ayarlar'dan etkinleştirebilir veya devre dışı bırakabilirsiniz", + "telemetry-wizard-subtitle": "Noctalia'yı geliştirmeye yardımcı olmak için anonim analizler ekledik", + "telemetry-wizard-title": "Gizlilik Güncellemesi", + "wallpaper": { + "choose-dir": "Duvar kâğıdı görüntülerinizi içeren bir dizin seçin", + "dir-browse": "Duvar kâğıdı klasörüne göz at", + "dir-description": "Duvar kâğıtlarınızı içeren klasörü seçin.", + "dir-label": "Duvar kâğıdı dizini", + "dir-select-title": "Duvar kâğıdı klasörünü seç", + "header": "Duvar kâğıdınızı seçin", + "no-dir": "Duvar kâğıdı dizini seçilmedi", + "no-valid": "Şurada geçerli görüntü dosyası bulunamadı: {dir}", + "none-in-dir": "Dizinde duvar kâğıdı bulunamadı", + "preview-error": "Görüntü yüklenemedi", + "select-prompt": "Aşağıdan bir duvar kâğıdı seçin", + "subheader": "Güzel bir arka planla ortamı ayarlayın." + }, + "welcome": "Hoş Geldiniz", + "welcome-note": "Başlamanız için birkaç temel ayar — tam seçenekler ayarlar'da", + "welcome-subtitle": "Masaüstünüzü size özel yapalım", + "welcome-title": "Noctalia'ya hoş geldiniz!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({koordinatlar})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "Hiçbir medya oynatıcı algılanmadı", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "Bilinmeyen uygulama", + "uptime": "Çalışma süresi: {uptime}", + "user-requested": "Kullanıcı istedi", + "welcome-back": "Hoş geldiniz,", + "widget-settings-title": "{widget} ayarları" + }, + "system-monitor": { + "core-usage": "Çekirdek {id} kullanımı", + "cpu-temp": "CPU sıcaklığı", + "cpu-usage": "CPU kullanımı", + "disk": "Disk", + "download-speed": "İndirme hızı", + "gpu-temp": "GPU sıcaklığı", + "load-average": "Ortalama yük", + "title": "Sistem i̇zleyici", + "upload-speed": "Yükleme hızı" + }, + "toast": { + "airplane-mode": { + "description": "Tüm kablosuz iletişimleri devre dışı bırak.", + "title": "Uçak modu" + }, + "battery": { + "critical": "Kritik pil seviyesi", + "critical-desc": "Pil {percent}% — lütfen şarj cihazını hemen bağlayın", + "low": "Düşük batarya", + "low-desc": "Pil {percent}% seviyesinde — lütfen şarj cihazını bağlayın" + }, + "bluetooth": { + "address-copied": "Adres panoya kopyalandı", + "auto-connect-disabled": "Otomatik bağlantı devre dışı", + "auto-connect-enabled": "Otomatik bağlanma etkin", + "auto-connecting": "{count} cihaza bağlanılıyor...", + "confirm-code": "Diğer cihazdaki {value} kodunu onayla.", + "connect-failed": "Cihaza bağlanılamadı", + "disconnect-failed": "Cihazla bağlantı kesilemedi", + "discoverable-change-failed": "Keşfedilebilir durum değiştirilemedi", + "discoverable-disabled": "Keşfedilebilir devre dışı", + "discoverable-enabled": "Keşfedilebilir etkinleştirildi", + "display-code": "Diğer cihazda {value} kodunu girin.", + "forget-failed": "Cihazı unutma başarısız oldu", + "pair-failed": "Cihaz eşleştirilemedi", + "passkey-required": "Cihaz tarafından istenen geçiş anahtarı.", + "pincode-required": "Cihaz tarafından PIN kodu gerekiyor.", + "state-change-failed": "Bluetooth durumu değiştirilemedi" + }, + "clipboard": { + "long-text": "Uzun metin", + "unavailable": "Panoya geçmişi kullanılamıyor", + "unavailable-desc": "'cliphist' uygulaması kurulu değil. Lütfen panoya geçmişi özelliklerini kullanmak için kurun" + }, + "custom-command-failed": { + "description": "Komut başarısız oldu: {command}\\nÇıkış kodu: {code}", + "title": "Özel komut başarısız oldu" + }, + "do-not-disturb": { + "disabled": "'Rahatsız etme' devre dışı", + "disabled-desc": "Tüm bildirimler gösteriliyor", + "enabled": "'Rahatsız etme' etin", + "enabled-desc": "Bu bildirimleri geçmişinizde bulacaksınız" + }, + "donation-opened": "Bağış sayfası tarayıcınızda açıldı", + "internet-limited": "İnternetsiz bağlandı", + "keyboard-layout": { + "changed": "Klavye düzeni {layout} olarak değiştirildi", + "title": "Klavye" + }, + "missing-control-center": { + "description": "Kontrol merkezi araç takımı araç çubuğundan kaldırıldı. Araç çubuğundan tekrar erişmek için araç takımını yeniden eklemeniz gerekecek. Araç çubuğuna sağ tıklayarak da açabilirsiniz", + "label": "Son kontrol merkezi araç takımı kaldırıldı" + }, + "night-light": { + "forced": "Zorla etkinleştirme", + "normal": "Normal mod", + "not-installed": "wlsunset kurulu değil" + }, + "noctalia-performance": { + "disabled": "Performans modu devre dışı bırakıldı", + "enabled": "Performans modu etkinleştirildi", + "label": "Noctalia Performans" + }, + "power-profile": { + "changed": "Güç profili değişti", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "Şablonlar işlenirken bir hatayla karşılaşıldı", + "title": "Şablon işleme başarısız oldu" + }, + "vpn": { + "connected": "'{name}' ile bağlantı kuruldu", + "disconnected": "'{name}' bağlantısı kesildi" + }, + "wallpaper-colors": { + "disabled": "Duvar kâğıdı renkleri devre dışı", + "enabled": "Duvar kâğıdı renkleri etkin", + "label": "Duvar kâğıdı renkleri oluşturma" + }, + "wifi": { + "connected": "'{ssid}' ile bağlandı", + "connection-failed": "Bağlantı başarısız", + "connection-timeout": "Bağlantı zaman aşımı", + "disconnected": "'{ssid}' bağlantısı kesildi", + "incorrect-password": "Yanlış şifre", + "network-not-found": "Ağ bulunamadı" + } + }, + "tooltips": { + "add-widget": "Araç takımı ekle", + "auto-connect": "Bu cihaz için otomatik bağlanmayı aç/kapa", + "bluetooth-auto-connect-off": "Otomatik bağlanma kapalı", + "bluetooth-auto-connect-on": "Otomatik bağlanma açık", + "bluetooth-devices": "Bluetooth cihazları", + "brightness-at": "Parlaklık: %{brightness}", + "click-to-start-recording": "Ekran kaydedici (kaydı başlat)", + "click-to-stop-recording": "Ekran kaydedici (kaydı durdur)", + "collapse": "Kenar çubuğunu daralt", + "copy-address": "Adresi kopyala", + "delete-notification": "Bildiriyi sil", + "dismiss-notification": "Bildirimi kapat", + "do-not-disturb-enabled": "Rahatsız etme", + "expand": "Kenar çubuğunu genişlet", + "forget-network": "Ağı unut", + "grid-view": "Izgara görünümü", + "hidden-files-hide": "Gizli dosyalar", + "hide-unnamed-devices": "Adsız cihazları gizle", + "home": "Ana dizin", + "input-muted": "Giriş sesini kapat/aç", + "keep-awake": "Uyanık kal", + "keyboard-layout": "{layout} klavye düzeni", + "list-view": "Liste görünümü", + "manage-vpn": "VPN bağlantıları", + "manage-wifi": "Kablosuz bağlantı", + "max-widgets-reached": "Maksimum araç sayısına ulaşıldı", + "microphone-volume-at": "Mikrofon sesi: %{volume}", + "move-to-section": "{section} bölümüne git", + "mute": "Sessiz", + "next-media": "Sonraki parça", + "next-month": "Sonraki ay", + "night-light-not-installed": "Gece ışığı (mevcut değil)", + "noctalia-performance-enabled": "Noctalia performans modu", + "open-annotation-tool": "Açıklama aracıyla aç", + "open-control-center": "Kontrol merkezi", + "open-notification-history-enable-dnd": "Bildirim geçmişi", + "open-settings": "Ayarlar", + "open-tray-dropdown": "Sistem tepsisi", + "output-muted": "Çıktı sesini kapat/aç", + "power-profile": "{profile} güç profili", + "previous-media": "Önceki parça", + "previous-month": "Önceki ay", + "refresh-devices": "Cihazları yenile", + "refresh-wallhaven": "Wallhaven sonuçlarını yenile", + "refresh-wallpaper-list": "Duvar kâğıdı listesini yenile", + "remove-widget": "Araç takımını kaldır", + "search-close": "Aramayı kapat", + "session-menu": "Oturum menüsü", + "show-all-devices": "Tüm cihazları göster", + "switch-to-dark-mode": "Koyu mod", + "switch-to-light-mode": "Açık mod", + "unmute": "Sessizliği aç", + "up": "Üst dizin", + "volume-at": "Çıkış sesi: %{volume}", + "wallpaper-selector": "Duvar kâğıdı seçici" + }, + "wallpaper": { + "browse": { + "empty-directory": "Bu dizin boş.", + "go-root": "Duvar kağıdı köküne git", + "go-up": "Üst klasöre git", + "go-up-hint": "Yukarı gitmek için geri düğmesini kullanın." + }, + "configure-directory": "Resimler içeren duvar kâğıdı dizininizi yapılandırın.", + "fill-modes": { + "crop": "Kırp (Doldur)", + "fit": "Sığ (İçer)", + "repeat": "Tekrarla (Döşe)", + "stretch": "Ger" + }, + "no-match": "Eşleşme bulunamadı.", + "no-wallpaper": "Duvar kâğıdı bulunamadı.", + "panel": { + "apikey-help": "NSFW içeriğe erişmek için bir API anahtarı gereklidir.", + "apikey-label": "API anahtarı", + "apikey-managed-by-env": "NOCTALIA_WALLHAVEN_API_KEY ortam değişkeni aracılığıyla yönetilir.", + "apikey-placeholder": "Wallhaven API anahtarınızı girin", + "appearance-dark-tab": "Koyu", + "appearance-light-tab": "Açık", + "categories-anime": "Anime", + "categories-label": "Kategoriler", + "categories-people": "İnsanlar", + "color-extraction-disabled": "Duvar kağıdı renk çıkarma kullan", + "color-extraction-enabled": "Önceden tanımlanmış renk şemalarını kullan", + "header-devices-apply-all-tooltip": "Monitör başına duvar kağıdı seç", + "header-devices-per-monitor-tooltip": "Tüm monitörlere uygula", + "header-link-light-dark-tooltip": "Tema değiştirirken duvar kağıdını koru", + "header-separate-light-dark-tooltip": "Temayla birlikte duvar kağıdını otomatik değiştir", + "header-sun-linked-tooltip": "Açık ve koyu duvar kağıtlarını ayır", + "header-sun-separate-tooltip": "Açık ve koyu için aynı duvar kağıdı", + "order-asc": "Artan", + "order-desc": "Azalan", + "order-label": "Sıra", + "purity-label": "İçerik filtresi", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Sketchy", + "ratios-all-portrait": "Tüm dikey", + "ratios-all-wide": "Tüm geniş", + "ratios-any": "Herhangi biri", + "ratios-label": "En boy oranı", + "resolution-atleast": "En az", + "resolution-exact": "Tam", + "resolution-label": "Çözünürlük", + "resolution-mode-label": "Mod", + "solid-color-tooltip": "Düz renk arka plan", + "sort-date-asc": "En eskiden en yeniye sırala", + "sort-date-desc": "En yeniden en eskiye sırala", + "sort-name-asc": "Ada göre sırala (A-Z)", + "sort-name-desc": "Ada göre sırala (Z-A)", + "sort-random": "Rastgele sırala", + "sorting-date-added": "Eklenme tarihi", + "sorting-favorites": "Favoriler", + "sorting-label": "Sırala", + "sorting-relevance": "Alaka", + "sorting-toplist": "En iyi liste", + "sorting-views": "Görüntülemeler", + "source-label": "Kaynak", + "source-wallhaven": "Wallhaven", + "title": "Duvar kâğıdı seçici", + "wallhaven-settings-title": "Wallhaven ayarları" + }, + "transitions": { + "disc": "Disk", + "fade": "Belirsizleşme", + "honeycomb": "Petek", + "pixelate": "Pikselleştir", + "stripes": "Çizgiler", + "wipe": "Silme" + }, + "try-different-search": "Farklı bir arama sorgusu deneyin.", + "wallhaven": { + "loading": "Duvar kâğıtları yükleniyor...", + "no-results": "Duvar kâğıdı bulunamadı. Farklı bir arama sorgusu deneyin.", + "page": "{current} / {total}", + "page-prefix": "Sayfa", + "page-suffix": "{total} taneden" + } + }, + "weather": { + "clear-sky": "Açık hava", + "drizzle": "Yağmurlu", + "fog": "Sisli", + "mainly-clear": "Genellikle açık", + "overcast": "Bulutlu", + "partly-cloudy": "Parçalı bulutlu", + "rain-showers": "Sağanak yağmur", + "snow": "Karlı", + "thunderstorm": "Fırtına" + }, + "widgets": { + "color-picker": { + "palette-description": "Önceden tanımlı renkler geniş yelpazesinden seçin.", + "palette-label": "Palet", + "palette-theme-colors": "Temanızın renk paletine hızlı erişim.", + "title": "Renk seçici" + }, + "datetime-tokens": { + "ampm-lowercase": "Küçük harfli am/pm", + "ampm-uppercase": "Büyük harfli AM/PM", + "common-12hour-time-minutes": "12 saatlik zaman ile dakika", + "common-24hour-time-minutes": "24 saatlik zaman ile dakika", + "common-24hour-time-seconds": "24 saatlik zaman ile saniye", + "common-european-date": "Avrupa tarih formatı", + "common-iso-date": "ISO tarih formatı", + "common-us-date": "ABD tarih formatı", + "common-weekday-date": "Tarih ile hafta günü", + "common-weekday-month-day": "Hafta günü, ay ve gün", + "day-abbreviated": "Kısaltılmış gün adı", + "day-full": "Tam gün adı", + "day-leading-zero": "Sıfır ile başlayan gün (01-31)", + "day-no-leading-zero": "Sıfır olmadan gün (1-31)", + "hour-leading-zero": "Sıfır ile başlayan saat (00-23) — 24 saat formatı", + "hour-no-leading-zero": "Sıfır olmadan saat (0-23) — 24 saat formatı", + "minute-leading-zero": "Sıfır ile başlayan dakika (00-59)", + "minute-no-leading-zero": "Sıfır olmadan dakika (0-59)", + "month-abbreviated": "Kısaltılmış ay adı", + "month-full": "Tam ay adı", + "month-number-leading-zero": "Sıfır ile başlayan sayı olarak ay (01-12)", + "month-number-no-zero": "Sıfır olmadan sayı olarak ay (1-12)", + "second-leading-zero": "Sıfır ile başlayan saniye (00-59)", + "second-no-leading-zero": "Sıfır olmadan saniye (0-59)", + "timezone-abbreviation": "Saat dilimi kısaltması", + "year-four-digit": "Dört haneli sayı olarak yıl", + "year-two-digit": "İki haneli sayı olarak yıl (00-99)" + }, + "file-picker": { + "item": "öğe", + "items": "öğeler", + "search-placeholder": "Dosya ve klasörlerde ara...", + "select-current": "Mevcudu seç", + "select-file": "Dosya seç", + "select-folder": "Klasör seç", + "selected": "Seçilen:", + "title": "Dosya seçici" + }, + "file-picker-title": "Bir dosya seçin", + "icon-picker": { + "title": "Simge seçici" + }, + "tooltip-placeholder": "Yer tutucu" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "Anonim Kimlik", + "ca-cert": "CA Sertifikası", + "eap-method": "EAP Yöntemi", + "password": "Kullanıcı Şifresi", + "phase2-auth": "Faz 2 Kimlik Doğrulama", + "username": "Kimlik / Kullanıcı Adı" + }, + "panel": { + "action-required": "Eylem gerekiyor", + "add-hidden-network": "Gizli ağ ekle", + "add-network": "Ağ ekle", + "available-interfaces": "Kullanılabilir arayüzler", + "available-networks": "Kullanılabilir ağlar", + "disabled": "Kablosuz bağlantı devre dışı", + "disconnecting": "Bağlantı kesiliyor...", + "dns": "DNS", + "enable-message": "Mevcut ağları görmek için kablosuz bağlantıyı etkinleştirin.", + "enter-password": "Parola girin...", + "forget": "Unut", + "forget-network": "Bu ağı unut?", + "forgetting": "Unutuluyor...", + "hidden-network": "Gizli ağ", + "interface": "Ağ arayüzü", + "internet-connected": "İnternete bağlı", + "internet-limited": "İnternet yok", + "internet-status": "İnternet durumu", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "Bilinen ağlar", + "link-speed": "Bağlantı hızı", + "network-name-ssid": "Ağ adı (SSID)", + "no-ethernet-devices": "Ethernet aygıtı algılanmadı", + "no-networks": "Ağ bulunamadı", + "saved": "Kaydedildi", + "scan-again": "Yeniden tara", + "searching": "Yakındaki ağlar aranıyor...", + "security-open": "Aç", + "security-wep": "WEP", + "security-wpa": "WPA/WPA2/WPA3 Kişisel", + "security-wpa-ent": "WPA Enterprise", + "security-wpa2-ent": "WPA2 Enterprise", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 Enterprise", + "show-password": "Şifreyi göster", + "title": "Kablosuz bağlantı" + }, + "signal": { + "excellent": "Mükemmel", + "fair": "Orta", + "good": "İyi", + "poor": "Kötü", + "weak": "Zayıf" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/uk-UA.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/uk-UA.json new file mode 100644 index 0000000..073933f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/uk-UA.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "Очистити історію", + "close-app": "Закрити {app}", + "connect-vpn": "Підключитися до {name}", + "cycle-visualizer": "Візуалізатор циклів", + "disable-bluetooth": "Вимкнути Bluetooth", + "disable-dnd": "Вимкнути режим 'Не турбувати'", + "disable-wifi": "Вимкнути Wi-Fi", + "disconnect-vpn": "Відключити {name}", + "dock-settings": "Налаштування дока", + "enable-bluetooth": "Увімкнути Bluetooth", + "enable-dnd": "Увімкнути режим 'Не турбувати'", + "enable-wifi": "Увімкнути Wi-Fi", + "launcher-settings": "Налаштування панелі запуску", + "lower-to-bottom": "Опустити донизу", + "open-calendar": "Відкрити календар", + "open-display-settings": "Параметри дисплея", + "open-launcher": "Відкрити панель запуску", + "open-settings": "Відкрити налаштування", + "raise-to-top": "Підняти вгору", + "random-wallpaper": "Випадкові шпалери", + "run-custom-command": "Виконати власну команду", + "show-active-only": "Показувати лише активні", + "show-all": "Показати все", + "toggle-mute": "Увімкнути/вимкнути звук", + "widget-settings": "Налаштування віджета" + }, + "authentication": { + "error": "Помилка автентифікації", + "failed": "Невдала автентифікація" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Застосувати кольори теми до значка активного вікна.", + "hide-mode-description": "Керує поведінкою віджета, коли немає активного вікна.", + "scrolling-mode-description": "Керування, коли увімкнено прокрутку тексту для довгих заголовків вікон.", + "show-app-icon-description": "Відображати значок застосунка біля заголовка вікна.", + "show-app-icon-label": "Показувати значок застосунка", + "show-app-text-description": "Відобразити заголовок активного вікна.", + "show-app-text-label": "Показувати заголовок вікна" + }, + "audio-visualizer": { + "color-name-description": "Вибрати колір для візуалізатора.", + "color-name-label": "Колір заповнення", + "height-description": "Користувацька ширина компонента.", + "hide-when-idle-description": "Коли увімкнено, візуалізатор приховано, якщо плеєр активно не відтворює.", + "hide-when-idle-label": "Приховати, коли медіа не відтворюється", + "width-description": "Власна ширина компонента." + }, + "battery": { + "device-default": "Пристрій відображення за замовчуванням", + "device-description": "Виберіть пристрій з акумулятором для відображення.", + "device-label": "Пристрій живлення від батареї", + "display-mode-description": "Виберіть, як відображається батарея на панелі.", + "display-mode-graphic": "Графічна батарея", + "display-mode-graphic-clean": "Графічна батарея (без %)", + "display-mode-icon-always": "Іконка - Завжди показувати %", + "display-mode-icon-hover": "Іконка - Показувати при наведенні", + "display-mode-icon-only": "Лише значок", + "hide-if-idle-description": "Приховати віджет, коли акумулятор не заряджається та не розряджається.", + "hide-if-idle-label": "Приховати, коли неактивний", + "hide-if-not-detected-description": "Приховати віджет, коли в системі не виявлено акумулятор.", + "hide-if-not-detected-label": "Приховати, якщо не виявлено", + "low-battery-threshold-label": "Поріг попередження про низький заряд батареї", + "show-noctalia-performance-description": "Відображати перемикач режиму продуктивності Noctalia в панелі батареї.
Вимикає тіні та анімації в Noctalia для зменшення використання ресурсів.", + "show-noctalia-performance-label": "Показувати перемикач Продуктивність Noctalia", + "show-power-profile-description": "Відображати вибір профілю живлення в панелі батареї.", + "show-power-profile-label": "Показувати елементи керування профілем живлення" + }, + "brightness": { + "apply-all-description": "Змінювати яскравість для всіх моніторів при використанні колеса прокрутки.", + "apply-all-label": "Застосувати зміни прокрутки до всіх моніторів" + }, + "clock": { + "clock-display-description": "Налаштуйте відображення годинника, додавши токени зі списку нижче. Для використання 12-годинного формату ви повинні включити токен 'AP'.", + "clock-display-label": "Відображення годинника", + "custom-font-description": "Вибрати власний шрифт для відображення годинника.", + "custom-font-label": "Власний шрифт", + "custom-font-placeholder": "Вибрати власний шрифт...", + "custom-font-search-placeholder": "Пошук шрифтів...", + "horizontal-bar-description": "Порада: Використовуйте \\n для створення розриву рядка.", + "horizontal-bar-label": "Горизонтальна панель", + "preview": "Попередній перегляд", + "tooltip-format-description": "Рядок формату для підказки, що відображається при наведенні на годинник. Залиште порожнім, щоб використовувати стандартну підказку.", + "tooltip-format-label": "Формат підказки", + "use-custom-font-description": "Замінити стандартний вибір шрифту власним шрифтом для годинника.", + "use-custom-font-label": "Використовувати власний шрифт", + "use-monospaced-font-description": "Коли увімкнено, годинник використовуватиме моноширинний шрифт.", + "use-monospaced-font-label": "Використовувати моноширинний шрифт", + "vertical-bar-description": "Використовуйте пробіл для розділення кожної частини на новий рядок.", + "vertical-bar-label": "Вертикальна панель" + }, + "control-center": { + "browse-file": "Огляд файлу", + "browse-library": "Огляд бібліотеки", + "color-selection-description": "Застосовує кольори теми до іконок.", + "enable-colorization-description": "Увімкнути розфарбовування іконки із застосуванням кольорів теми.", + "icon-description": "Вибрати значок з бібліотеки або власний файл.", + "select-custom-icon": "Вибрати власний значок", + "use-distro-logo-description": "Використовувати логотип вашого дистрибутива замість власного значка.", + "use-distro-logo-label": "Використовувати логотип дистрибутива замість значка" + }, + "custom-button": { + "collapse-condition-description": "Якщо вихідний текст збігається з цим значенням, кнопка згорнеться.", + "collapse-condition-label": "Умова згортання", + "color-selection-description": "Застосувати кольори теми до значка та тексту.", + "default-tooltip": "Користувацька кнопка, налаштуйте в налаштуваннях", + "display-command-output-description": "Введіть команду для запуску з регулярним інтервалом. Перший рядок її виведення відображатиметься як текст.", + "display-command-output-label": "Відображати виведення команди", + "display-command-output-stream-description": "Введіть команду для безперервного запуску.", + "dynamic-text": "Динамічний текст", + "enable-colorization-description": "Увімкнути розфарбовування для значка та тексту власної кнопки, застосовуючи кольори теми.", + "enable-colorization-label": "Увімкнути розфарбовування", + "general-tooltip-text-description": "Власний текст для відображення у підказці кнопки.", + "general-tooltip-text-label": "Користувацький текст підказки", + "hide-mode-always-expanded": "Завжди розширено", + "hide-mode-description": "Контролює видимість віджета, коли команда не має виведення.", + "hide-mode-expand-with-output": "Розширити, якщо є виведення", + "hide-mode-label": "Режим приховування", + "hide-mode-max-transparent": "Максимально розширено, але прозоро", + "icon-description": "Вибрати значок з бібліотеки.", + "ipc-identifier-description": "Унікальний ідентифікатор для команд IPC. Використовуйте цей ідентифікатор з 'qs -c noctalia-shell ipc call cb [дія] [ідентифікатор]', щоб керувати цією кнопкою через IPC.", + "ipc-identifier-label": "Ідентифікатор IPC", + "left-click-description": "Команда для виконання при лівому кліку на кнопку.", + "left-click-label": "Лівий клік", + "left-click-update-text": "Оновити текст, що відображається, при натисканні лівою кнопкою миші", + "max-text-length-horizontal-description": "Максимальна кількість символів для відображення в горизонтальній панелі (0 щоб приховати текст).", + "max-text-length-horizontal-label": "Макс. довжина тексту (горизонтально)", + "max-text-length-vertical-description": "Максимальна кількість символів для відображення в вертикальній панелі (0 щоб приховати текст).", + "max-text-length-vertical-label": "Макс. довжина тексту (вертикально)", + "middle-click-description": "Команда для виконання при середньому кліку на кнопку.", + "middle-click-label": "Середній клік", + "middle-click-update-text": "Оновити відображуваний текст при натисканні середньою кнопкою миші", + "parse-json-description": "Розбирати виведення команди як JSON-об'єкт для динамічного встановлення тексту та значка.", + "parse-json-label": "Розбирати виведення як JSON", + "refresh-interval-description": "Інтервал у мілісекундах.", + "refresh-interval-label": "Інтервал оновлення", + "right-click-description": "Команда для виконання при правому кліку на кнопку.", + "right-click-label": "Правий клік", + "right-click-update-text": "Оновити текст, що відображається, при натисканні правою кнопкою миші", + "show-exec-tooltip-description": "Показувати підказки з деталями команд (лівий/правий/середній клік, колесо).", + "show-exec-tooltip-label": "Показувати підказки команд", + "show-icon-description": "Перемикає видимість значка віджета.", + "show-icon-label": "Показати значок", + "show-text-tooltip-description": "Показувати підказки з виводом текстової команди.", + "show-text-tooltip-label": "Показувати динамічні текстові підказки", + "text-stream-description": "Потокові рядки з команди відображатимуться як текст на кнопці.", + "text-stream-label": "Потік", + "wheel-description": "Команда для виконання при використанні колеса прокрутки.
Використовуйте $delta для дельти колеса прокрутки в команді.", + "wheel-down": "Колесо вниз", + "wheel-down-description": "Команда для виконання при прокрутці колеса вниз.", + "wheel-down-label": "Команда прокрутки колеса вниз", + "wheel-label": "Колесо прокрутки", + "wheel-mode-separate-description": "Увімкнути окремі команди для колеса прокрутки вгору та вниз.", + "wheel-mode-separate-label": "Окремі команди колеса прокрутки", + "wheel-up": "Колесо вгору", + "wheel-up-description": "Команда для виконання при прокрутці колеса вгору.", + "wheel-up-label": "Команда прокрутки колеса вгору", + "wheel-update-text": "Оновити відображуваний текст при прокрутці" + }, + "keyboard-layout": { + "show-icon-description": "Показувати піктограму розкладки клавіатури." + }, + "lock-keys": { + "hide-when-off-description": "Приховувати індикатор, коли клавіша неактивна.", + "hide-when-off-label": "Приховувати коли вимкнено", + "show-caps-lock-description": "Відображати стан caps lock.", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "Відображати стан num lock.", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "Відображати стан scroll lock.", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "Увімкнути компактне розташування для панелі медіаплеєра.", + "compact-mode-label": "Компактний режим", + "hide-mode-description": "Керує поведінкою віджета, коли медіа не відтворюється.", + "max-width-description": "Встановлює максимальний горизонтальний розмір віджета. Віджет зменшується для коротшого вмісту.", + "no-active-player": "Немає активного плеєра", + "panel-section-description": "Налаштуйте зовнішній вигляд та поведінку панелі медіаплеєра.", + "panel-section-label": "Панель медіаплеєра", + "scrolling-mode-description": "Керування, коли увімкнено прокрутку тексту для довгих назв треків.", + "scrolling-mode-label": "Режим прокрутки", + "show-album-art-description": "Відображати обкладинку альбому для поточного треку.", + "show-album-art-label": "Показувати обкладинку альбому", + "show-artist-first-description": "Відображати виконавець - назва замість назва - виконавець.", + "show-artist-first-label": "Показувати спочатку виконавця", + "show-progress-ring-description": "Відображати круговий індикатор прогресу, що показує просування треку.", + "show-progress-ring-label": "Показувати кільце прогресу", + "show-visualizer-description": "Відображати аудіовізуалізатор під час відтворення музики.", + "show-visualizer-label": "Показувати візуалізатор", + "use-fixed-width-description": "Коли увімкнено, віджет завжди використовуватиме максимальну ширину замість динамічного налаштування до вмісту.", + "use-fixed-width-label": "Використовувати фіксовану ширину", + "visualizer-type-description": "Виберіть стиль аудіовізуалізатора для відображення.", + "visualizer-type-label": "Тип візуалізатора" + }, + "notification-history": { + "hide-widget-when-zero-description": "Приховувати значок сповіщень, якщо сповіщень немає.", + "hide-widget-when-zero-label": "Приховувати значок без сповіщень", + "hide-widget-when-zero-unread-description": "Приховувати значок сповіщень, якщо немає непрочитаних.", + "hide-widget-when-zero-unread-label": "Приховувати значок без непрочитаних", + "show-unread-badge-description": "Відображати значок з кількістю непрочитаних сповіщень.", + "show-unread-badge-label": "Показувати значок непрочитаних", + "unread-badge-color-description": "Виберіть колір для значка непрочитаних сповіщень.", + "unread-badge-color-label": "Колір значка непрочитаних" + }, + "section-editor": { + "placeholder": "Виберіть віджет для додавання...", + "search-placeholder": "Пошук віджета..." + }, + "spacer": { + "width-description": "Ширина відступу в пікселях." + }, + "system-monitor": { + "compact-mode-description": "Відображати статистику у вигляді міні-діаграм замість текстових значень. Запобігає зсуву макета.", + "compact-mode-label": "Компактний режим", + "cpu-cores-description": "Відображати використання ядер CPU окремо.", + "cpu-cores-label": "Ядра CPU", + "cpu-frequency-description": "Відобразити поточну тактову частоту CPU у ГГц.", + "cpu-frequency-label": "Показати частоту CPU", + "cpu-temperature-description": "Показувати показники температури ЦП, якщо доступно.", + "cpu-temperature-label": "Температура ЦП", + "cpu-usage-description": "Відображати поточний відсоток використання ЦП.", + "cpu-usage-label": "Використання ЦП", + "disk-path-description": "Виберіть точку монтування диска для моніторингу.", + "disk-path-label": "Шлях до диска", + "gpu-temperature-description": "Показувати показники температури GPU, якщо доступні.", + "load-average-description": "Показати середнє навантаження системи.", + "load-average-label": "Середнє навантаження", + "memory-percentage-description": "Показувати використання пам'яті у відсотках замість абсолютних значень.", + "memory-percentage-label": "Пам'ять у відсотках", + "memory-usage-description": "Відображати інформацію про поточне використання оперативної пам'яті.", + "memory-usage-label": "Використання пам'яті", + "network-traffic-description": "Відображати швидкість прийому та передачі даних.", + "network-traffic-label": "Мережевий трафік", + "storage-as-percentage-description": "Показувати місце на диску у відсотках замість абсолютних значень.", + "storage-as-percentage-label": "Диск у відсотках", + "storage-available-description": "Показує, скільки місця на диску доступно, замість того, скільки використовується.", + "storage-available-label": "Доступне місце на диску", + "storage-usage-description": "Показувати інформацію про використання дискового простору.", + "storage-usage-label": "Використання сховища", + "swap-usage-description": "Показати використання swap-пам'яті.", + "swap-usage-label": "Використання swap", + "use-monospace-font-description": "Використовуйте моноширинний шрифт для забезпечення сталої ширини символів.", + "use-monospace-font-label": "Моноширинний шрифт", + "use-padding-description": "Додає до текстових значень початкові пробіли, щоб запобігти зсуву макета.", + "use-padding-description-disabled-monospace-font": "Для цієї функції потрібен моноширинний шрифт.", + "use-padding-description-disabled-vertical": "Вертикальна панель завдань не підтримує текстові відступи.", + "use-padding-label": "Відступ Тексту" + }, + "taskbar": { + "colorize-icons-description": "Застосувати кольори теми до значків панелі завдань.", + "hide-mode-description": "Керує поведінкою віджета, коли немає відповідних вікон.", + "hide-mode-label": "Режим приховування", + "icon-scale-description": "Встановлює коефіцієнт масштабування для значків панелі завдань.", + "icon-scale-label": "Масштабування іконок", + "max-width-description": "Максимальна ширина панелі завдань у відсотках від ширини екрана.", + "max-width-label": "Максимальна ширина", + "only-active-workspaces-description": "Показувати тільки застосунки з активних робочих просторів.", + "only-active-workspaces-label": "Тільки з активних робочих просторів", + "only-same-monitor-description": "Показувати лише застосунки з монітора, де розташована панель.", + "only-same-monitor-label": "Тільки з того ж монітора", + "show-pinned-apps-description": "Показувати закріплені програми з док-панелі на панелі завдань.", + "show-pinned-apps-label": "Показати закріплені програми", + "show-title-description": "Відображати заголовки вікон на панелі завдань.", + "show-title-description-disabled": "Вертикальна панель завдань не підтримує відображення заголовків.", + "show-title-label": "Показати заголовок", + "smart-width-description": "Автоматично регулювати ширину записів залежно від їх кількості.", + "smart-width-label": "Розумна ширина", + "title-width-description": "Встановити ширину заголовків вікон на панелі завдань (у пікселях).", + "title-width-label": "Ширина заголовка", + "title-width-reset-tooltip": "Скинути ширину заголовка" + }, + "tray": { + "chevron-color-description": "Застосувати кольори теми до іконки шеврона шухляди.", + "chevron-color-label": "Колір стрілки", + "colorize-icons-description": "Застосувати кольори теми до значків трея.", + "colorize-icons-label": "Розфарбувати значки", + "drawer-enabled-description": "Коли увімкнено, не закріплені елементи трея відображаються на панелі ящика.
Коли вимкнено, всі елементи трея відображаються в рядку.", + "drawer-enabled-label": "Увімкнути висувну панель", + "hide-passive-description": "Якщо ввімкнено, елементи в треї зі статусом 'Пасивний' будуть приховані.", + "hide-passive-label": "Приховати пасивні предмети" + }, + "volume": { + "display-mode-description": "Виберіть, як ви хочете, щоб це значення відображалося." + }, + "workspace": { + "character-count-description": "Кількість символів для відображення з назв робочих просторів (1-10).", + "character-count-label": "Кількість символів", + "empty-color-description": "Встановити колір фону для порожніх Workspace.", + "empty-color-label": "Колір порожнього робочого столу", + "enable-scrollwheel-description": "Перемикайтеся між робочими столами за допомогою коліщатка миші.", + "enable-scrollwheel-label": "Прокрутіть, щоб перемикати робочі простори", + "focused-color-description": "Встановити колір тла для сфокусованого Workspace.", + "focused-color-label": "Колір сфокусованого робочого столу", + "follow-focused-screen-description": "Відображати робочі простори з поточного активного екрана, а не з екрана, на якому розташована панель.", + "follow-focused-screen-label": "Слідувати за активним екраном", + "font-weight-description": "Встановити візуальну вагу тексту в робочому просторі.", + "font-weight-label": "Товщина шрифту", + "grouped-border-opacity-description": "Встановити рівень непрозорості для меж контейнерів робочої області.", + "grouped-border-opacity-label": "Прозорість межі", + "hide-unoccupied-description": "Не відображати робочі простори без вікон.", + "hide-unoccupied-label": "Приховати незайняті", + "label-mode-description": "Виберіть, як відображаються мітки робочих просторів.", + "label-mode-label": "Режим міток", + "occupied-color-description": "Встановити колір фону для зайнятих Workspace.", + "occupied-color-label": "Колір зайнятого робочого столу", + "pill-size-description": "Налаштуйте розмір пігулок робочого простору (50%-100%).", + "pill-size-label": "Розмір капсули", + "reverse-scrolling-description": "Змінити напрямок перемикання робочих просторів під час прокручування.", + "reverse-scrolling-label": "Зворотне прокручування", + "show-applications-description": "Відображати значки програм у кожному робочому просторі.", + "show-applications-hover-description": "Відображати піктограми програм всередині кожного робочого простору при наведенні на нього.", + "show-applications-hover-label": "Показувати програми при наведенні", + "show-applications-label": "Показати застосунки", + "show-badge-description": "Показувати значок номера робочого столу в згрупованому режимі.", + "show-badge-label": "Показати значок робочого столу", + "show-labels-only-when-occupied-description": "Показувати мітки робочих просторів лише тоді, коли вони містять вікна.", + "show-labels-only-when-occupied-label": "Показувати мітки лише коли поле заповнене", + "unfocused-icons-opacity-description": "Встановити рівень непрозорості для неактивних іконок застосунків.", + "unfocused-icons-opacity-label": "Неактивні іконки, непрозорість" + } + }, + "battery": { + "all-batteries": "Усі батареї (об'єднані)", + "battery-health": "Стан батареї", + "battery-level": "Рівень заряду акумулятора", + "capacity-level": "Місткість: {level}", + "charging-rate": "Швидкість зарядки: {rate} Вт", + "discharging-rate": "Швидкість розрядки: {rate} Вт", + "health": "Стан: {percent}%", + "inhibit-idle-description": "Підтримує систему активною.", + "no-battery-detected": "Батарею не виявлено", + "plugged-in": "Підключено", + "power-profile": "Профіль живлення", + "time-left": "Залишилося: {time}", + "time-until-full": "До повного заряду: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Доступні пристрої", + "blocked": "Заблоковано", + "connected-devices": "Підключені пристрої", + "device-address": "Адреса пристрою", + "disabled": "Bluetooth вимкнено", + "discoverable": "Виявний", + "enable-message": "Увімкніть Bluetooth, щоб побачити доступні пристрої.", + "known-devices": "Відомі пристрої", + "no-devices": "Пристрої недоступні", + "paired-devices": "З'єднані пристрої", + "pairing-mode": "Переконайтеся, що ваш пристрій у режимі з'єднання.", + "scanning": "Сканування пристроїв...", + "signal-text-excellent": "Сигнал: Відмінний", + "signal-text-fair": "Сигнал: Задовільний", + "signal-text-good": "Сигнал: Добре", + "signal-text-poor": "Сигнал: Поганий", + "signal-text-unknown": "Сигнал: Невідомий", + "signal-text-very-poor": "Сигнал: Дуже слабкий" + } + }, + "changelog": { + "error": { + "fetch-failed": "Не вдалося завантажити дані журналу змін. Будь ласка, спробуйте пізніше.", + "rate-limit": "Перевищено ліміт GitHub. Спробуйте ще раз за кілька хвилин." + }, + "panel": { + "buttons-discord": "Приєднатися до нашого Discord", + "buttons-dismiss": "ОК", + "buttons-feedback": "Надати відгук", + "empty": "Примітки до релізу ще недоступні.", + "highlight-title": "Основні зміни", + "released": "Випущено {date}", + "subtitle-fresh": "Дякуємо, що встановили Noctalia! Ось що містить цей збірка.", + "subtitle-updated": "Оновлено з {previousVersion}", + "title": "Що нового у {version}", + "version": "Версія {version}", + "version-new-user": "Нове встановлення" + } + }, + "common": { + "actions": "Дії", + "active": "Активний", + "add": "Додати", + "appearance": "Зовнішній вигляд", + "apply": "Подати заявку", + "auto-connect": "Автоматичне підключення", + "automation": "Автоматизація", + "available": "Доступний", + "back": "Назад", + "battery": "Акумулятор", + "behavior": "Поведінка", + "bluetooth": "Bluetooth", + "brightness": "Яскравість", + "browse": "Перегляд", + "calendar": "Календар", + "calendar-panel": "Панель календаря", + "cancel": "Скасувати", + "cards": "Картки", + "charging": "Заряджання", + "clear": "Очистити", + "clipboard": "Буфер обміну", + "close": "Закрити", + "color-muted": "Вимкнено звук", + "colors": "Кольори", + "command": "Команда", + "confirm": "Підтвердити", + "connect": "Підключити", + "connected": "Підключено", + "connecting": "Підключення...", + "continue": "Продовжити", + "contributors": "Учасники", + "copied-to-clipboard": "Скопійовано до буфера обміну", + "countdown": "Зворотний відлік", + "customize": "Налаштувати", + "date": "Дата", + "default": "За замовчуванням", + "delete": "Видалити", + "devices": "Пристрої", + "disabled": "Вимкнено", + "discharging": "Розрядження", + "disconnect": "Від'єднати", + "disconnected": "Від'єднано", + "disconnecting": "Від'єднання...", + "display-mode": "Режим відображення", + "documentation": "Документація", + "download": "Завантажити", + "duration": "Тривалість", + "dysfunctional": "Дисфункціональний", + "edit": "Редагувати", + "enabled": "Увімкнено", + "error": "Помилка", + "ethernet": "Ethernet", + "events": "Події", + "execute": "Виконати", + "faithful": "Вірний", + "focus": "Зосередженість", + "font-weight-bold": "Жирний", + "font-weight-light": "Тонкий", + "font-weight-medium": "Середній", + "font-weight-regular": "Звичайний", + "font-weight-semibold": "Напівжирний", + "frequency": "Частота", + "gateway": "Шлюз", + "general": "Загальне", + "height": "Висота", + "hibernate": "Гібернація", + "history": "Історія", + "icon": "Іконка", + "idle": "Простій", + "info": "Інформація", + "input": "Вхід", + "install": "Встановити", + "installed": "Встановлено", + "interface": "Інтерфейс", + "internet": "Інтернет", + "keybind": "Прив'язка клавіш", + "language": "Мова", + "loading": "Завантаження...", + "local": "Місцевий", + "location": "Місцезнаходження", + "lock": "Заблокувати", + "logout": "Вийти", + "look": "Вигляд", + "margins": "Поля", + "media": "Медіа", + "media-player": "Медіапрогравач", + "memory": "Пам'ять", + "monitors": "Монітори", + "move-down": "Перемістити вниз", + "move-up": "Перемістити вгору", + "network": "Мережа", + "next": "Наступний", + "night-light": "Нічне світло", + "no": "Ні", + "no-results": "Немає результатів", + "no-summary": "Немає опису", + "none": "Жоден", + "not-found": "Не знайдено", + "notifications": "Сповіщення", + "official": "Офіційний", + "on-surface": "На поверхні", + "output": "Вихід", + "pair": "Спарувати", + "paired": "Спаровано", + "pairing": "Спарування...", + "panels": "Панелі", + "password": "Пароль", + "pause": "Пауза", + "performance": "Продуктивність", + "pin": "Закріпити", + "play": "Відтворити", + "polling": "Опитування", + "position": "Позиція", + "previous": "Попередній", + "primary": "Основний", + "random": "Випадковий", + "reboot": "Перезавантажити", + "reboot-to-uefi": "Перезавантажити в UEFI", + "record": "Почати запис", + "refresh": "Оновити", + "required": "(обов'язково)", + "reset": "Скинути", + "reset-to-default": "Скинути за Замовчуванням", + "result-count": "{count} результат", + "result-count-plural": "{count} результати", + "resume": "Продовжити", + "save": "Зберегти", + "scanning": "Сканування...", + "screen-corners": "Кути екрана", + "search": "Пошук", + "second": "{count} секунда", + "second-plural": "{count} секунд", + "secondary": "Вторинний", + "security": "Безпека", + "select": "Вибрати", + "select-color": "Вибрати колір", + "select-color-description": "Застосувати кольори теми для виділення.", + "select-icon-color": "Вибрати колір значка", + "settings": "Налаштування", + "shortcuts": "Ярлики", + "shutdown": "Вимкнути", + "signal": "Сигнал", + "sound": "Звук", + "sources": "Джерела", + "start": "Почати", + "stop": "Зупиніться", + "supporters": "Прихильники", + "suspend": "Призупинити", + "templates": "Шаблони", + "tertiary": "Третинний", + "test": "Тест", + "thresholds": "Порогові значення", + "title": "Назва", + "toast": "Toast", + "trusted": "Надійний", + "uninstall": "Видалити", + "unknown": "Невідомо", + "unpair": "Розірвати пару", + "unpin": "Відкріпити", + "update": "Оновлення", + "upload": "Вивантажити", + "username": "Ім'я користувача", + "userspace-reboot": "Перезавантажити простір користувача", + "version": "Версія", + "vibrant": "Яскравий", + "visualizer": "Візуалізатор", + "volume": "Об'єм", + "volumes": "Обсяги", + "wallpaper": "Шпалери", + "weather": "Погода", + "weather-loading": "Завантаження погоди...", + "weather-no-location": "Встановіть місцезнаходження в Налаштуваннях, щоб бачити погоду", + "week": "Тиждень", + "widgets": "Віджети", + "width": "Ширина", + "wifi": "Wi-Fi", + "windows": "Вікна", + "yes": "Так" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Профіль живлення", + "tooltip-disabled": "Встановіть power-profiles-daemon для використання профілів живлення" + }, + "wifi": { + "label-disconnected": "Wi-Fi відключено", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Завжди ховайся", + "always-show": "Завжди показувати", + "force-open": "Зламати силою", + "on-hover": "При наведенні" + }, + "hide-modes": { + "auto-hide": "Автоматичне приховування", + "hidden": "Приховувати, коли порожньо", + "idle": "Приховувати, коли неактивний", + "non-exclusive": "Неексклюзивний", + "transparent": "Прозорий, коли порожній", + "visible": "Завжди видимий" + }, + "launcher": { + "categories": { + "all": "Все", + "audiovideo": "Аудіо та відео", + "chat": "Чат", + "development": "Розвиток", + "education": "Освіта", + "emoji-activity": "Активність", + "emoji-animals": "Тварини", + "emoji-flags": "Прапори", + "emoji-food": "Їжа та напої", + "emoji-nature": "Природа", + "emoji-objects": "Об'єкти", + "emoji-people": "Люди та тіло", + "emoji-recent": "Нещодавні", + "emoji-symbols": "Символи", + "emoji-travel": "Подорожі та місця", + "game": "Ігри", + "graphics": "Графіка", + "misc": "Різне", + "office": "Офіс", + "pinned": "Закріплено", + "system": "Система", + "webbrowser": "Веб-браузер" + }, + "providers": { + "applications": "Застосунки", + "calculator": "Калькулятор", + "calculator-deprecated": "Команда >calc застаріла і скоро буде видалена. Будь ласка, введіть математичні вирази безпосередньо в пошуку.", + "calculator-description": "Калькулятор — обчислення математичних виразів", + "calculator-enter-expression": "Введіть математичний вираз", + "calculator-press-enter-to-copy": "Натисніть Enter, щоб скопіювати результат", + "clipboard": "Історія буфера обміну", + "clipboard-clear-description": "Очистити всю історію буфера обміну", + "clipboard-clear-description-full": "Видалити всі елементи з історії буфера обміну", + "clipboard-clear-history": "Очистити історію буфера обміну", + "clipboard-delete": "Видалити запис з буфера обміну", + "clipboard-history-disabled": "Історія буфера обміну вимкнена", + "clipboard-history-disabled-description": "Увімкніть історію буфера обміну в налаштуваннях або встановіть cliphist", + "clipboard-loading": "Завантаження історії буфера обміну...", + "clipboard-search-description": "Пошук в історії буфера обміну", + "command-description": "Виконати команди оболонки", + "emoji": "Обрати емодзі", + "emoji-loading": "Завантаження емодзі...", + "emoji-loading-description": "Зачекайте, будь ласка", + "emoji-no-recent": "Немає нещодавніх емодзі", + "emoji-search-description": "Пошук і копіювання емодзі", + "settings-search-description": "Пошук та перехід до налаштувань", + "windows-search-description": "Пошук і фокусування відкритих вікон" + } + }, + "lock-screen": { + "authenticating": "Автентифікація...", + "password": "Введіть ваш пароль...", + "restart": "Перезапустити", + "shut-down": "Вимкнути" + }, + "notifications": { + "panel": { + "click-to-collapse": "Натисніть, щоб згорнути", + "click-to-expand": "Клацніть для розгортання", + "description": "Ваші сповіщення з'являтимуться тут по мірі надходження.", + "no-notifications": "Немає сповіщень" + }, + "range": { + "earlier": "Раніше", + "today": "Сьогодні", + "yesterday": "Учора" + }, + "time": { + "diff-d": "1 день тому", + "diff-dd": "{diff} днів тому", + "diff-h": "1 годину тому", + "diff-hh": "{diff} годин тому", + "diff-m": "1 хвилину тому", + "diff-mm": "{diff} хвилин тому", + "now": "зараз" + } + }, + "options": { + "bar": { + "density-comfortable": "Просторий", + "density-compact": "Компактний", + "density-default": "Стандартний", + "density-mini": "Міні", + "density-spacious": "Просторий", + "display-mode-always-visible": "Завжди видимий", + "type-floating": "Плаваючий", + "type-framed": "У рамці", + "type-simple": "Простий" + }, + "control-center": { + "quick-settings-style-classic": "Класичний", + "quick-settings-style-modern": "Сучасний" + }, + "frame-rates-fps": "{fps} к/с", + "launcher-density": { + "comfortable": "Комфортний", + "compact": "Компактний", + "default": "За замовчуванням" + }, + "launcher-view-mode": { + "grid": "Сітка", + "list": "Список" + }, + "notification-density": { + "compact": "Компактний", + "default": "За замовчуванням" + }, + "scrolling-modes": { + "always": "Завжди прокручувати", + "hover": "Прокручувати при наведенні", + "never": "Ніколи не прокручувати" + }, + "session-menu-grid-layout": { + "grid": "Сітка", + "single-row": "Один рядок" + }, + "settings-panel-mode": { + "attached": "Панель прикріплена до панелі", + "centered": "Центрована панель", + "window": "Окреме вікно" + }, + "visualizer-types": { + "linear": "Лінійний", + "mirrored": "Дзеркальний", + "wave": "Хвиля" + }, + "visualizer-visibility": { + "with-background": "Тільки з фоном" + }, + "workspace-labels": { + "index": "Індекс", + "index-and-name": "Індекс та назва", + "name": "Назва" + } + }, + "panels": { + "about": { + "become-supporter": "Стати прихильником", + "changelog": "Переглянути журнал змін", + "changelog-on-startup": "Показувати журнал змін під час оновлення", + "changelog-on-startup-desc": "Автоматично показувати журнал змін при оновленні Noctalia.", + "contributors-description": "Подяка нашому {count} чудовому учаснику!", + "contributors-description-plural": "Подяка нашим {count} чудовим учасникам!", + "copy-info": "Копіювати інформацію", + "debug-disabled": "Режим налагодження вимкнено", + "debug-enabled": "Режим налагодження увімкнено", + "info-copied": "Інформацію скопійовано до буфера обміну", + "noctalia-available": "Доступно:", + "noctalia-desc": "Елегантна та мінімалістична оболонка робочого столу, ретельно створена для Wayland, побудована на Quickshell.", + "noctalia-git-commit": "Git коміт:", + "noctalia-installed-version": "Встановлена версія:", + "noctalia-latest-version": "Остання версія:", + "noctalia-qs-version": "Noctalia QS версія:", + "noctalia-title": "Оболонка Noctalia", + "privacy-policy": "Політика конфіденційності", + "support": "Підтримати нас", + "supporter-badge": "Прихильник", + "supporters-desc": "Величезне дякую нашому чудовому прихильнику!", + "supporters-desc-plural": "Величезна подяка нашим {count} чудовим прихильникам!", + "supporters-loading": "Завантаження прихильників...", + "system-board": "Материнська плата:", + "system-cpu": "ЦП:", + "system-disk": "Диск:", + "system-gpu": "GPU:", + "system-host": "хост:", + "system-install-hint": "Встановіть fastfetch для перегляду інформації про систему", + "system-kernel": "Ядро:", + "system-loading": "Завантаження системної інформації...", + "system-memory": "Пам'ять:", + "system-monitor": "Монітор:", + "system-not-installed": "fastfetch не встановлено", + "system-os": "ОС:", + "system-packages": "Пакунки:", + "system-product": "Продукт:", + "system-title": "Інформація про систему", + "system-uptime": "Час безперебійної роботи:", + "system-wm": "WM:\nВМ:", + "telemetry-data-copied": "Дані телеметрії скопійовано до буфера обміну", + "telemetry-desc": "Допоможіть покращити Noctalia, поділившись анонімною інформацією про систему (роздільна здатність екрана, композитор, дистрибутив). Надсилається один раз під час запуску, без відстеження, дані автоматично видаляються через 30 днів.", + "telemetry-enabled": "Відправити анонімну системну інформацію", + "telemetry-show-data": "Перегляд даних", + "telemetry-title": "Конфіденційність", + "title": "Про Noctalia", + "up-to-date": "У вас остання версія!", + "update-available": "Доступне оновлення", + "view-commit": "Переглянути коміт на GitHub" + }, + "audio": { + "devices-desc": "Налаштуйте доступні пристрої введення та виведення звуку.", + "devices-input-device-description": "Виберіть бажаний пристрій введення звуку.", + "devices-input-device-label": "Пристрій введення", + "devices-output-device-description": "Виберіть бажаний пристрій виведення звуку.", + "devices-output-device-label": "Пристрій виведення", + "devices-title": "Аудіопристрої", + "external-mixer-description": "Введіть команду або шлях до застосунку для запуску при активації функції зовнішнього аудіомікшера.", + "external-mixer-label": "Команда зовнішнього аудіомікшера", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Встановіть бажані та ігноровані медіазастосунки.", + "media-excluded-player-description": "Додайте ключові слова для плеєрів, які система має ігнорувати. Кожне ключове слово на новому рядку.", + "media-excluded-player-label": "Ігнорований плеєр", + "media-excluded-player-placeholder": "введіть частину назви та натисніть +", + "media-frame-rate-description": "Вищі значення плавніші, але споживають більше ресурсів.", + "media-frame-rate-label": "Частота кадрів", + "media-primary-player-description": "Введіть ключове слово для ідентифікації основного плеєра.", + "media-primary-player-label": "Основний плеєр", + "media-primary-player-placeholder": "напр. spotify, vlc, mpv", + "media-scrolling-speed-description": "Час у секундах для прокрутки назви від початку до кінця.", + "media-scrolling-speed-label": "Швидкість прокрутки", + "media-scrolling-title-description": "Увімкнути безперервну прокрутку для довгих назв медіа.", + "media-scrolling-title-label": "Прокрутка назви", + "media-title": "Медіаплеєри", + "on-middle-clicked-description": "Команда для виконання при натисканні середньою кнопкою миші.", + "panel-applications-empty": "Зараз жоден додаток не відтворює аудіо", + "spectrum-mirrored-description": "Віддзеркалюйте спектр, щоб низькі частоти були в центрі. Коли вимкнено, низькі частоти знаходяться ліворуч, а високі — праворуч.", + "spectrum-mirrored-label": "Дзеркальний спектр", + "title": "Аудіо", + "visualizer-type-description": "Виберіть тип візуалізації для відтворення медіа.", + "visualizer-type-label": "Тип візуалізації", + "volumes-desc": "Налаштуйте регулятори гучності та рівні звуку.", + "volumes-feedback-sound-file-description": "Шлях до звукового файлу, який відтворюється під час зміни гучності.", + "volumes-feedback-sound-file-label": "Звуковий файл зворотного зв'язку гучності", + "volumes-feedback-sound-file-select-title": "Вибрати звуковий файл зворотного зв'язку гучності", + "volumes-input-volume-description": "Рівень гучності мікрофона.", + "volumes-mute-input-description": "Вимкнути стандартний аудіовхід (мікрофон).", + "volumes-mute-input-label": "Вимкнути вхідний звук", + "volumes-mute-output-description": "Вимкнути основний аудіовихід системи.", + "volumes-mute-output-label": "Вимкнути вихідний звук", + "volumes-output-volume-description": "Системний рівень гучності.", + "volumes-step-size-description": "Налаштуйте крок зміни гучності (колесо миші, гарячі клавіші).", + "volumes-step-size-label": "Крок зміни гучності", + "volumes-volume-feedback-description": "Відтворювати звук зворотного зв'язку під час регулювання гучності.", + "volumes-volume-feedback-label": "Відтворити звук зворотного зв'язку гучності", + "volumes-volume-overdrive-description": "Дозволити підвищення гучності понад 100%. Може не підтримуватись усім обладнанням.", + "volumes-volume-overdrive-label": "Гучність понад 100%" + }, + "bar": { + "appearance-auto-hide-delay-description": "Час, через який Bar сховається після того, як курсор миші залишить його.", + "appearance-auto-hide-delay-label": "Затримка приховування", + "appearance-auto-hide-exclusive-note": "Примітка: Ексклюзивна зона автоматично вимикається, коли автоприховування активне.", + "appearance-auto-show-delay-description": "Час до появи панелі, коли миша входить у край.", + "appearance-auto-show-delay-label": "Затримка відображення", + "appearance-background-opacity-description": "Встановити прозорість фону спеціально для панелі.", + "appearance-background-opacity-label": "Прозорість фону панелі", + "appearance-capsule-color-description": "Виберіть колір для капсул панелі або не використовуйте жодного для стандартного кольору поверхні.", + "appearance-capsule-color-label": "Колір капсули", + "appearance-capsule-opacity-description": "Встановити рівень непрозорості для фону віджетів, коли показано капсулу.", + "appearance-capsule-opacity-label": "Непрозорість капсули", + "appearance-content-padding-description": "Налаштуйте відступ між краями панелі та віджетами.", + "appearance-content-padding-label": "Відступ вмісту", + "appearance-density-description": "Налаштуйте відступи панелі для компактного або просторого вигляду.", + "appearance-density-label": "Щільність панелі", + "appearance-desc": "Налаштуйте зовнішній вигляд та положення панелі.", + "appearance-display-mode-description": "Виберіть, коли панель видима.", + "appearance-enable-exclusion-zone-inset-description": "Зменшіть зону виключення на 1 фізичний піксель, щоб вікна впритул ідеально заходили під край панели.", + "appearance-enable-exclusion-zone-inset-label": "Внутрішня зона виключення", + "appearance-floating-description": "Відображати панель як плаваючу 'капсулу'. Примітка: Це перемістить кути екрана до країв.", + "appearance-floating-label": "Плаваюча панель (Острівець)", + "appearance-font-scale-description": "Налаштуйте масштаб розміру шрифту для тексту, що відображається на панелі.", + "appearance-font-scale-label": "Масштаб шрифту", + "appearance-frame-radius": "Внутрішній радіус", + "appearance-frame-settings-description": "Налаштуйте товщину рамки та внутрішній радіус кутів", + "appearance-frame-settings-label": "Налаштування рамки", + "appearance-frame-thickness": "Товщина", + "appearance-hide-on-overview-description": "Приховати панель і закрити панелі, коли активний огляд компонувальника.", + "appearance-hide-on-overview-label": "Приховати панель на огляді", + "appearance-margins-description": "Налаштуйте поля навколо плаваючої панелі.", + "appearance-margins-horizontal": "Горизонтальний відступ", + "appearance-margins-vertical": "Вертикальний відступ", + "appearance-outer-corners-description": "Відображає назовні закруглені кути на панелі.", + "appearance-outer-corners-label": "Зовнішні кути", + "appearance-position-description": "Виберіть, де розмістити панель на екрані.", + "appearance-position-label": "Положення панелі", + "appearance-show-capsule-description": "Показувати фон віджетів.", + "appearance-show-capsule-label": "Показувати капсулу", + "appearance-show-on-workspace-switch-description": "Автоматично коротко показувати Bar при зміні робочого простору.", + "appearance-show-on-workspace-switch-label": "Показувати панель при перемиканні робочого простору", + "appearance-show-outline-description": "Відображає видиму рамку навколо кожного віджета.", + "appearance-show-outline-label": "Показувати контури віджетів", + "appearance-type-description": "Виберіть стиль панелі: Простий, Плаваючий або У рамці.", + "appearance-type-label": "Тип панелі", + "appearance-use-separate-opacity-description": "Увімкнути використання окремого значення прозорості для фону панелі.", + "appearance-use-separate-opacity-label": "Використовувати окрему прозорість стовпців", + "appearance-widget-spacing-description": "Налаштуйте інтервал між кожним віджетом на панелі.", + "appearance-widget-spacing-label": "Відстань між віджетами", + "behavior-middle-click-command-description": "Команда для виконання за середнім кліком.", + "behavior-middle-click-command-label": "Команда Середнього Кліку", + "behavior-middle-click-command-placeholder": "niri msg action перемкнути огляд", + "behavior-middle-click-description": "Виберіть, що робить середній клік на порожніх ділянках Панелі.", + "behavior-middle-click-follow-mouse-description": "Відкрити вибрану панель середнього кліку в позиції курсора.", + "behavior-middle-click-follow-mouse-label": "Середній клік слідує за курсором", + "behavior-middle-click-label": "Дія середнього кліку на Панелі", + "behavior-right-click-command-description": "Команда для виконання при правому кліку.", + "behavior-right-click-command-label": "Команда Правого Кліка", + "behavior-right-click-command-placeholder": "notify-send \"Правий клік\"", + "behavior-right-click-description": "Виберіть, що робить правий клік на порожніх областях панелі.", + "behavior-right-click-follow-mouse-description": "Відкрити вибрану панель правого кліка в позиції курсора.", + "behavior-right-click-follow-mouse-label": "Правий клік слідує за курсором", + "behavior-right-click-label": "Дія правого кліку на Панелі", + "behavior-wheel-wrap-description": "Якщо ввімкнено, прокрутка продовжується від останнього елемента до першого.", + "behavior-wheel-wrap-label": "По колу", + "behavior-workspace-scroll-description": "Виберіть, що робить коліщатко миші на порожніх ділянках панелі.", + "behavior-workspace-scroll-label": "Дія колеса миші Панелі", + "behavior-workspace-scroll-option-content": "Вміст", + "behavior-workspace-scroll-option-workspace": "Робоча область", + "monitor-configure-widgets": "Налаштувати віджети", + "monitor-override-settings": "Перевизначити глобальні налаштування", + "monitor-override-settings-description": "Використовувати власні налаштування для цього монітора.", + "monitor-reset-all": "Скинути все", + "monitor-widgets-title": "Налаштування віджета - {monitor}", + "monitors-desc": "Показувати панель на певних моніторах. За замовчуванням на всіх, якщо не вибрано.", + "monitors-desc-new": "Налаштуйте, на яких моніторах відображати Bar, та налаштуйте параметри для кожного монітора.", + "monitors-title": "Відображення на моніторах", + "title": "Панель", + "tray-blacklist-description": "Додайте правила виключення з трея, підтримує шаблони (*).", + "tray-blacklist-label": "Чорний список", + "tray-blacklist-placeholder": "напр. nm-applet, Fcitx*", + "tray-pin-application": "Закріпити застосунок", + "tray-unpin-application": "Відкріпити застосунок", + "use-global-widgets": "Використовувати глобальні віджети", + "widgets-desc": "Перетягуйте віджети, щоб змінити їх порядок. Використовуйте контекстне меню (клік правою кнопкою миші), щоб переміщувати віджети між секціями або видаляти їх.", + "widgets-title": "Розміщення віджетів" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Створюйте колірні схеми зі своїх шпалер. Автоматично видобуває кольори для створення цілісної теми.", + "color-source-use-wallpaper-colors-label": "Використовувати кольори шпалер", + "dark-mode-mode-description": "Увімкнути автоматичне перемикання між світлим і темним режимом.", + "dark-mode-mode-label": "Розклад темного режиму", + "dark-mode-mode-manual": "Вручну", + "dark-mode-mode-off": "Вимкнено", + "dark-mode-switch-description": "Перемикає на темнішу тему для зручного перегляду вночі.", + "delete-error-description": "Не вдалося видалити {scheme}", + "delete-error-title": "Помилка видалення", + "delete-success-description": "{scheme} успішно видалено", + "delete-success-title": "Кольорову схему видалено", + "download-button": "Завантажити ще", + "download-downloading": "Завантаження...", + "download-empty": "Немає доступних кольорових схем", + "download-error-api-error": "Помилка API: {status}", + "download-error-description": "Не вдалося завантажити {scheme}", + "download-error-download-failed": "Помилка завантаження з кодом виходу: {code}", + "download-error-invalid-response": "Невірний формат відповіді API", + "download-error-no-files": "Файли для схеми не знайдено", + "download-error-parse-failed": "Помилка парсингу відповіді API: {error}", + "download-error-rate-limit": "Перевищено ліміт запитів GitHub API", + "download-error-title": "Помилка завантаження", + "download-fetching": "Отримання доступних кольорових схем...", + "download-success-description": "{scheme} успішно завантажено", + "download-success-title": "Кольорову схему завантажено", + "download-title": "Завантажити кольорові схеми", + "method-description": { + "content": "Схема Material Design з високоточною екстракцією кольорів, яка точно відповідає фактичним кольорам вихідного контенту.", + "dysfunctional": "Як Faithful, але вибирає другу за домінантністю колірну родину як основну.", + "faithful": "Намагається залишатися близьким до вихідного кольору, створюючи при цьому гармонійну палітру.", + "fruit-salad": "Схема Material Design, яка створює грайливу, яскраву палітру з різноманітними відтінками.", + "monochrome": "Схема Material Design, що використовує однотонну градацію сірого з мінімальним хроматичним вмістом.", + "muted": "Створює м'якіші, денасичені тони для спокійного та стриманого вигляду.", + "rainbow": "Схема Material Design, яка генерує кольори по всьому спектру для максимальної різноманітності кольорів.", + "tonal-spot": "Стандартний алгоритм Material Design. Створює збалансовані палітри, використовуючи тональні варіації вихідного кольору.", + "vibrant": "Максимізує насиченість кольорів для сміливих, привабливих палітр з яскравими тонами." + }, + "predefined-desc": "Виберіть із колекції попередньо визначених колірних схем.", + "predefined-generate-templates-label": "Генерувати шаблони для попередньо визначених схем", + "predefined-title": "Попередньо визначені колірні схеми", + "sync-gsettings-description": "Зіставити системну тему з активним світлим або темним варіантом.", + "sync-gsettings-label": "Синхронізувати системну тему", + "templates-desc": "Застосовувати кольори до зовнішніх застосунків.", + "templates-filter-description": "Показати шаблони з певної категорії.", + "templates-filter-label": "Фільтрувати за категорією", + "templates-misc-description": "Створіть власні шаблони.", + "templates-misc-label": "Розширено", + "templates-misc-user-templates-description": "Увімкніть лише якщо ви знаєте, що робите — зверніться до нашої онлайн-документації.", + "templates-misc-user-templates-label": "Увімкнути користувацькі шаблони", + "templates-none-detected": "Не виявлено", + "templates-write-path": "Записує: {filepath}", + "title": "Колірна схема", + "wallpaper-method-description": "Оберіть свій улюблений метод створення палітри.", + "wallpaper-method-label": "Метод створення палітри", + "wallpaper-monitor-source-description": "Виберіть монітор, який використовувати для вилучення кольорів шпалер.", + "wallpaper-monitor-source-label": "Джерело генерації кольору" + }, + "connections": { + "authentication-required": "Потрібна автентифікація", + "bluetooth-auto-connect-description": "Автоматично підключатися до довірених сполучених пристроїв, коли Bluetooth увімкнено.", + "bluetooth-auto-connect-label": "Автоматично підключати спарені пристрої", + "bluetooth-devices-unnamed": "Безіменні пристрої не відображаються.", + "bluetooth-discoverable": "Цей пристрій можна виявити як {hostName}, поки ця вкладка налаштувань відкрита.", + "bluetooth-rssi-polling-description": "Періодично вимірює RSSI для підключених пристроїв через bluetoothctl. Може бути недоступно для всіх пристроїв; використовує мінімальні ресурси, якщо увімкнено.", + "bluetooth-rssi-polling-interval-description": "Налаштуйте, як часто оновлювати рівень сигналу для підключених пристроїв.", + "bluetooth-rssi-polling-interval-label": "Інтервал опитування", + "bluetooth-rssi-polling-label": "Опитування сигналу Bluetooth", + "disable-discoverability-description": "Приховати ваш пристрій від найближчих пристроїв Bluetooth.", + "disable-discoverability-label": "Вимкнути видимість пристрою", + "hide-unnamed-devices-description": "Приховати пристрої, які відображаються лише як Bluetooth-адреси.", + "hide-unnamed-devices-label": "Приховати безіменні пристрої", + "pin-instructions": "Будь ласка, введіть PIN-код, що відображається на вашому пристрої.", + "title": "Підключення", + "wifi-header-text": "Використовуйте Wi-Fi, щоб переглядати доступні мережі та підключатися до інтернету." + }, + "control-center": { + "cards-desc": "Налаштуйте, які елементи керування з'являються в центрі керування та в якому порядку.", + "desc": "Налаштуйте положення та поведінку панелі центру керування.", + "open-at-mouse-description": "При натисканні правою кнопкою миші на порожній області Панелі, відкрийте Центр керування поруч з курсором. Якщо вимкнено, замість цього використовується налаштування позиції вище.", + "open-at-mouse-label": "Відкривати біля курсора за правим кліком", + "position-description": "Виберіть, де з'являється панель центру керування при відкритті.", + "shortcuts-custom-button-command-description": "Команда, яка буде виконана при натисканні кнопки.", + "shortcuts-custom-button-enable-on-state-logic-description": "Увімкнути другу іконку та 'гарячий' стан на основі команди перевірки.", + "shortcuts-custom-button-enable-on-state-logic-label": "Увімкнути логіку увімкненого стану", + "shortcuts-custom-button-icon-description": "Виберіть іконку з бібліотеки.", + "shortcuts-custom-button-on-clicked-label": "Команда лівого кліку", + "shortcuts-custom-button-on-middle-clicked-description": "Команда для виконання при натисканні середньої кнопки миші.", + "shortcuts-custom-button-on-state-command-description": "Команда для перевірки, чи повинна кнопка бути у стані 'увімкнено'. Повертає 0 для увімкненого стану, ненульове значення для вимкненого.", + "shortcuts-custom-button-on-state-command-label": "Команда перевірки увімкненого стану", + "shortcuts-custom-button-on-state-icon-description": "Іконка для кнопки, коли вона у стані 'увімкнено'.", + "shortcuts-custom-button-on-state-icon-label": "Іконка увімкненого стану", + "shortcuts-custom-button-state-checks-add": "Додати перевірку стану", + "shortcuts-custom-button-state-checks-command": "Команда для виконання для цієї перевірки стану", + "shortcuts-custom-button-state-checks-label": "Перевірки стану", + "shortcuts-custom-button-state-checks-remove": "Видалити", + "shortcuts-custom-button-tooltip-description": "Підказка, яка буде відображатися при наведенні на кнопку.", + "shortcuts-custom-button-tooltip-label": "Підказка", + "shortcuts-title": "Віджети швидкого доступу", + "system-monitor-disk-path-description": "Виберіть точку монтування диска, яку повинна відстежувати карта системного монітора в центрі керування.", + "system-monitor-disk-path-label": "Шлях до диска системного монітора", + "title": "Центр керування" + }, + "desktop-widgets": { + "clock-enabled-description": "Показати віджет годинника на робочому столі.", + "clock-enabled-label": "Увімкнути віджет годинника", + "clock-format-label": "Формат", + "clock-height-description": "Висота віджета годинника в пікселях.", + "clock-height-label": "Висота", + "clock-minimal-mode-description": "Використовувати мінімальне відображення годинника з часом і датою.", + "clock-minimal-mode-label": "Мінімальний режим", + "clock-rounded-corners-description": "Використовуйте заокруглені кути для фону віджета.", + "clock-rounded-corners-label": "Заокруглені кути", + "clock-show-background-description": "Показати фоновий контейнер для віджета годинника.", + "clock-show-background-label": "Показати фон", + "clock-show-date-description": "Показувати поточну дату під часом.", + "clock-show-date-label": "Показати дату", + "clock-show-seconds-description": "Показувати секунди в часі.", + "clock-show-seconds-label": "Показувати секунди", + "clock-style-analog": "Аналоговий", + "clock-style-binary": "Двійковий", + "clock-style-description": "Виберіть стиль відображення годинника.", + "clock-style-digital": "Цифровий", + "clock-style-label": "Стиль годинника", + "clock-style-minimal": "Мінімальний", + "clock-width-description": "Ширина віджета годинника в пікселях.", + "cpu-intensive-note": "Віджети, позначені !, використовують більше CPU, і їх слід вмикати обережно.", + "edit-mode-button-label": "Увійти в режим редагування", + "edit-mode-controls-explanation": "Клацання лівою кнопкою миші та перетягування: Перемістити або змінити розмір віджета.\nКлацання правою кнопкою миші: Відкрити контекстне меню з опціями.", + "edit-mode-description": "Увімкніть режим редагування, щоб переміщувати та змінювати розташування віджетів робочого столу. У ввімкненому стані віджети відображають контур перетягування, і їх можна переміщувати.", + "edit-mode-exit-button": "Вийти з режиму редагування", + "edit-mode-grid-snap-label": "Прив'язка до сітки", + "edit-mode-grid-snap-scale-label": "Масштаб прив'язки до сітки", + "edit-mode-label": "Режим редагування", + "enabled-description": "Увімкнути або вимкнути віджети робочого столу повністю.", + "enabled-label": "Увімкнути віджети робочого столу", + "general-desc": "Налаштуйте віджети, які відображаються на вашому робочому столі.", + "general-title": "Настільні віджети", + "media-player-enabled-description": "Показати віджет медіаплеєра на робочому столі.", + "media-player-enabled-label": "Увімкнути віджет медіаплеєра", + "media-player-rounded-corners-description": "Увімкнути заокруглені кути на краях віджета.", + "media-player-show-album-art-description": "Показати обкладинку альбому та інформацію про треки (назва та виконавець).", + "media-player-show-album-art-label": "Показувати обкладинку альбому та назву", + "media-player-show-background-description": "Показати фоновий контейнер для віджета медіаплеєра.", + "media-player-show-buttons-description": "Показати кнопки керування медіа (відтворення/пауза, попередній, наступний) на віджеті медіаплеєра.", + "media-player-show-buttons-label": "Показати кнопки", + "media-player-show-visualizer-description": "Показати накладання аудіовізуалізатора.", + "media-player-visualizer-type-description": "Оберіть тип візуалізації для фону медіаплеєра на робочому столі.", + "overview-enabled-description": "Показувати віджети робочого столу, поки огляд композитора відкритий.", + "overview-enabled-label": "Показати в огляді", + "system-stat-layout-bottom": "Низ", + "system-stat-layout-description": "Виберіть, як відображається легенда відносно графіка.", + "system-stat-layout-label": "Макет", + "system-stat-layout-side": "Сторона", + "system-stat-rounded-corners-description": "Використовувати заокруглені кути для фону віджета.", + "system-stat-rounded-corners-label": "Закруглені Кути", + "system-stat-show-background-description": "Показати фоновий контейнер для віджета системної статистики.", + "system-stat-show-background-label": "Показати фон", + "system-stat-stat-type-description": "Виберіть, яку системну статистику відображати.", + "system-stat-stat-type-label": "Тип Статистики", + "title": "Настільні віджети", + "weather-enabled-description": "Показувати погодний віджет на робочому столі.", + "weather-enabled-label": "Увімкнути віджет погоди", + "weather-show-background-description": "Показати фоновий контейнер для погодного віджета." + }, + "display": { + "monitors-backlight-device-auto-option": "За замовчуванням", + "monitors-backlight-device-description": "Виберіть пристрій підсвічування для цього виходу.", + "monitors-backlight-device-label": "Пристрій Підсвічування", + "monitors-brightness-step-description": "Налаштуйте крок зміни яскравості (колесо миші та гарячі клавіші).", + "monitors-brightness-step-label": "Крок зміни яскравості", + "monitors-brightness-unavailable-ddc-disabled": "Регулювання яскравості недоступне. Увімкніть \"Підтримку зовнішньої яскравості\", щоб керувати яскравістю цього дисплея.", + "monitors-brightness-unavailable-generic": "Регулювання яскравості для цього дисплея недоступне.", + "monitors-desc": "Налаштуйте яскравість для кожного дисплея.", + "monitors-enforce-minimum-description": "Вирішує проблему повного вимкнення підсвітки на деяких дисплеях при яскравості 0%.", + "monitors-enforce-minimum-label": "Примусова мінімальна яскравість (1%)", + "monitors-external-brightness-description": "Увімкнути підтримку DDCUtil для керування яскравістю на зовнішніх дисплеях через протокол DDC/CI.", + "monitors-external-brightness-label": "Підтримка зовнішньої яскравості", + "monitors-global-brightness-description": "Налаштувати яскравість усіх моніторів одночасно.", + "monitors-global-brightness-label": "Усі монітори", + "monitors-title": "Налаштування для кожного монітора", + "night-light-auto-schedule-description": "На основі часу заходу та сходу сонця в {location} — рекомендовано.", + "night-light-auto-schedule-label": "Автоматичне планування", + "night-light-desc": "Зменшіть випромінювання синього світла для кращого сну та зменшення втоми очей.", + "night-light-enable-description": "Застосувати теплий колірний фільтр для зменшення випромінювання синього світла.", + "night-light-enable-label": "Увімкнути нічне світло", + "night-light-force-activation-description": "Ігнорує розклад і негайно застосовує нічний фільтр.", + "night-light-force-activation-label": "Примусова активація", + "night-light-manual-schedule-description": "Встановіть власний час сходу та заходу сонця.", + "night-light-manual-schedule-label": "Ручне планування", + "night-light-manual-schedule-select-start": "Виберіть час початку", + "night-light-manual-schedule-select-stop": "Виберіть час зупинки", + "night-light-manual-schedule-sunrise": "Час сходу сонця", + "night-light-manual-schedule-sunset": "Час заходу сонця", + "night-light-temperature-day": "День", + "night-light-temperature-day-description": "Керує колірною температурою вдень.", + "night-light-temperature-description": "Встановіть теплоту кольору для нічного та денного часу.", + "night-light-temperature-label": "Колірна температура", + "night-light-temperature-night": "Ніч", + "night-light-temperature-night-description": "Керує колірною температурою вночі.", + "title": "Дисплей" + }, + "dock": { + "appearance-background-opacity-description": "Налаштуйте непрозорість фону дока.", + "appearance-border-radius-description": "Налаштуйте радіус заокруглення країв док-панелі.", + "appearance-border-radius-label": "Радіус заокруглення", + "appearance-colorize-icons-description": "Застосувати кольори теми до значків застосунків у доці (тільки неактивні застосунки).", + "appearance-colorize-icons-label": "Розфарбувати значки", + "appearance-dead-opacity-description": "Відрегулюйте прозорість іконок програм, які не запущені.", + "appearance-dead-opacity-label": "Мертва непрозорість", + "appearance-desc": "Налаштуйте поведінку та зовнішній вигляд дока.", + "appearance-display-auto-hide": "Автоприховування", + "appearance-display-description": "Виберіть, як поводиться док.", + "appearance-display-exclusive": "Винятковий", + "appearance-dock-indicator-description": "Показувати невеликий індикатор, коли док приховано.", + "appearance-dock-indicator-label": "Індикатор дока", + "appearance-floating-distance-description": "Встановіть відстань між доком і краєм екрана.", + "appearance-floating-distance-label": "Відстань плавання дока", + "appearance-group-apps-description": "Групувати кілька вікон з однієї програми в один запис Dock.", + "appearance-group-apps-label": "Групувати однакові програми", + "appearance-group-click-action-cycle": "Перемикати вікна", + "appearance-group-click-action-description": "Виберіть, що робить лівий клік для згрупованих програм.", + "appearance-group-click-action-label": "Дія за кліком згрупованої програми", + "appearance-group-click-action-list": "Відкрити список вікон", + "appearance-group-context-menu-mode-description": "Виберіть, як контекстне меню поводиться для згрупованих програм.", + "appearance-group-context-menu-mode-extended": "Розширений", + "appearance-group-context-menu-mode-label": "Режим згрупованого меню програм", + "appearance-group-context-menu-mode-list": "Список вікон", + "appearance-group-indicator-style-description": "Виберіть, як згруповані індикатори запущених програм відображають стан активного вікна.", + "appearance-group-indicator-style-dots": "Крапки", + "appearance-group-indicator-style-label": "Стиль згрупованого індикатора", + "appearance-group-indicator-style-number": "Число", + "appearance-hide-show-speed-description": "Налаштуйте швидкість анімації приховування/відображення дока.", + "appearance-hide-show-speed-label": "Швидкість приховування/відображення", + "appearance-icon-size-description": "Налаштуйте загальний розмір дока.", + "appearance-icon-size-label": "Розмір дока", + "appearance-inactive-indicators-description": "Відображати індикатори-пігулки для всіх програм, а не лише для поточної активної.", + "appearance-inactive-indicators-label": "Індикатори бігу", + "appearance-indicator-color-description": "Виберіть колір індикатора прихованого доку.", + "appearance-indicator-color-label": "Колір індикатора", + "appearance-indicator-opacity-description": "Налаштуйте непрозорість індикатора прихованого доку.", + "appearance-indicator-opacity-label": "Непрозорість індикатора", + "appearance-indicator-thickness-description": "Увімкнути товстіший індикатор прихованого доку (6 пікселів замість 3 пікселів).", + "appearance-indicator-thickness-label": "Товстіший індикатор", + "appearance-launcher-icon-description": "Виберіть піктограму, що використовується для Dock Launcher.", + "appearance-launcher-icon-label": "Піктограма лаунчера", + "appearance-launcher-position-description": "Виберіть, де значок запуску програм з'являється в доці.", + "appearance-launcher-position-end": "Кінець", + "appearance-launcher-position-label": "Позиція запускача", + "appearance-launcher-position-start": "Старт", + "appearance-launcher-use-distro-logo-description": "Використовуйте логотип вашого дистрибутива як значок Dock Launcher.", + "appearance-launcher-use-distro-logo-label": "Використовувати логотип дистрибутива замість іконки", + "appearance-pinned-static-description": "Завжди закріплюйте значки програм ліворуч у статичному порядку.", + "appearance-pinned-static-label": "Статично закріплені програми", + "appearance-position-description": "Виберіть, де док відображатиметься на екрані.", + "appearance-position-label": "Позиція", + "appearance-show-launcher-icon-description": "Показувати піктограму запуску програм у доці.", + "appearance-show-launcher-icon-label": "Показати запуск програм", + "appearance-sit-on-frame-description": "Вирівняйте док всередині межі рамки, а не поверх неї.", + "appearance-sit-on-frame-label": "Док розміщується на рамці", + "appearance-type-attached": "Закріплена", + "appearance-type-description": "Виберіть між плаваючою пігулкою або статичною панеллю, прикріпленою до краю.", + "appearance-type-floating": "Плаваючий", + "appearance-type-label": "Стиль дока", + "enabled-description": "Показати або сховати док повністю.", + "enabled-label": "Увімкнути док", + "monitors-desc": "Показувати док на певних моніторах. За замовчуванням на всіх, якщо не вибрано.", + "monitors-only-same-monitor-description": "Показувати лише застосунки з монітора, де розташовано Dock.", + "monitors-only-same-monitor-label": "Лише застосунки з того ж монітора", + "monitors-title": "Відображення на моніторах", + "title": "Док" + }, + "general": { + "copy-settings": "Копіювати налаштування", + "fonts-default-description": "Основний шрифт, що використовується в інтерфейсі.", + "fonts-default-label": "Стандартний шрифт", + "fonts-default-placeholder": "Виберіть стандартний шрифт...", + "fonts-default-scale-description": "Збільшити або зменшити розмір звичайного тексту.", + "fonts-default-scale-label": "Масштаб стандартного шрифту", + "fonts-default-search-placeholder": "Пошук шрифту...", + "fonts-desc": "Виберіть шрифти, що використовуються в інтерфейсі.", + "fonts-monospace-description": "Моноширинний шрифт для відображення чисел та статистики.", + "fonts-monospace-label": "Моноширинний шрифт", + "fonts-monospace-placeholder": "Виберіть моноширинний шрифт...", + "fonts-monospace-scale-description": "Збільшити або зменшити розмір моноширинного тексту.", + "fonts-monospace-scale-label": "Розмір моноширинного шрифту", + "fonts-monospace-search-placeholder": "Пошук моноширинного шрифту...", + "fonts-title": "Шрифти", + "keybinds-conflict-description": "Комбінація клавіш вже призначена для {action}.", + "keybinds-conflict-title": "Конфлікт гарячих клавіш", + "keybinds-description": "Налаштуйте глобальні клавіші навігації для панелей та запускача.", + "keybinds-down": "Перемістити вниз", + "keybinds-enter": "Підтвердити / Дія", + "keybinds-escape": "Закрити / Назад", + "keybinds-left": "Перемістити вліво", + "keybinds-modifier-description": "Комбінації клавіш повинні включати клавішу-модифікатор (Ctrl або Alt).", + "keybinds-modifier-title": "Потрібен модифікатор", + "keybinds-remove": "Видалити", + "keybinds-right": "Перемістити вправо", + "keybinds-title": "Прив'язки клавіш навігації", + "keybinds-up": "Перемістити вгору", + "language-desc": "Виберіть бажану мову застосунку.", + "language-select-auto-detect": "Автоматично", + "language-select-description": "Виберіть мову інтерфейсу застосунку.", + "language-select-label": "Мова застосунку", + "launch-setup-wizard": "Майстер налаштування", + "profile-desc": "Редагуйте дані користувача та аватар.", + "profile-picture-description": "Ваше фото профілю, що відображається в інтерфейсі.", + "profile-picture-label": "Фото профілю", + "profile-select-avatar": "Вибрати зображення аватара", + "profile-title": "Профіль", + "profile-tooltip": "Фото профілю", + "reverse-scrolling-description": "Обернути інтерпретований напрямок прокручування.", + "reverse-scrolling-label": "Зворотне прокручування", + "screen-corners-desc": "Налаштуйте заокруглення кутів екрана та візуальні ефекти.", + "screen-corners-radius-description": "Налаштуйте заокруглення кутів екрана.", + "screen-corners-radius-label": "Радіус кутів екрана", + "screen-corners-radius-reset": "Скинути радіус кутів екрана", + "screen-corners-show-corners-description": "Відображати заокруглені кути на краях екрана.", + "screen-corners-show-corners-label": "Показувати кути екрана", + "screen-corners-solid-black-description": "Використовувати суцільний чорний колір замість кольору фону панелі.", + "screen-corners-solid-black-label": "Суцільні чорні кути", + "screen-corners-title": "Кути екрана", + "settings-copied": "Налаштування скопійовано до буфера обміну", + "smooth-scrolling-description": "Анімувати прокручування списків для плавнішого досвіду використання колеса.", + "smooth-scrolling-label": "Плавне прокручування", + "tab-basics": "Основи", + "tab-keybinds": "Гарячі клавіші" + }, + "hooks": { + "color-generation-description": "Команда, яка буде виконана після завершення генерації кольору (наприклад, після застосування теми на основі шпалер).", + "color-generation-label": "Кольори згенеровано", + "color-generation-placeholder": "напр. notify-send \"Кольори\" \"Згенеровано\"", + "info-command-info-description": "• Команди виконуються через оболонку (sh -lc)
• Команди запускаються у фоні (відокремлено)
• Кнопки тестування виконують команди з поточними значеннями", + "info-parameters-description": "• Wallpaper hook: $1 = шлях до шпалер, $2 = назва екрана, $3 = тема (темна/світла)
• Theme toggle hook: $1 = true/false (стан темного режиму)
• Screen lock/unlock hooks: $1 = lock/unlock (стан блокування екрана)
• Performance mode hooks: Без параметрів
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "Доступні параметри", + "noctalia-started-description": "Команда для виконання після завершення завантаження Noctalia.", + "noctalia-started-label": "Noctalia запущено", + "noctalia-started-placeholder": "напр. notify-send 'Noctalia завантажено'", + "performance-mode-disabled-description": "Команда для виконання при вимкненні режиму продуктивності Noctalia.", + "performance-mode-disabled-label": "Режим продуктивності вимкнено", + "performance-mode-disabled-placeholder": "напр. notify-send \"Продуктивність\" \"Режим вимкнено\"", + "performance-mode-enabled-description": "Команда для виконання при увімкненні режиму продуктивності Noctalia.", + "performance-mode-enabled-label": "Режим продуктивності увімкнено", + "performance-mode-enabled-placeholder": "напр. notify-send \"Продуктивність\" \"Режим увімкнено\"", + "screen-lock-description": "Команда для виконання при блокуванні екрана.", + "screen-lock-label": "Екран заблоковано", + "screen-lock-placeholder": "напр. notify-send \"Екран\" \"Заблоковано\"", + "screen-unlock-description": "Команда для виконання при розблокуванні екрана.", + "screen-unlock-label": "Екран розблоковано", + "screen-unlock-placeholder": "напр. notify-send \"Екран\" \"Розблоковано\"", + "session-description": "Команда, що виконується перед вимкненням або перезавантаженням. Отримує тип дії як $1 (вимкнення/перезавантаження).", + "session-label": "Завершення сесії", + "session-placeholder": "напр. notify-send \"Session\" \"$1\"", + "system-hooks-desc": "Налаштуйте команди для виконання при системних подіях.", + "system-hooks-enable-description": "Увімкнути або вимкнути всі команди хуків.", + "system-hooks-enable-label": "Увімкнути хуки", + "system-hooks-title": "Системні хуки", + "theme-changed-description": "Команда для виконання при перемиканні теми між темним і світлим режимом.", + "theme-changed-label": "Тему змінено", + "theme-changed-placeholder": "напр. notify-send \"Тема\" \"Перемкнуто\"", + "title": "Хуки", + "wallpaper-changed-description": "Команда для виконання при зміні шпалер.", + "wallpaper-changed-label": "Шпалери змінено", + "wallpaper-changed-placeholder": "напр. notify-send \"Шпалери\" \"Змінено\"" + }, + "idle": { + "custom-add": "Додати команду", + "custom-description": "Виконати команду оболонки після періоду бездіяльності.", + "custom-entry-command": "Команда", + "custom-entry-delete": "Видалити", + "custom-entry-edit": "Редагувати Власну Команду", + "custom-entry-name": "Ім'я", + "custom-entry-name-placeholder": "напр. Вимкнути світло", + "custom-entry-new": "Нова Користувацька Команда", + "custom-entry-timeout": "Час бездіяльності", + "custom-entry-timeout-format": "{count} секунда", + "custom-entry-timeout-format-plural": "{count} секунд", + "custom-entry-unnamed": "Безіменна команда", + "custom-label": "Користувацькі команди бездіяльності", + "enable-description": "Автоматично вимикати екран, блокувати або призупиняти роботу після періоду бездіяльності.", + "enable-label": "Увімкнути керування бездіяльністю", + "fade-duration-description": "Секунди для анімації затемнення до чорного перед виконанням кожної дії. Будь-який рух миші скасовує затемнення.", + "fade-duration-label": "Тривалість згасання", + "lock-description": "Секунди бездіяльності до активації екрана блокування.", + "lock-label": "Заблокувати екран", + "resume-command-label": "Відновити команду", + "screen-off-description": "Секунди бездіяльності до вимкнення моніторів.", + "screen-off-label": "Вимкнути екран", + "status-description": "Час простою, який повідомляє композитор.", + "status-label": "Час простою", + "suspend-description": "Секунди бездіяльності до призупинення роботи системи.", + "tab-behavior": "Поведінка", + "tab-custom": "Користувацький", + "timeouts-description": "Встановіть 0, щоб вимкнути етап. Тайм-аути призупиняються, поки 'Заборона сну' активний.", + "timeouts-label": "Тайм-аути", + "title": "Бездіяльність", + "unavailable": "Нативний моніторинг бездіяльності недоступний на цьому Compositorі." + }, + "indicator": { + "default-value": "За замовчуванням: {value}", + "system-default": "Системні налаштування за замовчуванням" + }, + "launcher": { + "clipboard-desc": "Отримуйте доступ до історії буфера обміну та керуйте нею з панелі запуску.", + "execute-desc": "Налаштуйте спосіб запуску програм.", + "execute-title": "Виконання", + "settings-annotation-tool-description": "Команда для запуску при натисканні кнопки анотування в історії буфера обміну. Зображення буде передано в цю команду.", + "settings-annotation-tool-label": "Інструмент анотацій", + "settings-annotation-tool-placeholder": "напр. 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Автоматично вставити вибраний елемент з буфера обміну. Потребує wtype.", + "settings-auto-paste-label": "Автоматична вставка", + "settings-clip-chips-description": "Показати панель вкладок для фільтрації історії буфера обміну за типом (Зображення, Посилання, Файли, Код тощо).", + "settings-clip-chips-label": "Увімкнути чіпи категорій", + "settings-clip-preview-description": "Показувати попередній перегляд вмісту буфера обміну при використанні команди >clip.", + "settings-clip-preview-label": "Попередній перегляд буфера обміну", + "settings-clip-smart-icons-description": "Показувати спеціальні іконки для посилань, файлів, кольорів та коду замість загальної іконки буфера обміну.", + "settings-clip-smart-icons-label": "Увімкнути розумні іконки", + "settings-clip-wrap-text-description": "Переносити текст у списку буфера обміну замість обрізання.", + "settings-clip-wrap-text-label": "Загорнути текст з буфера обміну", + "settings-clipboard-history-description": "Отримати доступ до раніше скопійованих елементів із лаунчера.", + "settings-clipboard-history-label": "Увімкнути історію буфера обміну", + "settings-clipboard-watch-image-description": "Повний рядок команди, переданий до wl-paste для зміни зображень (потрібен перезапуск).", + "settings-clipboard-watch-image-label": "Команда Спостереження за Зображеннями", + "settings-clipboard-watch-text-description": "Повний рядок команди, переданий до wl-paste для змін тексту (потребує перезапуску).", + "settings-clipboard-watch-text-label": "Команда спостереження за текстом", + "settings-custom-launch-prefix-description": "Додати префікс до команд запуску лаунчером (напр. 'runapp' для інтеграції з systemd).", + "settings-custom-launch-prefix-enabled-description": "Використовувати власний префікс для запуску застосунків замість стандартного методу.", + "settings-custom-launch-prefix-enabled-label": "Увімкнути користувацький префікс запуску", + "settings-custom-launch-prefix-label": "Користувацький префікс запуску", + "settings-density-description": "Налаштуйте розмір іконок програм і щільність Launcher.", + "settings-density-label": "Щільність", + "settings-desc": "Налаштуйте поведінку та зовнішній вигляд лаунчера.", + "settings-enable-session-search-description": "Показувати дії сеансу (блокування, вимкнення, перезавантаження тощо) під час пошуку в Launcher.", + "settings-enable-session-search-label": "Увімкнути пошук сесії", + "settings-enable-settings-search-description": "Показувати результати налаштувань під час пошуку в Launcher.", + "settings-enable-settings-search-label": "Увімкнути пошук у налаштуваннях", + "settings-enable-windows-search-description": "Шукати та фокусувати активні вікна.", + "settings-enable-windows-search-label": "Увімкнути пошук вікон", + "settings-grid-view-description": "Показувати елементи у вигляді сітки замість списку.", + "settings-icon-mode-description": "Використовувати нативні системні іконки замість іконок Tabler.", + "settings-icon-mode-label": "Використовувати нативні іконки", + "settings-ignore-mouse-input-description": "Вимкнути взаємодію з мишею та коліщатко прокрутки в лаунчері.", + "settings-ignore-mouse-input-label": "Ігнорувати введення миші", + "settings-overlay-layer-description": "Відображати запускач на шарі накладання, над повноекранними вікнами. Коли увімкнено, запускач не буде прикріплено до панелі.", + "settings-overlay-layer-label": "Показувати над повноекранним режимом", + "settings-position-description": "Виберіть, де з'являється панель лаунчера.", + "settings-show-categories-description": "Показувати вкладки категорій для фільтрації додатків.", + "settings-show-categories-label": "Показувати категорії", + "settings-show-icon-background-description": "Показувати заокруглений прямокутник як фон за іконками.", + "settings-show-icon-background-label": "Показати фон іконки", + "settings-sort-by-usage-description": "Коли увімкнено, часто використовувані застосунки з'являються першими в списку.", + "settings-sort-by-usage-label": "Сортувати за використанням", + "settings-terminal-command-description": "Команда для запуску терміналу. Напр. 'kitty -e' або 'gnome-terminal --'.", + "settings-terminal-command-label": "Команда терміналу", + "settings-use-app2unit-description": "Використовує альтернативний метод запуску для кращого керування процесами застосунків і запобігання проблемам.", + "settings-use-app2unit-label": "Використовувати App2Unit для запуску застосунків", + "settings-view-mode-description": "Виберіть макет для записів Запускача.", + "settings-view-mode-label": "Режим перегляду", + "title": "Лаунчер" + }, + "location": { + "auto-locate-description": "Автоматично визначати ваше місцезнаходження періодично, використовуючи вашу IP-адресу.", + "auto-locate-label": "Автоматичне Визначення", + "calendar-cards-desc": "Організуйте та вмикайте/вимикайте картки на панелі календаря.", + "calendar-cards-title": "Календарні картки", + "calendar-header-label": "Заголовок календаря", + "calendar-month-label": "Місяць календаря", + "date-time-12hour-format-description": "Відображати час у 12-годинному форматі на екрані блокування та в календарі. Годинник на панелі має власні налаштування.", + "date-time-12hour-format-label": "Використовувати 12-годинний формат часу", + "date-time-desc": "Налаштуйте, як відображаються дата та час.", + "date-time-first-day-of-week-automatic": "Автоматично (використовувати системну локаль)", + "date-time-first-day-of-week-description": "Виберіть, який день починає тиждень у календарі.", + "date-time-first-day-of-week-label": "Перший день тижня", + "date-time-show-events-description": "Відображати події на панелі календаря.", + "date-time-show-events-label": "Показувати події календаря", + "date-time-title": "Дата і час", + "date-time-use-analog-description": "Показувати аналоговий годинник у вікні календаря.", + "date-time-use-analog-label": "Використовувати аналоговий годинник", + "date-time-week-numbers-description": "Відображати номер тижня року (напр. тиждень 38) у календарі.", + "date-time-week-numbers-label": "Показувати номери тижнів", + "geolocate-now-button": "Знайти зараз", + "location-desc": "Отримуйте точну погоду та планування нічного світла, встановивши ваше розташування.", + "location-search-description": "напр. Торонто, ОН", + "location-search-label": "Шукати розташування", + "location-search-placeholder": "Введіть назву місця", + "location-title": "Ваше розташування", + "weather-desc": "Виберіть бажану одиницю температури.", + "weather-enabled-description": "Показувати інформацію про погоду в інтерфейсі та отримувати дані про погоду. Коли вимкнено, всі елементи погоди будуть приховані та мережеві запити не виконуватимуться.", + "weather-enabled-label": "Увімкнути погоду", + "weather-fahrenheit-description": "Відображати температуру в Фаренгейтах замість Цельсія.", + "weather-fahrenheit-label": "Відображати температуру в Фаренгейтах (°F)", + "weather-hide-city-description": "Приховати назву міста з відображення погоди в усьому інтерфейсі.", + "weather-hide-city-label": "Приховати назву міста", + "weather-hide-timezone-description": "Приховати абревіатуру часового поясу з відображення погоди в усьому інтерфейсі.", + "weather-hide-timezone-label": "Приховати часовий пояс", + "weather-show-effects-description": "Показує додаткові візуальні ефекти (наприклад, дощ, сніг або блискавку) на картці погоди.", + "weather-show-effects-label": "Відображати погодні ефекти", + "weather-show-in-calendar-description": "Показувати щоденний прогноз погоди безпосередньо в календарі.", + "weather-show-in-calendar-label": "Відображати погоду в календарі", + "weather-talia-mascot-always-description": "Завжди використовуйте Talia emojis замість tabler icons.", + "weather-talia-mascot-always-label": "Завжди показувати погодні емодзі Talia" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "Коли fprintd (автентифікація за відбитком пальця) активний, ця опція дозволяє входити в систему за допомогою пароля замість відбитка пальця.", + "allow-password-with-fprintd-label": "Дозволити вхід за паролем за допомогою fprintd", + "auto-start-auth-description": "Автоматично запускає автентифікацію за відбитком пальця без необхідності натискання клавіші або клацання кнопки.", + "auto-start-auth-label": "Автоматичний запуск автентифікації", + "clock-format-description": "Налаштуйте формат годинника, використовуючи токени синтаксису дати/часу.", + "clock-format-label": "Формат годинника", + "clock-style-analog": "Аналоговий", + "clock-style-custom": "Користувацький", + "clock-style-description": "Виберіть візуальний стиль годинника на Екрані блокування.", + "clock-style-digital": "Цифровий", + "clock-style-label": "Стиль Годинника", + "compact-lockscreen-description": "Показувати тільки поле входу та системні елементи керування, приховуючи віджети погоди та медіа.", + "compact-lockscreen-label": "Компактний екран блокування", + "enable-lockscreen-media-controls-description": "Показувати інтерактивні елементи керування відтворенням медіа на екрані блокування.", + "enable-lockscreen-media-controls-label": "Елементи Керування Медіа на Екрані Блокування", + "lock-on-suspend-description": "Автоматично блокувати екран при призупиненні системи.", + "lock-on-suspend-label": "Блокувати при призупиненні", + "lock-screen-animations-description": "Увімкнути або вимкнути анімації екрана блокування.", + "lock-screen-animations-label": "Анімації Екрана Блокування", + "lock-screen-blur-strength-description": "Застосовує ефект розмиття до шпалер екрана блокування.", + "lock-screen-blur-strength-label": "Сила розмиття екрана блокування", + "lock-screen-tint-strength-description": "Застосовує накладання відтінку до шпалер екрана блокування.", + "lock-screen-tint-strength-label": "Інтенсивність тонування екрана блокування", + "monitors-desc": "Показати екран блокування на певних моніторах. За замовчуванням на всіх, якщо жоден не обрано.", + "password-chars-description": "Милі іконки, що використовуються для приховування вашого пароля.", + "password-chars-label": "Випадкові іконки паролів", + "show-hibernate-description": "Показувати опцію 'сплячий режим' в елементах керування живленням.", + "show-hibernate-label": "Показувати сплячий режим", + "show-session-buttons-description": "Дозволити доступ до налаштувань живлення з екрана блокування.", + "show-session-buttons-label": "Елементи керування живленням", + "title": "Екран блокування" + }, + "network": { + "bluetooth-description": "Активувати керування Bluetooth.", + "bluetooth-rssi-polling-description": "Періодично збирати дані RSSI для підключених пристроїв через bluetoothctl. Може бути недоступним для всіх пристроїв; використовує мінімум ресурсів, коли ввімкнено.", + "bluetooth-rssi-polling-label": "Опитування сигналу Bluetooth", + "desc": "Керувати підключеннями Wi-Fi та Bluetooth.", + "wifi-description": "Керування бездротовими мережами (потребує NetworkManager)." + }, + "notifications": { + "duration-critical-urgency-description": "Як довго залишаються видимими критичні сповіщення.", + "duration-critical-urgency-label": "Критична терміновість", + "duration-desc": "Налаштуйте, як довго сповіщення залишаються видимими залежно від рівня терміновості.", + "duration-low-urgency-description": "Як довго залишаються видимими сповіщення низького пріоритету.", + "duration-low-urgency-label": "Низька терміновість", + "duration-normal-urgency-description": "Як довго залишаються видимими сповіщення звичайного пріоритету.", + "duration-normal-urgency-label": "Звичайна терміновість", + "duration-respect-expire-description": "Використовувати тривалість показу, встановлений у сповіщенні.", + "duration-respect-expire-label": "Враховувати тривалість показу", + "duration-title": "Тривалість сповіщень", + "history-clear-dismiss-description": "Видаляти сповіщення з історії після закриття.", + "history-clear-dismiss-label": "Очищати при закритті", + "history-critical-urgency-description": "Зберігати сповіщення з критичним пріоритетом в історії.", + "history-critical-urgency-label": "Зберігати критичну терміновість в історії", + "history-desc": "Керування тим, які сповіщення зберігаються в історії залежно від рівня терміновості.", + "history-low-urgency-description": "Зберігати сповіщення з низьким пріоритетом в історії.", + "history-low-urgency-label": "Зберігати низьку терміновість в історії", + "history-normal-urgency-description": "Зберігати сповіщення з нормальним пріоритетом в історії.", + "history-normal-urgency-label": "Зберігати нормальну терміновість в історії", + "monitors-desc": "Показувати сповіщення на певних моніторах. За замовчуванням на всіх, якщо не вибрано.", + "rules-action-block": "Блок", + "rules-action-block-desc": "Пропускає повністю.", + "rules-action-hide": "Сховати", + "rules-action-hide-desc": "Без спливаючих вікон, без звуку, додається до історії.", + "rules-action-label": "Дія", + "rules-action-mute": "Без звуку", + "rules-action-mute-desc": "Немає звуку, все одно показує спливаюче вікно та в історії.", + "rules-add": "Додати правило", + "rules-delete": "Видалити правило", + "rules-description": "Зіставити назву застосунку або вміст. Правила перевіряються по черзі, і перше співпадіння застосовується.", + "rules-edit": "Редагувати правило", + "rules-label": "Правила Фільтрації", + "rules-pattern-label": "Шаблон", + "rules-pattern-placeholder": "firefox, discord, або /regex/", + "rules-tab": "Правила", + "settings-always-on-top-description": "Відображати сповіщення поверх повноекранних вікон та інших шарів.", + "settings-background-opacity-description": "Налаштуйте непрозорість фону сповіщень.", + "settings-density-description": "Виберіть щільність картки сповіщень.", + "settings-density-label": "Щільність", + "settings-desc": "Налаштуйте зовнішній вигляд та поведінку сповіщень.", + "settings-do-not-disturb-description": "Вимкнути всі спливаючі сповіщення, коли увімкнено.", + "settings-enabled-description": "Увімкнути або вимкнути демон сповіщень, вимагає перезапуску Noctalia shell.", + "settings-enabled-label": "Увімкнути сповіщення", + "settings-location-description": "Де з'являються сповіщення на екрані.", + "settings-markdown-description": "Відображати вміст сповіщень, використовуючи форматування Markdown.", + "settings-markdown-label": "Увімкнути Markdown", + "sounds-desc": "Налаштуйте звукові ефекти та гучність сповіщень.", + "sounds-enabled-description": "Увімкнути звукові ефекти для вхідних сповіщень.", + "sounds-enabled-label": "Увімкнути звуки сповіщень", + "sounds-excluded-apps-description": "Пропускати відтворення налаштованого звуку сповіщень для певних програм, які мають власні вбудовані звуки.", + "sounds-excluded-apps-label": "Виключені програми", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Шлях до звукового файлу, що відтворюється для сповіщень із критичним пріоритетом.", + "sounds-files-critical-label": "Звук критичної терміновості", + "sounds-files-critical-select-title": "Вибрати файл звуку критичної терміновості", + "sounds-files-desc": "Налаштуйте власні звукові файли для різних рівнів терміновості сповіщень.", + "sounds-files-low-description": "Шлях до звукового файлу, що відтворюється для сповіщень із низьким пріоритетом.", + "sounds-files-low-label": "Звук низької терміновості", + "sounds-files-low-select-title": "Вибрати файл звуку низької терміновості", + "sounds-files-normal-description": "Шлях до звукового файлу, що відтворюється для сповіщень із нормальним пріоритетом.", + "sounds-files-normal-label": "Звук нормальної терміновості", + "sounds-files-normal-select-title": "Вибрати файл звуку нормальної терміновості", + "sounds-files-placeholder": "Введіть шлях до звукового файлу", + "sounds-files-select-file": "Вибрати звуковий файл", + "sounds-files-title": "Звукові файли", + "sounds-files-unified-description": "Шлях до звукового файлу, що відтворюється для сповіщень.", + "sounds-files-unified-label": "Звук сповіщення", + "sounds-files-unified-select-title": "Вибрати файл звуку сповіщення", + "sounds-separate-description": "Використовувати різні звукові файли для сповіщень із низьким, нормальним та критичним пріоритетом.", + "sounds-separate-label": "Використовувати різні звуки для різних пріоритетів", + "sounds-title": "Налаштування звуку", + "sounds-unavailable-description": "Встановіть Qt6 Multimedia, щоб увімкнути звуки сповіщень.", + "sounds-unavailable-label": "Звуки сповіщень недоступні", + "sounds-volume-description": "Налаштуйте рівень гучності для звуків сповіщень.", + "sounds-volume-label": "Гучність звуку", + "toast-battery-description": "Показувати попередження, коли рівень заряду батареї падає нижче цього відсотка.", + "toast-battery-label": "Попередження про батарею", + "toast-desc": "Налаштуйте зовнішній вигляд і поведінку спливаючих повідомлень.", + "toast-keyboard-description": "Показувати сповіщення при зміні розкладки клавіатури.", + "toast-keyboard-label": "Розкладка клавіатури", + "toast-media-description": "Показувати спливаюче повідомлення, коли змінюється стан відтворення медіа.", + "toast-media-label": "Медіа" + }, + "osd": { + "always-on-top-description": "Відображати екранну індикацію поверх повноекранних вікон та інших шарів.", + "always-on-top-label": "Завжди зверху", + "background-opacity-description": "Керує прозорістю фону OSD.", + "background-opacity-label": "Прозорість фону", + "description": "Налаштуйте екранні індикатори, такі як накладки гучності та яскравості.", + "duration-auto-hide-description": "Налаштуйте час до зникнення екранної індикації.", + "duration-auto-hide-label": "Автоприховування через", + "duration-desc": "Як довго екранна індикація залишається видимою перед автоматичним приховуванням.", + "duration-title": "Час автоприховування", + "enabled-description": "Показувати зміни гучності та яскравості в реальному часі.", + "enabled-label": "Увімкнути екранну індикацію", + "events-desc": "Виберіть, які події викликають відображення на екрані.", + "general-desc": "Налаштуйте видимість і поведінку екранного меню (OSD).", + "location-description": "Де з'являються екранні індикації.", + "monitors-desc": "Показувати екранну індикацію на певних моніторах. За замовчуванням на всіх, якщо не вибрано.", + "title": "Екранна індикація", + "types-brightness-description": "Показувати OSD, коли змінюється яскравість екрана.", + "types-brightness-label": "Яскравість", + "types-custom-text-description": "Показувати OSD для користувацьких текстових повідомлень через IPC.", + "types-custom-text-label": "Користувацький текст", + "types-desc": "Виберіть події, які запускають екранне меню.", + "types-input-volume-description": "Показувати OSD, коли змінюється гучність мікрофона.", + "types-input-volume-label": "Вхідна гучність", + "types-lockkey-description": "Показувати OSD, коли перемикаються Caps Lock, Num Lock або Scroll Lock.", + "types-lockkey-label": "Клавіші блокування", + "types-media-description": "Показувати Екранне меню (OSD) при зміні стану відтворення медіа (відтворення, пауза, пропуск).", + "types-media-label": "Відтворення медіа", + "types-title": "Події, що запускають OSD", + "types-volume-description": "Показувати OSD, коли змінюється гучність аудіовиходу.", + "types-volume-label": "Вихідна гучність" + }, + "plugins": { + "auto-update": "Автоматичне оновлення плагінів", + "auto-update-description": "Автоматично оновлювати всі плагіни при запуску оболонки.", + "available-description": "Переглядайте та встановлюйте плагіни з налаштованих джерел.", + "available-label": "Доступні плагіни", + "available-no-plugins-description": "Перевірте вихідні коди вашого плагіна або оновіть список.", + "available-no-plugins-label": "Немає доступних плагінів", + "check-for-updates": "Перевірити оновлення", + "checking-for-updates": "Перевірка оновлень...", + "collision-already-installed": "Цей плагін вже встановлено", + "collision-custom-version-exists": "Вже встановлено власну версію з \"{source}\"", + "collision-official-version-exists": "Офіційна версія цього плагіна вже встановлена", + "development-disable": "Вимкнути режим розробки для цього плагіна", + "development-enable": "Увімкнути режим розробки для цього плагіна.", + "filter-downloaded": "Завантажено", + "filter-not-downloaded": "Не завантажено", + "filter-tags-description": "Фільтрувати плагіни за категорією або статусом завантаження.", + "filter-tags-label": "Теги", + "hot-reload-description": "Автоматично перезавантажувати плагіни при зміні їхніх файлів. Корисно для розробки плагінів.", + "hot-reload-label": "Гаряче перезавантаження (режим розробки)", + "hot-reloaded": "Перезавантажено плагін: {name}", + "install-error": "Не вдалося встановити: {error}", + "install-incompatible": "{plugin} вимагає Noctalia v{version} або вище", + "install-success": "Успішно встановлено {plugin}", + "installed-description": "Керуйте та налаштовуйте всі локально встановлені плагіни.", + "installed-label": "Встановлені плагіни", + "installed-no-plugins-description": "Встановіть плагіни з розділу \"Доступний\".", + "installed-no-plugins-label": "Немає встановлених плагінів", + "installing": "Встановлення {plugin}...", + "notify-updates": "Сповіщення про оновлення плагінів", + "notify-updates-description": "Показувати спливаюче сповіщення, коли доступні нові оновлення плагінів.", + "open-plugin-page": "Відкрити сторінку плагіна", + "open-plugins-tab": "Відкрити налаштування плагінів", + "plugin-settings-title": "Налаштування {plugin}", + "refresh-refreshing": "Оновлення списку плагінів...", + "refresh-tooltip": "Оновити доступні плагіни", + "settings-error-not-loaded": "Плагін не завантажено", + "settings-saved": "Налаштування плагіна збережено", + "settings-tooltip": "Налаштування плагіна", + "source-custom": "Власне джерело", + "sources-add-custom": "Додати власний репозиторій", + "sources-add-dialog-description": "Додати репозиторій GitHub як джерело плагінів.", + "sources-add-dialog-error": "Не вдалося додати джерело плагінів", + "sources-add-dialog-name": "Назва репозиторію", + "sources-add-dialog-name-placeholder": "Мої власні плагіни", + "sources-add-dialog-success": "Джерело плагіна успішно додано", + "sources-add-dialog-title": "Додати джерело плагіна", + "sources-add-dialog-url": "URL репозиторію", + "sources-description": "Керування репозиторіями плагінів.", + "sources-placeholder": "Мій крутий репозиторій", + "sources-remove-tooltip": "Видалити джерело плагіна", + "title": "Плагіни", + "translations-reloaded": "Переклади перезавантажено: {name}", + "uninstall-dialog-description": "Ви впевнені, що хочете видалити {plugin}? Це призведе до видалення всіх даних плагіна.", + "uninstall-dialog-title": "Видалити плагін", + "uninstall-error": "Не вдалося видалити: {error}", + "uninstall-success": "Успішно видалено {plugin}", + "uninstalling": "Видалення {plugin}...", + "update-all": "Оновити все ({count})", + "update-all-success": "Усі плагіни успішно оновлено", + "update-available": "Доступне нове оновлення плагіна", + "update-available-plural": "Доступні нові оновлення плагінів ({count})", + "update-error": "Не вдалося оновити плагін: {plugin}: {error}", + "update-pending": "v{current} → v{new} (потрібно Noctalia v{required})", + "update-success": "Оновлено {plugin} до версії {version}", + "update-version": "v{current} → v{new}", + "updating": "Оновлення..." + }, + "region": { + "title": "Регіон" + }, + "session-menu": { + "countdown-duration-description": "Встановити, як довго триває таймер зворотного відліку перед виконанням дій живлення.", + "countdown-duration-label": "Тривалість зворотного відліку", + "enable-countdown-description": "Показувати таймер зворотного відліку перед виконанням дій живлення.", + "enable-countdown-label": "Увімкнути таймер зворотного відліку", + "entries-desc": "Налаштувати, які дії живлення відображаються в меню сеансу та в якому порядку.", + "entries-title": "Дії живлення", + "entry-settings-command-description": "Спеціальна команда для виконання цієї дії. Залиште поле порожнім, щоб використовувати системну команду за замовчуванням.", + "entry-settings-command-placeholder": "напр. systemctl poweroff", + "entry-settings-default-command-lock": "Внутрішній екран блокування (без команди)", + "entry-settings-default-command-logout": "Внутрішній вихід з системи (без команди)", + "entry-settings-default-info-description": "Якщо не вказано жодної власної команди, буде використано системну команду за замовчуванням.", + "entry-settings-default-info-label": "Команда за замовчуванням", + "entry-settings-keybind-description": "Призначте комбінацію клавіш для запуску цієї дії, коли меню сесії відкрито.", + "entry-settings-keybind-placeholder": "Натисніть Запис, щоб призначити клавішу...", + "entry-settings-keybind-recording": "Запис... Натисніть будь-яку комбінацію клавіш", + "entry-settings-title": "Налаштувати {entry}", + "entry-settings-tooltip": "Налаштувати команду", + "general-desc": "Налаштувати поведінку та зовнішній вигляд панелі меню сеансу.", + "large-buttons-layout-description": "Виберіть спосіб відображення кнопок меню сеансу.", + "large-buttons-layout-label": "Розкладка з великими кнопками", + "large-buttons-style-description": "Відобразити меню сеансу великими кнопками у вигляді сітки.", + "large-buttons-style-label": "Стиль великих кнопок", + "position-description": "Виберіть, де з'являється панель меню сеансу при відкритті.", + "show-header-description": "Відображати заголовок та кнопку закриття в верхній частині меню сеансу.", + "show-header-label": "Показувати заголовок", + "show-keybinds-description": "Показувати підказки щодо комбінацій клавіш у параметрах сеансу.", + "show-keybinds-label": "Показати комбінації клавіш" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Залишати віджети робочого столу видимими, поки режим продуктивності Noctalia увімкнено.", + "noctalia-performance-disable-desktop-widgets-label": "Увімкнути віджети робочого столу в режимі продуктивності", + "noctalia-performance-disable-wallpaper-description": "Залишайте шпалери робочого столу, огляду та екрана блокування видимими, коли режим продуктивності Noctalia увімкнено.", + "noctalia-performance-disable-wallpaper-label": "Увімкнути відтворення шпалер у режимі продуктивності", + "title": "Система" + }, + "system-monitor": { + "critical-color-label": "Критичний колір", + "custom-highlight-colors-title-label": "Власні кольори підсвічування", + "disk-available-label": "Доступно місця на диску", + "disk-section-label": "Використання диска", + "enable-dgpu-monitoring-description": "Увага: Це розбудить ваш дискретний графічний процесор (NVIDIA/AMD), що може значно вплинути на час роботи акумулятора на ноутбуках з гібридною графікою.", + "enable-dgpu-monitoring-label": "Увімкнути моніторинг дискретного GPU", + "external-monitor-description": "Введіть команду або шлях до програми для запуску при активації зовнішньої програми моніторингу системи.", + "external-monitor-label": "Команда зовнішнього монітора системи", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "Налаштуйте поведінку та зовнішній вигляд системного монітора.", + "gpu-section-label": "Температура GPU", + "highlight-colors-section-label": "Кольори підсвічування", + "polling-interval-label": "Інтервал опитування", + "polling-section-description": "Налаштуйте, як часто оновлюється кожна системна метрика.", + "polling-section-label": "Інтервали опитування", + "threshold-critical": "Критичний", + "threshold-warning": "Попередження", + "thresholds-section-description": "Налаштуйте пороги попередження/критичні пороги та інтервали опитування для кожного системного показника.", + "use-custom-highlight-colors-description": "Якщо вимкнено, використовуються кольори підсвічування за замовчуванням теми.", + "use-custom-highlight-colors-label": "Використовувати власні кольори підсвічування", + "warning-color-label": "Колір попередження" + }, + "user-interface": { + "allow-panels-without-bar-description": "Якщо ввімкнено, панелі можуть відкриватися на будь-якому екрані. Якщо вимкнено, панелі відкриватимуться лише на екранах, де є панель завдань, що може зменшити використання пам'яті.", + "allow-panels-without-bar-label": "Дозволити панелі на екранах без панелі завдань", + "animation-disable-description": "Вимкнути всі анімації для швидшого, більш відгукового досвіду.", + "animation-disable-label": "Вимкнути анімації інтерфейсу", + "animation-speed-description": "Налаштувати глобальну швидкість анімації.", + "animation-speed-label": "Швидкість анімації", + "animation-speed-reset": "Скинути швидкість анімації", + "appearance-desc": "Налаштовуйте візуальні елементи, такі як підказки, рамки та тіні.", + "blur-behind-description": "Розмиває область за панелями та спливаючими вікнами, використовуючи протокол розмиття композитора.", + "blur-behind-label": "Розмиття фону", + "box-border-description": "Відображає контур навколо областей вмісту.", + "box-border-label": "Контур контейнера", + "box-border-radius-description": "Налаштовує заокруглення кутів основних розділів макета, таких як бічні панелі, картки та панелі контенту.", + "box-border-radius-label": "Радіус контейнера", + "control-border-radius-description": "Керує кривизною інтерактивних елементів, включно з кнопками, перемикачами та текстовими полями.", + "control-border-radius-label": "Введіть радіус", + "desc": "Налаштуйте вигляд, відчуття та поведінку інтерфейсу.", + "dim-desktop-description": "Затемнювати робочий стіл, коли відкриті панелі або меню.", + "dim-desktop-label": "Приглушити робочий стіл", + "dimmer-opacity-description": "Встановити рівень непрозорості для затемнення робочого столу.", + "dimmer-opacity-label": "Непрозорість приглушеного робочого столу", + "panel-background-opacity-description": "Встановити прозорість фону для всіх панелей (панель задач, панель запуску, налаштування тощо...).", + "panel-background-opacity-label": "Прозорість фону панелей", + "panels-attached-to-bar-description": "Панелі фіксуються до штанги та країв екрана, створюючи бездоганний вигляд зі стильними перевернутими кутами.", + "panels-attached-to-bar-label": "Прив'язувати панелі до країв", + "panels-overlay-description": "Забезпечує видимість панелей і панелі завдань, навіть поверх повноекранних застосунків.", + "panels-overlay-label": "Тримати панелі та панель зверху", + "scaling-description": "Змінює розмір загального користувацького інтерфейсу, окрім панелі.", + "scaling-label": "Масштабування інтерфейсу", + "scrollbar-always-visible-description": "Завжди показувати смуги прокрутки, коли вміст можна прокручувати, замість того, щоб відображати їх лише при наведенні.", + "scrollbar-always-visible-label": "Завжди показувати смуги прокрутки", + "settings-panel-header": "Панель налаштувань", + "settings-panel-mode-description": "Виберіть макет налаштувань (може знадобитися перезапуск).", + "settings-panel-mode-label": "Режим панелі налаштувань", + "settings-panel-sidebar-card-style-description": "Обгортає бічну панель налаштувань у заповнений фон із заокругленими кутами.", + "settings-panel-sidebar-card-style-label": "Стиль карток бічної панелі", + "shadows-description": "Увімкнути тіні під панелями та смугами.", + "shadows-direction-description": "Виберіть, куди відкидається тінь.", + "shadows-direction-label": "Напрямок тіні", + "shadows-label": "Тіні", + "title": "Користувацький інтерфейс", + "tooltips-description": "Увімкнути або вимкнути підказки в інтерфейсі.", + "tooltips-label": "Показувати підказки", + "translucent-widgets-description": "Зробіть кнопки, вкладки та інші віджети всередині панелей напівпрозорими.", + "translucent-widgets-label": "Напівпрозорі віджети" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Алфавітний", + "automation-change-mode-description": "Виберіть, як вибираються шпалери при автоматичній зміні.", + "automation-change-mode-label": "Режим зміни", + "automation-custom-interval-description": "Введіть час як ГГ:ХХ (напр. 01:30).", + "automation-custom-interval-label": "Власний інтервал", + "automation-interval-description": "Як часто автоматично змінювати шпалери.", + "automation-interval-label": "Інтервал шпалер", + "automation-random-wallpaper-description": "Планувати випадкову зміну шпалер через регулярні інтервали.", + "automation-scheduled-change-description": "Автоматично змінювати шпалери через регулярні інтервали.", + "automation-scheduled-change-label": "Запланована зміна", + "look-feel-edge-smoothness-description": "Застосовує м'який, розмитий ефект до краю переходів.", + "look-feel-edge-smoothness-label": "Пом'якшення краю переходу", + "look-feel-fill-color-description": "Виберіть колір заповнення, що може з'являтися за шпалерами.", + "look-feel-fill-mode-description": "Виберіть, як зображення має масштабуватися відповідно до роздільної здатності монітора.", + "look-feel-fill-mode-label": "Режим заповнення", + "look-feel-skip-startup-transition-description": "Пропускати анімацію шпалер при запуску оболонки.", + "look-feel-skip-startup-transition-label": "Пропустити стартову анімацію", + "look-feel-title": "Зовнішній вигляд", + "look-feel-transition-duration-description": "Тривалість анімацій переходу в секундах.", + "look-feel-transition-duration-label": "Тривалість переходу", + "look-feel-transition-type-description": "Тип анімації при перемиканні між шпалерами.", + "look-feel-transition-type-label": "Тип переходу", + "settings-clear-cache-button": "Очистити кеш", + "settings-clear-cache-description": "Очистити кешовані масштабовані шпалери, щоб звільнити місце на диску.", + "settings-clear-cache-label": "Кеш шпалер", + "settings-clear-cache-toast": "Кеш шпалер очищено", + "settings-desc": "Керуйте управлінням та відображенням шпалер.", + "settings-enable-management-description": "Керувати шпалерами за допомогою Noctalia. Вимкніть, якщо надаєте перевагу іншому застосунку.", + "settings-enable-management-label": "Увімкнути керування шпалерами", + "settings-enable-overview-description": "Застосовує розмиті та затемнені шпалери до екрана огляду.", + "settings-enable-overview-label": "Увімкнути шпалери огляду", + "settings-folder-description": "Шлях до основної теки шпалер.", + "settings-folder-label": "Тека шпалер", + "settings-hide-wallpaper-filenames-tooltip-hide": "Приховати імена файлів", + "settings-hide-wallpaper-filenames-tooltip-show": "Відображати імена файлів", + "settings-monitor-specific-description": "Встановити різні теки шпалер для кожного монітора.", + "settings-monitor-specific-label": "Окремі теки для моніторів", + "settings-monitor-specific-tooltip": "Тека шпалер монітора", + "settings-overview-blur-strength-description": "Застосовує силу розмиття до огляду.", + "settings-overview-blur-strength-label": "Сила розмиття огляду", + "settings-overview-tint-description": "Застосовує інтенсивність відтінку до огляду.", + "settings-overview-tint-label": "Інтенсивність тонування огляду", + "settings-recursive-search-description": "Також шукати шпалери в підтеках теки шпалер.", + "settings-recursive-search-label": "Шукати в підтеках", + "settings-select-monitor-folder": "Вибрати теку шпалер монітора", + "settings-selector-description": "Виберіть свої шпалери.", + "settings-selector-position-description": "Виберіть, де відображатиметься панель вибору шпалер.", + "settings-show-hidden-files-tooltip-hide": "Приховати приховані файли", + "settings-show-hidden-files-tooltip-show": "Показати приховані файли", + "settings-title": "Налаштування шпалер", + "settings-use-original-images-description": "Пропускати зміну розміру шпалер перед відображенням. Економить місце на диску та зменшує використання CPU під час зміни шпалер, але може використовувати більше пам'яті для дуже великих зображень.", + "settings-use-original-images-label": "Використовувати оригінальні зображення", + "settings-view-mode-description": "Оберіть спосіб відображення шпалер з вашого каталогу.", + "settings-view-mode-label": "Режим перегляду", + "view-mode-browse": "Перегляд каталогів", + "view-mode-cycle-tooltip": "Режим перегляду: {mode} (натисніть, щоб змінити)", + "view-mode-recursive": "Зведені підкаталоги", + "view-mode-single": "Кореневий каталог" + } + }, + "placeholders": { + "add-new-keybind": "Додати нову прив'язку клавіш", + "command-example": "echo \"Привіт, Світ\"", + "enter-command": "Введіть команду для виконання (застосунок або власний скрипт)", + "enter-ipc-identifier": "Введіть унікальний ідентифікатор для команд IPC", + "enter-path": "Введіть шлях...", + "enter-text-to-collapse": "напр. 'нічого не відтворюється'. Використовуйте /regex/ для шаблонів.", + "enter-tooltip": "Введіть підказку", + "enter-width-pixels": "Введіть ширину в пікселях", + "keybind-recording": "Запис комбінації клавіш...", + "search": "Пошук...", + "search-icons": "напр. noctalia, niri, battery, cloud", + "search-launcher": "Пошук записів... або використовуйте > для команд", + "search-wallhaven": "Пошук у Wallhaven...", + "search-wallpapers": "Введіть для фільтрації шпалер..." + }, + "positions": { + "bottom": "Знизу", + "bottom-center": "Знизу по центру", + "bottom-left": "Знизу ліворуч", + "bottom-right": "Знизу праворуч", + "center": "Центр", + "center-left": "Зліва по центру", + "center-right": "Справа по центру", + "close-to-bar": "Біля кнопки панелі", + "follow-bar": "Слідувати за панеллю", + "left": "Лівий", + "right": "Правий", + "top": "Зверху", + "top-center": "Зверху по центру", + "top-left": "Згори ліворуч", + "top-right": "Згори праворуч" + }, + "session-menu": { + "action-in-seconds": "{action} через {seconds} секунд...", + "cancel-timer": "Скасувати таймер", + "title": "Меню сеансу" + }, + "setup": { + "all-done": "Готово!", + "appearance": { + "subheader": "Виберіть темний режим і джерела кольорів (шпалери або попередньо визначені)." + }, + "customize": { + "header": "Налаштуйте ваш досвід", + "subheader": "Налаштуйте положення панелі, щільність, масштабування та інше." + }, + "skip-setup": "Пропустити налаштування", + "telemetry-wizard-done": "Зрозуміло!", + "telemetry-wizard-note": "Ви все контролюєте — увімкніть або вимкніть це будь-коли в налаштуваннях", + "telemetry-wizard-subtitle": "Ми додали анонімну аналітику, щоб допомогти покращити Noctalia", + "telemetry-wizard-title": "Оновлення конфіденційності", + "wallpaper": { + "choose-dir": "Виберіть теку, що містить ваші зображення шпалер", + "dir-browse": "Огляд теки шпалер", + "dir-description": "Виберіть теку, що містить ваші шпалери.", + "dir-label": "Тека шпалер", + "dir-select-title": "Вибрати теку шпалер", + "header": "Виберіть ваші шпалери", + "no-dir": "Тека шпалер не вибрана", + "no-valid": "Не знайдено дійсних файлів зображень у: {dir}", + "none-in-dir": "Шпалери не знайдено в теці", + "preview-error": "Не вдалося завантажити зображення", + "select-prompt": "Виберіть шпалери нижче", + "subheader": "Створіть настрій за допомогою красивого фону." + }, + "welcome": "Ласкаво просимо", + "welcome-note": "Лише кілька основних речей для початку — повні опції в налаштуваннях", + "welcome-subtitle": "Зробімо ваш робочий стіл унікальним", + "welcome-title": "Вітаємо в Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "Медіаплеєр не виявлено", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "Невідомий застосунок", + "uptime": "Час роботи: {uptime}", + "user-requested": "На запит користувача", + "welcome-back": "З поверненням,", + "widget-settings-title": "Налаштування {widget}" + }, + "system-monitor": { + "core-usage": "Використання ядра {id}", + "cpu-temp": "Температура ЦП", + "cpu-usage": "Використання ЦП", + "disk": "Диск", + "download-speed": "Швидкість завантаження", + "gpu-temp": "Температура GPU", + "load-average": "Середнє навантаження", + "title": "Системний монітор", + "upload-speed": "Швидкість завантаження" + }, + "toast": { + "airplane-mode": { + "description": "Вимкнути всі бездротові комунікації.", + "title": "Режим польоту" + }, + "battery": { + "critical": "Критичний рівень заряду батареї", + "critical-desc": "Батарея заряджена на {percent}% — негайно підключіть зарядний пристрій", + "low": "Низький заряд батареї", + "low-desc": "Батарея заряджена на {percent}% — будь ласка, підключіть зарядний пристрій" + }, + "bluetooth": { + "address-copied": "Адресу скопійовано до буфера обміну", + "auto-connect-disabled": "Автоматичне підключення вимкнено", + "auto-connect-enabled": "Автоматичне підключення увімкнено", + "auto-connecting": "Підключення до {count} пристрою(їв)...", + "confirm-code": "Підтвердьте код {value} на іншому пристрої.", + "connect-failed": "Не вдалося під'єднатися до пристрою", + "disconnect-failed": "Не вдалося від'єднатися від пристрою", + "discoverable-change-failed": "Не вдалося змінити стан видимості", + "discoverable-disabled": "Виявлення вимкнено", + "discoverable-enabled": "Виявлення ввімкнено", + "display-code": "Введіть код {value} на іншому пристрої.", + "forget-failed": "Не вдалося забути пристрій", + "pair-failed": "Не вдалося створити пару з пристроєм", + "passkey-required": "Потрібен ключ доступу, що зберігається на пристрої.", + "pincode-required": "PIN-код, необхідний пристроєм.", + "state-change-failed": "Не вдалося змінити стан Bluetooth" + }, + "clipboard": { + "long-text": "Довгий текст", + "unavailable": "Історія буфера обміну недоступна", + "unavailable-desc": "Застосунок 'cliphist' не встановлений. Будь ласка, встановіть його для використання функцій історії буфера обміну" + }, + "custom-command-failed": { + "description": "Команда не виконана: {command}\\nКод виходу: {code}", + "title": "Користувацька команда не виконана" + }, + "do-not-disturb": { + "disabled": "'Не турбувати' вимкнено", + "disabled-desc": "Показ усіх сповіщень", + "enabled": "'Не турбувати' увімкнено", + "enabled-desc": "Ви знайдете ці сповіщення в своїй історії" + }, + "donation-opened": "Сторінка пожертв відкрито у вашому браузері", + "internet-limited": "Підключено без інтернету", + "keyboard-layout": { + "changed": "Розкладка клавіатури змінена на {layout}", + "title": "Клавіатура" + }, + "missing-control-center": { + "description": "Віджет центру керування видалено з панелі. Щоб знову отримати до нього доступ з панелі, вам потрібно повторно додати віджет. Ви також можете відкрити його, клацнувши правою кнопкою миші на панелі", + "label": "Останній віджет центру керування видалено" + }, + "night-light": { + "forced": "Примусова активація", + "normal": "Звичайний режим", + "not-installed": "wlsunset не встановлено" + }, + "noctalia-performance": { + "disabled": "Режим продуктивності вимкнено", + "enabled": "Режим продуктивності ввімкнено", + "label": "Продуктивність Noctalia" + }, + "power-profile": { + "changed": "Профіль живлення змінено", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "Виникла помилка під час обробки шаблонів", + "title": "Помилка обробки шаблонів" + }, + "vpn": { + "connected": "Підключено до '{name}'", + "disconnected": "Відключено від '{name}'" + }, + "wallpaper-colors": { + "disabled": "Кольори шпалер вимкнено", + "enabled": "Кольори шпалер увімкнено", + "label": "Генерація кольорів шпалер" + }, + "wifi": { + "connected": "Підключено до '{ssid}'", + "connection-failed": "З'єднання не вдалося", + "connection-timeout": "Час очікування з'єднання вичерпано", + "disconnected": "Відключено від '{ssid}'", + "incorrect-password": "Неправильний пароль", + "network-not-found": "Мережу не знайдено" + } + }, + "tooltips": { + "add-widget": "Додати віджет", + "auto-connect": "Перемкнути автопідключення для цього пристрою", + "bluetooth-auto-connect-off": "Автоматичне підключення вимкнено", + "bluetooth-auto-connect-on": "Автоматичне підключення увімкнено", + "bluetooth-devices": "Пристрої Bluetooth", + "brightness-at": "Яскравість: {brightness}%", + "click-to-start-recording": "Запис екрана (почати запис)", + "click-to-stop-recording": "Запис екрана (зупинити запис)", + "collapse": "Згорнути бічну панель", + "copy-address": "Копіювати адресу", + "delete-notification": "Видалити сповіщення", + "dismiss-notification": "Відхилити сповіщення", + "do-not-disturb-enabled": "Не турбувати", + "expand": "Розгорнути бічну панель", + "forget-network": "Забути мережу", + "grid-view": "Мережевий вигляд", + "hidden-files-hide": "Приховані файли", + "hide-unnamed-devices": "Приховати пристрої без назви", + "home": "Домашній каталог", + "input-muted": "Увімкнути/вимкнути вимкнення звуку мікрофона", + "keep-awake": "Заборона сну", + "keyboard-layout": "Розкладка клавіатури {layout}", + "list-view": "Список", + "manage-vpn": "Підключення VPN", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "Досягнуто максимальної кількості віджетів", + "microphone-volume-at": "Гучність мікрофона: {volume}%", + "move-to-section": "Перейти до {section}", + "mute": "Вимкнути звук", + "next-media": "Наступний трек", + "next-month": "Наступний місяць", + "night-light-not-installed": "Нічне світло (недоступне)", + "noctalia-performance-enabled": "Режим продуктивності Noctalia", + "open-annotation-tool": "Відкрити за допомогою інструменту анотацій", + "open-control-center": "Центр керування", + "open-notification-history-enable-dnd": "Історія сповіщень", + "open-settings": "Налаштування", + "open-tray-dropdown": "Системний трей", + "output-muted": "Вимкнути звук виводу", + "power-profile": "{profile} профіль живлення", + "previous-media": "Попередній трек", + "previous-month": "Попередній місяць", + "refresh-devices": "Оновити пристрої", + "refresh-wallhaven": "Оновити результати Wallhaven", + "refresh-wallpaper-list": "Оновити список шпалер", + "remove-widget": "Видалити віджет", + "search-close": "Закрити пошук", + "session-menu": "Меню сеансу", + "show-all-devices": "Показати всі пристрої", + "switch-to-dark-mode": "Темний режим", + "switch-to-light-mode": "Світлий режим", + "unmute": "Увімкнути звук", + "up": "Батьківський каталог", + "volume-at": "Вихідна гучність: {volume}%", + "wallpaper-selector": "Вибір шпалер" + }, + "wallpaper": { + "browse": { + "empty-directory": "Цей каталог порожній.", + "go-root": "Перейти до кореневої папки шпалер", + "go-up": "Перейти до батьківської папки", + "go-up-hint": "Використовуйте кнопку «Назад» для навігації вгору." + }, + "configure-directory": "Налаштуйте вашу теку шпалер зі зображеннями.", + "fill-modes": { + "crop": "Обрізати (Заповнити)", + "fit": "Вмістити (Вписати)", + "repeat": "Повторити (Плитка)", + "stretch": "Розтягнути" + }, + "no-match": "Збігів не знайдено.", + "no-wallpaper": "Шпалери не знайдено.", + "panel": { + "apikey-help": "Для доступу до контенту NSFW потрібен ключ API.", + "apikey-label": "Ключ API", + "apikey-managed-by-env": "Керується через змінну середовища NOCTALIA_WALLHAVEN_API_KEY.", + "apikey-placeholder": "Введіть свій API-ключ Wallhaven", + "appearance-dark-tab": "Темний", + "appearance-light-tab": "Світлий", + "categories-anime": "Аніме", + "categories-label": "Категорії", + "categories-people": "Люди", + "color-extraction-disabled": "Використовувати вилучення кольорів шпалер", + "color-extraction-enabled": "Використовувати попередньо визначені кольорові схеми", + "header-devices-apply-all-tooltip": "Вибрати шпалери для кожного монітора", + "header-devices-per-monitor-tooltip": "Застосувати до всіх моніторів", + "header-link-light-dark-tooltip": "Зберігати шпалери при зміні теми", + "header-separate-light-dark-tooltip": "Автоматична зміна шпалер з темою", + "header-sun-linked-tooltip": "Окремі світлі та темні шпалери", + "header-sun-separate-tooltip": "Однакові шпалери для світлого та темного режимів", + "order-asc": "За зростанням", + "order-desc": "За спаданням", + "order-label": "Порядок", + "purity-label": "Фільтр контенту", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Sketchy", + "ratios-all-portrait": "Усі портретні", + "ratios-all-wide": "Усі широкі", + "ratios-any": "Будь-який", + "ratios-label": "Співвідношення сторін", + "resolution-atleast": "Принаймні", + "resolution-exact": "Точно", + "resolution-label": "Роздільна здатність", + "resolution-mode-label": "Режим", + "solid-color-tooltip": "Однотонний фон", + "sort-date-asc": "Сортувати від найстаріших до нових", + "sort-date-desc": "Сортувати від нових до найстаріших", + "sort-name-asc": "Сортувати за назвою (А-Я)", + "sort-name-desc": "Сортувати за назвою (Я-А)", + "sort-random": "Сортувати випадково", + "sorting-date-added": "Дата додавання", + "sorting-favorites": "Обране", + "sorting-label": "Сортувати за", + "sorting-relevance": "Релевантність", + "sorting-toplist": "Топ-лист", + "sorting-views": "Перегляди", + "source-label": "Джерело", + "source-wallhaven": "Wallhaven", + "title": "Вибір шпалер", + "wallhaven-settings-title": "Налаштування Wallhaven" + }, + "transitions": { + "disc": "Диск", + "fade": "Затухання", + "honeycomb": "Стільники", + "pixelate": "Пікселізувати", + "stripes": "Смуги", + "wipe": "Змітання" + }, + "try-different-search": "Спробуйте інший пошуковий запит.", + "wallhaven": { + "loading": "Завантаження шпалер...", + "no-results": "Шпалери не знайдено. Спробуйте інший пошуковий запит.", + "page": "{current} з {total}", + "page-prefix": "Сторінка", + "page-suffix": "з {total}" + } + }, + "weather": { + "clear-sky": "Ясне небо", + "drizzle": "Мряка", + "fog": "Туман", + "mainly-clear": "Переважно ясно", + "overcast": "Похмуро", + "partly-cloudy": "Частково хмарно", + "rain-showers": "Дощові зливи", + "snow": "Сніг", + "thunderstorm": "Гроза" + }, + "widgets": { + "color-picker": { + "palette-description": "Виберіть із широкого діапазону попередньо визначених кольорів.", + "palette-label": "Палітра", + "palette-theme-colors": "Швидкий доступ до колірної палітри вашої теми.", + "title": "Вибір кольору" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm малими літерами", + "ampm-uppercase": "AM/PM великими літерами", + "common-12hour-time-minutes": "12-годинний час з хвилинами", + "common-24hour-time-minutes": "24-годинний час з хвилинами", + "common-24hour-time-seconds": "24-годинний час з секундами", + "common-european-date": "Європейський формат дати", + "common-iso-date": "Формат дати ISO", + "common-us-date": "Формат дати США", + "common-weekday-date": "День тижня і дата", + "common-weekday-month-day": "День тижня, місяць і день", + "day-abbreviated": "Скорочена назва дня", + "day-full": "Повна назва дня", + "day-leading-zero": "День з нулем попереду (01-31)", + "day-no-leading-zero": "День без нуля попереду (1-31)", + "hour-leading-zero": "Година з нулем попереду (00-23) — 24-годинний формат", + "hour-no-leading-zero": "Година без нуля попереду (0-23) — 24-годинний формат", + "minute-leading-zero": "Хвилина з нулем попереду (00-59)", + "minute-no-leading-zero": "Хвилина без нуля попереду (0-59)", + "month-abbreviated": "Скорочена назва місяця", + "month-full": "Повна назва місяця", + "month-number-leading-zero": "Місяць як число з нулем попереду (01-12)", + "month-number-no-zero": "Місяць як число без нуля попереду (1-12)", + "second-leading-zero": "Секунда з нулем попереду (00-59)", + "second-no-leading-zero": "Секунда без нуля попереду (0-59)", + "timezone-abbreviation": "Абревіатура часового поясу", + "year-four-digit": "Рік як чотиризначне число", + "year-two-digit": "Рік як двозначне число (00-99)" + }, + "file-picker": { + "item": "елемент", + "items": "елементи", + "search-placeholder": "Пошук файлів та тек...", + "select-current": "Вибрати поточне", + "select-file": "Вибрати файл", + "select-folder": "Вибрати теку", + "selected": "Вибрано:", + "title": "Вибір файлу" + }, + "file-picker-title": "Виберіть файл", + "icon-picker": { + "title": "Вибір значка" + }, + "tooltip-placeholder": "Заповнювач" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "Анонімна Ідентичність", + "ca-cert": "Сертифікат CA", + "eap-method": "Метод EAP", + "password": "Пароль користувача", + "phase2-auth": "Автентифікація Фази 2", + "username": "Ідентифікатор / Ім'я Користувача" + }, + "panel": { + "action-required": "Вимагається дія", + "add-hidden-network": "Додати приховану мережу", + "add-network": "Додати мережу", + "available-interfaces": "Доступні інтерфейси", + "available-networks": "Доступні мережі", + "disabled": "Wi-Fi вимкнено", + "disconnecting": "Відключення...", + "dns": "DNS", + "enable-message": "Увімкніть Wi-Fi, щоб побачити доступні мережі.", + "enter-password": "Введіть пароль...", + "forget": "Забути", + "forget-network": "Забути цю мережу?", + "forgetting": "Забування...", + "hidden-network": "Прихована мережа", + "interface": "Мережевий інтерфейс", + "internet-connected": "Підключено до Інтернету", + "internet-limited": "Немає інтернету", + "internet-status": "Статус Інтернету", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "Відомі мережі", + "link-speed": "Швидкість з'єднання", + "network-name-ssid": "Ім'я мережі (SSID)", + "no-ethernet-devices": "Не виявлено Ethernet-пристроїв", + "no-networks": "Мереж не знайдено", + "saved": "Збережено", + "scan-again": "Сканувати знову", + "searching": "Пошук близьких мереж...", + "security-open": "Відкрити", + "security-wep": "WEP", + "security-wpa": "WPA/WPA2/WPA3 Персональний", + "security-wpa-ent": "WPA Enterprise", + "security-wpa2-ent": "WPA2 Enterprise", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 Ентерпрайз", + "show-password": "Показати пароль", + "title": "Wi-Fi" + }, + "signal": { + "excellent": "Відмінний", + "fair": "Середній", + "good": "Хороший", + "poor": "Поганий", + "weak": "Слабкий" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/vi.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/vi.json new file mode 100644 index 0000000..f9e5dd3 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/vi.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "Xóa lịch sử", + "close-app": "Đóng {app}", + "connect-vpn": "Kết nối tới {name}", + "cycle-visualizer": "Chuyển đổi visualizer cho âm thanh", + "disable-bluetooth": "Tắt Bluetooth", + "disable-dnd": "Tắt chế độ không làm phiền", + "disable-wifi": "Tắt Wi-Fi", + "disconnect-vpn": "Ngắt kết nối {name}", + "dock-settings": "Cài đặt Dock", + "enable-bluetooth": "Bật Bluetooth", + "enable-dnd": "Bật Không làm phiền", + "enable-wifi": "Bật Wi-Fi", + "launcher-settings": "Cài đặt Trình khởi chạy", + "lower-to-bottom": "Chuyển xuống dưới cùng", + "open-calendar": "Mở lịch", + "open-display-settings": "Cài đặt hiển thị", + "open-launcher": "Mở trình khởi chạy", + "open-settings": "Mở cài đặt", + "raise-to-top": "Đưa lên trên cùng", + "random-wallpaper": "Hình nền ngẫu nhiên", + "run-custom-command": "Chạy câu lệnh tự tùy chỉnh", + "show-active-only": "Chỉ hiển thị đang hoạt động", + "show-all": "Hiển thị tất cả", + "toggle-mute": "Bật/tắt tiếng", + "widget-settings": "Cài đặt Tiện ích" + }, + "authentication": { + "error": "Lỗi xác thực", + "failed": "Xác thực thất bại" + }, + "bar": { + "active-window": { + "colorize-icons-description": "Áp dụng màu giao diện cho biểu tượng cửa sổ đang hoạt động.", + "hide-mode-description": "Điều khiển cách tiện ích hoạt động khi không có cửa sổ đang hoạt động.", + "scrolling-mode-description": "Điều khiển khi nào bật cuộn văn bản cho các tiêu đề cửa sổ dài.", + "show-app-icon-description": "Hiển thị biểu tượng ứng dụng bên cạnh tiêu đề cửa sổ.", + "show-app-icon-label": "Hiển thị biểu tượng ứng dụng", + "show-app-text-description": "Hiển thị tiêu đề của cửa sổ đang hoạt động.", + "show-app-text-label": "Hiển thị tiêu đề cửa sổ" + }, + "audio-visualizer": { + "color-name-description": "Chọn màu cho bộ hiển thị trực quan.", + "color-name-label": "Tô toàn bộ màu cho audio visualizer", + "height-description": "Tùy chỉnh độ cao của thành phần", + "hide-when-idle-description": "Khi bật, bộ hiển thị sẽ bị ẩn trừ khi có trình phát đang phát.", + "hide-when-idle-label": "Ẩn khi không phát đa phương tiện", + "width-description": "Tùy chỉnh độ rộng của thành phần" + }, + "battery": { + "device-default": "Mặc định (Thiết bị hiển thị)", + "device-description": "Chọn pin để hiển thị.", + "device-label": "Pin", + "display-mode-description": "Chọn cách pin được hiển thị trên thanh Bar.", + "display-mode-graphic": "Dạng pin có đồ họa", + "display-mode-graphic-clean": "Dạng pin có đồ họa (không %)", + "display-mode-icon-always": "Biểu tượng - Luôn hiển thị %", + "display-mode-icon-hover": "Biểu tượng - Hiển thị khi di chuột", + "display-mode-icon-only": "Chỉ biểu tượng", + "hide-if-idle-description": "Ẩn tiện ích khi pin không sạc hoặc xả.", + "hide-if-idle-label": "Ẩn khi không hoạt động", + "hide-if-not-detected-description": "Ẩn tiện ích khi không tìm thấy pin trên hệ thống.", + "hide-if-not-detected-label": "Ẩn khi không được phát hiện", + "low-battery-threshold-label": "Ngưỡng cảnh báo pin yếu", + "show-noctalia-performance-description": "Hiển thị nút chuyển đổi sang chế độ hiệu năng của Noctalia bên trong bảng điều khiển pin.
Tắt đổ bóng và hoạt ảnh trong Noctalia để giảm mức sử dụng tài nguyên.", + "show-noctalia-performance-label": "Hiển thị công tắc hiệu năng của Noctalia", + "show-power-profile-description": "Hiển thị lựa chọn cấu hình nguồn bên trong bảng điều khiển pin.", + "show-power-profile-label": "Hiển thị điều khiển hồ sơ năng lượng" + }, + "brightness": { + "apply-all-description": "Thay đổi độ sáng cho mọi màn hình khi cuộn chuột.", + "apply-all-label": "Áp dụng thay đổi cuộn cho tất cả màn hình" + }, + "clock": { + "clock-display-description": "Tùy chỉnh hiển thị đồng hồ của bạn bằng cách thêm các token từ danh sách bên dưới. Để sử dụng định dạng 12 giờ, bạn phải bao gồm token 'AP'.", + "clock-display-label": "Hiển thị Đồng hồ", + "custom-font-description": "Chọn một phông chữ tùy chỉnh cho hiển thị đồng hồ.", + "custom-font-label": "Phông chữ tùy chỉnh", + "custom-font-placeholder": "Chọn phông chữ tùy chỉnh...", + "custom-font-search-placeholder": "Tìm kiếm phông chữ...", + "horizontal-bar-description": "Mẹo: Dùng \\n để xuống dòng.", + "horizontal-bar-label": "Thanh ngang", + "preview": "Xem trước", + "tooltip-format-description": "Chuỗi định dạng cho chú giải công cụ hiển thị khi di chuột qua đồng hồ. Để trống để dùng chú giải công cụ mặc định.", + "tooltip-format-label": "Định dạng chú giải công cụ", + "use-custom-font-description": "Ghi đè lựa chọn phông chữ mặc định bằng một phông chữ tùy chỉnh cho đồng hồ.", + "use-custom-font-label": "Sử dụng phông chữ tùy chỉnh", + "use-monospaced-font-description": "Khi được bật, đồng hồ sẽ sử dụng phông chữ đơn cách.", + "use-monospaced-font-label": "Sử dụng phông chữ đơn cách", + "vertical-bar-description": "Sử dụng một dấu cách để phân tách mỗi phần xuống một dòng mới.", + "vertical-bar-label": "Thanh dọc" + }, + "control-center": { + "browse-file": "Duyệt Tệp", + "browse-library": "Duyệt Thư viện", + "color-selection-description": "Áp dụng màu sắc chủ đề cho biểu tượng.", + "enable-colorization-description": "Bật tô màu cho biểu tượng, áp dụng màu sắc chủ đề.", + "icon-description": "Chọn một biểu tượng từ thư viện hoặc một tệp tùy chỉnh.", + "select-custom-icon": "Chọn một biểu tượng tùy chỉnh", + "use-distro-logo-description": "Sử dụng biểu tượng của bản phân phối của bạn thay vì một biểu tượng tùy chỉnh.", + "use-distro-logo-label": "Sử dụng logo distro thay vì biểu tượng" + }, + "custom-button": { + "collapse-condition-description": "Nếu văn bản đầu ra khớp với giá trị này, nút sẽ thu gọn.", + "collapse-condition-label": "Điều kiện thu gọn", + "color-selection-description": "Áp dụng màu sắc chủ đề cho biểu tượng và văn bản.", + "default-tooltip": "Nút tùy chỉnh, cấu hình trong cài đặt", + "display-command-output-description": "Nhập một lệnh để chạy theo một khoảng thời gian đều đặn. Dòng đầu tiên của đầu ra của nó sẽ được hiển thị dưới dạng văn bản.", + "display-command-output-label": "Hiển thị kết quả lệnh", + "display-command-output-stream-description": "Nhập lệnh để chạy liên tục.", + "dynamic-text": "Văn bản động", + "enable-colorization-description": "Bật tô màu cho biểu tượng và văn bản nút tùy chỉnh, áp dụng màu sắc chủ đề.", + "enable-colorization-label": "Bật tô màu", + "general-tooltip-text-description": "Văn bản tùy chỉnh hiển thị trong chú giải công cụ của nút.", + "general-tooltip-text-label": "Văn bản chú giải tùy chỉnh", + "hide-mode-always-expanded": "Luôn mở rộng", + "hide-mode-description": "Điều khiển hiển thị tiện ích khi lệnh không có đầu ra.", + "hide-mode-expand-with-output": "Mở rộng khi có đầu ra", + "hide-mode-label": "Chế độ ẩn", + "hide-mode-max-transparent": "Mở rộng tối đa nhưng trong suốt", + "icon-description": "Chọn một biểu tượng từ thư viện.", + "ipc-identifier-description": "Mã định danh duy nhất cho các lệnh IPC. Sử dụng mã định danh này với 'qs -c noctalia-shell ipc call cb [action] [identifier]' để điều khiển nút này qua IPC.", + "ipc-identifier-label": "Mã định danh IPC", + "left-click-description": "Lệnh thực thi khi nút được nhấp chuột trái.", + "left-click-label": "Nhấp chuột trái", + "left-click-update-text": "Cập nhật văn bản hiển thị khi nhấp chuột trái", + "max-text-length-horizontal-description": "Số ký tự tối đa hiển thị trên Thanh ngang (0 để ẩn văn bản).", + "max-text-length-horizontal-label": "Độ dài văn bản tối đa (ngang)", + "max-text-length-vertical-description": "Số ký tự tối đa hiển thị trong thanh dọc (0 để ẩn văn bản).", + "max-text-length-vertical-label": "Chiều dài văn bản tối đa (chiều dọc)", + "middle-click-description": "Lệnh sẽ thực thi khi nút được nhấp chuột giữa.", + "middle-click-label": "Nhấp chuột giữa", + "middle-click-update-text": "Cập nhật chữ hiển thị khi nhấp chuột giữa", + "parse-json-description": "Phân tích đầu ra của lệnh dưới dạng một đối tượng JSON để thiết lập động văn bản và biểu tượng.", + "parse-json-label": "Phân tích đầu ra dưới dạng JSON", + "refresh-interval-description": "Khoảng thời gian tính bằng mili giây.", + "refresh-interval-label": "Khoảng thời gian làm mới", + "right-click-description": "Lệnh thực thi khi nhấp chuột phải vào nút.", + "right-click-label": "Nhấp chuột phải", + "right-click-update-text": "Cập nhật văn bản hiển thị khi nhấp chuột phải", + "show-exec-tooltip-description": "Hiển thị chú giải công cụ với chi tiết lệnh (nhấp chuột trái/phải/giữa, cuộn).", + "show-exec-tooltip-label": "Hiển thị chú giải công cụ lệnh.", + "show-icon-description": "Bật/tắt hiển thị biểu tượng của tiện ích.", + "show-icon-label": "Hiển thị biểu tượng", + "show-text-tooltip-description": "Hiển thị chú giải công cụ với đầu ra từ lệnh văn bản.", + "show-text-tooltip-label": "Hiển thị chú giải công cụ văn bản động", + "text-stream-description": "Các dòng đầu ra từ lệnh sẽ được hiển thị dưới dạng văn bản trên nút.", + "text-stream-label": "Luồng", + "wheel-description": "Lệnh để thực thi khi con lăn chuột được sử dụng.
Sử dụng $delta cho giá trị delta của con lăn chuột trong lệnh.", + "wheel-down": "Cuộn xuống", + "wheel-down-description": "Lệnh thực thi khi con lăn chuột cuộn xuống.", + "wheel-down-label": "Lệnh cuộn xuống", + "wheel-label": "Con lăn chuột", + "wheel-mode-separate-description": "Bật các lệnh riêng biệt cho cuộn lên và xuống.", + "wheel-mode-separate-label": "Tách riêng các lệnh cuộn", + "wheel-up": "Cuộn lên", + "wheel-up-description": "Lệnh thực thi khi con lăn chuột cuộn lên.", + "wheel-up-label": "Lệnh cuộn lên", + "wheel-update-text": "Cập nhật văn bản hiển thị khi cuộn" + }, + "keyboard-layout": { + "show-icon-description": "Hiển thị biểu tượng bố cục bàn phím." + }, + "lock-keys": { + "hide-when-off-description": "Ẩn chỉ báo khi phím không hoạt động.", + "hide-when-off-label": "Ẩn khi tắt", + "show-caps-lock-description": "Hiển thị trạng thái Caps Lock.", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "Hiển thị trạng thái Num Lock.", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "Hiển thị trạng thái Scroll Lock.", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "Bật bố cục tiết kiệm không gian cho bảng điều khiển trình phát media.", + "compact-mode-label": "Chế độ thu gọn", + "hide-mode-description": "Điều khiển cách tiện ích hoạt động khi không có phương tiện nào đang phát.", + "max-width-description": "Đặt kích thước ngang tối đa của tiện ích. Tiện ích sẽ co lại để vừa với nội dung ngắn hơn.", + "no-active-player": "Không có trình phát nào đang hoạt động", + "panel-section-description": "Cấu hình giao diện và hành vi của bảng điều khiển trình phát media.", + "panel-section-label": "Bảng điều khiển trình phát đa phương tiện", + "scrolling-mode-description": "Điều khiển khi tính năng cuộn văn bản được bật cho các tiêu đề bài hát dài.", + "scrolling-mode-label": "Chế độ cuộn", + "show-album-art-description": "Hiển thị ảnh bìa album của bài hát đang phát.", + "show-album-art-label": "Hiển thị ảnh bìa album", + "show-artist-first-description": "Hiển thị nghệ sĩ - tiêu đề thay vì tiêu đề - nghệ sĩ.", + "show-artist-first-label": "Hiện nghệ sĩ trước", + "show-progress-ring-description": "Hiển thị chỉ báo tiến độ dạng vòng tròn cho biết tiến độ bài hát.", + "show-progress-ring-label": "Hiển thị vòng tiến trình", + "show-visualizer-description": "Hiển thị trực quan hóa âm thanh khi nhạc đang phát.", + "show-visualizer-label": "Hiển thị trình hiển thị", + "use-fixed-width-description": "Khi được bật, tiện ích sẽ luôn sử dụng chiều rộng tối đa thay vì tự động điều chỉnh theo nội dung.", + "use-fixed-width-label": "Sử dụng chiều rộng cố định", + "visualizer-type-description": "Chọn kiểu trực quan hóa âm thanh để hiển thị.", + "visualizer-type-label": "Kiểu hiển thị" + }, + "notification-history": { + "hide-widget-when-zero-description": "Ẩn biểu tượng thông báo khi không có thông báo nào.", + "hide-widget-when-zero-label": "Ẩn biểu tượng khi không có thông báo", + "hide-widget-when-zero-unread-description": "Ẩn biểu tượng thông báo khi không có thông báo chưa đọc nào.", + "hide-widget-when-zero-unread-label": "Ẩn biểu tượng khi không có thông báo chưa đọc", + "show-unread-badge-description": "Hiển thị một chỉ báo cho biết số lượng thông báo chưa đọc.", + "show-unread-badge-label": "Hiển thị dấu hiệu chưa đọc", + "unread-badge-color-description": "Chọn màu cho huy hiệu thông báo chưa đọc.", + "unread-badge-color-label": "Màu huy hiệu chưa đọc" + }, + "section-editor": { + "placeholder": "Chọn một tiện ích để thêm...", + "search-placeholder": "Tìm kiếm tiện ích..." + }, + "spacer": { + "width-description": "Chiều rộng khoảng cách theo pixel." + }, + "system-monitor": { + "compact-mode-description": "Hiển thị số liệu thống kê dưới dạng biểu đồ thanh nhỏ thay vì giá trị văn bản. Ngăn chặn việc dịch chuyển bố cục.", + "compact-mode-label": "Chế độ thu gọn", + "cpu-cores-description": "Hiển thị mức sử dụng từng lõi CPU riêng lẻ.", + "cpu-cores-label": "Lõi CPU", + "cpu-frequency-description": "Hiển thị tốc độ xung nhịp CPU hiện tại tính bằng GHz.", + "cpu-frequency-label": "Hiển thị tần số CPU", + "cpu-temperature-description": "Hiển thị nhiệt độ CPU nếu có.", + "cpu-temperature-label": "Nhiệt độ CPU", + "cpu-usage-description": "Hiển thị phần trăm sử dụng CPU hiện tại.", + "cpu-usage-label": "Mức sử dụng CPU", + "disk-path-description": "Chọn điểm gắn kết đĩa nào để giám sát.", + "disk-path-label": "Đường dẫn ổ đĩa", + "gpu-temperature-description": "Hiển thị thông số nhiệt độ GPU nếu có.", + "load-average-description": "Hiển thị mức tải trọng trung bình của hệ thống.", + "load-average-label": "Mức tải trọng trung bình", + "memory-percentage-description": "Hiển thị mức sử dụng bộ nhớ dưới dạng phần trăm thay vì giá trị tuyệt đối.", + "memory-percentage-label": "Bộ nhớ theo phần trăm", + "memory-usage-description": "Hiển thị thông tin sử dụng RAM hiện tại.", + "memory-usage-label": "Sử dụng bộ nhớ", + "network-traffic-description": "Hiển thị tốc độ tải lên và tải xuống mạng.", + "network-traffic-label": "Lưu lượng mạng", + "storage-as-percentage-description": "Hiển thị dung lượng đĩa dưới dạng phần trăm thay vì giá trị tuyệt đối.", + "storage-as-percentage-label": "Ổ đĩa dưới dạng phần trăm", + "storage-available-description": "Hiển thị dung lượng đĩa khả dụng thay vì dung lượng đã sử dụng.", + "storage-available-label": "Dung lượng đĩa khả dụng", + "storage-usage-description": "Hiển thị thông tin dung lượng ổ đĩa.", + "storage-usage-label": "Sử dụng bộ nhớ", + "swap-usage-description": "Hiển thị mức sử dụng bộ nhớ swap.", + "swap-usage-label": "Mức sử dụng bộ nhớ swap", + "use-monospace-font-description": "Sử dụng phông chữ đơn cách để có chiều rộng ký tự nhất quán.", + "use-monospace-font-label": "Phông chữ đơn cách", + "use-padding-description": "Đệm các giá trị văn bản bằng khoảng trắng đầu dòng để ngăn chặn sự dịch chuyển bố cục.", + "use-padding-description-disabled-monospace-font": "Tính năng này cần một phông chữ đơn cách.", + "use-padding-description-disabled-vertical": "Thanh tác vụ dọc không hỗ trợ khoảng đệm văn bản.", + "use-padding-label": "Khoảng đệm văn bản" + }, + "taskbar": { + "colorize-icons-description": "Áp dụng màu sắc chủ đề cho biểu tượng thanh tác vụ.", + "hide-mode-description": "Điều khiển cách tiện ích con hoạt động khi không có cửa sổ phù hợp nào.", + "hide-mode-label": "Chế độ ẩn", + "icon-scale-description": "Đặt hệ số tỉ lệ cho các biểu tượng trên thanh tác vụ.", + "icon-scale-label": "Kích thước biểu tượng", + "max-width-description": "Chiều rộng tối đa của taskbar dưới dạng phần trăm chiều rộng màn hình.", + "max-width-label": "Chiều rộng tối đa", + "only-active-workspaces-description": "Chỉ hiển thị ứng dụng từ không gian làm việc đang hoạt động.", + "only-active-workspaces-label": "Chỉ từ các không gian làm việc đang hoạt động", + "only-same-monitor-description": "Chỉ hiển thị ứng dụng từ màn hình nơi Bar được đặt.", + "only-same-monitor-label": "Chỉ từ cùng màn hình", + "show-pinned-apps-description": "Hiển thị ứng dụng đã ghim từ thanh Dock trong thanh tác vụ.", + "show-pinned-apps-label": "Hiển thị ứng dụng đã ghim", + "show-title-description": "Hiển thị tiêu đề cửa sổ trên thanh tác vụ.", + "show-title-description-disabled": "Thanh tác vụ dọc không hỗ trợ hiển thị tiêu đề.", + "show-title-label": "Hiển thị tiêu đề", + "smart-width-description": "Tự động điều chỉnh độ rộng mục nhập dựa trên số lượng mục nhập.", + "smart-width-label": "Chiều rộng thông minh", + "title-width-description": "Đặt chiều rộng của tiêu đề cửa sổ trên thanh tác vụ (bằng pixel).", + "title-width-label": "Chiều rộng tiêu đề", + "title-width-reset-tooltip": "Đặt lại chiều rộng tiêu đề" + }, + "tray": { + "chevron-color-description": "Áp dụng màu chủ đề cho biểu tượng mũi tên ngăn kéo.", + "chevron-color-label": "Màu mũi tên", + "colorize-icons-description": "Áp dụng màu sắc chủ đề cho các biểu tượng khay.", + "colorize-icons-label": "Tô màu biểu tượng", + "drawer-enabled-description": "Khi bật, các mục khay chưa ghim sẽ hiển thị trong một Panel ngăn kéo.
Khi tắt, tất cả các mục khay sẽ hiển thị trực tiếp.", + "drawer-enabled-label": "Bật ngăn", + "hide-passive-description": "Khi bật, các mục khay có trạng thái 'Passive' sẽ bị ẩn.", + "hide-passive-label": "Ẩn các mục không hoạt động" + }, + "volume": { + "display-mode-description": "Chọn cách bạn muốn giá trị này hiển thị." + }, + "workspace": { + "character-count-description": "Số ký tự hiển thị từ tên không gian làm việc (1-10).", + "character-count-label": "Số ký tự", + "empty-color-description": "Đặt màu nền cho các không gian làm việc trống.", + "empty-color-label": "Màu không gian làm việc trống", + "enable-scrollwheel-description": "Chuyển đổi giữa các màn hình làm việc bằng con lăn chuột.", + "enable-scrollwheel-label": "Cuộn để đổi không gian làm việc", + "focused-color-description": "Đặt màu nền cho không gian làm việc đang hoạt động.", + "focused-color-label": "Màu sắc không gian làm việc đang chọn", + "follow-focused-screen-description": "Hiển thị các không gian làm việc từ màn hình đang được tập trung, thay vì màn hình nơi Thanh được đặt.", + "follow-focused-screen-label": "Theo dõi màn hình đang hoạt động", + "font-weight-description": "Điều chỉnh độ đậm nhạt của chữ trong không gian làm việc.", + "font-weight-label": "Độ đậm phông chữ", + "grouped-border-opacity-description": "Đặt độ mờ cho viền vùng làm việc.", + "grouped-border-opacity-label": "Độ mờ viền", + "hide-unoccupied-description": "Không hiển thị không gian làm việc không có cửa sổ.", + "hide-unoccupied-label": "Ẩn khi trống", + "label-mode-description": "Chọn cách nhãn không gian làm việc được hiển thị.", + "label-mode-label": "Chế độ nhãn", + "occupied-color-description": "Đặt màu nền cho các không gian làm việc đang sử dụng.", + "occupied-color-label": "Màu không gian làm việc đang sử dụng", + "pill-size-description": "Điều chỉnh kích thước của các chỉ báo không gian làm việc.", + "pill-size-label": "Kích thước viên nang", + "reverse-scrolling-description": "Đảo ngược hướng chuyển đổi không gian làm việc khi cuộn.", + "reverse-scrolling-label": "Đảo ngược cuộn", + "show-applications-description": "Hiển thị biểu tượng ứng dụng trong mỗi không gian làm việc.", + "show-applications-hover-description": "Hiển thị biểu tượng ứng dụng bên trong mỗi không gian làm việc khi di chuột qua.", + "show-applications-hover-label": "Hiển thị ứng dụng khi di chuột qua", + "show-applications-label": "Hiển thị ứng dụng", + "show-badge-description": "Hiển thị huy hiệu số không gian làm việc ở chế độ nhóm.", + "show-badge-label": "Hiển thị huy hiệu không gian làm việc", + "show-labels-only-when-occupied-description": "Chỉ hiển thị nhãn không gian làm việc khi chúng chứa cửa sổ.", + "show-labels-only-when-occupied-label": "Hiển thị nhãn chỉ khi có nội dung", + "unfocused-icons-opacity-description": "Đặt độ trong suốt cho các biểu tượng ứng dụng không được lấy nét.", + "unfocused-icons-opacity-label": "Độ mờ của biểu tượng không được lấy nét" + } + }, + "battery": { + "all-batteries": "Tất cả pin (kết hợp)", + "battery-health": "Tình trạng pin", + "battery-level": "Mức pin", + "capacity-level": "Dung lượng: {level}", + "charging-rate": "Tốc độ sạc: {rate} W", + "discharging-rate": "Tốc độ xả: {rate} W", + "health": "Sức khỏe: {percent}%", + "inhibit-idle-description": "Giữ cho hệ thống không ngủ.", + "no-battery-detected": "Không phát hiện thấy pin", + "plugged-in": "Đã cắm sạc", + "power-profile": "Chế độ năng lượng", + "time-left": "Thời gian còn lại: {time}", + "time-until-full": "Thời gian sạc đầy: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "Thiết bị khả dụng", + "blocked": "Đã chặn", + "connected-devices": "Thiết bị đã kết nối", + "device-address": "Địa chỉ thiết bị", + "disabled": "Bluetooth đã tắt", + "discoverable": "Có thể phát hiện", + "enable-message": "Bật Bluetooth để xem các thiết bị khả dụng.", + "known-devices": "Thiết bị đã biết", + "no-devices": "Không có thiết bị nào", + "paired-devices": "Thiết bị đã ghép nối", + "pairing-mode": "Đảm bảo thiết bị của bạn đang ở chế độ ghép nối.", + "scanning": "Đang quét thiết bị...", + "signal-text-excellent": "Tín hiệu: Tuyệt vời", + "signal-text-fair": "Tín hiệu: Khá", + "signal-text-good": "Tín hiệu: Tốt", + "signal-text-poor": "Tín hiệu: Yếu", + "signal-text-unknown": "Tín hiệu: Không xác định", + "signal-text-very-poor": "Tín hiệu: Rất yếu" + } + }, + "changelog": { + "error": { + "fetch-failed": "Không thể tải dữ liệu nhật ký thay đổi. Vui lòng thử lại sau.", + "rate-limit": "Đã vượt quá giới hạn tốc độ của GitHub. Vui lòng thử lại sau vài phút." + }, + "panel": { + "buttons-discord": "Tham gia Discord của chúng tôi", + "buttons-dismiss": "Đồng ý", + "buttons-feedback": "Gửi phản hồi", + "empty": "Ghi chú phát hành chưa có sẵn.", + "highlight-title": "Điểm nổi bật", + "released": "Phát hành vào {date}", + "subtitle-fresh": "Cảm ơn bạn đã cài đặt Noctalia! Đây là những gì có trong bản dựng này.", + "subtitle-updated": "Đã cập nhật từ {previousVersion}", + "title": "Có gì mới trong {version}", + "version": "Phiên bản {version}", + "version-new-user": "Cài đặt mới" + } + }, + "common": { + "actions": "Hành động", + "active": "Hoạt động", + "add": "Thêm", + "appearance": "Giao diện", + "apply": "Áp dụng", + "auto-connect": "Tự động kết nối", + "automation": "Tự động hóa", + "available": "Khả dụng", + "back": "Quay lại", + "battery": "Pin", + "behavior": "Hành vi", + "bluetooth": "Bluetooth", + "brightness": "Độ sáng", + "browse": "Duyệt", + "calendar": "Lịch", + "calendar-panel": "Bảng Lịch", + "cancel": "Hủy", + "cards": "Thẻ", + "charging": "Đang sạc", + "clear": "Xóa", + "clipboard": "Bảng tạm", + "close": "Đóng", + "color-muted": "Tắt tiếng", + "colors": "Màu sắc", + "command": "Lệnh", + "confirm": "Xác nhận", + "connect": "Kết nối", + "connected": "Đã kết nối", + "connecting": "Đang kết nối...", + "continue": "Tiếp tục", + "contributors": "Người đóng góp", + "copied-to-clipboard": "Đã sao chép vào bộ nhớ tạm", + "countdown": "Đếm ngược", + "customize": "Tùy chỉnh", + "date": "Ngày", + "default": "Mặc định", + "delete": "Xóa", + "devices": "Thiết bị", + "disabled": "Đã tắt", + "discharging": "Đang xả", + "disconnect": "Ngắt kết nối", + "disconnected": "Đã ngắt kết nối", + "disconnecting": "Đang ngắt kết nối...", + "display-mode": "Chế độ hiển thị", + "documentation": "Tài liệu", + "download": "Tải xuống", + "duration": "Thời lượng", + "dysfunctional": "Trục trặc", + "edit": "Chỉnh sửa", + "enabled": "Đã bật", + "error": "Lỗi", + "ethernet": "Ethernet", + "events": "Sự kiện", + "execute": "Thực thi", + "faithful": "Trung thành", + "focus": "Tập trung", + "font-weight-bold": "In đậm", + "font-weight-light": "Sáng", + "font-weight-medium": "Trung bình", + "font-weight-regular": "Thường", + "font-weight-semibold": "Bán đậm", + "frequency": "Tần suất", + "gateway": "Cổng", + "general": "Chung", + "height": "Chiều cao", + "hibernate": "Ngủ đông", + "history": "Lịch sử", + "icon": "Biểu tượng", + "idle": "Không hoạt động", + "info": "Thông tin", + "input": "Đầu vào", + "install": "Cài đặt", + "installed": "Đã cài đặt", + "interface": "Giao diện", + "internet": "Internet", + "keybind": "Phím tắt", + "language": "Ngôn ngữ", + "loading": "Đang tải...", + "local": "Cục bộ", + "location": "Vị trí", + "lock": "Khóa", + "logout": "Đăng xuất", + "look": "Giao diện", + "margins": "Lề", + "media": "Đa phương tiện", + "media-player": "Trình phát đa phương tiện", + "memory": "Bộ nhớ", + "monitors": "Màn hình", + "move-down": "Di chuyển xuống", + "move-up": "Di chuyển lên", + "network": "Mạng", + "next": "Tiếp theo", + "night-light": "Lọc ánh sáng xanh", + "no": "Không", + "no-results": "Không có kết quả", + "no-summary": "Không có tóm tắt", + "none": "Không", + "not-found": "Không tìm thấy", + "notifications": "Thông báo", + "official": "Chính thức", + "on-surface": "Trên màn hình", + "output": "Đầu ra", + "pair": "Ghép nối", + "paired": "Đã ghép nối", + "pairing": "Đang ghép nối...", + "panels": "Các bảng điều khiển", + "password": "Mật khẩu", + "pause": "Tạm dừng", + "performance": "Hiệu năng", + "pin": "Ghim", + "play": "Phát", + "polling": "Thăm dò", + "position": "Vị trí", + "previous": "Trước", + "primary": "Chính", + "random": "Ngẫu nhiên", + "reboot": "Khởi động lại", + "reboot-to-uefi": "Khởi động lại vào UEFI", + "record": "Bắt đầu ghi hình", + "refresh": "Làm mới", + "required": "(bắt buộc)", + "reset": "Đặt lại", + "reset-to-default": "Đặt lại về Mặc định", + "result-count": "{count} kết quả", + "result-count-plural": "{count} kết quả", + "resume": "Tiếp tục", + "save": "Lưu", + "scanning": "Đang quét...", + "screen-corners": "Các góc màn hình", + "search": "Tìm kiếm", + "second": "{count} giây", + "second-plural": "{count} giây", + "secondary": "Thứ cấp", + "security": "Bảo mật", + "select": "Chọn", + "select-color": "Chọn màu", + "select-color-description": "Áp dụng màu sắc chủ đề để nhấn mạnh.", + "select-icon-color": "Chọn màu biểu tượng", + "settings": "Cài đặt", + "shortcuts": "Phím tắt", + "shutdown": "Tắt máy", + "signal": "Tín hiệu", + "sound": "Âm thanh", + "sources": "Nguồn", + "start": "Bắt đầu", + "stop": "Dừng", + "supporters": "Người ủng hộ", + "suspend": "Tạm dừng", + "templates": "Mẫu", + "tertiary": "Thứ ba", + "test": "Kiểm tra", + "thresholds": "Ngưỡng", + "title": "Tiêu đề", + "toast": "Thông báo nhanh", + "trusted": "Đáng tin cậy", + "uninstall": "Gỡ cài đặt", + "unknown": "Không xác định", + "unpair": "Hủy ghép đôi", + "unpin": "Bỏ ghim", + "update": "Cập nhật", + "upload": "Tải lên", + "username": "Tên người dùng", + "userspace-reboot": "Khởi động lại không gian người dùng", + "version": "Phiên bản", + "vibrant": "Sống động", + "visualizer": "Trình hiển thị", + "volume": "Âm lượng", + "volumes": "Âm lượng", + "wallpaper": "Hình nền", + "weather": "Thời tiết", + "weather-loading": "Đang tải thời tiết...", + "weather-no-location": "Đặt vị trí trong Cài đặt để xem thời tiết", + "week": "Tuần", + "widgets": "Tiện ích", + "width": "Chiều rộng", + "wifi": "Wi-Fi", + "windows": "Cửa sổ", + "yes": "Có" + }, + "control-center": { + "power-profile": { + "tooltip-action": "Cấu hình Nguồn", + "tooltip-disabled": "Cài đặt power-profiles-daemon để sử dụng các chế độ nguồn" + }, + "wifi": { + "label-disconnected": "Wi-Fi đã ngắt kết nối", + "label-ethernet": "Ethernet" + } + }, + "display-modes": { + "always-hide": "Luôn ẩn", + "always-show": "Luôn hiển thị", + "force-open": "Buộc mở", + "on-hover": "Khi di chuột" + }, + "hide-modes": { + "auto-hide": "Tự động ẩn", + "hidden": "Ẩn khi trống", + "idle": "Ẩn khi không hoạt động", + "non-exclusive": "Không độc quyền", + "transparent": "Trong suốt khi trống", + "visible": "Luôn hiển thị" + }, + "launcher": { + "categories": { + "all": "Tất cả", + "audiovideo": "Âm thanh & Video", + "chat": "Trò chuyện", + "development": "Phát triển", + "education": "Giáo dục", + "emoji-activity": "Hoạt động", + "emoji-animals": "Động vật", + "emoji-flags": "Cờ", + "emoji-food": "Đồ ăn & Thức uống", + "emoji-nature": "Thiên nhiên", + "emoji-objects": "Đối tượng", + "emoji-people": "Người & Cơ thể", + "emoji-recent": "Gần đây", + "emoji-symbols": "Biểu tượng", + "emoji-travel": "Du lịch & Địa điểm", + "game": "Trò chơi", + "graphics": "Đồ họa", + "misc": "Khác", + "office": "Văn phòng", + "pinned": "Đã ghim", + "system": "Hệ thống", + "webbrowser": "Trình duyệt web" + }, + "providers": { + "applications": "Ứng dụng", + "calculator": "Máy tính", + "calculator-deprecated": "Lệnh >calc đã lỗi thời và sẽ sớm bị gỡ bỏ. Thay vào đó, vui lòng nhập các biểu thức toán học trực tiếp vào mục tìm kiếm.", + "calculator-description": "Máy tính — tính toán các biểu thức toán học", + "calculator-enter-expression": "Nhập một biểu thức toán học", + "calculator-press-enter-to-copy": "Nhấn Enter để sao chép kết quả", + "clipboard": "Lịch sử bảng nhớ tạm", + "clipboard-clear-description": "Xóa toàn bộ lịch sử bảng nhớ tạm", + "clipboard-clear-description-full": "Xóa tất cả mục khỏi bảng nhớ tạm", + "clipboard-clear-history": "Xóa lịch sử bảng nhớ tạm", + "clipboard-delete": "Xóa mục khỏi bảng nhớ tạm", + "clipboard-history-disabled": "Lịch sử bảng nhớ tạm đã tắt", + "clipboard-history-disabled-description": "Bật bảng nhớ tạm trong phần cài đặt hoặc tải cliphist", + "clipboard-loading": "Đang tải lịch sử bảng nhớ tạm...", + "clipboard-search-description": "Tìm kiếm lịch sử bảng nhớ tạm", + "command-description": "Chạy lệnh shell", + "emoji": "Bảng chọn biểu tượng cảm xúc", + "emoji-loading": "Đang tải biểu tượng cảm xúc...", + "emoji-loading-description": "Vui lòng đợi", + "emoji-no-recent": "Chưa có biểu tượng cảm xúc nào gần đây", + "emoji-search-description": "Tìm kiếm và sao chép biểu tượng cảm xúc", + "settings-search-description": "Tìm kiếm và đi tới cài đặt", + "windows-search-description": "Tìm kiếm và tập trung vào các cửa sổ đang mở" + } + }, + "lock-screen": { + "authenticating": "Đang xác thực...", + "password": "Nhập mật khẩu của bạn...", + "restart": "Khởi động lại", + "shut-down": "Tắt máy" + }, + "notifications": { + "panel": { + "click-to-collapse": "Nhấn để thu gọn", + "click-to-expand": "Nhấn để mở rộng", + "description": "Thông báo của bạn sẽ hiển thị ở đây khi chúng đến.", + "no-notifications": "Không có thông báo" + }, + "range": { + "earlier": "Trước đó", + "today": "Hôm nay", + "yesterday": "Hôm qua" + }, + "time": { + "diff-d": "1 ngày trước", + "diff-dd": "{diff} ngày trước", + "diff-h": "1 giờ trước", + "diff-hh": "{diff} giờ trước", + "diff-m": "1 phút trước", + "diff-mm": "{diff} phút trước", + "now": "bây giờ" + } + }, + "options": { + "bar": { + "density-comfortable": "Thoải mái", + "density-compact": "Gọn", + "density-default": "Mặc định", + "density-mini": "Mini", + "density-spacious": "Rộng rãi", + "display-mode-always-visible": "Luôn hiển thị", + "type-floating": "Nổi", + "type-framed": "Được đóng khung", + "type-simple": "Đơn giản" + }, + "control-center": { + "quick-settings-style-classic": "Cổ điển", + "quick-settings-style-modern": "Hiện đại" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "Thoải mái", + "compact": "Gọn", + "default": "Mặc định" + }, + "launcher-view-mode": { + "grid": "Lưới", + "list": "Danh sách" + }, + "notification-density": { + "compact": "Gọn", + "default": "Mặc định" + }, + "scrolling-modes": { + "always": "Luôn cuộn", + "hover": "Cuộn khi di chuột", + "never": "Không cuộn" + }, + "session-menu-grid-layout": { + "grid": "Lưới", + "single-row": "Một hàng" + }, + "settings-panel-mode": { + "attached": "Bảng điều khiển gắn vào thanh", + "centered": "Bảng giữa", + "window": "Cửa sổ riêng biệt" + }, + "visualizer-types": { + "linear": "Tuyến tính", + "mirrored": "Đã nhân bản", + "wave": "Sóng" + }, + "visualizer-visibility": { + "with-background": "Chỉ nền" + }, + "workspace-labels": { + "index": "Chỉ mục", + "index-and-name": "Chỉ mục và tên", + "name": "Tên" + } + }, + "panels": { + "about": { + "become-supporter": "Trở thành người ủng hộ", + "changelog": "Xem nhật ký thay đổi", + "changelog-on-startup": "Hiển thị nhật ký thay đổi khi cập nhật", + "changelog-on-startup-desc": "Tự động hiển thị nhật ký thay đổi khi Noctalia được cập nhật", + "contributors-description": "Lời cảm ơn đến {count} người đóng góp tuyệt vời của chúng tôi!", + "contributors-description-plural": "Xin gửi lời cảm ơn đến {count} người đóng góp tuyệt vời của chúng tôi!", + "copy-info": "Sao chép thông tin", + "debug-disabled": "Chế độ gỡ lỗi đã tắt", + "debug-enabled": "Chế độ gỡ lỗi đã bật", + "info-copied": "Thông tin đã sao chép vào bảng tạm", + "noctalia-available": "Khả dụng:", + "noctalia-desc": "Một giao diện desktop gọn gàng và tối giản, được chế tác tỉ mỉ dành cho Wayland, xây dựng bằng Quickshell.", + "noctalia-git-commit": "Git commit:", + "noctalia-installed-version": "Phiên bản đã cài đặt:", + "noctalia-latest-version": "Phiên bản mới nhất:", + "noctalia-qs-version": "Noctalia QS phiên bản:", + "noctalia-title": "Giao diện Noctalia", + "privacy-policy": "Chính sách quyền riêng tư", + "support": "Ủng hộ chúng tôi", + "supporter-badge": "Người ủng hộ", + "supporters-desc": "Xin chân thành cảm ơn người ủng hộ tuyệt vời của chúng tôi!", + "supporters-desc-plural": "Cảm ơn rất nhiều tới {count} người ủng hộ tuyệt vời của chúng tôi!", + "supporters-loading": "Đang tải những người ủng hộ...", + "system-board": "Bảng:", + "system-cpu": "CPU:", + "system-disk": "Ổ đĩa:", + "system-gpu": "GPU:", + "system-host": "Máy chủ:", + "system-install-hint": "Cài đặt fastfetch để xem thông tin hệ thống", + "system-kernel": "Nhân:", + "system-loading": "Đang tải thông tin hệ thống...", + "system-memory": "Bộ nhớ:", + "system-monitor": "Màn hình:", + "system-not-installed": "fastfetch chưa được cài đặt", + "system-os": "HĐH:", + "system-packages": "Gói:", + "system-product": "Sản phẩm:", + "system-title": "Thông tin hệ thống", + "system-uptime": "Thời gian hoạt động:", + "system-wm": "WM:", + "telemetry-data-copied": "Dữ liệu đo từ xa đã sao chép vào bảng tạm", + "telemetry-desc": "Giúp cải thiện Noctalia bằng cách chia sẻ thông tin hệ thống ẩn danh (độ phân giải màn hình, compositor, distro). Gửi một lần khi khởi động, không theo dõi, dữ liệu tự động xóa sau 30 ngày.", + "telemetry-enabled": "Gửi thông tin hệ thống ẩn danh", + "telemetry-show-data": "Xem dữ liệu", + "telemetry-title": "Quyền riêng tư", + "title": "Giới thiệu", + "up-to-date": "Bạn đã cập nhật!", + "update-available": "Có bản cập nhật mới", + "view-commit": "Xem commit trên GitHub" + }, + "audio": { + "devices-desc": "Cấu hình các thiết bị đầu vào và đầu ra âm thanh có sẵn.", + "devices-input-device-description": "Chọn thiết bị đầu vào âm thanh mong muốn.", + "devices-input-device-label": "Thiết bị đầu vào", + "devices-output-device-description": "Chọn thiết bị đầu ra âm thanh mong muốn.", + "devices-output-device-label": "Thiết bị đầu ra", + "devices-title": "Thiết bị âm thanh", + "external-mixer-description": "Nhập lệnh hoặc đường dẫn ứng dụng để khởi chạy khi kích hoạt tính năng bộ trộn âm thanh ngoài.", + "external-mixer-label": "Lệnh bộ trộn âm thanh bên ngoài", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "Thiết lập các ứng dụng đa phương tiện ưa thích và bị bỏ qua của bạn.", + "media-excluded-player-description": "Thêm từ khóa cho người chơi mà bạn muốn hệ thống bỏ qua. Mỗi từ khóa nên nằm trên một dòng mới.", + "media-excluded-player-label": "Trình phát bị loại trừ", + "media-excluded-player-placeholder": "nhập chuỗi con và nhấn +", + "media-frame-rate-description": "Tốc độ cao hơn mượt mà hơn nhưng tốn nhiều tài nguyên hơn.", + "media-frame-rate-label": "Tốc độ khung hình", + "media-primary-player-description": "Nhập một từ khóa để xác định trình phát chính của bạn.", + "media-primary-player-label": "Trình phát chính", + "media-primary-player-placeholder": "ví dụ: spotify, vlc, mpv", + "media-scrolling-speed-description": "Thời gian tính bằng giây để tiêu đề cuộn từ đầu đến cuối.", + "media-scrolling-speed-label": "Tốc độ cuộn", + "media-scrolling-title-description": "Bật cuộn liên tục cho các tiêu đề media dài.", + "media-scrolling-title-label": "Tiêu đề cuộn", + "media-title": "Trình phát đa phương tiện", + "on-middle-clicked-description": "Lệnh để thực thi khi nút được nhấp chuột giữa.", + "panel-applications-empty": "Không có ứng dụng nào đang phát âm thanh", + "spectrum-mirrored-description": "Đảo ngược phổ tần để các tần số thấp nằm ở giữa. Khi tắt, các tần số thấp nằm ở bên trái và các tần số cao nằm ở bên phải.", + "spectrum-mirrored-label": "Quang phổ phản chiếu", + "title": "Âm thanh", + "visualizer-type-description": "Chọn kiểu trực quan hóa cho phát lại đa phương tiện.", + "visualizer-type-label": "Kiểu hiển thị", + "volumes-desc": "Điều chỉnh âm lượng và mức âm thanh.", + "volumes-feedback-sound-file-description": "Đường dẫn đến tệp âm thanh được phát khi thay đổi âm lượng.", + "volumes-feedback-sound-file-label": "Tệp âm thanh phản hồi âm lượng", + "volumes-feedback-sound-file-select-title": "Chọn tệp âm thanh phản hồi âm lượng", + "volumes-input-volume-description": "Mức âm lượng đầu vào micrô.", + "volumes-mute-input-description": "Tắt tiếng đầu vào âm thanh mặc định (micrô).", + "volumes-mute-input-label": "Tắt tiếng đầu vào âm thanh", + "volumes-mute-output-description": "Tắt tiếng đầu ra âm thanh chính của hệ thống.", + "volumes-mute-output-label": "Tắt tiếng đầu ra âm thanh", + "volumes-output-volume-description": "Mức âm lượng toàn hệ thống.", + "volumes-step-size-description": "Điều chỉnh bước nhảy khi thay đổi âm lượng (con lăn chuột, phím tắt).", + "volumes-step-size-label": "Kích thước bước âm lượng", + "volumes-volume-feedback-description": "Phát âm thanh phản hồi khi điều chỉnh âm lượng.", + "volumes-volume-feedback-label": "Phát âm thanh phản hồi âm lượng", + "volumes-volume-overdrive-description": "Cho phép tăng âm lượng lên trên 100%. Có thể không được hỗ trợ bởi tất cả phần cứng.", + "volumes-volume-overdrive-label": "Cho phép tăng âm lượng vượt mức" + }, + "bar": { + "appearance-auto-hide-delay-description": "Thời gian trước khi thanh ẩn đi sau khi chuột rời đi", + "appearance-auto-hide-delay-label": "Độ trễ ẩn", + "appearance-auto-hide-exclusive-note": "Lưu ý: Vùng độc quyền tự động bị vô hiệu hóa khi chế độ tự động ẩn đang hoạt động", + "appearance-auto-show-delay-description": "Thời gian chờ thanh hiển thị khi chuột di chuyển vào cạnh", + "appearance-auto-show-delay-label": "Thời gian trễ hiển thị", + "appearance-background-opacity-description": "Đặt độ mờ nền riêng cho thanh.", + "appearance-background-opacity-label": "Độ mờ nền Thanh", + "appearance-capsule-color-description": "Chọn màu cho các Capsule của Thanh, hoặc không chọn để dùng màu nền mặc định.", + "appearance-capsule-color-label": "Màu vùng chứa", + "appearance-capsule-opacity-description": "Đặt độ mờ cho nền của các widget khi capsule được hiển thị.", + "appearance-capsule-opacity-label": "Độ mờ đục của vỏ bọc", + "appearance-content-padding-description": "Điều chỉnh khoảng đệm giữa các cạnh của thanh và các tiện ích.", + "appearance-content-padding-label": "Khoảng đệm Nội dung", + "appearance-density-description": "Điều chỉnh khoảng đệm của thanh để có giao diện gọn gàng hoặc rộng rãi.", + "appearance-density-label": "Mật độ Thanh", + "appearance-desc": "Tùy chỉnh giao diện và vị trí của Thanh.", + "appearance-display-mode-description": "Chọn khi nào thanh hiển thị.", + "appearance-enable-exclusion-zone-inset-description": "Giảm vùng loại trừ đi 1 pixel vật lý để các cửa sổ sát mép tràn hoàn hảo xuống dưới mép thanh.", + "appearance-enable-exclusion-zone-inset-label": "Vùng loại trừ có đệm", + "appearance-floating-description": "Hiển thị Thanh dưới dạng 'viên thuốc' nổi.", + "appearance-floating-label": "Thanh nổi", + "appearance-font-scale-description": "Điều chỉnh tỷ lệ kích thước phông chữ cho văn bản hiển thị trên Bar.", + "appearance-font-scale-label": "Kích thước phông chữ", + "appearance-frame-radius": "Bán kính trong", + "appearance-frame-settings-description": "Điều chỉnh độ dày khung và bán kính góc trong", + "appearance-frame-settings-label": "Cài đặt Khung", + "appearance-frame-thickness": "Độ dày", + "appearance-hide-on-overview-description": "Ẩn Thanh và đóng các Bảng điều khiển khi chế độ Tổng quan của trình tổng hợp đang hoạt động.", + "appearance-hide-on-overview-label": "Ẩn thanh khi ở tổng quan", + "appearance-margins-description": "Điều chỉnh các lề xung quanh thanh nổi.", + "appearance-margins-horizontal": "Lề Ngang", + "appearance-margins-vertical": "Lề Dọc", + "appearance-outer-corners-description": "Hiển thị các góc bo cong ra ngoài trên Thanh.", + "appearance-outer-corners-label": "Các góc ngoài", + "appearance-position-description": "Chọn vị trí đặt Thanh trên màn hình.", + "appearance-position-label": "Vị trí Thanh", + "appearance-show-capsule-description": "Hiển thị nền của tiện ích.", + "appearance-show-capsule-label": "Hiển thị viên nang", + "appearance-show-on-workspace-switch-description": "Tự động hiển thị thanh Bar thoáng qua khi không gian làm việc thay đổi.", + "appearance-show-on-workspace-switch-label": "Hiển thị thanh khi chuyển đổi không gian làm việc", + "appearance-show-outline-description": "Hiển thị một đường viền rõ ràng xung quanh mỗi tiện ích.", + "appearance-show-outline-label": "Hiển thị đường viền tiện ích", + "appearance-type-description": "Chọn kiểu của thanh Bar: Đơn giản, Nổi hoặc Có khung.", + "appearance-type-label": "Kiểu thanh", + "appearance-use-separate-opacity-description": "Bật để sử dụng một giá trị độ mờ riêng biệt cho nền của thanh.", + "appearance-use-separate-opacity-label": "Sử dụng độ mờ đục riêng cho Thanh", + "appearance-widget-spacing-description": "Điều chỉnh khoảng cách giữa các thành phần trên thanh Bar.", + "appearance-widget-spacing-label": "Khoảng cách tiện ích", + "behavior-middle-click-command-description": "Lệnh để thực thi khi nhấp chuột giữa.", + "behavior-middle-click-command-label": "Lệnh Nhấp Chuột Giữa", + "behavior-middle-click-command-placeholder": "niri msg action toggle-overview", + "behavior-middle-click-description": "Chọn điều gì xảy ra khi nhấp chuột giữa vào các khu vực trống của thanh.", + "behavior-middle-click-follow-mouse-description": "Mở panel nhấp chuột giữa đã chọn ở vị trí con trỏ.", + "behavior-middle-click-follow-mouse-label": "Nhấp chuột giữa theo con trỏ chuột", + "behavior-middle-click-label": "Hành động nhấp chuột giữa thanh", + "behavior-right-click-command-description": "Lệnh để thực thi khi nhấp chuột phải.", + "behavior-right-click-command-label": "Lệnh nhấp chuột phải", + "behavior-right-click-command-placeholder": "notify-send \"Nhấp chuột phải\"", + "behavior-right-click-description": "Chọn chức năng của nhấp chuột phải trên các vùng trống của thanh.", + "behavior-right-click-follow-mouse-description": "Mở bảng chuột phải đã chọn ở vị trí con trỏ.", + "behavior-right-click-follow-mouse-label": "Menu chuột phải theo con trỏ", + "behavior-right-click-label": "Hành động nhấp chuột phải trên Thanh", + "behavior-wheel-wrap-description": "Khi được bật, cuộn tiếp tục từ mục cuối cùng đến mục đầu tiên.", + "behavior-wheel-wrap-label": "Cuộn vòng", + "behavior-workspace-scroll-description": "Chọn chức năng của con lăn chuột trên các vùng trống của Bar.", + "behavior-workspace-scroll-label": "Hành động bánh xe chuột trên Thanh", + "behavior-workspace-scroll-option-content": "Nội dung", + "behavior-workspace-scroll-option-workspace": "Không gian làm việc", + "monitor-configure-widgets": "Cấu hình tiện ích", + "monitor-override-settings": "Ghi đè cài đặt chung", + "monitor-override-settings-description": "Sử dụng cài đặt tùy chỉnh cho màn hình này.", + "monitor-reset-all": "Đặt lại tất cả", + "monitor-widgets-title": "Cấu hình Tiện ích - {monitor}", + "monitors-desc": "Hiển thị thanh trên các màn hình cụ thể. Mặc định là tất cả nếu không có màn hình nào được chọn.", + "monitors-desc-new": "Cấu hình màn hình nào hiển thị thanh Bar và tùy chỉnh cài đặt cho từng màn hình.", + "monitors-title": "Hiển thị Màn hình", + "title": "Thanh", + "tray-blacklist-description": "Thêm quy tắc loại trừ khay hệ thống, hỗ trợ ký tự đại diện (*).", + "tray-blacklist-label": "Danh sách đen", + "tray-blacklist-placeholder": "Ví dụ: nm-applet, Fcitx*", + "tray-pin-application": "Ghim ứng dụng", + "tray-unpin-application": "Gỡ ghim ứng dụng", + "use-global-widgets": "Sử dụng tiện ích toàn cục", + "widgets-desc": "Kéo các tiện ích để thay đổi thứ tự của chúng. Dùng menu chuột phải để chuyển các tiện ích giữa các phần hoặc xóa chúng.", + "widgets-title": "Vị trí Tiện ích" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "Tạo sơ đồ màu từ hình nền của bạn. Tự động trích xuất màu sắc để tạo một chủ đề mạch lạc.", + "color-source-use-wallpaper-colors-label": "Sử dụng màu sắc hình nền", + "dark-mode-mode-description": "Bật chuyển đổi tự động giữa Chế độ Sáng và Chế độ Tối.", + "dark-mode-mode-label": "Lịch trình Chế độ tối", + "dark-mode-mode-manual": "Hướng dẫn", + "dark-mode-mode-off": "Tắt", + "dark-mode-switch-description": "Chuyển sang giao diện tối hơn để dễ nhìn hơn vào ban đêm.", + "delete-error-description": "Không thể xóa {scheme}", + "delete-error-title": "Xóa thất bại", + "delete-success-description": "Đã xóa thành công {scheme}", + "delete-success-title": "Sơ đồ màu đã xóa", + "download-button": "Tải xuống thêm", + "download-downloading": "Đang tải xuống...", + "download-empty": "Không có bảng màu nào khả dụng", + "download-error-api-error": "Lỗi API: {status}", + "download-error-description": "Tải xuống {scheme} thất bại", + "download-error-download-failed": "Tải xuống thất bại với mã thoát: {code}", + "download-error-invalid-response": "Định dạng phản hồi API không hợp lệ", + "download-error-no-files": "Không tìm thấy tệp nào cho lược đồ", + "download-error-parse-failed": "Không thể phân tích phản hồi API: {error}", + "download-error-rate-limit": "Đã vượt quá giới hạn tốc độ API GitHub", + "download-error-title": "Tải xuống thất bại", + "download-fetching": "Đang tải các bảng màu có sẵn...", + "download-success-description": "Đã tải xuống thành công {scheme}", + "download-success-title": "Bảng màu đã tải xuống", + "download-title": "Tải Xuống Sơ Đồ Màu", + "method-description": { + "content": "Sơ đồ Material Design với tính năng trích xuất màu sắc độ trung thực cao, khớp sát với màu sắc thực tế của nội dung nguồn.", + "dysfunctional": "Giống như Faithful nhưng chọn nhóm màu nổi nổi bật thứ hai làm màu chính.", + "faithful": "Cố gắng bám sát màu gốc trong khi vẫn tạo ra một bảng màu hài hòa.", + "fruit-salad": "Sơ đồ Material Design tạo ra một bảng màu sống động, vui tươi với các sắc thái đa dạng và phong phú.", + "monochrome": "Sơ đồ Material Design sử dụng thang độ xám đơn sắc với nội dung màu sắc tối thiểu.", + "muted": "Tạo ra các tông màu dịu nhẹ, ít bão hòa để có vẻ ngoài trầm tĩnh và tinh tế.", + "rainbow": "Phối màu Material Design tạo ra màu sắc trên toàn bộ quang phổ để đạt được sự đa dạng màu sắc tối đa.", + "tonal-spot": "Thuật toán mặc định của Material Design. Tạo ra các bảng màu cân bằng bằng cách sử dụng các biến thể tông màu của màu gốc.", + "vibrant": "Tối đa hóa độ bão hòa màu sắc cho các bảng màu đậm, bắt mắt với tông màu sống động." + }, + "predefined-desc": "Chọn từ một bộ sưu tập các bảng màu có sẵn.", + "predefined-generate-templates-label": "Tạo mẫu cho các lược đồ cài sẵn", + "predefined-title": "Sơ Đồ Màu Định Sẵn", + "sync-gsettings-description": "Khớp chủ đề hệ thống với biến thể sáng hoặc tối đang hoạt động.", + "sync-gsettings-label": "Đồng bộ chủ đề hệ thống", + "templates-desc": "Áp dụng màu sắc cho các ứng dụng bên ngoài.", + "templates-filter-description": "Hiển thị các mẫu từ một danh mục cụ thể.", + "templates-filter-label": "Lọc theo danh mục", + "templates-misc-description": "Tạo mẫu của riêng bạn.", + "templates-misc-label": "Nâng cao", + "templates-misc-user-templates-description": "Chỉ bật nếu bạn biết mình đang làm gì — tham khảo tài liệu trực tuyến của chúng tôi.", + "templates-misc-user-templates-label": "Bật các mẫu người dùng", + "templates-none-detected": "Không phát hiện", + "templates-write-path": "Ghi: {filepath}", + "title": "Sơ Đồ Màu", + "wallpaper-method-description": "Chọn phương pháp tạo bảng màu yêu thích của bạn.", + "wallpaper-method-label": "Phương pháp tạo bảng màu", + "wallpaper-monitor-source-description": "Chọn màn hình để trích xuất màu nền.", + "wallpaper-monitor-source-label": "Nguồn tạo màu" + }, + "connections": { + "authentication-required": "Yêu cầu xác thực", + "bluetooth-auto-connect-description": "Tự động kết nối với các thiết bị đã ghép nối đáng tin cậy khi Bluetooth được bật.", + "bluetooth-auto-connect-label": "Tự động kết nối thiết bị đã ghép đôi", + "bluetooth-devices-unnamed": "Các thiết bị không tên không hiển thị.", + "bluetooth-discoverable": "Thiết bị này có thể được phát hiện là {hostName} khi tab cài đặt này đang mở.", + "bluetooth-rssi-polling-description": "Lấy mẫu RSSI định kỳ cho các thiết bị đã kết nối qua bluetoothctl. Có thể không khả dụng cho tất cả thiết bị; sử dụng tài nguyên tối thiểu khi được bật.", + "bluetooth-rssi-polling-interval-description": "Cấu hình tần suất cập nhật cường độ tín hiệu cho các thiết bị đã kết nối.", + "bluetooth-rssi-polling-interval-label": "Chu kỳ kiểm tra", + "bluetooth-rssi-polling-label": "Kiểm tra tín hiệu Bluetooth định kỳ", + "disable-discoverability-description": "Ẩn thiết bị của bạn khỏi các thiết bị Bluetooth lân cận.", + "disable-discoverability-label": "Tắt hiển thị thiết bị", + "hide-unnamed-devices-description": "Ẩn các thiết bị chỉ hiển thị dưới dạng địa chỉ Bluetooth.", + "hide-unnamed-devices-label": "Ẩn các thiết bị không tên", + "pin-instructions": "Vui lòng nhập mã PIN hiển thị trên thiết bị của bạn.", + "title": "Các kết nối", + "wifi-header-text": "Sử dụng Wi-Fi để xem các mạng khả dụng và kết nối Internet." + }, + "control-center": { + "cards-desc": "Tùy chỉnh các điều khiển nào xuất hiện trong Trung tâm điều khiển và theo thứ tự nào.", + "desc": "Cấu hình vị trí và hoạt động của bảng điều khiển Trung tâm điều khiển.", + "open-at-mouse-description": "Khi nhấp chuột phải vào một khu vực trống của Bar, mở Control Center gần con trỏ. Khi tắt, cài đặt vị trí ở trên sẽ được sử dụng thay thế.", + "open-at-mouse-label": "Mở tại vị trí con trỏ khi nhấp chuột phải", + "position-description": "Chọn vị trí bảng điều khiển Trung tâm điều khiển xuất hiện khi mở.", + "shortcuts-custom-button-command-description": "Lệnh sẽ thực thi khi nút được nhấp.", + "shortcuts-custom-button-enable-on-state-logic-description": "Bật biểu tượng thứ hai và trạng thái 'hot' dựa trên một lệnh kiểm tra.", + "shortcuts-custom-button-enable-on-state-logic-label": "Bật logic cho trạng thái bật", + "shortcuts-custom-button-icon-description": "Chọn một biểu tượng từ thư viện.", + "shortcuts-custom-button-on-clicked-label": "Lệnh nhấp chuột trái", + "shortcuts-custom-button-on-middle-clicked-description": "Lệnh để thực thi khi nút được nhấp chuột giữa.", + "shortcuts-custom-button-on-state-command-description": "Lệnh thực thi để kiểm tra xem nút có nên ở trạng thái 'bật' hay không. Trả về 0 nếu bật, khác 0 nếu tắt.", + "shortcuts-custom-button-on-state-command-label": "Lệnh kiểm tra trạng thái bật", + "shortcuts-custom-button-on-state-icon-description": "Biểu tượng cho nút khi nó ở trạng thái 'bật'.", + "shortcuts-custom-button-on-state-icon-label": "Biểu tượng khi bật", + "shortcuts-custom-button-state-checks-add": "Thêm kiểm tra trạng thái", + "shortcuts-custom-button-state-checks-command": "Lệnh để thực thi cho việc kiểm tra trạng thái này", + "shortcuts-custom-button-state-checks-label": "Kiểm tra trạng thái", + "shortcuts-custom-button-state-checks-remove": "Xóa", + "shortcuts-custom-button-tooltip-description": "Chú giải công cụ hiển thị khi di chuột qua nút.", + "shortcuts-custom-button-tooltip-label": "Chú giải công cụ", + "shortcuts-title": "Tiện ích Lối tắt", + "system-monitor-disk-path-description": "Chọn điểm gắn kết đĩa mà thẻ giám sát hệ thống trong Trung tâm điều khiển sẽ theo dõi.", + "system-monitor-disk-path-label": "Giám Sát Hệ Thống Đường Dẫn Ổ Đĩa", + "title": "Trung tâm điều khiển" + }, + "desktop-widgets": { + "clock-enabled-description": "Hiển thị một tiện ích đồng hồ trên màn hình nền.", + "clock-enabled-label": "Bật tiện ích đồng hồ", + "clock-format-label": "Định dạng", + "clock-height-description": "Chiều cao của tiện ích đồng hồ tính bằng điểm ảnh.", + "clock-height-label": "Chiều cao", + "clock-minimal-mode-description": "Sử dụng màn hình đồng hồ tối giản với thời gian và ngày tháng.", + "clock-minimal-mode-label": "Chế độ tối giản", + "clock-rounded-corners-description": "Sử dụng góc bo tròn cho nền tiện ích.", + "clock-rounded-corners-label": "Góc Bo Tròn", + "clock-show-background-description": "Hiển thị vỏ bọc cho tiện ích đồng hồ.", + "clock-show-background-label": "Hiển thị nền", + "clock-show-date-description": "Hiển thị ngày bên dưới thời gian.", + "clock-show-date-label": "Hiển thị ngày", + "clock-show-seconds-description": "Hiển thị giây trong đồng hồ.", + "clock-show-seconds-label": "Hiển thị giây", + "clock-style-analog": "Đồng hồ kim", + "clock-style-binary": "Nhị phân", + "clock-style-description": "Chọn kiểu hiển thị đồng hồ.", + "clock-style-digital": "Đồng hồ số", + "clock-style-label": "Kiểu Đồng Hồ", + "clock-style-minimal": "Tối giản", + "clock-width-description": "Chiều rộng của tiện ích đồng hồ đồng hồ tính bằng pixel.", + "cpu-intensive-note": "Các Widget được đánh dấu ! sử dụng nhiều CPU hơn và nên được bật một cách cẩn trọng.", + "edit-mode-button-label": "Vào chế độ chỉnh sửa", + "edit-mode-controls-explanation": "Nhấp chuột trái & kéo: Di chuyển hoặc thay đổi kích thước tiện ích. Nhấp chuột phải: Mở các tùy chọn menu ngữ cảnh.", + "edit-mode-description": "Bật chế độ chỉnh sửa để di chuyển và sắp xếp lại các widget trên màn hình nền. Khi được bật, các widget sẽ hiển thị đường viền kéo và có thể được sắp xếp lại.", + "edit-mode-exit-button": "Thoát chế độ chỉnh sửa", + "edit-mode-grid-snap-label": "Căn theo lưới", + "edit-mode-grid-snap-scale-label": "Độ chia lưới", + "edit-mode-label": "Chế độ chỉnh sửa", + "enabled-description": "Bật hoặc tắt hoàn toàn các tiện ích trên màn hình nền.", + "enabled-label": "Bật tiện ích màn hình nền", + "general-desc": "Cấu hình các tiện ích hiển thị trên màn hình của bạn.", + "general-title": "Tiện ích màn hình nền", + "media-player-enabled-description": "Hiển thị tiện ích trình phát đa phương tiện trên màn hình nền.", + "media-player-enabled-label": "Bật tiện ích trình phát đa phương tiện", + "media-player-rounded-corners-description": "Bật bo tròn góc cho các tiện ích.", + "media-player-show-album-art-description": "Hiển thị ảnh bìa album và thông tin bài hát (tiêu đề và nghệ sĩ).", + "media-player-show-album-art-label": "Hiển thị ảnh bìa album và tiêu đề", + "media-player-show-background-description": "Hiển thị vùng chứa nền.", + "media-player-show-buttons-description": "Hiển thị các nút điều khiển đa phương tiện (phát/tạm dừng, trước, tiếp theo).", + "media-player-show-buttons-label": "Hiển thị Nút", + "media-player-show-visualizer-description": "Hiển thị lớp phủ trực quan hóa âm thanh.", + "media-player-visualizer-type-description": "Chọn một loại hiển thị.", + "overview-enabled-description": "Hiển thị widget màn hình nền trong khi chế độ Overview của trình tổng hợp đang mở.", + "overview-enabled-label": "Hiển thị trong tổng quan", + "system-stat-layout-bottom": "Dưới", + "system-stat-layout-description": "Chọn cách chú giải được hiển thị so với biểu đồ.", + "system-stat-layout-label": "Bố cục", + "system-stat-layout-side": "Cạnh", + "system-stat-rounded-corners-description": "Sử dụng góc bo tròn cho nền tiện ích.", + "system-stat-rounded-corners-label": "Góc bo tròn", + "system-stat-show-background-description": "Hiển thị khung nền cho tiện ích thống kê hệ thống.", + "system-stat-show-background-label": "Hiển thị nền", + "system-stat-stat-type-description": "Chọn số liệu thống kê hệ thống nào để hiển thị.", + "system-stat-stat-type-label": "Loại Thống Kê", + "title": "Tiện ích Màn hình nền", + "weather-enabled-description": "Hiển thị một tiện ích thời tiết trên màn hình nền.", + "weather-enabled-label": "Bật tiện ích thời tiết", + "weather-show-background-description": "Hiển thị vỏ bọc cho tiện ích thời tiết." + }, + "display": { + "monitors-backlight-device-auto-option": "Mặc định", + "monitors-backlight-device-description": "Chọn một thiết bị đèn nền cho đầu ra này.", + "monitors-backlight-device-label": "Thiết bị đèn nền", + "monitors-brightness-step-description": "Điều chỉnh bước nhảy cho các thay đổi độ sáng (con lăn chuột và phím tắt).", + "monitors-brightness-step-label": "Kích thước bước độ sáng", + "monitors-brightness-unavailable-ddc-disabled": "Điều khiển độ sáng không khả dụng. Bật 'Hỗ trợ độ sáng ngoài' để điều khiển độ sáng của màn hình này.", + "monitors-brightness-unavailable-generic": "Điều khiển độ sáng không khả dụng cho màn hình này.", + "monitors-desc": "Điều chỉnh độ sáng màn hình cho từng màn hình.", + "monitors-enforce-minimum-description": "Giải quyết vấn đề đèn nền tắt hoàn toàn trên một số màn hình ở độ sáng 0%.", + "monitors-enforce-minimum-label": "Buộc độ sáng tối thiểu (1%)", + "monitors-external-brightness-description": "Bật hỗ trợ DDCUtil để điều khiển độ sáng trên màn hình ngoài qua giao thức DDC/CI.", + "monitors-external-brightness-label": "Hỗ trợ độ sáng bên ngoài", + "monitors-global-brightness-description": "Điều chỉnh độ sáng cho tất cả màn hình cùng một lúc.", + "monitors-global-brightness-label": "Tất cả màn hình", + "monitors-title": "Cài Đặt Theo Màn Hình", + "night-light-auto-schedule-description": "Dựa trên thời gian mặt trời lặn và mọc tại {location} — được đề xuất.", + "night-light-auto-schedule-label": "Lên lịch tự động", + "night-light-desc": "Giảm phát xạ ánh sáng xanh để giúp bạn ngủ ngon hơn và giảm mỏi mắt.", + "night-light-enable-description": "Áp dụng bộ lọc màu ấm để giảm phát xạ ánh sáng xanh.", + "night-light-enable-label": "Bật Bộ lọc ánh sáng xanh", + "night-light-force-activation-description": "Bỏ qua lịch trình và áp dụng bộ lọc ánh sáng xanh ngay lập tức.", + "night-light-force-activation-label": "Buộc kích hoạt", + "night-light-manual-schedule-description": "Đặt thời gian tùy chỉnh cho bình minh và hoàng hôn.", + "night-light-manual-schedule-label": "Lên lịch thủ công", + "night-light-manual-schedule-select-start": "Chọn thời gian bắt đầu", + "night-light-manual-schedule-select-stop": "Chọn thời gian dừng", + "night-light-manual-schedule-sunrise": "Thời gian mặt trời mọc", + "night-light-manual-schedule-sunset": "Thời gian mặt trời lặn", + "night-light-temperature-day": "Ngày", + "night-light-temperature-day-description": "Điều khiển nhiệt độ vào ban ngày.", + "night-light-temperature-description": "Điều chỉnh độ ấm màu sắc cho ban đêm và ban ngày.", + "night-light-temperature-label": "Nhiệt độ màu", + "night-light-temperature-night": "Đêm", + "night-light-temperature-night-description": "Điều khiển nhiệt độ vào ban đêm.", + "title": "Màn hình" + }, + "dock": { + "appearance-background-opacity-description": "Điều chỉnh độ mờ nền của Dock.", + "appearance-border-radius-description": "Điều chỉnh bán kính bo góc của Dock.", + "appearance-border-radius-label": "Bán kính bo góc", + "appearance-colorize-icons-description": "Áp dụng màu sắc chủ đề cho các biểu tượng ứng dụng trên thanh Dock (chỉ các ứng dụng không được tập trung).", + "appearance-colorize-icons-label": "Tô màu biểu tượng", + "appearance-dead-opacity-description": "Điều chỉnh độ mờ của các biểu tượng ứng dụng không đang chạy.", + "appearance-dead-opacity-label": "Độ mờ khi không hoạt động", + "appearance-desc": "Tùy chỉnh hành vi và giao diện của Dock.", + "appearance-display-auto-hide": "Tự động ẩn", + "appearance-display-description": "Chọn cách thanh Dock hoạt động.", + "appearance-display-exclusive": "Đặc biệt", + "appearance-dock-indicator-description": "Hiển thị một chỉ báo nhỏ khi dock bị ẩn.", + "appearance-dock-indicator-label": "Chỉ báo Dock", + "appearance-floating-distance-description": "Đặt khoảng cách giữa Dock và mép màn hình.", + "appearance-floating-distance-label": "Khoảng cách nổi của Thanh Dock", + "appearance-group-apps-description": "Nhóm nhiều cửa sổ của cùng một ứng dụng vào một mục trên thanh ứng dụng.", + "appearance-group-apps-label": "Nhóm ứng dụng cùng loại", + "appearance-group-click-action-cycle": "Chuyển đổi cửa sổ", + "appearance-group-click-action-description": "Chọn hành động khi nhấp chuột trái cho các ứng dụng được nhóm.", + "appearance-group-click-action-label": "Thao tác nhấp ứng dụng nhóm", + "appearance-group-click-action-list": "Mở danh sách cửa sổ", + "appearance-group-context-menu-mode-description": "Chọn cách menu ngữ cảnh hoạt động đối với các ứng dụng được nhóm.", + "appearance-group-context-menu-mode-extended": "Mở rộng", + "appearance-group-context-menu-mode-label": "Chế độ menu ứng dụng được nhóm", + "appearance-group-context-menu-mode-list": "Danh sách cửa sổ", + "appearance-group-indicator-style-description": "Chọn cách các chỉ báo nhóm ứng dụng đang chạy hiển thị trạng thái cửa sổ đang tập trung.", + "appearance-group-indicator-style-dots": "Các chấm", + "appearance-group-indicator-style-label": "Kiểu chỉ báo nhóm", + "appearance-group-indicator-style-number": "Số", + "appearance-hide-show-speed-description": "Điều chỉnh tốc độ hoạt ảnh ẩn/hiện của Thanh Dock.", + "appearance-hide-show-speed-label": "Ẩn/hiện tốc độ", + "appearance-icon-size-description": "Điều chỉnh kích thước tổng thể của Dock.", + "appearance-icon-size-label": "Kích thước Dock", + "appearance-inactive-indicators-description": "Hiển thị chấm chỉ báo cho tất cả ứng dụng, không chỉ ứng dụng đang hoạt động hiện tại.", + "appearance-inactive-indicators-label": "Chỉ báo đang chạy", + "appearance-indicator-color-description": "Chọn màu của chỉ báo dock ẩn.", + "appearance-indicator-color-label": "Màu Chỉ Báo", + "appearance-indicator-opacity-description": "Điều chỉnh độ mờ của chỉ báo Dock ẩn.", + "appearance-indicator-opacity-label": "Độ mờ chỉ báo", + "appearance-indicator-thickness-description": "Bật/Tắt chỉ báo Dock ẩn dày hơn (6px thay vì 3px).", + "appearance-indicator-thickness-label": "Chỉ báo đậm hơn", + "appearance-launcher-icon-description": "Chọn biểu tượng được sử dụng cho Trình khởi chạy Dock.", + "appearance-launcher-icon-label": "Biểu tượng Trình khởi chạy", + "appearance-launcher-position-description": "Chọn nơi biểu tượng Trình khởi chạy xuất hiện trên Dock.", + "appearance-launcher-position-end": "Kết thúc", + "appearance-launcher-position-label": "Vị trí Trình khởi chạy", + "appearance-launcher-position-start": "Bắt đầu", + "appearance-launcher-use-distro-logo-description": "Sử dụng biểu tượng phân phối của bạn làm biểu tượng dock launcher.", + "appearance-launcher-use-distro-logo-label": "Sử dụng logo bản phân phối thay vì biểu tượng", + "appearance-pinned-static-description": "Luôn luôn đẩy các biểu tượng ứng dụng đã ghim sang bên trái theo thứ tự cố định.", + "appearance-pinned-static-label": "Ứng dụng ghim cố định", + "appearance-position-description": "Chọn vị trí hiển thị của Dock trên màn hình.", + "appearance-position-label": "Vị trí", + "appearance-show-launcher-icon-description": "Hiển thị biểu tượng trình khởi chạy ứng dụng trên thanh Dock.", + "appearance-show-launcher-icon-label": "Hiển thị trình khởi chạy ứng dụng", + "appearance-sit-on-frame-description": "Căn chỉnh thanh Dock bên trong viền khung thay vì nằm chồng lên trên.", + "appearance-sit-on-frame-label": "Thanh Dock gắn vào khung", + "appearance-type-attached": "Đính kèm", + "appearance-type-description": "Chọn giữa một thanh nổi hình viên thuốc hoặc một thanh cố định gắn vào cạnh.", + "appearance-type-floating": "Nổi", + "appearance-type-label": "Kiểu Dock", + "enabled-description": "Hiển thị hoặc ẩn Dock hoàn toàn.", + "enabled-label": "Bật thanh dock", + "monitors-desc": "Hiển thị Dock trên các màn hình cụ thể. Mặc định là tất cả nếu không có màn hình nào được chọn.", + "monitors-only-same-monitor-description": "Chỉ hiển thị ứng dụng từ màn hình nơi thanh Dock nằm.", + "monitors-only-same-monitor-label": "Chỉ ứng dụng từ cùng màn hình", + "monitors-title": "Màn hình", + "title": "Thanh Dock" + }, + "general": { + "copy-settings": "Sao chép cài đặt", + "fonts-default-description": "Phông chữ chính được sử dụng trên toàn bộ giao diện.", + "fonts-default-label": "Phông chữ Mặc định", + "fonts-default-placeholder": "Chọn phông chữ mặc định...", + "fonts-default-scale-description": "Tăng hoặc giảm kích thước của văn bản tiêu chuẩn.", + "fonts-default-scale-label": "Cỡ Chữ Mặc Định", + "fonts-default-search-placeholder": "Tìm phông chữ...", + "fonts-desc": "Chọn phông chữ được sử dụng trên toàn bộ giao diện.", + "fonts-monospace-description": "Phông chữ đơn cách được sử dụng để hiển thị số và chỉ số.", + "fonts-monospace-label": "Phông chữ đơn cách", + "fonts-monospace-placeholder": "Chọn phông chữ đơn cách...", + "fonts-monospace-scale-description": "Tăng hoặc giảm kích thước của văn bản đơn cách.", + "fonts-monospace-scale-label": "Cỡ chữ đơn cách", + "fonts-monospace-search-placeholder": "Tìm kiếm phông chữ đơn cách...", + "fonts-title": "Phông chữ", + "keybinds-conflict-description": "Phím tắt đã được gán cho {action}.", + "keybinds-conflict-title": "Xung đột phím tắt", + "keybinds-description": "Cấu hình phím điều hướng toàn cục cho các bảng điều khiển và trình khởi chạy.", + "keybinds-down": "Di chuyển xuống", + "keybinds-enter": "Xác nhận / Hành động", + "keybinds-escape": "Đóng / Quay lại", + "keybinds-left": "Di chuyển sang trái", + "keybinds-modifier-description": "Phím tắt phải bao gồm một phím bổ trợ (Ctrl hoặc Alt).", + "keybinds-modifier-title": "Cần phím bổ trợ", + "keybinds-remove": "Xóa", + "keybinds-right": "Di chuyển sang phải", + "keybinds-title": "Phím tắt điều hướng", + "keybinds-up": "Di chuyển lên", + "language-desc": "Chọn ngôn ngữ bạn muốn dùng cho ứng dụng.", + "language-select-auto-detect": "Tự động", + "language-select-description": "Chọn ngôn ngữ được sử dụng trong giao diện ứng dụng.", + "language-select-label": "Ngôn ngữ ứng dụng", + "launch-setup-wizard": "Trình hướng dẫn cài đặt", + "profile-desc": "Chỉnh sửa thông tin người dùng và ảnh đại diện của bạn.", + "profile-picture-description": "Ảnh đại diện của bạn xuất hiện trên toàn bộ giao diện.", + "profile-picture-label": "Ảnh đại diện", + "profile-select-avatar": "Chọn ảnh đại diện", + "profile-title": "Hồ sơ", + "profile-tooltip": "Ảnh đại diện", + "reverse-scrolling-description": "Đảo ngược hướng cuộn được hiểu", + "reverse-scrolling-label": "Cuộn đảo ngược", + "screen-corners-desc": "Tùy chỉnh bo góc màn hình và hiệu ứng hình ảnh.", + "screen-corners-radius-description": "Điều chỉnh các góc bo tròn của màn hình.", + "screen-corners-radius-label": "Độ bo tròn góc màn hình", + "screen-corners-radius-reset": "Đặt lại bán kính góc màn hình", + "screen-corners-show-corners-description": "Hiển thị các góc bo tròn ở cạnh màn hình.", + "screen-corners-show-corners-label": "Hiển thị các góc màn hình", + "screen-corners-solid-black-description": "Sử dụng màu đen tuyền thay vì màu nền của thanh Bar.", + "screen-corners-solid-black-label": "Góc đen đặc", + "screen-corners-title": "Các góc màn hình", + "settings-copied": "Cài đặt đã sao chép vào bộ nhớ tạm", + "smooth-scrolling-description": "Tạo hiệu ứng cuộn danh sách để có trải nghiệm cuộn mượt mà hơn.", + "smooth-scrolling-label": "Cuộn mượt", + "tab-basics": "Cơ bản", + "tab-keybinds": "Phím Tắt" + }, + "hooks": { + "color-generation-description": "Lệnh sẽ được thực thi khi quá trình tạo màu hoàn tất (ví dụ: sau khi áp dụng chủ đề dựa trên hình nền).", + "color-generation-label": "Màu sắc đã tạo", + "color-generation-placeholder": "Ví dụ: notify-send \"Colors\" \"Generated\"", + "info-command-info-description": "• Lệnh được thực thi qua shell (sh -lc)
• Lệnh chạy ngầm (tách rời)
• Nút kiểm tra thực thi với các giá trị hiện tại", + "info-parameters-description": "• Hook hình nền: $1 = đường dẫn hình nền, $2 = tên màn hình, $3 = chủ đề (sáng/tối)
• Hook tạo màu: $1 = chủ đề (sáng/tối)
• Hook chuyển đổi chủ đề: $1 = true/false (trạng thái Dark Mode)
• Hook khóa/mở khóa màn hình: $1 = khóa/mở khóa (trạng thái khóa màn hình)
• Hook chế độ hiệu suất: Không có tham số
• Hook phiên: $1 = hành động (tắt máy/khởi động lại)", + "info-parameters-label": "Các tham số có sẵn", + "noctalia-started-description": "Lệnh để thực thi khi Noctalia đã tải xong.", + "noctalia-started-label": "Noctalia đã khởi chạy", + "noctalia-started-placeholder": "Ví dụ: notify-send 'Noctalia Loaded'", + "performance-mode-disabled-description": "Lệnh sẽ chạy khi chế độ hiệu suất của Noctalia bị tắt.", + "performance-mode-disabled-label": "Chế độ hiệu suất đã tắt", + "performance-mode-disabled-placeholder": "Ví dụ: notify-send \"Performance\" \"Mode disabled\"", + "performance-mode-enabled-description": "Lệnh sẽ được chạy khi chế độ hiệu năng của Noctalia được bật.", + "performance-mode-enabled-label": "Chế độ hiệu năng đã bật", + "performance-mode-enabled-placeholder": "Ví dụ: notify-send \"Performance\" \"Mode enabled\"", + "screen-lock-description": "Lệnh được thực thi khi màn hình bị khóa.", + "screen-lock-label": "Màn hình đã khóa", + "screen-lock-placeholder": "Ví dụ: notify-send \"Screen\" \"Locked\"", + "screen-unlock-description": "Lệnh sẽ được thực thi khi màn hình khóa được mở khóa.", + "screen-unlock-label": "Màn hình đã mở khóa", + "screen-unlock-placeholder": "Ví dụ: notify-send \"Screen\" \"Unlocked\"", + "session-description": "Lệnh sẽ được thực thi trước khi tắt máy hoặc khởi động lại. Nhận loại hành động dưới dạng $1 (tắt máy/khởi động lại).", + "session-label": "Kết thúc phiên", + "session-placeholder": "Ví dụ: notify-send \"Session\" \"$1\"", + "system-hooks-desc": "Cấu hình các lệnh sẽ được thực thi khi các sự kiện hệ thống xảy ra.", + "system-hooks-enable-description": "Bật hoặc tắt tất cả lệnh hook.", + "system-hooks-enable-label": "Bật móc nối", + "system-hooks-title": "System Hooks", + "theme-changed-description": "Lệnh sẽ được thực thi khi chủ đề chuyển đổi giữa Chế độ tối và Chế độ sáng.", + "theme-changed-label": "Chủ đề đã thay đổi", + "theme-changed-placeholder": "Ví dụ: notify-send \"Theme\" \"Toggled\"", + "title": "Móc nối", + "wallpaper-changed-description": "Lệnh sẽ được thực thi khi hình nền thay đổi.", + "wallpaper-changed-label": "Hình nền đã thay đổi", + "wallpaper-changed-placeholder": "Ví dụ: notify-send \"Wallpaper\" \"Changed\"" + }, + "idle": { + "custom-add": "Thêm lệnh", + "custom-description": "Chạy một lệnh shell sau một khoảng thời gian không hoạt động.", + "custom-entry-command": "Lệnh", + "custom-entry-delete": "Xóa", + "custom-entry-edit": "Chỉnh Sửa Lệnh Tùy Chỉnh", + "custom-entry-name": "Tên", + "custom-entry-name-placeholder": "Ví dụ: Tắt đèn", + "custom-entry-new": "Lệnh Tùy Chỉnh Mới", + "custom-entry-timeout": "Thời gian không hoạt động", + "custom-entry-timeout-format": "{count} giây", + "custom-entry-timeout-format-plural": "{count} giây", + "custom-entry-unnamed": "Lệnh không tên", + "custom-label": "Lệnh Tùy Chỉnh Khi Không Hoạt Động", + "enable-description": "Tự động tắt màn hình, khóa hoặc tạm dừng sau một khoảng thời gian không hoạt động.", + "enable-label": "Bật quản lý chế độ chờ", + "fade-duration-description": "Số giây cho hiệu ứng mờ dần sang đen trước khi mỗi hành động được kích hoạt. Mọi chuyển động chuột sẽ hủy bỏ hiệu ứng mờ.", + "fade-duration-label": "Thời lượng mờ dần", + "lock-description": "Số giây không hoạt động trước khi Màn hình khóa kích hoạt.", + "lock-label": "Màn hình khóa", + "resume-command-label": "Lệnh tiếp tục", + "screen-off-description": "Số giây không hoạt động trước khi tắt màn hình.", + "screen-off-label": "Tắt màn hình", + "status-description": "Thời gian không hoạt động được báo cáo bởi compositor.", + "status-label": "Thời gian nhàn rỗi", + "suspend-description": "Số giây không hoạt động trước khi hệ thống tạm dừng.", + "tab-behavior": "Hành vi", + "tab-custom": "Tùy chỉnh", + "timeouts-description": "Đặt thành 0 để tắt một giai đoạn. Thời gian chờ bị tạm dừng khi Giữ màn hình bật được kích hoạt.", + "timeouts-label": "Thời gian chờ", + "title": "Không hoạt động", + "unavailable": "Giám sát trạng thái rảnh rỗi tích hợp không khả dụng trên compositor này." + }, + "indicator": { + "default-value": "Mặc định: {value}", + "system-default": "Mặc định hệ thống" + }, + "launcher": { + "clipboard-desc": "Truy cập và quản lý lịch sử khay nhớ tạm của bạn từ Trình khởi chạy.", + "execute-desc": "Cấu hình cách ứng dụng được khởi chạy.", + "execute-title": "Thực thi", + "settings-annotation-tool-description": "Lệnh để chạy khi nhấp vào nút chú thích trong lịch sử bảng tạm. Hình ảnh sẽ được chuyển qua đường ống tới lệnh này.", + "settings-annotation-tool-label": "Công cụ chú thích", + "settings-annotation-tool-placeholder": "Ví dụ: 'gradia', 'satty -f -'", + "settings-auto-paste-description": "Tự động dán mục đã chọn từ khay nhớ tạm. Yêu cầu wtype.", + "settings-auto-paste-label": "Tự động dán", + "settings-clip-chips-description": "Hiển thị một thanh tab để lọc lịch sử bảng tạm theo loại (Hình ảnh, Liên kết, Tệp, Mã, v.v.).", + "settings-clip-chips-label": "Bật Thẻ Danh mục", + "settings-clip-preview-description": "Hiển thị bản xem trước nội dung khay nhớ tạm khi dùng lệnh >clip.", + "settings-clip-preview-label": "Bật xem trước clip", + "settings-clip-smart-icons-description": "Hiển thị các biểu tượng cụ thể cho liên kết, tệp, màu sắc và mã thay vì biểu tượng bảng tạm chung chung.", + "settings-clip-smart-icons-label": "Bật Biểu tượng thông minh", + "settings-clip-wrap-text-description": "Ngắt dòng văn bản trong danh sách bảng tạm thay vì cắt bớt.", + "settings-clip-wrap-text-label": "Bọc văn bản đã sao chép", + "settings-clipboard-history-description": "Truy cập các mục đã sao chép trước đó từ Trình khởi chạy.", + "settings-clipboard-history-label": "Bật lịch sử khay nhớ tạm", + "settings-clipboard-watch-image-description": "Chuỗi lệnh đầy đủ được truyền tới wl-paste để thay đổi hình ảnh. (yêu cầu khởi động lại)", + "settings-clipboard-watch-image-label": "Lệnh theo dõi hình ảnh", + "settings-clipboard-watch-text-description": "Chuỗi lệnh đầy đủ được truyền cho wl-paste để thay đổi văn bản. (yêu cầu khởi động lại)", + "settings-clipboard-watch-text-label": "Lệnh theo dõi văn bản", + "settings-custom-launch-prefix-description": "Tiền tố các lệnh bằng một trình khởi chạy tùy chỉnh (ví dụ: 'runapp' để tích hợp systemd).", + "settings-custom-launch-prefix-enabled-description": "Sử dụng tiền tố tùy chỉnh để khởi chạy ứng dụng thay vì phương pháp mặc định.", + "settings-custom-launch-prefix-enabled-label": "Bật tiền tố khởi chạy tùy chỉnh", + "settings-custom-launch-prefix-label": "Tiền Tố Khởi Chạy Tùy Chỉnh", + "settings-density-description": "Điều chỉnh kích thước biểu tượng ứng dụng và mật độ của Trình khởi chạy.", + "settings-density-label": "Mật độ", + "settings-desc": "Tùy chỉnh hành vi và giao diện của Trình khởi chạy", + "settings-enable-session-search-description": "Hiển thị các hành động cho phiên hoạt động(khóa, tắt máy, khởi động lại, v.v.) khi tìm kiếm trong trình khởi chạy.", + "settings-enable-session-search-label": "Bật tìm kiếm phiên", + "settings-enable-settings-search-description": "Hiển thị kết quả cài đặt khi tìm kiếm trong Trình khởi chạy.", + "settings-enable-settings-search-label": "Bật tìm kiếm cài đặt", + "settings-enable-windows-search-description": "Tìm kiếm và tập trung vào các cửa sổ đang hoạt động.", + "settings-enable-windows-search-label": "Bật tìm kiếm cửa sổ", + "settings-grid-view-description": "Hiển thị các mục theo bố cục dạng lưới thay vì danh sách.", + "settings-icon-mode-description": "Sử dụng biểu tượng hệ thống gốc thay vì Tabler icons.", + "settings-icon-mode-label": "Sử dụng biểu tượng gốc", + "settings-ignore-mouse-input-description": "Vô hiệu hóa tương tác chuột và con lăn chuột trong Trình khởi chạy.", + "settings-ignore-mouse-input-label": "Bỏ qua đầu vào của chuột", + "settings-overlay-layer-description": "Hiển thị trình khởi chạy trên lớp phủ, phía trên các cửa sổ toàn màn hình. Khi được bật, trình khởi chạy sẽ không gắn vào thanh Bar.", + "settings-overlay-layer-label": "Hiển thị trên toàn màn hình", + "settings-position-description": "Chọn vị trí hiển thị của Bảng điều khiển Trình khởi chạy.", + "settings-show-categories-description": "Hiển thị các tab danh mục để lọc ứng dụng.", + "settings-show-categories-label": "Hiển thị danh mục", + "settings-show-icon-background-description": "Hiển thị nền hình chữ nhật bo tròn phía sau các biểu tượng.", + "settings-show-icon-background-label": "Hiển thị nền sau biểu tượng", + "settings-sort-by-usage-description": "Khi được bật, các ứng dụng thường xuyên mở sẽ xuất hiện đầu tiên trong danh sách.", + "settings-sort-by-usage-label": "Sắp xếp theo mức độ sử dụng nhiều nhất", + "settings-terminal-command-description": "Lệnh để khởi chạy một terminal. Ví dụ: 'kitty -e' hoặc 'gnome-terminal --'.", + "settings-terminal-command-label": "Lệnh Terminal", + "settings-use-app2unit-description": "Sử dụng một phương pháp khởi chạy thay thế để quản lý tốt hơn các tiến trình ứng dụng và ngăn ngừa sự cố.", + "settings-use-app2unit-label": "Dùng App2Unit để khởi chạy ứng dụng", + "settings-view-mode-description": "Chọn bố cục cho các mục của Trình khởi chạy.", + "settings-view-mode-label": "Chế độ xem", + "title": "Trình khởi chạy" + }, + "location": { + "auto-locate-description": "Tự động phát hiện vị trí của bạn định kỳ bằng địa chỉ IP của bạn.", + "auto-locate-label": "Tự Động Định Vị", + "calendar-cards-desc": "Sắp xếp và bật/tắt các thẻ trong bảng điều khiển lịch.", + "calendar-cards-title": "Thẻ lịch", + "calendar-header-label": "Tiêu đề Lịch", + "calendar-month-label": "Tháng Lịch", + "date-time-12hour-format-description": "Hiển thị thời gian theo định dạng 12 giờ trên màn hình khóa và lịch. Đồng hồ trên thanh có cài đặt riêng.", + "date-time-12hour-format-label": "Sử dụng định dạng thời gian 12 giờ", + "date-time-desc": "Tùy chỉnh cách hiển thị ngày và giờ.", + "date-time-first-day-of-week-automatic": "Tự động (sử dụng ngôn ngữ hệ thống)", + "date-time-first-day-of-week-description": "Chọn ngày bắt đầu tuần trong lịch.", + "date-time-first-day-of-week-label": "Ngày đầu tiên của tuần", + "date-time-show-events-description": "Hiển thị sự kiện trong bảng điều khiển lịch.", + "date-time-show-events-label": "Hiện sự kiện lịch", + "date-time-title": "Ngày & giờ", + "date-time-use-analog-description": "Hiển thị đồng hồ kim trên cửa sổ lịch.", + "date-time-use-analog-label": "Sử dụng đồng hồ kiểu kim", + "date-time-week-numbers-description": "Hiển thị tuần trong năm (ví dụ: tuần 38) trong lịch.", + "date-time-week-numbers-label": "Hiển thị số tuần", + "geolocate-now-button": "Định vị ngay", + "location-desc": "Nhận thông tin thời tiết chính xác và lên lịch Ánh sáng đêm bằng cách đặt vị trí của bạn.", + "location-search-description": "ví dụ: Toronto, ON", + "location-search-label": "Tìm kiếm một địa điểm", + "location-search-placeholder": "Nhập tên địa điểm", + "location-title": "Vị trí của bạn", + "weather-desc": "Chọn đơn vị nhiệt độ ưa thích của bạn.", + "weather-enabled-description": "Hiển thị thông tin thời tiết trên toàn bộ giao diện và tìm nạp dữ liệu thời tiết. Khi bị tắt, tất cả các yếu tố thời tiết sẽ bị ẩn và không có yêu cầu mạng nào được thực hiện.", + "weather-enabled-label": "Bật thời tiết", + "weather-fahrenheit-description": "Hiển thị nhiệt độ bằng độ Fahrenheit thay vì độ Celsius.", + "weather-fahrenheit-label": "Hiển thị nhiệt độ theo độ Fahrenheit (°F)", + "weather-hide-city-description": "Ẩn tên thành phố khỏi các hiển thị thời tiết khắp giao diện.", + "weather-hide-city-label": "Ẩn tên thành phố", + "weather-hide-timezone-description": "Ẩn chữ viết tắt múi giờ khỏi hiển thị thời tiết trên toàn bộ giao diện.", + "weather-hide-timezone-label": "Ẩn múi giờ", + "weather-show-effects-description": "Hiển thị các hiệu ứng hình ảnh bổ sung (như mưa, tuyết, hoặc sấm sét) trên thẻ thời tiết.", + "weather-show-effects-label": "Hiển thị hiệu ứng thời tiết", + "weather-show-in-calendar-description": "Hiển thị dự báo thời tiết hàng ngày trực tiếp trong chế độ xem lịch của bạn.", + "weather-show-in-calendar-label": "Hiển thị thời tiết trong lịch", + "weather-talia-mascot-always-description": "Luôn sử dụng Talia emojis thay vì tabler icons.", + "weather-talia-mascot-always-label": "Luôn hiển thị biểu tượng cảm xúc thời tiết Talia" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "Khi fprintd (xác thực vân tay) đang hoạt động, tùy chọn này cho phép bạn vẫn đăng nhập bằng mật khẩu được thay vì vân tay", + "allow-password-with-fprintd-label": "Cho phép đăng nhập bằng mật khẩu với fprintd", + "auto-start-auth-description": "Tự động bắt đầu xác thực vân tay mà không yêu cầu nhấn phím hoặc nhấp nút.", + "auto-start-auth-label": "Tự động bắt đầu xác thực vân tay", + "clock-format-description": "Tùy chỉnh định dạng đồng hồ bằng các token cú pháp ngày/giờ.", + "clock-format-label": "Định dạng đồng hồ", + "clock-style-analog": "Analog", + "clock-style-custom": "Tùy chỉnh", + "clock-style-description": "Chọn kiểu hiển thị của đồng hồ trên Màn hình khóa.", + "clock-style-digital": "Kỹ thuật số", + "clock-style-label": "Kiểu Đồng Hồ", + "compact-lockscreen-description": "Chỉ hiển thị trường nhập đăng nhập và các điều khiển hệ thống, ẩn các tiện ích thời tiết và đa phương tiện.", + "compact-lockscreen-label": "Màn hình khóa thu gọn", + "enable-lockscreen-media-controls-description": "Hiển thị điều khiển phát đa phương tiện tương tác trên màn hình khóa.", + "enable-lockscreen-media-controls-label": "Điều khiển phương tiện trên màn hình khóa", + "lock-on-suspend-description": "Tự động khóa màn hình khi hệ thống tạm dừng.", + "lock-on-suspend-label": "Khóa khi treo máy", + "lock-screen-animations-description": "Bật hoặc tắt hoạt ảnh màn hình khóa.", + "lock-screen-animations-label": "Hoạt ảnh Màn hình khóa", + "lock-screen-blur-strength-description": "Áp dụng hiệu ứng làm mờ cho hình nền màn hình khóa.", + "lock-screen-blur-strength-label": "Độ mờ màn hình khóa", + "lock-screen-tint-strength-description": "Áp dụng một lớp phủ màu lên hình nền Màn hình khóa.", + "lock-screen-tint-strength-label": "Cường độ lớp phủ màu màn hình khóa", + "monitors-desc": "Hiển thị Màn hình khóa trên các màn hình cụ thể. Mặc định là tất cả nếu không có màn hình nào được chọn.", + "password-chars-description": "Icon dễ thương dùng để ẩn mật khẩu của bạn.", + "password-chars-label": "Biểu tượng mật khẩu ngẫu nhiên", + "show-hibernate-description": "Hiển thị tùy chọn 'ngủ đông' trong các điều khiển nguồn.", + "show-hibernate-label": "Hiển thị ngủ đông", + "show-session-buttons-description": "Cho phép truy cập cài đặt nguồn từ màn hình khóa.", + "show-session-buttons-label": "Điều khiển nguồn", + "title": "Màn hình khóa" + }, + "network": { + "bluetooth-description": "Kích hoạt quản lý Bluetooth.", + "bluetooth-rssi-polling-description": "Định kỳ lấy mẫu RSSI cho các thiết bị đã kết nối qua bluetoothctl. Có thể không khả dụng cho tất cả thiết bị; sử dụng tài nguyên tối thiểu khi được bật.", + "bluetooth-rssi-polling-label": "Thăm dò tín hiệu Bluetooth", + "desc": "Quản lý kết nối Wi-Fi và Bluetooth.", + "wifi-description": "Quản lý mạng không dây (yêu cầu NetworkManager)." + }, + "notifications": { + "duration-critical-urgency-description": "Thời gian hiển thị của thông báo ưu tiên khẩn cấp.", + "duration-critical-urgency-label": "Mức độ khẩn cấp nghiêm trọng", + "duration-desc": "Cấu hình thời gian hiển thị thông báo dựa trên mức độ khẩn cấp của chúng.", + "duration-low-urgency-description": "Thời gian hiển thị thông báo ưu tiên thấp.", + "duration-low-urgency-label": "Ưu tiên thấp", + "duration-normal-urgency-description": "Thời gian hiển thị của thông báo ưu tiên bình thường.", + "duration-normal-urgency-label": "Ưu tiên bình thường", + "duration-respect-expire-description": "Sử dụng thời gian chờ hết hạn được đặt trong thông báo.", + "duration-respect-expire-label": "Tuân thủ thời gian chờ hết hạn", + "duration-title": "Thời lượng thông báo", + "history-clear-dismiss-description": "Xóa thông báo khỏi lịch sử khi đóng.", + "history-clear-dismiss-label": "Xóa khi đóng", + "history-critical-urgency-description": "Lưu thông báo ưu tiên quan trọng vào lịch sử.", + "history-critical-urgency-label": "Lưu khẩn cấp nghiêm trọng vào lịch sử", + "history-desc": "Kiểm soát những thông báo nào được lưu vào lịch sử dựa trên mức độ khẩn cấp của chúng.", + "history-low-urgency-description": "Lưu thông báo ưu tiên thấp vào lịch sử.", + "history-low-urgency-label": "Lưu thông báo độ ưu tiên thấp vào lịch sử", + "history-normal-urgency-description": "Lưu thông báo ưu tiên bình thường vào lịch sử.", + "history-normal-urgency-label": "Lưu độ khẩn cấp thông thường vào lịch sử", + "monitors-desc": "Hiển thị thông báo trên các màn hình cụ thể. Mặc định là tất cả nếu không có màn hình nào được chọn.", + "rules-action-block": "Khối", + "rules-action-block-desc": "Bỏ qua hoàn toàn.", + "rules-action-hide": "Ẩn", + "rules-action-hide-desc": "Không bật lên, không âm thanh, thêm vào lịch sử.", + "rules-action-label": "Hành động", + "rules-action-mute": "Tắt tiếng", + "rules-action-mute-desc": "Không có âm thanh, vẫn hiển thị cửa sổ bật lên và trong lịch sử.", + "rules-add": "Thêm quy tắc", + "rules-delete": "Xóa quy tắc", + "rules-description": "Khớp tên ứng dụng hoặc nội dung. Các quy tắc được kiểm tra theo thứ tự, và kết quả khớp đầu tiên sẽ được áp dụng.", + "rules-edit": "Sửa quy tắc", + "rules-label": "Quy tắc lọc", + "rules-pattern-label": "Mẫu", + "rules-pattern-placeholder": "firefox, discord, hoặc /regex/", + "rules-tab": "Quy tắc", + "settings-always-on-top-description": "Hiển thị thông báo trên các cửa sổ toàn màn hình và các lớp khác.", + "settings-background-opacity-description": "Điều chỉnh độ trong suốt của nền thông báo.", + "settings-density-description": "Chọn mật độ thẻ thông báo.", + "settings-density-label": "Mật độ", + "settings-desc": "Cấu hình giao diện và hành vi của thông báo.", + "settings-do-not-disturb-description": "Tắt tất cả các cửa sổ thông báo bật lên khi được bật.", + "settings-enabled-description": "Bật hoặc tắt tiến trình nền thông báo, yêu cầu khởi động lại Noctalia shell.", + "settings-enabled-label": "Bật thông báo", + "settings-location-description": "Nơi thông báo xuất hiện trên màn hình.", + "settings-markdown-description": "Hiển thị nội dung thông báo bằng định dạng Markdown.", + "settings-markdown-label": "Bật Markdown", + "sounds-desc": "Cấu hình hiệu ứng âm thanh thông báo và âm lượng.", + "sounds-enabled-description": "Bật hiệu ứng âm thanh cho thông báo đến.", + "sounds-enabled-label": "Bật âm thanh thông báo", + "sounds-excluded-apps-description": "Bỏ qua việc phát âm thanh thông báo đã cấu hình cho các ứng dụng cụ thể có âm thanh tích hợp riêng của chúng.", + "sounds-excluded-apps-label": "Ứng dụng bị loại trừ", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "Đường dẫn đến tệp âm thanh phát cho thông báo ưu tiên khẩn cấp.", + "sounds-files-critical-label": "Âm thanh khẩn cấp", + "sounds-files-critical-select-title": "Chọn tệp âm thanh khẩn cấp", + "sounds-files-desc": "Cấu hình các tệp âm thanh tùy chỉnh cho các mức độ khẩn cấp thông báo khác nhau.", + "sounds-files-low-description": "Đường dẫn đến tệp âm thanh được phát cho các thông báo ưu tiên thấp.", + "sounds-files-low-label": "Âm thanh ưu tiên thấp", + "sounds-files-low-select-title": "Chọn tệp âm thanh ưu tiên thấp", + "sounds-files-normal-description": "Đường dẫn đến tệp âm thanh được phát cho các thông báo ưu tiên bình thường.", + "sounds-files-normal-label": "Âm thanh khẩn cấp thông thường", + "sounds-files-normal-select-title": "Chọn tệp âm thanh cho độ khẩn cấp thông thường", + "sounds-files-placeholder": "Nhập đường dẫn đến tệp âm thanh", + "sounds-files-select-file": "Chọn tệp âm thanh", + "sounds-files-title": "Tệp âm thanh", + "sounds-files-unified-description": "Đường dẫn đến tệp âm thanh được phát cho thông báo.", + "sounds-files-unified-label": "Âm thanh thông báo", + "sounds-files-unified-select-title": "Chọn tệp âm thanh thông báo", + "sounds-separate-description": "Sử dụng các tệp âm thanh khác nhau cho thông báo ưu tiên thấp, bình thường và quan trọng.", + "sounds-separate-label": "Dùng âm thanh khác nhau cho từng ưu tiên.", + "sounds-title": "Cài đặt âm thanh", + "sounds-unavailable-description": "Cài đặt Qt6 Multimedia để bật âm thanh thông báo.", + "sounds-unavailable-label": "Âm thanh thông báo không khả dụng", + "sounds-volume-description": "Điều chỉnh mức âm lượng cho âm thanh thông báo.", + "sounds-volume-label": "Âm lượng", + "toast-battery-description": "Hiển thị cảnh báo khi mức pin xuống dưới phần trăm này.", + "toast-battery-label": "Cảnh báo pin", + "toast-desc": "Cấu hình giao diện và hành vi của thông báo nhanh.", + "toast-keyboard-description": "Hiển thị thông báo nhanh khi bố cục bàn phím thay đổi.", + "toast-keyboard-label": "Bố cục bàn phím", + "toast-media-description": "Hiện thông báo nhanh khi trạng thái phát đa phương tiện thay đổi.", + "toast-media-label": "Đa phương tiện" + }, + "osd": { + "always-on-top-description": "Hiển thị OSD trên cửa sổ toàn màn hình và các lớp khác.", + "always-on-top-label": "Luôn hiển thị trên cùng", + "background-opacity-description": "Điều khiển độ trong suốt của nền OSD.", + "background-opacity-label": "Độ mờ nền", + "description": "Cấu hình các chỉ báo trên màn hình như lớp phủ âm lượng và độ sáng.", + "duration-auto-hide-description": "Điều chỉnh thời gian trước khi OSD biến mất.", + "duration-auto-hide-label": "Tự động ẩn sau", + "duration-desc": "Thời gian OSD hiển thị trước khi tự động ẩn đi.", + "duration-title": "Thời gian tự động ẩn", + "enabled-description": "Hiển thị thay đổi âm lượng và độ sáng theo thời gian thực.", + "enabled-label": "Bật hiển thị trên màn hình", + "events-desc": "Chọn những sự kiện nào kích hoạt OSD.", + "general-desc": "Cấu hình hiển thị và hành vi của OSD.", + "location-description": "Nơi các OSD xuất hiện.", + "monitors-desc": "Hiển thị OSD trên các màn hình cụ thể. Mặc định là tất cả nếu không có màn hình nào được chọn.", + "title": "Hiển thị trên màn hình", + "types-brightness-description": "Hiển thị OSD khi độ sáng màn hình thay đổi.", + "types-brightness-label": "Độ sáng", + "types-custom-text-description": "Hiển thị OSD cho các tin nhắn văn bản tùy chỉnh từ IPC.", + "types-custom-text-label": "Văn bản Tùy chỉnh", + "types-desc": "Chọn các sự kiện kích hoạt OSD.", + "types-input-volume-description": "Hiển thị OSD khi âm lượng micrô thay đổi.", + "types-input-volume-label": "Âm lượng đầu vào", + "types-lockkey-description": "Hiển thị OSD khi Caps Lock, Num Lock hoặc Scroll Lock được bật/tắt.", + "types-lockkey-label": "Các phím khóa", + "types-media-description": "Hiển thị OSD khi trạng thái phát đa phương tiện thay đổi (phát, tạm dừng, bỏ qua).", + "types-media-label": "Phát lại đa phương tiện", + "types-title": "Sự kiện kích hoạt OSD", + "types-volume-description": "Hiển thị OSD khi âm lượng đầu ra thay đổi.", + "types-volume-label": "Âm lượng đầu ra" + }, + "plugins": { + "auto-update": "Tự động cập nhật plugin", + "auto-update-description": "Tự động cập nhật tất cả các plugin khi shell khởi động.", + "available-description": "Duyệt và cài đặt các plugin từ các nguồn đã cấu hình.", + "available-label": "Các plugin có sẵn", + "available-no-plugins-description": "Kiểm tra nguồn plugin của bạn hoặc làm mới danh sách.", + "available-no-plugins-label": "Không có plugin nào khả dụng", + "check-for-updates": "Kiểm tra cập nhật", + "checking-for-updates": "Đang kiểm tra cập nhật...", + "collision-already-installed": "Plugin này đã được cài đặt", + "collision-custom-version-exists": "Một phiên bản tùy chỉnh từ \"{source}\" đã được cài đặt", + "collision-official-version-exists": "Phiên bản chính thức của plugin này đã được cài đặt", + "development-disable": "Tắt chế độ phát triển cho plugin này", + "development-enable": "Bật chế độ phát triển cho plugin này", + "filter-downloaded": "Đã tải xuống", + "filter-not-downloaded": "Chưa tải xuống", + "filter-tags-description": "Lọc plugin theo danh mục hoặc trạng thái tải xuống.", + "filter-tags-label": "Thẻ", + "hot-reload-description": "Tự động tải lại các plugin khi tệp của chúng thay đổi. Hữu ích cho việc phát triển plugin.", + "hot-reload-label": "Hot reload (chế độ dev)", + "hot-reloaded": "Đã tải lại tiện ích: {name}", + "install-error": "Không thể cài đặt: {error}", + "install-incompatible": "{plugin} yêu cầu Noctalia v{version} trở lên", + "install-success": "Đã cài đặt thành công {plugin}", + "installed-description": "Quản lý và cấu hình tất cả các bổ trợ đã cài đặt cục bộ.", + "installed-label": "Tiện ích đã cài đặt", + "installed-no-plugins-description": "Cài đặt plugin từ phần \"Khả dụng\".", + "installed-no-plugins-label": "Chưa cài đặt plugin nào", + "installing": "Đang cài đặt {plugin}...", + "notify-updates": "Thông báo cập nhật tiện ích", + "notify-updates-description": "Hiển thị một thông báo bật lên khi có bản cập nhật plugin mới.", + "open-plugin-page": "Mở trang tiện ích mở rộng", + "open-plugins-tab": "Mở cài đặt plugin", + "plugin-settings-title": "Cài đặt {plugin}", + "refresh-refreshing": "Đang làm mới danh sách plugin...", + "refresh-tooltip": "Làm mới các plugin có sẵn", + "settings-error-not-loaded": "Plugin chưa tải được", + "settings-saved": "Đã lưu cài đặt plugin", + "settings-tooltip": "Cài đặt Plugin", + "source-custom": "Nguồn tùy chỉnh", + "sources-add-custom": "Thêm kho lưu trữ tùy chỉnh", + "sources-add-dialog-description": "Thêm một kho lưu trữ GitHub làm nguồn plugin.", + "sources-add-dialog-error": "Không thể thêm nguồn plugin", + "sources-add-dialog-name": "Tên kho lưu trữ", + "sources-add-dialog-name-placeholder": "Các Plugin Tùy Chỉnh Của Tôi", + "sources-add-dialog-success": "Đã thêm nguồn plugin thành công", + "sources-add-dialog-title": "Thêm Nguồn Plugin", + "sources-add-dialog-url": "URL Kho lưu trữ", + "sources-description": "Quản lý kho plugin.", + "sources-placeholder": "Kho lưu trữ tuyệt vời của tôi", + "sources-remove-tooltip": "Gỡ bỏ nguồn plugin", + "title": "Plugin", + "translations-reloaded": "Đã tải lại bản dịch: {name}", + "uninstall-dialog-description": "Bạn có chắc chắn muốn gỡ cài đặt {plugin} không? Thao tác này sẽ xóa tất cả dữ liệu của plugin.", + "uninstall-dialog-title": "Gỡ cài đặt tiện ích mở rộng", + "uninstall-error": "Gỡ cài đặt thất bại: {error}", + "uninstall-success": "Đã gỡ cài đặt thành công {plugin}", + "uninstalling": "Đang gỡ cài đặt {plugin}...", + "update-all": "Cập nhật tất cả ({count})", + "update-all-success": "Tất cả plugin đã cập nhật thành công", + "update-available": "Có bản cập nhật plugin mới", + "update-available-plural": "Có {count} bản cập nhật Plugin", + "update-error": "Cập nhật plugin thất bại: {plugin}: {error}", + "update-pending": "v{current} → v{new} (yêu cầu Noctalia v{required})", + "update-success": "Đã cập nhật {plugin} lên v{version}", + "update-version": "v{current} → v{new}", + "updating": "Đang cập nhật..." + }, + "region": { + "title": "Vùng" + }, + "session-menu": { + "countdown-duration-description": "Thiết lập thời gian đếm ngược kéo dài bao lâu trước khi thực hiện các hành động nguồn.", + "countdown-duration-label": "Thời lượng đếm ngược", + "enable-countdown-description": "Hiển thị đếm ngược trước khi thực hiện các hành động nguồn.", + "enable-countdown-label": "Bật bộ đếm ngược", + "entries-desc": "Tùy chỉnh những hành động nguồn nào xuất hiện trong Session Menu và theo thứ tự nào.", + "entries-title": "Hành động liên quan tới nguồn", + "entry-settings-command-description": "Lệnh tùy chỉnh để thực thi cho hành động này. Để trống để sử dụng lệnh hệ thống mặc định.", + "entry-settings-command-placeholder": "ví dụ systemctl poweroff", + "entry-settings-default-command-lock": "Màn hình khóa nội bộ (không có lệnh)", + "entry-settings-default-command-logout": "Đăng xuất nội bộ (không lệnh)", + "entry-settings-default-info-description": "Nếu không có lệnh tùy chỉnh nào được chỉ định, lệnh hệ thống mặc định sẽ được sử dụng.", + "entry-settings-default-info-label": "Lệnh Mặc Định", + "entry-settings-keybind-description": "Gán một tổ hợp phím để kích hoạt hành động này khi menu phiên đang mở.", + "entry-settings-keybind-placeholder": "Nhấn Ghi để gán phím...", + "entry-settings-keybind-recording": "Đang ghi... Nhấn bất kỳ tổ hợp phím nào", + "entry-settings-title": "Cấu hình {entry}", + "entry-settings-tooltip": "Cấu hình lệnh", + "general-desc": "Cấu hình hành vi và giao diện của bảng menu phiên.", + "large-buttons-layout-description": "Chọn cách hiển thị các nút trong Menu phiên.", + "large-buttons-layout-label": "Bố cục nút lớn", + "large-buttons-style-description": "Hiển thị Menu Phiên với các nút lớn trong bố cục dạng lưới.", + "large-buttons-style-label": "Kiểu nút lớn", + "position-description": "Chọn vị trí bảng điều khiển thực đơn nguồn xuất hiện khi được mở.", + "show-header-description": "Hiển thị tiêu đề và nút đóng ở đầu menu phiên.", + "show-header-label": "Hiển thị tiêu đề", + "show-keybinds-description": "Hiển thị gợi ý phím tắt trên các tùy chọn phiên làm việc.", + "show-keybinds-label": "Hiện phím tắt" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "Vẫn hiển thị các tiện ích màn hình nền khi chế độ hiệu suất của Noctalia được bật.", + "noctalia-performance-disable-desktop-widgets-label": "Bật widget màn hình nền ở chế độ hiệu suất", + "noctalia-performance-disable-wallpaper-description": "Giữ hình nền màn hình nền, tổng quan và màn hình khóa hiển thị khi chế độ hiệu suất của Noctalia được bật.", + "noctalia-performance-disable-wallpaper-label": "Bật hiển thị hình nền ở chế độ hiệu suất", + "title": "Hệ thống" + }, + "system-monitor": { + "critical-color-label": "Màu nguy cấp", + "custom-highlight-colors-title-label": "Màu Nhấn Tùy Chỉnh", + "disk-available-label": "Dung lượng đĩa khả dụng", + "disk-section-label": "Sử dụng đĩa", + "enable-dgpu-monitoring-description": "Cảnh báo: Điều này sẽ kích hoạt GPU rời (NVIDIA/AMD) của bạn, có thể ảnh hưởng đáng kể đến thời lượng pin trên laptop có đồ họa lai.", + "enable-dgpu-monitoring-label": "Bật theo dõi GPU rời", + "external-monitor-description": "Nhập lệnh hoặc đường dẫn ứng dụng để khởi chạy khi kích hoạt ứng dụng giám sát hệ thống bên ngoài.", + "external-monitor-label": "Lệnh Giám sát Hệ thống Bên ngoài", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "Cấu hình hành vi và giao diện của giám sát hệ thống.", + "gpu-section-label": "Nhiệt độ GPU", + "highlight-colors-section-label": "Màu sắc điểm nhấn", + "polling-interval-label": "Khoảng thời gian thăm dò", + "polling-section-description": "Cấu hình tần suất cập nhật của mỗi chỉ số hệ thống.", + "polling-section-label": "Khoảng thời gian thăm dò", + "threshold-critical": "Ngưỡng nguy hiểm", + "threshold-warning": "Ngưỡng cảnh báo", + "thresholds-section-description": "Điều chỉnh ngưỡng cảnh báo/nguy hiểm cho từng chỉ số hệ thống.", + "use-custom-highlight-colors-description": "Khi tắt, màu nổi bật mặc định của chủ đề sẽ được sử dụng.", + "use-custom-highlight-colors-label": "Sử dụng màu nổi bật tùy chỉnh", + "warning-color-label": "Màu cảnh báo" + }, + "user-interface": { + "allow-panels-without-bar-description": "Khi được bật, các bảng điều khiển có thể mở trên bất kỳ màn hình nào. Khi bị tắt, các bảng điều khiển sẽ chỉ mở trên các màn hình có thanh, điều này có thể giảm mức sử dụng bộ nhớ.", + "allow-panels-without-bar-label": "Cho phép bảng điều khiển trên màn hình không có thanh", + "animation-disable-description": "Tắt tất cả hoạt ảnh để có trải nghiệm nhanh hơn, phản hồi tốt hơn.", + "animation-disable-label": "Tắt hiệu ứng động giao diện người dùng", + "animation-speed-description": "Điều chỉnh tốc độ hoạt ảnh toàn cục.", + "animation-speed-label": "Tốc độ hoạt ảnh", + "animation-speed-reset": "Đặt lại tốc độ hoạt ảnh", + "appearance-desc": "Tùy chỉnh các yếu tố hình ảnh như chú giải công cụ, viền và bóng đổ.", + "blur-behind-description": "Làm mờ khu vực phía sau các Thanh và Bảng điều khiển bằng giao thức làm mờ của trình tổng hợp.", + "blur-behind-label": "Làm mờ nền", + "box-border-description": "Hiển thị đường viền xung quanh các khu vực nội dung.", + "box-border-label": "Viền Vùng Chứa", + "box-border-radius-description": "Điều chỉnh độ bo tròn góc của các phần bố cục chính, như thanh bên, thẻ và bảng nội dung.", + "box-border-radius-label": "Bo Góc Vùng Chứa", + "control-border-radius-description": "Điều khiển độ cong của các yếu tố tương tác, bao gồm các nút, công tắc và trường văn bản.", + "control-border-radius-label": "Nhập bán kính", + "desc": "Tùy chỉnh vẻ ngoài, cảm nhận và hành vi của giao diện.", + "dim-desktop-description": "Làm mờ màn hình nền khi các Bảng điều khiển hoặc menu đang mở.", + "dim-desktop-label": "Làm mờ màn hình nền", + "dimmer-opacity-description": "Đặt mức độ mờ cho tính năng làm mờ màn hình nền.", + "dimmer-opacity-label": "Độ mờ đục màn hình nền đã làm mờ", + "panel-background-opacity-description": "Đặt độ mờ nền cho tất cả các bảng điều khiển (thanh, trình khởi chạy, cài đặt, v.v...).", + "panel-background-opacity-label": "Độ mờ nền của Bảng điều khiển", + "panels-attached-to-bar-description": "Các Bảng điều khiển khóa vào Thanh và các cạnh màn hình, tạo ra một giao diện liền mạch với các góc bo ngược đầy phong cách.", + "panels-attached-to-bar-label": "Hít bảng vào các cạnh", + "panels-overlay-description": "Đảm bảo các Panel và Bar luôn hiển thị, ngay cả trên các ứng dụng toàn màn hình.", + "panels-overlay-label": "Giữ bảng điều khiển & thanh ở trên cùng", + "scaling-description": "Thay đổi kích thước của giao diện người dùng chung, trừ thanh Bar.", + "scaling-label": "Tỷ lệ giao diện", + "scrollbar-always-visible-description": "Giữ thanh cuộn hiển thị bất cứ khi nào nội dung có thể cuộn được, thay vì chỉ hiển thị chúng khi di chuột qua.", + "scrollbar-always-visible-label": "Luôn hiển thị thanh cuộn", + "settings-panel-header": "Bảng Cài Đặt", + "settings-panel-mode-description": "Chọn bố cục cài đặt (có thể yêu cầu mở lại).", + "settings-panel-mode-label": "Chế độ bảng cài đặt", + "settings-panel-sidebar-card-style-description": "Bọc thanh bên cài đặt trong một nền được tô đầy với các góc bo tròn.", + "settings-panel-sidebar-card-style-label": "Kiểu Thẻ Thanh Bên", + "shadows-description": "Bật đổ bóng dưới thanh và bảng.", + "shadows-direction-description": "Chọn vị trí đổ bóng.", + "shadows-direction-label": "Hướng đổ bóng", + "shadows-label": "Bóng đổ", + "title": "Giao Diện Người Dùng", + "tooltips-description": "Bật hoặc tắt gợi ý trên toàn bộ giao diện.", + "tooltips-label": "Hiển thị chú giải công cụ", + "translucent-widgets-description": "Làm cho các nút, tab và các tiện ích khác bên trong các Bảng điều khiển trong suốt một phần.", + "translucent-widgets-label": "Tiện ích trong mờ" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "Theo thứ tự bảng chữ cái", + "automation-change-mode-description": "Chọn cách hình nền được chọn khi tự động thay đổi.", + "automation-change-mode-label": "Thay Đổi Chế Độ", + "automation-custom-interval-description": "Nhập thời gian theo định dạng HH:MM (ví dụ: 01:30).", + "automation-custom-interval-label": "Khoảng thời gian tùy chỉnh", + "automation-interval-description": "Tần suất thay đổi hình nền tự động.", + "automation-interval-label": "Khoảng thời gian hình nền", + "automation-random-wallpaper-description": "Lên lịch thay đổi hình nền ngẫu nhiên theo các khoảng thời gian đều đặn.", + "automation-scheduled-change-description": "Tự động thay đổi hình nền định kỳ.", + "automation-scheduled-change-label": "Thay đổi theo lịch trình", + "look-feel-edge-smoothness-description": "Áp dụng hiệu ứng làm mờ viền mềm mại cho các chuyển đổi.", + "look-feel-edge-smoothness-label": "Làm mềm cạnh chuyển tiếp", + "look-feel-fill-color-description": "Chọn một màu nền có thể xuất hiện phía sau hình nền.", + "look-feel-fill-mode-description": "Chọn cách hình ảnh sẽ điều chỉnh kích thước để phù hợp với độ phân giải màn hình của bạn.", + "look-feel-fill-mode-label": "Chế độ lấp đầy", + "look-feel-skip-startup-transition-description": "Bỏ qua hoạt ảnh hình nền khi shell khởi động.", + "look-feel-skip-startup-transition-label": "Bỏ qua chuyển tiếp khởi động", + "look-feel-title": "Giao diện & Trải nghiệm", + "look-feel-transition-duration-description": "Thời lượng của các hoạt ảnh chuyển tiếp, tính bằng giây.", + "look-feel-transition-duration-label": "Thời lượng chuyển tiếp", + "look-feel-transition-type-description": "Kiểu hoạt ảnh khi chuyển đổi giữa các hình nền.", + "look-feel-transition-type-label": "Kiểu chuyển tiếp", + "settings-clear-cache-button": "Xóa bộ nhớ đệm", + "settings-clear-cache-description": "Xóa hình nền đã thay đổi kích thước được lưu vào bộ nhớ đệm để giải phóng dung lượng đĩa.", + "settings-clear-cache-label": "Bộ nhớ đệm hình nền", + "settings-clear-cache-toast": "Đã xóa bộ nhớ đệm hình nền", + "settings-desc": "Kiểm soát cách hình nền được quản lý và hiển thị.", + "settings-enable-management-description": "Quản lý hình nền với Noctalia. Bỏ chọn nếu bạn thích dùng ứng dụng khác.", + "settings-enable-management-label": "Bật quản lý hình nền", + "settings-enable-overview-description": "Áp dụng hình nền mờ và tối vào màn hình tổng quan.", + "settings-enable-overview-label": "Bật hình nền tổng quan", + "settings-folder-description": "Đường dẫn đến thư mục hình nền chính của bạn.", + "settings-folder-label": "Thư mục hình nền", + "settings-hide-wallpaper-filenames-tooltip-hide": "Ẩn tên tệp", + "settings-hide-wallpaper-filenames-tooltip-show": "Hiển thị tên tệp", + "settings-monitor-specific-description": "Đặt một thư mục hình nền khác cho mỗi màn hình.", + "settings-monitor-specific-label": "Thư mục dành riêng cho màn hình", + "settings-monitor-specific-tooltip": "Theo dõi thư mục hình nền", + "settings-overview-blur-strength-description": "Áp dụng cường độ làm mờ cho tổng quan.", + "settings-overview-blur-strength-label": "Cường độ làm mờ Tổng quan", + "settings-overview-tint-description": "Áp dụng cường độ sắc thái cho chế độ tổng quan.", + "settings-overview-tint-label": "Cường độ màu phủ tổng quan", + "settings-recursive-search-description": "Cũng tìm kiếm hình nền trong các thư mục con của thư mục hình nền.", + "settings-recursive-search-label": "Tìm kiếm thư mục con", + "settings-select-monitor-folder": "Chọn thư mục hình nền màn hình", + "settings-selector-description": "Chọn hình nền của bạn.", + "settings-selector-position-description": "Chọn nơi bảng chọn hình nền xuất hiện.", + "settings-show-hidden-files-tooltip-hide": "Ẩn các tệp ẩn", + "settings-show-hidden-files-tooltip-show": "Hiện tệp ẩn", + "settings-title": "Cài đặt Hình nền", + "settings-use-original-images-description": "Bỏ qua việc thay đổi kích thước hình nền trước khi hiển thị. Tiết kiệm dung lượng đĩa và giảm mức sử dụng CPU trong quá trình thay đổi hình nền, nhưng có thể sử dụng nhiều bộ nhớ hơn cho các hình ảnh rất lớn.", + "settings-use-original-images-label": "Sử dụng hình ảnh gốc", + "settings-view-mode-description": "Chọn cách hình nền được hiển thị từ thư mục của bạn.", + "settings-view-mode-label": "Chế độ xem", + "view-mode-browse": "Duyệt thư mục", + "view-mode-cycle-tooltip": "Chế độ xem: {mode} (nhấn để thay đổi)", + "view-mode-recursive": "Thư mục con được làm phẳng", + "view-mode-single": "Thư mục gốc" + } + }, + "placeholders": { + "add-new-keybind": "Thêm phím tắt mới", + "command-example": "echo \"Hello World\"", + "enter-command": "Nhập lệnh để thực thi (ứng dụng hoặc tập lệnh tùy chỉnh)", + "enter-ipc-identifier": "Nhập mã định danh duy nhất cho các lệnh IPC", + "enter-path": "Nhập đường dẫn...", + "enter-text-to-collapse": "ví dụ: 'không có gì đang phát'. Sử dụng /regex/ cho các mẫu.", + "enter-tooltip": "Nhập Chú Giải Công Cụ", + "enter-width-pixels": "Nhập chiều rộng (pixel)", + "keybind-recording": "Đang ghi phím tắt...", + "search": "Tìm kiếm...", + "search-icons": "Ví dụ: noctalia, niri, battery, cloud", + "search-launcher": "Tìm kiếm mục nhập... hoặc dùng > cho lệnh", + "search-wallhaven": "Tìm kiếm Wallhaven...", + "search-wallpapers": "Gõ để lọc hình nền..." + }, + "positions": { + "bottom": "Dưới cùng", + "bottom-center": "Giữa dưới", + "bottom-left": "Dưới cùng bên trái", + "bottom-right": "Dưới cùng bên phải", + "center": "Giữa", + "center-left": "Giữa bên trái", + "center-right": "Giữa phải", + "close-to-bar": "Nút đóng về Bar", + "follow-bar": "Theo dõi thanh", + "left": "Trái", + "right": "Phải", + "top": "Trên cùng", + "top-center": "Giữa trên cùng", + "top-left": "Trên cùng bên trái", + "top-right": "Trên cùng bên phải" + }, + "session-menu": { + "action-in-seconds": "{action} trong {seconds} giây...", + "cancel-timer": "Hủy hẹn giờ", + "title": "Menu Hệ thống" + }, + "setup": { + "all-done": "Hoàn tất!", + "appearance": { + "subheader": "Chọn Chế độ tối và nguồn màu (hình nền hoặc cài đặt sẵn)." + }, + "customize": { + "header": "Tùy chỉnh trải nghiệm của bạn", + "subheader": "Điều chỉnh vị trí, mật độ, tỉ lệ của Thanh Bar và nhiều hơn nữa." + }, + "skip-setup": "Bỏ qua thiết lập", + "telemetry-wizard-done": "Đã hiểu!", + "telemetry-wizard-note": "Bạn có toàn quyền kiểm soát — bật hoặc tắt tính năng này bất cứ lúc nào trong cài đặt", + "telemetry-wizard-subtitle": "Chúng tôi đã thêm phân tích ẩn danh để giúp cải thiện Noctalia", + "telemetry-wizard-title": "Cập nhật Quyền riêng tư", + "wallpaper": { + "choose-dir": "Chọn một thư mục chứa hình nền của bạn", + "dir-browse": "Duyệt thư mục hình nền", + "dir-description": "Chọn thư mục chứa hình nền của bạn.", + "dir-label": "Thư mục hình nền", + "dir-select-title": "Chọn thư mục hình nền", + "header": "Chọn hình nền", + "no-dir": "Chưa chọn thư mục hình nền", + "no-valid": "Không tìm thấy tệp hình ảnh hợp lệ trong: {dir}", + "none-in-dir": "Không tìm thấy hình nền nào trong thư mục", + "preview-error": "Không thể tải hình ảnh", + "select-prompt": "Chọn một hình nền bên dưới", + "subheader": "Tạo không khí với một hình nền đẹp." + }, + "welcome": "Chào mừng", + "welcome-note": "Chỉ một vài điều cơ bản để bạn bắt đầu — các tùy chọn đầy đủ nằm trong cài đặt", + "welcome-subtitle": "Hãy cùng cá nhân hóa màn hình của bạn một cách độc đáo.", + "welcome-title": "Chào mừng đến với Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "Không phát hiện trình phát đa phương tiện", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "Ứng dụng không xác định", + "uptime": "Thời gian hoạt động: {uptime}", + "user-requested": "Người dùng yêu cầu", + "welcome-back": "Chào mừng trở lại,", + "widget-settings-title": "Cài đặt {widget}" + }, + "system-monitor": { + "core-usage": "Mức sử dụng lõi {id}", + "cpu-temp": "Nhiệt độ CPU", + "cpu-usage": "Mức sử dụng CPU", + "disk": "Ổ đĩa", + "download-speed": "Tốc độ tải xuống", + "gpu-temp": "Nhiệt độ GPU", + "load-average": "Tải trọng trung bình ", + "title": "Giám sát Hệ thống", + "upload-speed": "Tốc độ tải lên" + }, + "toast": { + "airplane-mode": { + "description": "Tắt tất cả các kết nối không dây.", + "title": "Chế độ Máy bay" + }, + "battery": { + "critical": "Pin yếu nghiêm trọng", + "critical-desc": "Pin còn {percent}% — vui lòng kết nối bộ sạc ngay lập tức", + "low": "Pin yếu", + "low-desc": "Pin còn {percent}% — vui lòng kết nối bộ sạc" + }, + "bluetooth": { + "address-copied": "Địa chỉ đã sao chép vào bảng tạm", + "auto-connect-disabled": "Tự động kết nối đã tắt", + "auto-connect-enabled": "Tự động kết nối đã bật", + "auto-connecting": "Đang kết nối với {count} thiết bị...", + "confirm-code": "Xác nhận mã {value} trên thiết bị khác.", + "connect-failed": "Không thể kết nối với thiết bị", + "disconnect-failed": "Không thể ngắt kết nối khỏi thiết bị", + "discoverable-change-failed": "Không thể thay đổi trạng thái hiển thị", + "discoverable-disabled": "Không thể phát hiện", + "discoverable-enabled": "Chế độ khám phá đã bật", + "display-code": "Nhập mã {value} trên thiết bị khác.", + "forget-failed": "Không thể quên thiết bị", + "pair-failed": "Ghép nối thiết bị thất bại", + "passkey-required": "Thiết bị yêu cầu mã khóa.", + "pincode-required": "Mã PIN được yêu cầu bởi thiết bị.", + "state-change-failed": "Không thể thay đổi trạng thái Bluetooth" + }, + "clipboard": { + "long-text": "Văn bản dài", + "unavailable": "Lịch sử khay nhớ tạm không khả dụng", + "unavailable-desc": "Ứng dụng 'cliphist' chưa được cài đặt. Vui lòng cài đặt nó để sử dụng tính năng lịch sử khay nhớ tạm." + }, + "custom-command-failed": { + "description": "Lệnh thất bại: {command}\nMã thoát: {code}", + "title": "Lệnh tùy chỉnh thất bại" + }, + "do-not-disturb": { + "disabled": "Chế độ Không làm phiền đã tắt", + "disabled-desc": "Đang hiển thị tất cả thông báo", + "enabled": "Không làm phiền đã bật", + "enabled-desc": "Bạn sẽ tìm thấy những thông báo này trong lịch sử của mình" + }, + "donation-opened": "Trang quyên góp đã mở trong trình duyệt của bạn", + "internet-limited": "Đã kết nối, không có Internet", + "keyboard-layout": { + "changed": "Bố cục bàn phím đã đổi thành {layout}", + "title": "Bàn phím" + }, + "missing-control-center": { + "description": "Tiện ích Trung tâm điều khiển đã bị xóa khỏi thanh. Để truy cập lại từ thanh, bạn cần thêm lại tiện ích. Bạn cũng có thể mở nó bằng cách nhấp chuột phải vào thanh.", + "label": "Đã xóa tiện ích Trung tâm điều khiển cuối cùng" + }, + "night-light": { + "forced": "Kích hoạt bắt buộc", + "normal": "Chế độ bình thường", + "not-installed": "wlsunset chưa cài đặt" + }, + "noctalia-performance": { + "disabled": "Chế độ hiệu năng đã tắt", + "enabled": "Chế độ hiệu năng đã bật", + "label": "Chế độ hiệu năng của Noctalia" + }, + "power-profile": { + "changed": "Cấu hình nguồn đã thay đổi", + "profile-name": "Cấu hình" + }, + "theming-processor-failed": { + "desc-generic": "Gặp lỗi khi xử lý mẫu", + "title": "Xử lý mẫu thất bại" + }, + "vpn": { + "connected": "Đã kết nối với '{name}'", + "disconnected": "Đã ngắt kết nối khỏi '{name}'" + }, + "wallpaper-colors": { + "disabled": "Màu sắc hình nền đã tắt", + "enabled": "Màu nền đã bật", + "label": "Màu sắc hình nền" + }, + "wifi": { + "connected": "Đã kết nối với '{ssid}'", + "connection-failed": "Kết nối thất bại", + "connection-timeout": "Hết thời gian chờ kết nối", + "disconnected": "Đã ngắt kết nối khỏi '{ssid}'", + "incorrect-password": "Mật khẩu không đúng", + "network-not-found": "Không tìm thấy mạng" + } + }, + "tooltips": { + "add-widget": "Thêm tiện ích", + "auto-connect": "Bật/Tắt tự động kết nối cho thiết bị này", + "bluetooth-auto-connect-off": "Tự động kết nối đang tắt", + "bluetooth-auto-connect-on": "Tự động kết nối đang bật", + "bluetooth-devices": "Thiết bị Bluetooth", + "brightness-at": "Độ sáng: {brightness}%", + "click-to-start-recording": "Máy ghi màn hình (bắt đầu ghi)", + "click-to-stop-recording": "Ghi màn hình (dừng ghi)", + "collapse": "Thu gọn thanh bên", + "copy-address": "Sao chép địa chỉ", + "delete-notification": "Xóa thông báo", + "dismiss-notification": "Bỏ qua thông báo", + "do-not-disturb-enabled": "Không làm phiền", + "expand": "Mở rộng thanh bên", + "forget-network": "Quên mạng", + "grid-view": "Dạng Lưới", + "hidden-files-hide": "Tệp ẩn", + "hide-unnamed-devices": "Ẩn thiết bị không tên", + "home": "Thư mục chính", + "input-muted": "Bật/Tắt tiếng đầu vào", + "keep-awake": "Giữ Màn Hình Luôn Bật", + "keyboard-layout": "Bố cục bàn phím {layout}", + "list-view": "Chế độ xem danh sách", + "manage-vpn": "Kết nối VPN", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "Đã đạt số lượng tiện ích tối đa", + "microphone-volume-at": "Âm lượng micro: {volume}%", + "move-to-section": "Chuyển đến {section}", + "mute": "Tắt tiếng", + "next-media": "Bài hát tiếp theo", + "next-month": "Tháng tới", + "night-light-not-installed": "Ánh sáng đêm (không khả dụng)", + "noctalia-performance-enabled": "Chế độ hiệu năng Noctalia", + "open-annotation-tool": "Mở bằng công cụ chú thích", + "open-control-center": "Trung tâm điều khiển", + "open-notification-history-enable-dnd": "Lịch sử thông báo", + "open-settings": "Cài đặt", + "open-tray-dropdown": "Khay hệ thống", + "output-muted": "Bật/tắt tiếng đầu ra", + "power-profile": "{profile} cấu hình nguồn", + "previous-media": "Bài hát trước", + "previous-month": "Tháng trước", + "refresh-devices": "Làm mới thiết bị", + "refresh-wallhaven": "Làm mới kết quả Wallhaven", + "refresh-wallpaper-list": "Làm mới danh sách hình nền", + "remove-widget": "Gỡ bỏ tiện ích", + "search-close": "Đóng tìm kiếm", + "session-menu": "Menu Hệ thống", + "show-all-devices": "Hiển thị tất cả thiết bị", + "switch-to-dark-mode": "Chế độ tối", + "switch-to-light-mode": "Chế độ sáng", + "unmute": "Bật tiếng", + "up": "Thư mục cha", + "volume-at": "Âm lượng đầu ra: {volume}%", + "wallpaper-selector": "Bộ chọn hình nền" + }, + "wallpaper": { + "browse": { + "empty-directory": "Thư mục này trống.", + "go-root": "Đi đến thư mục gốc hình nền", + "go-up": "Đi tới thư mục cha", + "go-up-hint": "Sử dụng nút quay lại để trở về." + }, + "configure-directory": "Cấu hình thư mục hình nền của bạn với các hình ảnh.", + "fill-modes": { + "crop": "Cắt (Lấp đầy)", + "fit": "Vừa (Giữ nguyên tỷ lệ)", + "repeat": "Lặp lại (Xếp kề)", + "stretch": "Kéo giãn" + }, + "no-match": "Không tìm thấy kết quả phù hợp.", + "no-wallpaper": "Không tìm thấy hình nền.", + "panel": { + "apikey-help": "Cần có khóa API để truy cập nội dung NSFW.", + "apikey-label": "Khóa API", + "apikey-managed-by-env": "Được quản lý thông qua biến môi trường NOCTALIA_WALLHAVEN_API_KEY.", + "apikey-placeholder": "Nhập Wallhaven API Key của bạn", + "appearance-dark-tab": "Tối", + "appearance-light-tab": "Sáng", + "categories-anime": "Anime", + "categories-label": "Danh mục", + "categories-people": "Mọi người", + "color-extraction-disabled": "Sử dụng trích xuất màu hình nền.", + "color-extraction-enabled": "Sử dụng các bảng màu tích hợp sẵn", + "header-devices-apply-all-tooltip": "Chọn hình nền cho từng màn hình", + "header-devices-per-monitor-tooltip": "Áp dụng cho tất cả màn hình", + "header-link-light-dark-tooltip": "Giữ hình nền khi chuyển đổi chủ đề", + "header-separate-light-dark-tooltip": "Tự động chuyển đổi hình nền theo chủ đề", + "header-sun-linked-tooltip": "Tách riêng hình nền sáng và tối", + "header-sun-separate-tooltip": "Cùng hình nền cho chế độ sáng và tối", + "order-asc": "Tăng dần", + "order-desc": "Giảm dần", + "order-label": "Thứ tự", + "purity-label": "Bộ lọc nội dung", + "purity-nsfw": "NSFW", + "purity-sfw": "SFW", + "purity-sketchy": "Đáng ngờ", + "ratios-all-portrait": "Tất cả chế độ dọc", + "ratios-all-wide": "Rộng toàn bộ", + "ratios-any": "Bất kỳ", + "ratios-label": "Tỷ lệ khung hình", + "resolution-atleast": "Ít nhất", + "resolution-exact": "Chính xác", + "resolution-label": "Độ phân giải", + "resolution-mode-label": "Chế độ", + "solid-color-tooltip": "Nền màu đơn sắc", + "sort-date-asc": "Sắp xếp theo cũ nhất trước", + "sort-date-desc": "Sắp xếp theo mới nhất trước", + "sort-name-asc": "Sắp xếp theo tên (A-Z)", + "sort-name-desc": "Sắp xếp theo tên (Z-A)", + "sort-random": "Sắp xếp ngẫu nhiên", + "sorting-date-added": "Ngày thêm", + "sorting-favorites": "Mục yêu thích", + "sorting-label": "Sắp xếp theo", + "sorting-relevance": "Mức độ liên quan", + "sorting-toplist": "Top", + "sorting-views": "Các chế độ xem", + "source-label": "Nguồn", + "source-wallhaven": "Wallhaven", + "title": "Trình chọn Hình nền", + "wallhaven-settings-title": "Cài đặt Wallhaven" + }, + "transitions": { + "disc": "Ổ đĩa", + "fade": "Mờ dần", + "honeycomb": "Tổ ong", + "pixelate": "Pixel hóa", + "stripes": "Sọc", + "wipe": "Xóa" + }, + "try-different-search": "Thử một truy vấn tìm kiếm khác.", + "wallhaven": { + "loading": "Đang tải hình nền...", + "no-results": "Không tìm thấy hình nền nào. Thử một truy vấn tìm kiếm khác.", + "page": "{current} trên {total}", + "page-prefix": "Trang", + "page-suffix": "trong tổng số {total}" + } + }, + "weather": { + "clear-sky": "Trời quang", + "drizzle": "Mưa phùn", + "fog": "Sương mù", + "mainly-clear": "Chủ yếu quang đãng", + "overcast": "U Ám", + "partly-cloudy": "Trời nhiều mây", + "rain-showers": "Mưa rào", + "snow": "Tuyết", + "thunderstorm": "Bão" + }, + "widgets": { + "color-picker": { + "palette-description": "Chọn từ nhiều màu sắc có sẵn.", + "palette-label": "Bảng màu", + "palette-theme-colors": "Truy cập nhanh vào bảng màu của chủ đề của bạn.", + "title": "Bộ chọn màu" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm chữ thường", + "ampm-uppercase": "AM/PM chữ hoa", + "common-12hour-time-minutes": "Thời gian 12 giờ kèm phút", + "common-24hour-time-minutes": "Thời gian 24 giờ kèm phút", + "common-24hour-time-seconds": "Thời gian 24 giờ kèm giây", + "common-european-date": "Định dạng ngày tháng kiểu châu Âu", + "common-iso-date": "Định dạng ngày ISO", + "common-us-date": "Định dạng ngày tháng kiểu Mỹ", + "common-weekday-date": "Thứ, Ngày", + "common-weekday-month-day": "Thứ, ngày và tháng", + "day-abbreviated": "Tên ngày viết tắt", + "day-full": "Tên ngày đầy đủ", + "day-leading-zero": "Ngày với số 0 đứng đầu (01-31)", + "day-no-leading-zero": "Ngày không có số 0 ở đầu (1-31)", + "hour-leading-zero": "Giờ có số 0 đứng đầu (00-23) — định dạng 24 giờ", + "hour-no-leading-zero": "Giờ không có số 0 đứng đầu (0-23) — định dạng 24 giờ", + "minute-leading-zero": "Phút với số 0 ở đầu (00-59)", + "minute-no-leading-zero": "Phút không có số 0 đứng đầu (0-59)", + "month-abbreviated": "Tên tháng viết tắt", + "month-full": "Tên tháng đầy đủ", + "month-number-leading-zero": "Tháng dưới dạng số với số 0 ở đầu (01-12)", + "month-number-no-zero": "Tháng dưới dạng số không có số 0 ở đầu (1-12)", + "second-leading-zero": "Giây với số 0 đứng trước (00-59)", + "second-no-leading-zero": "Giây không có số 0 ở đầu (0-59)", + "timezone-abbreviation": "Viết tắt múi giờ", + "year-four-digit": "Năm dưới dạng số có bốn chữ số", + "year-two-digit": "Năm dưới dạng số có hai chữ số (00-99)" + }, + "file-picker": { + "item": "mục", + "items": "mục", + "search-placeholder": "Tìm kiếm tệp và thư mục...", + "select-current": "Chọn Hiện tại", + "select-file": "Chọn Tệp", + "select-folder": "Chọn Thư mục", + "selected": "Đã chọn:", + "title": "Trình chọn tệp" + }, + "file-picker-title": "Chọn một tệp", + "icon-picker": { + "title": "Bộ chọn biểu tượng" + }, + "tooltip-placeholder": "Giữ chỗ" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "Danh tính Ẩn danh", + "ca-cert": "Chứng chỉ CA", + "eap-method": "Phương thức EAP", + "password": "Mật khẩu người dùng", + "phase2-auth": "Xác thực giai đoạn 2", + "username": "Danh tính / Tên người dùng" + }, + "panel": { + "action-required": "Cần hành động", + "add-hidden-network": "Thêm mạng ẩn", + "add-network": "Thêm mạng", + "available-interfaces": "Các giao diện có sẵn", + "available-networks": "Mạng khả dụng", + "disabled": "Wi‑Fi đã tắt", + "disconnecting": "Đang ngắt kết nối...", + "dns": "DNS", + "enable-message": "Bật Wi‑Fi để xem các mạng khả dụng.", + "enter-password": "Nhập mật khẩu Wi‑Fi...", + "forget": "Quên", + "forget-network": "Quên mạng này", + "forgetting": "Đang quên...", + "hidden-network": "Mạng ẩn", + "interface": "Giao diện mạng", + "internet-connected": "Đã kết nối Internet", + "internet-limited": "Không có internet", + "internet-status": "Trạng thái Internet", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "Mạng đã biết", + "link-speed": "Tốc độ liên kết", + "network-name-ssid": "Tên mạng (SSID)", + "no-ethernet-devices": "Không phát hiện thiết bị Ethernet nào", + "no-networks": "Không tìm thấy mạng Wi-Fi nào", + "saved": "Đã lưu", + "scan-again": "Quét lại", + "searching": "Đang tìm kiếm mạng...", + "security-open": "Mở", + "security-wep": "WEP", + "security-wpa": "WPA", + "security-wpa-ent": "WPA Enterprise", + "security-wpa2-ent": "WPA2 Doanh nghiệp", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 Doanh nghiệp", + "show-password": "Hiện mật khẩu", + "title": "Wi‑Fi" + }, + "signal": { + "excellent": "Tuyệt vời", + "fair": "Vừa phải", + "good": "Tốt", + "poor": "Kém", + "weak": "Yếu" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/zh-CN.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/zh-CN.json new file mode 100644 index 0000000..71be369 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/zh-CN.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "清除历史记录", + "close-app": "关闭 {app}", + "connect-vpn": "连接到 {name}", + "cycle-visualizer": "切换可视化器样式", + "disable-bluetooth": "关闭蓝牙", + "disable-dnd": "关闭勿扰模式", + "disable-wifi": "关闭 Wi-Fi", + "disconnect-vpn": "断开与 {name} 的连接", + "dock-settings": "Dock 设置", + "enable-bluetooth": "启用蓝牙", + "enable-dnd": "启用勿扰模式", + "enable-wifi": "启用 Wi-Fi", + "launcher-settings": "启动器设置", + "lower-to-bottom": "降至底部", + "open-calendar": "打开日历", + "open-display-settings": "显示设置", + "open-launcher": "打开启动器", + "open-settings": "打开设置", + "raise-to-top": "置顶", + "random-wallpaper": "随机壁纸", + "run-custom-command": "运行自定义命令", + "show-active-only": "仅显示活动项", + "show-all": "显示全部", + "toggle-mute": "切换静音", + "widget-settings": "小部件设置" + }, + "authentication": { + "error": "认证错误", + "failed": "认证失败" + }, + "bar": { + "active-window": { + "colorize-icons-description": "将主题颜色应用到活动窗口图标。", + "hide-mode-description": "控制当没有活动窗口时小部件的行为。", + "scrolling-mode-description": "控制何时为长窗口标题启用文本滚动。", + "show-app-icon-description": "在窗口标题旁边显示应用程序图标。", + "show-app-icon-label": "显示应用图标", + "show-app-text-description": "显示活动窗口的标题。", + "show-app-text-label": "显示窗口标题" + }, + "audio-visualizer": { + "color-name-description": "选择可视化工具的颜色。", + "color-name-label": "填充颜色", + "height-description": "自定义组件宽度。", + "hide-when-idle-description": "启用后,除非正在播放媒体,否则隐藏可视化显示。", + "hide-when-idle-label": "无媒体播放时隐藏", + "width-description": "自定义组件的宽度。" + }, + "battery": { + "device-default": "默认(显示设备)", + "device-description": "选择要显示的电池设备。", + "device-label": "电池设备", + "display-mode-description": "选择电池在任务栏中的显示方式。", + "display-mode-graphic": "图形化电池", + "display-mode-graphic-clean": "图形电池(无%)", + "display-mode-icon-always": "图标 - 始终显示 %", + "display-mode-icon-hover": "图标 - 悬停时显示", + "display-mode-icon-only": "仅图标", + "hide-if-idle-description": "当电池空闲时(没有充电或者放电电流时),隐藏小部件。", + "hide-if-idle-label": "空闲时隐藏", + "hide-if-not-detected-description": "当系统未检测到电池时,隐藏该小部件。", + "hide-if-not-detected-label": "未检测到电池时,隐藏该小部件", + "low-battery-threshold-label": "低电量警告阈值", + "show-noctalia-performance-description": "在电池面板中显示 Noctalia 性能模式开关。
该模式通过禁用 Noctalia 中的阴影和动画减少性能开销。", + "show-noctalia-performance-label": "显示 Noctalia 性能模式开关", + "show-power-profile-description": "在电池面板中显示电源配置文件选项。", + "show-power-profile-label": "显示电源配置文件控件" + }, + "brightness": { + "apply-all-description": "使用滚轮时更改所有显示器的亮度。", + "apply-all-label": "将滚动更改应用到所有显示器" + }, + "clock": { + "clock-display-description": "通过从下面的列表添加标记来自定义时钟样式。要使用 12 小时制,则必须包含 'AP' 标记。", + "clock-display-label": "时钟显示", + "custom-font-description": "自定义时钟使用的字体。", + "custom-font-label": "自定义字体", + "custom-font-placeholder": "选择自定义字体...", + "custom-font-search-placeholder": "搜索字体...", + "horizontal-bar-description": "提示:使用 \\n 创建换行。", + "horizontal-bar-label": "水平栏", + "preview": "预览", + "tooltip-format-description": "自定义鼠标悬停在时钟上时显示的提示信息,可以使用格式化字符串。留空以使用默认提示信息。", + "tooltip-format-label": "提示信息格式", + "use-custom-font-description": "使用自定义字体覆盖时钟的默认字体选择。", + "use-custom-font-label": "使用自定义字体", + "use-monospaced-font-description": "启用后,时钟将使用等宽字体。", + "use-monospaced-font-label": "使用等宽字体", + "vertical-bar-description": "使用空格将每个部分分隔到新行。", + "vertical-bar-label": "垂直栏" + }, + "control-center": { + "browse-file": "浏览文件", + "browse-library": "浏览库", + "color-selection-description": "将主题颜色应用于图标。", + "enable-colorization-description": "为图标启用着色,应用主题颜色。", + "icon-description": "从库中选择图标或自定义文件。", + "select-custom-icon": "选择自定义图标", + "use-distro-logo-description": "使用您的发行版徽标而不是自定义图标。", + "use-distro-logo-label": "使用发行版徽标而不是图标" + }, + "custom-button": { + "collapse-condition-description": "如果输出文本与此值匹配,按钮将折叠。", + "collapse-condition-label": "折叠条件", + "color-selection-description": "将主题颜色应用到图标和文本。", + "default-tooltip": "自定义按钮,在设置中配置", + "display-command-output-description": "输入一个定期运行的命令。其输出的第一行将显示为文本。", + "display-command-output-label": "显示命令输出", + "display-command-output-stream-description": "输入一个要持续运行的命令。", + "dynamic-text": "动态文本", + "enable-colorization-description": "启用自定义按钮图标和文本的着色,应用主题颜色。", + "enable-colorization-label": "启用着色", + "general-tooltip-text-description": "在按钮工具提示中显示的自定义文本。", + "general-tooltip-text-label": "自定义工具提示文本", + "hide-mode-always-expanded": "始终展开", + "hide-mode-description": "控制命令无输出时小部件的可见性。", + "hide-mode-expand-with-output": "有输出时展开", + "hide-mode-label": "隐藏模式", + "hide-mode-max-transparent": "最大展开但透明", + "icon-description": "从库中选择图标。", + "ipc-identifier-description": "IPC 命令的唯一标识符。使用此标识符以及“qs -c noctalia-shell ipc call cb [操作] [标识符]”通过 IPC 控制此按钮。", + "ipc-identifier-label": "IPC标识符", + "left-click-description": "左键单击按钮时执行的命令。", + "left-click-label": "左键单击", + "left-click-update-text": "左键单击时更新显示的文本", + "max-text-length-horizontal-description": "在水平栏中显示的最大字符数(0 为隐藏文本)。", + "max-text-length-horizontal-label": "最大文本长度(水平)", + "max-text-length-vertical-description": "在垂直栏中显示的最大字符数(0 为隐藏文本)。", + "max-text-length-vertical-label": "最大文本长度(垂直)", + "middle-click-description": "中键单击按钮时执行的命令。", + "middle-click-label": "中键单击", + "middle-click-update-text": "中键单击时更新显示的文本", + "parse-json-description": "将命令输出解析为 JSON 对象,以动态设置文本和图标。", + "parse-json-label": "将输出解析为 JSON", + "refresh-interval-description": "间隔时间(毫秒)。", + "refresh-interval-label": "刷新间隔", + "right-click-description": "右键单击按钮时执行的命令。", + "right-click-label": "右键单击", + "right-click-update-text": "右键单击时更新显示的文本", + "show-exec-tooltip-description": "显示带有命令详情的工具提示 (左键/右键/中键点击,滚轮)。", + "show-exec-tooltip-label": "显示命令工具提示", + "show-icon-description": "切换小部件图标的可见性。", + "show-icon-label": "显示图标", + "show-text-tooltip-description": "显示带有文本命令输出的工具提示。", + "show-text-tooltip-label": "显示动态文本工具提示", + "text-stream-description": "来自命令的流式输出行将作为文本显示在按钮上。", + "text-stream-label": "流", + "wheel-description": "使用滚轮时执行的命令。
在命令中使用 $delta 表示滚轮增量。", + "wheel-down": "滚轮 下", + "wheel-down-description": "滚轮向下滚动时执行的命令。", + "wheel-down-label": "滚轮向下命令", + "wheel-label": "滚轮", + "wheel-mode-separate-description": "为滚轮向上和向下启用单独的命令。", + "wheel-mode-separate-label": "分开滚轮命令", + "wheel-up": "滚轮 上", + "wheel-up-description": "滚轮向上滚动时执行的命令。", + "wheel-up-label": "滚轮向上命令", + "wheel-update-text": "滚轮滚动时更新显示的文本" + }, + "keyboard-layout": { + "show-icon-description": "显示键盘布局图标。" + }, + "lock-keys": { + "hide-when-off-description": "当按键不活跃时隐藏指示器。", + "hide-when-off-label": "关闭时隐藏", + "show-caps-lock-description": "显示大写锁定状态。", + "show-caps-lock-label": "大写锁定", + "show-num-lock-description": "显示数字锁定键状态。", + "show-num-lock-label": "数字锁定", + "show-scroll-lock-description": "显示滚动锁定状态。", + "show-scroll-lock-label": "滚动锁定" + }, + "media-mini": { + "compact-mode-description": "启用媒体播放器面板的节省空间布局。", + "compact-mode-label": "紧凑模式", + "hide-mode-description": "控制当没有媒体播放时小部件的行为。", + "max-width-description": "设置小部件的最大水平尺寸。当内容较短时,小部件会自动收缩以适应内容。", + "no-active-player": "无活动播放器", + "panel-section-description": "配置媒体播放器面板的外观和行为。", + "panel-section-label": "媒体播放器面板", + "scrolling-mode-description": "控制何时为长曲目标题启用文本滚动。", + "scrolling-mode-label": "滚动模式", + "show-album-art-description": "显示当前播放曲目的专辑封面。", + "show-album-art-label": "显示专辑封面", + "show-artist-first-description": "显示艺术家 - 标题,而不是标题 - 艺术家。", + "show-artist-first-label": "先显示艺术家", + "show-progress-ring-description": "显示显示曲目进度的圆形进度指示器。", + "show-progress-ring-label": "显示进度环", + "show-visualizer-description": "播放音乐时显示音频可视化器。", + "show-visualizer-label": "显示可视化器", + "use-fixed-width-description": "启用后,小部件将始终使用最大宽度,而不根据内容动态调整。", + "use-fixed-width-label": "使用固定宽度", + "visualizer-type-description": "选择要显示的音频可视化器样式。", + "visualizer-type-label": "可视化器类型" + }, + "notification-history": { + "hide-widget-when-zero-description": "没有通知时隐藏通知图标。", + "hide-widget-when-zero-label": "无通知时隐藏图标", + "hide-widget-when-zero-unread-description": "没有未读通知时隐藏通知图标。", + "hide-widget-when-zero-unread-label": "无未读时隐藏图标", + "show-unread-badge-description": "显示一个用于展示未读通知数量的徽章。", + "show-unread-badge-label": "显示未读徽章", + "unread-badge-color-description": "选择未读通知徽章的颜色。", + "unread-badge-color-label": "未读徽章颜色" + }, + "section-editor": { + "placeholder": "选择要添加的小部件...", + "search-placeholder": "搜索小部件..." + }, + "spacer": { + "width-description": "间距宽度(像素)。" + }, + "system-monitor": { + "compact-mode-description": "将数据以条形图显示以防止布局偏移。", + "compact-mode-label": "紧凑模式", + "cpu-cores-description": "单独显示 CPU 核心使用率。", + "cpu-cores-label": "CPU 核心", + "cpu-frequency-description": "显示当前 CPU 时钟速度(GHz)。", + "cpu-frequency-label": "显示 CPU 频率", + "cpu-temperature-description": "显示 CPU 温度(如果可用)。", + "cpu-temperature-label": "CPU 温度", + "cpu-usage-description": "以百分比显示当前 CPU 使用率", + "cpu-usage-label": "CPU 使用率", + "disk-path-description": "选择要监控的磁盘挂载点。", + "disk-path-label": "磁盘路径", + "gpu-temperature-description": "显示 GPU 温度(如果可用)。", + "load-average-description": "显示系统平均负载。", + "load-average-label": "平均负载", + "memory-percentage-description": "以百分比(而非绝对值)显示内存用量。", + "memory-percentage-label": "内存百分比", + "memory-usage-description": "显示当前内存用量信息。", + "memory-usage-label": "内存使用率", + "network-traffic-description": "显示网络上传和下载速度。", + "network-traffic-label": "网络流量", + "storage-as-percentage-description": "以百分比而非绝对值显示磁盘空间。", + "storage-as-percentage-label": "磁盘百分比", + "storage-available-description": "显示可用磁盘空间,而非已用空间。", + "storage-available-label": "可用磁盘空间", + "storage-usage-description": "显示磁盘空间使用情况。", + "storage-usage-label": "存储用量", + "swap-usage-description": "显示 Swap 使用情况。", + "swap-usage-label": "Swap", + "use-monospace-font-description": "使用等宽字体以保持字符宽度一致。", + "use-monospace-font-label": "等宽字体", + "use-padding-description": "用前导空格填充文本值,以防止布局偏移。", + "use-padding-description-disabled-monospace-font": "此功能需要等宽字体。", + "use-padding-description-disabled-vertical": "垂直任务栏不支持文本内边距。", + "use-padding-label": "文本填充" + }, + "taskbar": { + "colorize-icons-description": "将主题颜色应用到任务栏图标。", + "hide-mode-description": "控制无匹配窗口时小部件的行为。", + "hide-mode-label": "隐藏模式", + "icon-scale-description": "设置任务栏图标的缩放比例。", + "icon-scale-label": "图标缩放", + "max-width-description": "任务栏的最大宽度(占屏幕宽度的百分比)。", + "max-width-label": "最大宽度", + "only-active-workspaces-description": "仅显示来自活动工作区的应用程序。", + "only-active-workspaces-label": "仅显示活动工作区", + "only-same-monitor-description": "仅显示任务栏所在显示器上的应用。", + "only-same-monitor-label": "仅限同一显示器", + "show-pinned-apps-description": "在任务栏中显示 Dock 栏上固定的应用。", + "show-pinned-apps-label": "显示已置顶的应用", + "show-title-description": "在任务栏中显示窗口标题。", + "show-title-description-disabled": "垂直任务栏不支持显示标题。", + "show-title-label": "显示标题", + "smart-width-description": "根据条目数量自动调整条目宽度。", + "smart-width-label": "智能宽度", + "title-width-description": "设置任务栏中窗口标题的宽度(以像素为单位)。", + "title-width-label": "标题宽度", + "title-width-reset-tooltip": "重置标题宽度" + }, + "tray": { + "chevron-color-description": "将主题颜色应用于抽屉的箭头图标。", + "chevron-color-label": "箭头颜色", + "colorize-icons-description": "将主题颜色应用到系统托盘图标。", + "colorize-icons-label": "着色图标", + "drawer-enabled-description": "启用时,未固定的托盘项显示在抽屉面板中。
禁用时,所有托盘项都内联显示。", + "drawer-enabled-label": "启用抽屉", + "hide-passive-description": "启用后,将隐藏状态为“被动”的托盘图标。", + "hide-passive-label": "隐藏被动物品" + }, + "volume": { + "display-mode-description": "选择您希望此值显示的方式。" + }, + "workspace": { + "character-count-description": "显示工作区名称的字符数量(1-10)。", + "character-count-label": "字符数量", + "empty-color-description": "设置空工作区的背景颜色。", + "empty-color-label": "空白工作区颜色", + "enable-scrollwheel-description": "使用鼠标滚轮切换工作区。", + "enable-scrollwheel-label": "滚动切换工作区", + "focused-color-description": "设置焦点 Workspace 的背景颜色。", + "focused-color-label": "聚焦工作区颜色", + "follow-focused-screen-description": "显示当前焦点屏幕的工作区,而不是任务栏所在屏幕的工作区。", + "follow-focused-screen-label": "跟随焦点屏幕", + "font-weight-description": "设置工作区内文本的视觉权重。", + "font-weight-label": "字体粗细", + "grouped-border-opacity-description": "设置工作区容器边框的不透明度级别。", + "grouped-border-opacity-label": "边框不透明度", + "hide-unoccupied-description": "不显示没有窗口的工作区。", + "hide-unoccupied-label": "隐藏未占用", + "label-mode-description": "选择工作区标签的显示方式。", + "label-mode-label": "标签模式", + "occupied-color-description": "设置已占用Workspace的背景颜色。", + "occupied-color-label": "已占用工作区颜色", + "pill-size-description": "调整工作区指示器的大小 (50%-100%)。", + "pill-size-label": "胶囊大小", + "reverse-scrolling-description": "滚动时反转工作区切换方向。", + "reverse-scrolling-label": "反转滚动", + "show-applications-description": "在每个工作区内显示应用程序图标。", + "show-applications-hover-description": "当鼠标悬停在工作区上时,在每个工作区内显示应用程序图标。", + "show-applications-hover-label": "鼠标悬停时显示应用程序", + "show-applications-label": "显示应用程序", + "show-badge-description": "在分组模式下显示工作区编号徽章。", + "show-badge-label": "显示工作区徽章", + "show-labels-only-when-occupied-description": "仅在工作区包含窗口时显示工作区标签。", + "show-labels-only-when-occupied-label": "仅在被占用时显示标签", + "unfocused-icons-opacity-description": "设置未聚焦应用图标的不透明度级别。", + "unfocused-icons-opacity-label": "未聚焦图标不透明度" + } + }, + "battery": { + "all-batteries": "所有电池(组合)", + "battery-health": "电池健康", + "battery-level": "电池电量", + "capacity-level": "容量:{level}", + "charging-rate": "充电速率:{rate} W", + "discharging-rate": "放电速率:{rate} W", + "health": "电池健康:{percent}%", + "inhibit-idle-description": "保持系统唤醒。", + "no-battery-detected": "未检测到电池", + "plugged-in": "已接通电源", + "power-profile": "电源模式", + "time-left": "续航剩余:{time}", + "time-until-full": "充满所需时间:{time}" + }, + "bluetooth": { + "panel": { + "available-devices": "可用设备", + "blocked": "已阻止", + "connected-devices": "已连接设备", + "device-address": "设备地址", + "disabled": "蓝牙已禁用", + "discoverable": "可发现的", + "enable-message": "启用蓝牙以查看可用设备。", + "known-devices": "已知设备", + "no-devices": "没有可用设备", + "paired-devices": "已配对的设备", + "pairing-mode": "确保您的设备处于配对模式。", + "scanning": "正在扫描设备...", + "signal-text-excellent": "信号:极佳", + "signal-text-fair": "信号:良好", + "signal-text-good": "信号:良好", + "signal-text-poor": "信号:弱", + "signal-text-unknown": "信号:未知", + "signal-text-very-poor": "信号:非常差" + } + }, + "changelog": { + "error": { + "fetch-failed": "无法加载更新日志数据,请稍后再试。", + "rate-limit": "已达到 GitHub 速率限制,请稍后再试。" + }, + "panel": { + "buttons-discord": "加入我们的 Discord", + "buttons-dismiss": "确定", + "buttons-feedback": "提供反馈", + "empty": "暂时没有可用的发行说明。", + "highlight-title": "重点更新", + "released": "{date} 发布", + "subtitle-fresh": "感谢安装 Noctalia!以下是本次构建包含的内容。", + "subtitle-updated": "已从 {previousVersion} 更新", + "title": "{version} 有哪些更新", + "version": "版本 {version}", + "version-new-user": "全新安装" + } + }, + "common": { + "actions": "操作", + "active": "活跃", + "add": "添加", + "appearance": "外观", + "apply": "应用", + "auto-connect": "自动连接", + "automation": "自动化", + "available": "可用", + "back": "返回", + "battery": "电池", + "behavior": "行为", + "bluetooth": "蓝牙", + "brightness": "亮度", + "browse": "浏览", + "calendar": "日历", + "calendar-panel": "日历面板", + "cancel": "取消", + "cards": "卡片", + "charging": "充电", + "clear": "清除", + "clipboard": "剪贴板", + "close": "关闭", + "color-muted": "已静音", + "colors": "颜色", + "command": "命令", + "confirm": "确认", + "connect": "连接", + "connected": "已连接", + "connecting": "正在连接...", + "continue": "继续", + "contributors": "贡献者", + "copied-to-clipboard": "已复制到剪贴板", + "countdown": "倒计时", + "customize": "自定义", + "date": "日期", + "default": "默认", + "delete": "删除", + "devices": "设备", + "disabled": "已禁用", + "discharging": "放电", + "disconnect": "断开连接", + "disconnected": "已断开连接", + "disconnecting": "正在断开连接...", + "display-mode": "显示模式", + "documentation": "文档", + "download": "下载", + "duration": "时长", + "dysfunctional": "功能失调", + "edit": "编辑", + "enabled": "已启用", + "error": "错误", + "ethernet": "以太网", + "events": "事件", + "execute": "执行", + "faithful": "忠实", + "focus": "专注", + "font-weight-bold": "粗体", + "font-weight-light": "细", + "font-weight-medium": "中", + "font-weight-regular": "常规", + "font-weight-semibold": "半粗体", + "frequency": "频率", + "gateway": "网关", + "general": "通用", + "height": "高度", + "hibernate": "休眠", + "history": "历史", + "icon": "图标", + "idle": "空闲", + "info": "信息", + "input": "输入", + "install": "安装", + "installed": "已安装", + "interface": "界面", + "internet": "互联网", + "keybind": "按键绑定", + "language": "语言", + "loading": "正在加载...", + "local": "本地", + "location": "位置", + "lock": "锁屏", + "logout": "退出登录", + "look": "样式", + "margins": "边距", + "media": "媒体", + "media-player": "媒体播放器", + "memory": "内存", + "monitors": "显示器", + "move-down": "下移", + "move-up": "上移", + "network": "网络", + "next": "下一个", + "night-light": "夜间模式", + "no": "否", + "no-results": "没有结果", + "no-summary": "无摘要", + "none": "无", + "not-found": "未找到", + "notifications": "通知", + "official": "官方", + "on-surface": "在表面上", + "output": "输出", + "pair": "配对", + "paired": "已配对", + "pairing": "正在配对...", + "panels": "面板", + "password": "密码", + "pause": "暂停", + "performance": "性能", + "pin": "固定", + "play": "播放", + "polling": "数据轮询", + "position": "位置", + "previous": "上一个", + "primary": "主要的", + "random": "随机", + "reboot": "重启", + "reboot-to-uefi": "重启到 UEFI", + "record": "开始录制", + "refresh": "刷新", + "required": "(必要)", + "reset": "重置", + "reset-to-default": "重置为默认值", + "result-count": "{count} 结果", + "result-count-plural": "{count} 结果", + "resume": "继续", + "save": "保存", + "scanning": "扫描中...", + "screen-corners": "屏幕边角", + "search": "搜索", + "second": "{count} 秒", + "second-plural": "{count} 秒", + "secondary": "辅助", + "security": "安全", + "select": "选择", + "select-color": "选择颜色", + "select-color-description": "应用主题颜色以示强调。", + "select-icon-color": "选择图标颜色", + "settings": "设置", + "shortcuts": "快捷方式", + "shutdown": "关机", + "signal": "信号", + "sound": "声音", + "sources": "来源", + "start": "开始", + "stop": "停止", + "supporters": "支持者", + "suspend": "挂起", + "templates": "模板", + "tertiary": "第三", + "test": "测试", + "thresholds": "阈值", + "title": "标题", + "toast": "Toast 消息", + "trusted": "可信赖的", + "uninstall": "卸载", + "unknown": "未知", + "unpair": "取消配对", + "unpin": "取消固定", + "update": "更新", + "upload": "上传", + "username": "用户名", + "userspace-reboot": "重启用户空间", + "version": "版本", + "vibrant": "鲜艳", + "visualizer": "可视化工具", + "volume": "音量", + "volumes": "音量", + "wallpaper": "壁纸", + "weather": "天气", + "weather-loading": "正在加载天气...", + "weather-no-location": "在设置中设置位置以查看天气", + "week": "周", + "widgets": "小部件", + "width": "宽度", + "wifi": "Wi-Fi", + "windows": "窗口", + "yes": "是" + }, + "control-center": { + "power-profile": { + "tooltip-action": "电源模式", + "tooltip-disabled": "安装 power-profiles-daemon 以使用电源模式" + }, + "wifi": { + "label-disconnected": "Wi-Fi 已断开", + "label-ethernet": "以太网" + } + }, + "display-modes": { + "always-hide": "始终隐藏", + "always-show": "总是显示", + "force-open": "强行打开", + "on-hover": "悬停时" + }, + "hide-modes": { + "auto-hide": "自动隐藏", + "hidden": "当为空时隐藏", + "idle": "空闲时隐藏", + "non-exclusive": "非独占", + "transparent": "空时透明", + "visible": "始终可见" + }, + "launcher": { + "categories": { + "all": "全部", + "audiovideo": "音频和视频", + "chat": "聊天", + "development": "开发", + "education": "教育", + "emoji-activity": "活动", + "emoji-animals": "动物", + "emoji-flags": "旗帜", + "emoji-food": "食物和饮料", + "emoji-nature": "自然", + "emoji-objects": "物品", + "emoji-people": "人物和肢体", + "emoji-recent": "最近使用", + "emoji-symbols": "符号", + "emoji-travel": "旅行和地点", + "game": "游戏", + "graphics": "图形", + "misc": "杂项", + "office": "办公", + "pinned": "已固定", + "system": "系统", + "webbrowser": "网页浏览器" + }, + "providers": { + "applications": "应用程序", + "calculator": "计算器", + "calculator-deprecated": ">calc 命令已弃用,即将移除。请直接在搜索中输入数学表达式。", + "calculator-description": "计算器 — 计算数学表达式", + "calculator-enter-expression": "输入数学表达式", + "calculator-press-enter-to-copy": "按 Enter 键复制结果", + "clipboard": "剪贴板历史记录", + "clipboard-clear-description": "清除所有剪贴板历史记录", + "clipboard-clear-description-full": "从剪贴板历史记录中移除所有项目", + "clipboard-clear-history": "清除剪贴板历史记录", + "clipboard-delete": "删除剪贴板条目", + "clipboard-history-disabled": "剪贴板历史记录已禁用", + "clipboard-history-disabled-description": "在设置中启用剪贴板历史记录或安装 cliphist", + "clipboard-loading": "正在加载剪贴板历史记录...", + "clipboard-search-description": "搜索剪贴板历史记录", + "command-description": "运行 shell 命令", + "emoji": "表情符号选择器", + "emoji-loading": "正在加载表情符号...", + "emoji-loading-description": "请稍候", + "emoji-no-recent": "尚无最近使用的表情符号", + "emoji-search-description": "搜索和复制表情符号", + "settings-search-description": "搜索并前往设置", + "windows-search-description": "搜索并聚焦打开的窗口" + } + }, + "lock-screen": { + "authenticating": "正在验证...", + "password": "请输入密码...", + "restart": "重启", + "shut-down": "关机" + }, + "notifications": { + "panel": { + "click-to-collapse": "点击折叠", + "click-to-expand": "点击展开", + "description": "您的通知将在到达时显示在此处。", + "no-notifications": "无通知" + }, + "range": { + "earlier": "更早", + "today": "今天", + "yesterday": "昨天" + }, + "time": { + "diff-d": "1 天前", + "diff-dd": "{diff} 天前", + "diff-h": "1 小时前", + "diff-hh": "{diff} 小时前", + "diff-m": "1 分钟前", + "diff-mm": "{diff} 分钟前", + "now": "现在" + } + }, + "options": { + "bar": { + "density-comfortable": "舒适", + "density-compact": "紧凑", + "density-default": "默认", + "density-mini": "迷你", + "density-spacious": "宽敞", + "display-mode-always-visible": "始终可见", + "type-floating": "悬浮", + "type-framed": "边框", + "type-simple": "简约" + }, + "control-center": { + "quick-settings-style-classic": "经典", + "quick-settings-style-modern": "现代" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "舒适", + "compact": "紧凑", + "default": "默认" + }, + "launcher-view-mode": { + "grid": "网格", + "list": "列表" + }, + "notification-density": { + "compact": "紧凑", + "default": "默认" + }, + "scrolling-modes": { + "always": "始终滚动", + "hover": "悬停时滚动", + "never": "从不滚动" + }, + "session-menu-grid-layout": { + "grid": "网格", + "single-row": "单行" + }, + "settings-panel-mode": { + "attached": "附属于状态栏显示", + "centered": "居中屏幕显示", + "window": "显示为独立窗口" + }, + "visualizer-types": { + "linear": "线性", + "mirrored": "镜像", + "wave": "波形" + }, + "visualizer-visibility": { + "with-background": "仅在有背景时" + }, + "workspace-labels": { + "index": "索引", + "index-and-name": "索引和名称", + "name": "名称" + } + }, + "panels": { + "about": { + "become-supporter": "成为支持者", + "changelog": "查看更改日志", + "changelog-on-startup": "更新时显示更新日志", + "changelog-on-startup-desc": "Noctalia 更新时自动显示更新日志。", + "contributors-description": "向我们 {count} 位超棒的贡献者致敬!", + "contributors-description-plural": "向我们 {count} 位超棒的贡献者致敬!", + "copy-info": "复制信息", + "debug-disabled": "调试模式已禁用", + "debug-enabled": "调试模式已启用", + "info-copied": "信息已复制到剪贴板", + "noctalia-available": "可用:", + "noctalia-desc": "一款为 Wayland 精心打造的时尚简约桌面 shell,基于 Quickshell 构建。", + "noctalia-git-commit": "Git 提交:", + "noctalia-installed-version": "已安装版本:", + "noctalia-latest-version": "最新版本:", + "noctalia-qs-version": "Noctalia QS 版本:", + "noctalia-title": "Noctalia shell", + "privacy-policy": "隐私政策", + "support": "支持我们", + "supporter-badge": "支持者", + "supporters-desc": "非常感谢我们出色的支持者!", + "supporters-desc-plural": "非常感谢我们的{count}位出色支持者!", + "supporters-loading": "正在加载支持者...", + "system-board": "主板:", + "system-cpu": "中央处理器:", + "system-disk": "磁盘:", + "system-gpu": "图形处理器:", + "system-host": "设备型号:", + "system-install-hint": "安装 fastfetch 以查看系统信息", + "system-kernel": "内核:", + "system-loading": "正在加载系统信息...", + "system-memory": "内存:", + "system-monitor": "监视器:", + "system-not-installed": "未安装 fastfetch", + "system-os": "操作系统:", + "system-packages": "软件包:", + "system-product": "产品:", + "system-title": "系统信息", + "system-uptime": "正常运行时间:", + "system-wm": "窗口管理器:", + "telemetry-data-copied": "遥测数据已复制到剪贴板", + "telemetry-desc": "通过分享匿名系统信息(屏幕分辨率、合成器、发行版)来帮助改进 Noctalia。 启动时发送一次,不进行跟踪,数据会在 30 天后自动删除。", + "telemetry-enabled": "发送匿名系统信息", + "telemetry-show-data": "查看数据", + "telemetry-title": "隐私", + "title": "关于", + "up-to-date": "您已是最新版本!", + "update-available": "有可用更新", + "view-commit": "在 GitHub 上查看提交" + }, + "audio": { + "devices-desc": "配置可用的音频输入和输出设备。", + "devices-input-device-description": "选择所需的音频输入设备。", + "devices-input-device-label": "输入设备", + "devices-output-device-description": "选择所需的音频输出设备。", + "devices-output-device-label": "输出设备", + "devices-title": "音频设备", + "external-mixer-description": "输入激活外部音频混音器功能时要启动的命令或应用程序路径。", + "external-mixer-label": "外部音频混音器命令", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "设置您首选和要忽略的媒体应用程序。", + "media-excluded-player-description": "添加您希望系统忽略的播放器关键词。每个关键词应单独占一行。", + "media-excluded-player-label": "排除的播放器", + "media-excluded-player-placeholder": "输入关键词并按 + 添加", + "media-frame-rate-description": "帧率越高越流畅,但会占用更多资源。", + "media-frame-rate-label": "帧率", + "media-primary-player-description": "输入关键词以识别您的主要播放器。", + "media-primary-player-label": "主要播放器", + "media-primary-player-placeholder": "例如:spotify, vlc, mpv", + "media-scrolling-speed-description": "标题从头到尾滚动所需的时间(秒)。", + "media-scrolling-speed-label": "滚动速度", + "media-scrolling-title-description": "为长媒体标题启用连续滚动。", + "media-scrolling-title-label": "标题滚动", + "media-title": "媒体播放器", + "on-middle-clicked-description": "鼠标中键点击时执行的命令。", + "panel-applications-empty": "目前没有应用程序正在播放音频", + "spectrum-mirrored-description": "镜像频谱,使低频位于中心。关闭时,低频在左侧,高频在右侧。", + "spectrum-mirrored-label": "镜像频谱", + "title": "音频", + "visualizer-type-description": "选择媒体播放的可视化类型。", + "visualizer-type-label": "可视化类型", + "volumes-desc": "调整音量控制和音频级别。", + "volumes-feedback-sound-file-description": "更改音量时播放的声音文件路径。", + "volumes-feedback-sound-file-label": "音量反馈声音文件", + "volumes-feedback-sound-file-select-title": "选择音量反馈声音文件", + "volumes-input-volume-description": "麦克风输入音量级别。", + "volumes-mute-input-description": "静音默认音频输入(麦克风)。", + "volumes-mute-input-label": "静音输入", + "volumes-mute-output-description": "静音系统主音频输出。", + "volumes-mute-output-label": "静音输出", + "volumes-output-volume-description": "系统全局音量级别。", + "volumes-step-size-description": "调整音量变化的步长(滚轮、键盘快捷键)。", + "volumes-step-size-label": "音量步长", + "volumes-volume-feedback-description": "调整音量时播放反馈音效。", + "volumes-volume-feedback-label": "播放音量反馈", + "volumes-volume-overdrive-description": "允许将音量提高到100%以上。可能不被所有硬件支持。", + "volumes-volume-overdrive-label": "允许音量过载" + }, + "bar": { + "appearance-auto-hide-delay-description": "鼠标离开后Bar隐藏所需时间", + "appearance-auto-hide-delay-label": "隐藏延迟", + "appearance-auto-hide-exclusive-note": "注意: 当自动隐藏处于活动状态时,专属区域会自动禁用。", + "appearance-auto-show-delay-description": "鼠标进入边缘时,栏显示前的延迟时间。", + "appearance-auto-show-delay-label": "显示延迟", + "appearance-background-opacity-description": "为状态栏设置背景不透明度。", + "appearance-background-opacity-label": "状态栏背景不透明度", + "appearance-capsule-color-description": "为栏胶囊选择一种颜色,或不使用任何颜色以采用默认表面颜色。", + "appearance-capsule-color-label": "胶囊颜色", + "appearance-capsule-opacity-description": "设置显示胶囊时小部件背景的不透明度。", + "appearance-capsule-opacity-label": "胶囊不透明度", + "appearance-content-padding-description": "调整栏边缘与小部件之间的内边距。", + "appearance-content-padding-label": "内容内边距", + "appearance-density-description": "调整状态栏的内边距以获得紧凑或宽松的外观。", + "appearance-density-label": "状态栏密度", + "appearance-desc": "自定义状态栏的外观和位置。", + "appearance-display-mode-description": "选择栏何时可见。", + "appearance-enable-exclusion-zone-inset-description": "减少排除区域1个物理像素,以便齐平的窗口完美地延伸到状态栏边缘下方。", + "appearance-enable-exclusion-zone-inset-label": "内嵌排除区域", + "appearance-floating-description": "将状态栏显示为浮动的“药丸”形状。", + "appearance-floating-label": "浮动状态栏", + "appearance-font-scale-description": "调整栏中显示的文本的字体大小比例。", + "appearance-font-scale-label": "字体缩放", + "appearance-frame-radius": "内圆角", + "appearance-frame-settings-description": "调整边框粗细和内圆角半径", + "appearance-frame-settings-label": "边框设置", + "appearance-frame-thickness": "粗细", + "appearance-hide-on-overview-description": "当合成器概览处于活动状态时,隐藏栏并关闭面板。", + "appearance-hide-on-overview-label": "在概览中隐藏栏", + "appearance-margins-description": "调整浮动状态栏周围的边距。", + "appearance-margins-horizontal": "水平边距", + "appearance-margins-vertical": "垂直边距", + "appearance-outer-corners-description": "在栏上显示向外弯曲的角。", + "appearance-outer-corners-label": "外角", + "appearance-position-description": "选择在屏幕上的位置。", + "appearance-position-label": "状态栏位置", + "appearance-show-capsule-description": "为每个小部件添加背景。", + "appearance-show-capsule-label": "显示组件背景", + "appearance-show-on-workspace-switch-description": "切换工作区时自动短暂显示状态栏。", + "appearance-show-on-workspace-switch-label": "工作区切换时显示状态栏", + "appearance-show-outline-description": "在每个小部件周围显示可见边框。", + "appearance-show-outline-label": "显示小部件轮廓", + "appearance-type-description": "选择栏的样式:简约、悬浮或边框。", + "appearance-type-label": "栏类型", + "appearance-use-separate-opacity-description": "启用后为状态栏背景使用单独的不透明度值。", + "appearance-use-separate-opacity-label": "使用单独的状态栏不透明度", + "appearance-widget-spacing-description": "调整栏中每个小部件之间的间距。", + "appearance-widget-spacing-label": "小部件间距", + "behavior-middle-click-command-description": "中键点击时执行的命令。", + "behavior-middle-click-command-label": "中键点击命令", + "behavior-middle-click-command-placeholder": "niri msg action 切换概览", + "behavior-middle-click-description": "选择在栏的空白区域中键单击的操作。", + "behavior-middle-click-follow-mouse-description": "在光标位置打开选定的中键点击面板。", + "behavior-middle-click-follow-mouse-label": "中键点击跟随鼠标", + "behavior-middle-click-label": "栏中键点击操作", + "behavior-right-click-command-description": "右键点击时执行的命令。", + "behavior-right-click-command-label": "右键点击命令", + "behavior-right-click-command-placeholder": "notify-send \"右键点击\"", + "behavior-right-click-description": "选择在栏的空白区域右键点击时执行的操作。", + "behavior-right-click-follow-mouse-description": "在光标位置打开选定的右键面板。", + "behavior-right-click-follow-mouse-label": "右键跟随鼠标", + "behavior-right-click-label": "栏右键点击操作", + "behavior-wheel-wrap-description": "启用后,滚动会从最后一项继续到第一项。", + "behavior-wheel-wrap-label": "循环", + "behavior-workspace-scroll-description": "选择鼠标滚轮在栏的空白区域执行的操作。", + "behavior-workspace-scroll-label": "栏鼠标滚轮操作", + "behavior-workspace-scroll-option-content": "内容", + "behavior-workspace-scroll-option-workspace": "工作区", + "monitor-configure-widgets": "配置小部件", + "monitor-override-settings": "覆盖全局设置", + "monitor-override-settings-description": "为此显示器使用自定义设置。", + "monitor-reset-all": "全部重置", + "monitor-widgets-title": "小部件配置 - {monitor}", + "monitors-desc": "在特定显示器上显示状态栏。如果未选择,则默认为全部。", + "monitors-desc-new": "配置哪些显示器显示 Bar,并自定义每个显示器的设置。", + "monitors-title": "显示器显示", + "title": "状态栏", + "tray-blacklist-description": "添加托盘排除规则,支持通配符 (*)。", + "tray-blacklist-label": "黑名单", + "tray-blacklist-placeholder": "例如:nm-applet, Fcitx*", + "tray-pin-application": "固定应用程序", + "tray-unpin-application": "取消固定应用程序", + "use-global-widgets": "使用全局小部件", + "widgets-desc": "拖动小部件以更改它们的顺序。使用右键菜单在各部分之间转移小部件或移除它们。", + "widgets-title": "小部件定位" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "从壁纸生成配色方案。自动提取颜色以创建统一的主题。", + "color-source-use-wallpaper-colors-label": "使用壁纸颜色", + "dark-mode-mode-description": "启用自动切换浅色和深色模式。", + "dark-mode-mode-label": "深色模式计划", + "dark-mode-mode-manual": "手动", + "dark-mode-mode-off": "关", + "dark-mode-switch-description": "切换到更暗的主题,便于夜间观看。", + "delete-error-description": "删除 {scheme} 失败", + "delete-error-title": "删除失败", + "delete-success-description": "成功删除 {scheme}", + "delete-success-title": "配色方案已删除", + "download-button": "下载更多", + "download-downloading": "正在下载...", + "download-empty": "没有可用的配色方案", + "download-error-api-error": "API 错误: {status}", + "download-error-description": "下载 {scheme} 失败", + "download-error-download-failed": "下载失败,退出代码: {code}", + "download-error-invalid-response": "无效的 API 响应格式", + "download-error-no-files": "未找到方案的文件", + "download-error-parse-failed": "解析 API 响应失败: {error}", + "download-error-rate-limit": "GitHub API 速率限制已超出", + "download-error-title": "下载失败", + "download-fetching": "正在获取可用的配色方案...", + "download-success-description": "成功下载 {scheme}", + "download-success-title": "配色方案已下载", + "download-title": "下载配色方案", + "method-description": { + "content": "具有高保真色彩提取的 Material Design 方案,可与源内容的实际颜色紧密匹配。", + "dysfunctional": "与 Faithful 类似,但选择第二主导的颜色家族作为主要颜色。", + "faithful": "尝试在生成和谐调色板的同时,尽可能接近源颜色。", + "fruit-salad": "Material Design 方案,产生一个俏皮、充满活力的调色板,具有多样和变化的色调。", + "monochrome": "使用单色调灰阶和最少色彩内容的 Material Design 方案。", + "muted": "生成更柔和、更低饱和度的色调,营造平静而低调的外观。", + "rainbow": "Material Design 方案,可在整个光谱中生成颜色,以实现最大的颜色多样性。", + "tonal-spot": "Material Design 默认算法。使用源颜色的色调变化创建平衡的调色板。", + "vibrant": "最大化色彩饱和度,打造大胆醒目的调色板,呈现生动色调。" + }, + "predefined-desc": "从预定义配色方案集合中选择。", + "predefined-generate-templates-label": "为预定义方案生成模板", + "predefined-title": "预定义配色方案", + "sync-gsettings-description": "将系统主题与活动的浅色或深色变体匹配。", + "sync-gsettings-label": "同步系统主题", + "templates-desc": "将颜色应用于外部应用程序。", + "templates-filter-description": "显示特定类别的模板。", + "templates-filter-label": "按类别筛选", + "templates-misc-description": "创建您自己的模板。", + "templates-misc-label": "高级", + "templates-misc-user-templates-description": "仅在您知道自己在做什么时启用,请参阅我们的在线文档。", + "templates-misc-user-templates-label": "启用用户模板", + "templates-none-detected": "未检测到", + "templates-write-path": "写入:{filepath}", + "title": "配色方案", + "wallpaper-method-description": "选择您喜欢的调色板生成方法。", + "wallpaper-method-label": "调色板生成方法", + "wallpaper-monitor-source-description": "选择用于提取壁纸颜色的显示器。", + "wallpaper-monitor-source-label": "颜色生成源" + }, + "connections": { + "authentication-required": "需要认证", + "bluetooth-auto-connect-description": "当 Bluetooth 启用时,自动连接到受信任的已配对设备。", + "bluetooth-auto-connect-label": "自动连接已配对设备", + "bluetooth-devices-unnamed": "未命名设备不显示。", + "bluetooth-discoverable": "当此设置选项卡打开时,此设备可被发现为 {hostName}。", + "bluetooth-rssi-polling-description": "通过 bluetoothctl 定期采样已连接设备的 RSSI。可能并非所有设备都可用;启用时占用最少资源。", + "bluetooth-rssi-polling-interval-description": "配置已连接设备的信号强度更新频率。", + "bluetooth-rssi-polling-interval-label": "轮询间隔", + "bluetooth-rssi-polling-label": "蓝牙信号轮询", + "disable-discoverability-description": "将您的设备从附近的 Bluetooth 设备中隐藏。", + "disable-discoverability-label": "禁用设备可见性", + "hide-unnamed-devices-description": "隐藏仅显示为 Bluetooth 地址的设备。", + "hide-unnamed-devices-label": "隐藏未命名设备", + "pin-instructions": "请输入您设备上显示的PIN码。", + "title": "连接", + "wifi-header-text": "使用 Wi-Fi 查看可用网络并连接到互联网。" + }, + "control-center": { + "cards-desc": "自定义在控制中心显示的控制项及其顺序。", + "desc": "配置控制中心面板的位置和行为。", + "open-at-mouse-description": "右键点击栏的空白区域时,在光标附近打开控制中心。禁用时,将使用上方的位置设置。", + "open-at-mouse-label": "右键点击时在光标处打开", + "position-description": "选择打开控制中心时面板出现的位置。", + "shortcuts-custom-button-command-description": "单击按钮时执行的命令。", + "shortcuts-custom-button-enable-on-state-logic-description": "根据检查命令启用第二个图标和“高亮”状态。", + "shortcuts-custom-button-enable-on-state-logic-label": "启用开启状态逻辑", + "shortcuts-custom-button-icon-description": "从库中选择一个图标。", + "shortcuts-custom-button-on-clicked-label": "左键单击命令", + "shortcuts-custom-button-on-middle-clicked-description": "当按钮被中键点击时执行的命令。", + "shortcuts-custom-button-on-state-command-description": "执行以检查按钮是否应处于“开启”状态的命令。返回 0 表示开启,非零表示关闭。", + "shortcuts-custom-button-on-state-command-label": "开启状态检查命令", + "shortcuts-custom-button-on-state-icon-description": "按钮处于“开启”状态时的图标。", + "shortcuts-custom-button-on-state-icon-label": "开启状态图标", + "shortcuts-custom-button-state-checks-add": "添加状态检查", + "shortcuts-custom-button-state-checks-command": "要为此状态检查执行的命令", + "shortcuts-custom-button-state-checks-label": "状态检查", + "shortcuts-custom-button-state-checks-remove": "移除", + "shortcuts-custom-button-tooltip-description": "悬停在按钮上时显示的工具提示。", + "shortcuts-custom-button-tooltip-label": "提示信息(Tooltip)", + "shortcuts-title": "快捷方式小部件", + "system-monitor-disk-path-description": "选择控制中心中的系统监视器卡片应监视的磁盘挂载点。", + "system-monitor-disk-path-label": "系统监视器磁盘路径", + "title": "控制中心" + }, + "desktop-widgets": { + "clock-enabled-description": "在桌面上显示一个时钟小部件。", + "clock-enabled-label": "启用时钟小部件", + "clock-format-label": "格式", + "clock-height-description": "时钟小部件的高度,以像素为单位。", + "clock-height-label": "高度", + "clock-minimal-mode-description": "使用包含时间和日期的简约时钟显示。", + "clock-minimal-mode-label": "简约模式", + "clock-rounded-corners-description": "使用圆角作为小部件背景。", + "clock-rounded-corners-label": "圆角", + "clock-show-background-description": "显示时钟小部件的背景容器。", + "clock-show-background-label": "显示背景", + "clock-show-date-description": "在时间下方显示当前日期。", + "clock-show-date-label": "显示日期", + "clock-show-seconds-description": "在时间中显示秒。", + "clock-show-seconds-label": "显示秒数", + "clock-style-analog": "模拟", + "clock-style-binary": "二进制", + "clock-style-description": "选择时钟显示样式。", + "clock-style-digital": "数字", + "clock-style-label": "时钟样式", + "clock-style-minimal": "简约", + "clock-width-description": "时钟小部件的宽度,以像素为单位。", + "cpu-intensive-note": "标有 ! 的Widget会占用更多 CPU,请谨慎启用。", + "edit-mode-button-label": "进入编辑模式", + "edit-mode-controls-explanation": "左键点击并拖动:移动或调整小部件大小。\n右键点击:打开上下文菜单选项。", + "edit-mode-description": "启用编辑模式以移动和重新定位桌面小组件。启用后,小组件会显示拖动轮廓,并且可以重新定位。", + "edit-mode-exit-button": "退出编辑模式", + "edit-mode-grid-snap-label": "网格对齐", + "edit-mode-grid-snap-scale-label": "网格吸附比例", + "edit-mode-label": "编辑模式", + "enabled-description": "完全启用或禁用桌面小部件。", + "enabled-label": "启用桌面小部件", + "general-desc": "配置显示在您桌面上的小部件。", + "general-title": "桌面小部件", + "media-player-enabled-description": "在桌面上显示媒体播放器小部件。", + "media-player-enabled-label": "启用媒体播放器小部件", + "media-player-rounded-corners-description": "启用小部件边缘的圆角。", + "media-player-show-album-art-description": "显示专辑封面和曲目信息(标题和艺术家)。", + "media-player-show-album-art-label": "显示专辑封面和标题", + "media-player-show-background-description": "显示媒体播放器小部件的背景容器。", + "media-player-show-buttons-description": "在媒体播放器小部件上显示媒体控制按钮(播放/暂停、上一个、下一个)。", + "media-player-show-buttons-label": "显示按钮", + "media-player-show-visualizer-description": "显示音频可视化叠加层。", + "media-player-visualizer-type-description": "选择桌面媒体播放器的背景可视化类型。", + "overview-enabled-description": "当合成器概览打开时,显示桌面小部件。", + "overview-enabled-label": "在概览中显示", + "system-stat-layout-bottom": "底部", + "system-stat-layout-description": "选择图例相对于图表的显示方式。", + "system-stat-layout-label": "布局", + "system-stat-layout-side": "边", + "system-stat-rounded-corners-description": "小组件背景使用圆角。", + "system-stat-rounded-corners-label": "圆角", + "system-stat-show-background-description": "显示系统统计小部件的背景容器。", + "system-stat-show-background-label": "显示背景", + "system-stat-stat-type-description": "选择要显示的系统统计信息。", + "system-stat-stat-type-label": "统计类型", + "title": "桌面小部件", + "weather-enabled-description": "在桌面上显示天气小部件。", + "weather-enabled-label": "启用天气小部件", + "weather-show-background-description": "显示天气小部件的背景容器。" + }, + "display": { + "monitors-backlight-device-auto-option": "默认", + "monitors-backlight-device-description": "为此输出选择一个背光设备。", + "monitors-backlight-device-label": "背光设备", + "monitors-brightness-step-description": "调整亮度变化的步长(滚轮和键盘快捷键)。", + "monitors-brightness-step-label": "亮度步长", + "monitors-brightness-unavailable-ddc-disabled": "亮度控制不可用。启用“外部亮度支持”以控制此显示器的亮度。", + "monitors-brightness-unavailable-generic": "此显示器无法使用亮度控制。", + "monitors-desc": "调整每个显示器的亮度设置。", + "monitors-enforce-minimum-description": "解决亮度为 0% 时部分显示器背光完全关闭的问题。", + "monitors-enforce-minimum-label": "限制最小亮度为 1%", + "monitors-external-brightness-description": "启用 DDCUtil 支持,通过 DDC/CI 协议控制外部显示器的亮度。", + "monitors-external-brightness-label": "外部亮度支持", + "monitors-global-brightness-description": "同时调整所有显示器的亮度。", + "monitors-global-brightness-label": "所有显示器", + "monitors-title": "显示器设置", + "night-light-auto-schedule-description": "基于 {location} 的日落和日出时间 — 推荐。", + "night-light-auto-schedule-label": "自动调度", + "night-light-desc": "减少蓝光发射,帮助您更好地睡眠并减轻眼睛疲劳。", + "night-light-enable-description": "应用暖色滤镜以减少蓝光发射。", + "night-light-enable-label": "启用夜间模式", + "night-light-force-activation-description": "忽略调度并立即应用夜间滤镜。", + "night-light-force-activation-label": "强制激活", + "night-light-manual-schedule-description": "为日出和日落设置自定义时间。", + "night-light-manual-schedule-label": "手动调度", + "night-light-manual-schedule-select-start": "选择开始时间", + "night-light-manual-schedule-select-stop": "选择停止时间", + "night-light-manual-schedule-sunrise": "日出时间", + "night-light-manual-schedule-sunset": "日落时间", + "night-light-temperature-day": "白天", + "night-light-temperature-day-description": "控制白天的色温。", + "night-light-temperature-description": "设置夜间和白天的颜色温暖度。", + "night-light-temperature-label": "色温", + "night-light-temperature-night": "夜间", + "night-light-temperature-night-description": "控制夜间的色温。", + "title": "显示" + }, + "dock": { + "appearance-background-opacity-description": "调整 Dock 栏的背景不透明度。", + "appearance-border-radius-description": "调整 Dock 栏的边框半径。", + "appearance-border-radius-label": "边框半径", + "appearance-colorize-icons-description": "将主题颜色应用到 Dock 栏应用图标(仅限非焦点应用)。", + "appearance-colorize-icons-label": "着色图标", + "appearance-dead-opacity-description": "调整未运行的应用程序图标的不透明度。", + "appearance-dead-opacity-label": "非活跃应用不透明度", + "appearance-desc": "自定义 Dock 栏的行为和外观。", + "appearance-display-auto-hide": "自动隐藏", + "appearance-display-description": "选择 Dock 栏的行为方式。", + "appearance-display-exclusive": "独占", + "appearance-dock-indicator-description": "当 Dock 隐藏时,显示一个小指示器。", + "appearance-dock-indicator-label": "Dock 指示器", + "appearance-floating-distance-description": "调整距离屏幕边缘的浮动距离。", + "appearance-floating-distance-label": "Dock 栏浮动距离", + "appearance-group-apps-description": "将同一应用的多个窗口分组到一个 Dock 条目中。", + "appearance-group-apps-label": "分组相同应用", + "appearance-group-click-action-cycle": "切换窗口", + "appearance-group-click-action-description": "选择左键单击对分组应用执行的操作。", + "appearance-group-click-action-label": "分组应用点击操作", + "appearance-group-click-action-list": "打开窗口列表", + "appearance-group-context-menu-mode-description": "选择分组应用上下文菜单的行为方式。", + "appearance-group-context-menu-mode-extended": "扩展", + "appearance-group-context-menu-mode-label": "分组应用菜单模式", + "appearance-group-context-menu-mode-list": "窗口列表", + "appearance-group-indicator-style-description": "选择分组运行指示器如何显示焦点窗口状态。", + "appearance-group-indicator-style-dots": "点", + "appearance-group-indicator-style-label": "分组指示器样式", + "appearance-group-indicator-style-number": "数字", + "appearance-hide-show-speed-description": "调整 Dock 栏隐藏/显示动画的速度。", + "appearance-hide-show-speed-label": "隐藏/显示速度", + "appearance-icon-size-description": "调整 Dock 栏的整体大小。", + "appearance-icon-size-label": "Dock 栏大小", + "appearance-inactive-indicators-description": "为所有应用显示指示器小药丸,而不仅仅是当前活跃的应用。", + "appearance-inactive-indicators-label": "运行指示器", + "appearance-indicator-color-description": "选择隐藏停靠栏指示器的颜色。", + "appearance-indicator-color-label": "指示器颜色", + "appearance-indicator-opacity-description": "调整隐藏停靠栏指示器的不透明度。", + "appearance-indicator-opacity-label": "指示器不透明度", + "appearance-indicator-thickness-description": "启用较粗的隐藏停靠栏指示器(从 3px 增加到 6px)。", + "appearance-indicator-thickness-label": "较粗的指示器", + "appearance-launcher-icon-description": "选择用于 Dock Launcher 的图标。", + "appearance-launcher-icon-label": "启动器图标", + "appearance-launcher-position-description": "选择启动器图标在停靠栏中的显示位置。", + "appearance-launcher-position-end": "结束", + "appearance-launcher-position-label": "启动器位置", + "appearance-launcher-position-start": "开始", + "appearance-launcher-use-distro-logo-description": "使用你的发行版标志作为 Dock Launcher 图标。", + "appearance-launcher-use-distro-logo-label": "使用发行版标志而非图标", + "appearance-pinned-static-description": "始终将固定的应用图标按静态顺序推到左侧。", + "appearance-pinned-static-label": "静态固定应用", + "appearance-position-description": "选择 Dock 栏在屏幕上显示的位置。", + "appearance-position-label": "位置", + "appearance-show-launcher-icon-description": "在 Dock 中显示应用程序启动器图标。", + "appearance-show-launcher-icon-label": "显示应用启动器", + "appearance-sit-on-frame-description": "将停靠栏对齐在框架边框内,而不是位于顶部。", + "appearance-sit-on-frame-label": "停靠栏位于框架上", + "appearance-type-attached": "停靠", + "appearance-type-description": "选择浮动药丸样式或附着在边缘的静态栏。", + "appearance-type-floating": "浮动", + "appearance-type-label": "Dock 样式", + "enabled-description": "完全显示或隐藏 Dock 栏。", + "enabled-label": "启用 Dock 栏", + "monitors-desc": "在特定显示器上显示 Dock 栏。如果未选择任何显示器,则默认在所有显示器上显示。", + "monitors-only-same-monitor-description": "仅显示 Dock 栏所在显示器上的应用程序。", + "monitors-only-same-monitor-label": "仅限来自同一显示器的应用", + "monitors-title": "显示器显示", + "title": "Dock 栏" + }, + "general": { + "copy-settings": "复制设置", + "fonts-default-description": "整个界面使用的主要字体。", + "fonts-default-label": "默认字体", + "fonts-default-placeholder": "选择默认字体...", + "fonts-default-scale-description": "增大或减小标准文本的尺寸。", + "fonts-default-scale-label": "默认字体大小", + "fonts-default-search-placeholder": "搜索字体...", + "fonts-desc": "选择整个界面中使用的字体。", + "fonts-monospace-description": "用于数字和统计信息显示的等宽字体。", + "fonts-monospace-label": "等宽字体", + "fonts-monospace-placeholder": "选择等宽字体...", + "fonts-monospace-scale-description": "增大或减小等宽文本的尺寸。", + "fonts-monospace-scale-label": "默认等宽字体大小", + "fonts-monospace-search-placeholder": "搜索等宽字体...", + "fonts-title": "字体", + "keybinds-conflict-description": "此按键组合已分配给 {action}。", + "keybinds-conflict-title": "按键绑定冲突", + "keybinds-description": "配置面板和启动器的全局导航键。", + "keybinds-down": "下移", + "keybinds-enter": "确认 / 操作", + "keybinds-escape": "关闭 / 返回", + "keybinds-left": "左移", + "keybinds-modifier-description": "快捷键必须包含修饰键 (Ctrl 或 Alt)。", + "keybinds-modifier-title": "需要修饰键", + "keybinds-remove": "删除", + "keybinds-right": "右移", + "keybinds-title": "导航按键绑定", + "keybinds-up": "上移", + "language-desc": "选择您偏好的应用程序语言。", + "language-select-auto-detect": "自动检测", + "language-select-description": "选择应用程序界面中使用的语言。", + "language-select-label": "应用程序语言", + "launch-setup-wizard": "设置向导", + "profile-desc": "编辑您的用户详细信息和头像。", + "profile-picture-description": "在整个界面中显示您的个人头像。", + "profile-picture-label": "头像", + "profile-select-avatar": "选择头像图片", + "profile-title": "个人资料", + "profile-tooltip": "头像", + "reverse-scrolling-description": "反转解释的滚动方向。", + "reverse-scrolling-label": "反转滚动", + "screen-corners-desc": "自定义屏幕边角的圆度和视觉效果。", + "screen-corners-radius-description": "调整屏幕圆角的弧度。", + "screen-corners-radius-label": "屏幕边角半径", + "screen-corners-radius-reset": "重置屏幕圆角半径", + "screen-corners-show-corners-description": "在屏幕边缘显示圆角。", + "screen-corners-show-corners-label": "显示屏幕圆角", + "screen-corners-solid-black-description": "使用实心黑色而非状态栏背景色。", + "screen-corners-solid-black-label": "实心黑色边角", + "screen-corners-title": "屏幕边角", + "settings-copied": "设置已复制到剪贴板", + "smooth-scrolling-description": "为列表滚动添加动画,以获得更流畅的滚轮体验。", + "smooth-scrolling-label": "平滑滚动", + "tab-basics": "基础", + "tab-keybinds": "按键绑定" + }, + "hooks": { + "color-generation-description": "颜色生成完成后执行的命令(例如,基于壁纸的主题设置后)。", + "color-generation-label": "颜色已生成", + "color-generation-placeholder": "例如:notify-send \"颜色\" \"已生成\"", + "info-command-info-description": "• 命令通过 shell 执行(sh -lc)
• 命令在后台运行(分离)
• 测试按钮使用当前值执行", + "info-parameters-description": "• Wallpaper hook: $1 = 壁纸路径, $2 = 屏幕名称, $3 = 主题 (深色/浅色)
• Theme toggle hook: $1 = true/false (深色模式状态)
• Screen lock/unlock hooks: $1 = lock/unlock (屏幕锁定状态)
• Performance mode hooks: 无参数
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "可用参数", + "noctalia-started-description": "Noctalia 加载完成后要执行的命令。", + "noctalia-started-label": "Noctalia 已启动", + "noctalia-started-placeholder": "例如:notify-send 'Noctalia 已加载'", + "performance-mode-disabled-description": "禁用 Noctalia 性能模式时执行的命令。", + "performance-mode-disabled-label": "性能模式已禁用", + "performance-mode-disabled-placeholder": "例如:notify-send \"性能\" \"模式已禁用\"", + "performance-mode-enabled-description": "启用 Noctalia 性能模式时执行的命令。", + "performance-mode-enabled-label": "性能模式已启用", + "performance-mode-enabled-placeholder": "例如:notify-send \"性能\" \"模式已启用\"", + "screen-lock-description": "屏幕锁定时执行的命令。", + "screen-lock-label": "屏幕已锁定", + "screen-lock-placeholder": "例如:notify-send \"屏幕\" \"已锁定\"", + "screen-unlock-description": "屏幕解锁时执行的命令。", + "screen-unlock-label": "屏幕已解锁", + "screen-unlock-placeholder": "例如:notify-send \"屏幕\" \"已解锁\"", + "session-description": "在关机或重启前执行的命令。接收操作类型作为 $1(关机/重启)。", + "session-label": "会话结束", + "session-placeholder": "例如:notify-send \"Session\" \"$1\"", + "system-hooks-desc": "配置在系统事件发生时执行的命令。", + "system-hooks-enable-description": "启用或禁用所有钩子命令。", + "system-hooks-enable-label": "启用钩子", + "system-hooks-title": "系统钩子", + "theme-changed-description": "主题在深色和浅色模式之间切换时执行的命令。", + "theme-changed-label": "主题已更改", + "theme-changed-placeholder": "例如:notify-send \"主题\" \"已切换\"", + "title": "钩子", + "wallpaper-changed-description": "壁纸更改时执行的命令。", + "wallpaper-changed-label": "壁纸已更改", + "wallpaper-changed-placeholder": "例如:notify-send \"壁纸\" \"已更改\"" + }, + "idle": { + "custom-add": "添加命令", + "custom-description": "在一段时间不活动后运行 shell 命令。", + "custom-entry-command": "命令", + "custom-entry-delete": "删除", + "custom-entry-edit": "编辑自定义命令", + "custom-entry-name": "名称", + "custom-entry-name-placeholder": "例如:关灯", + "custom-entry-new": "新建自定义命令", + "custom-entry-timeout": "空闲时间", + "custom-entry-timeout-format": "{count} 秒", + "custom-entry-timeout-format-plural": "{count} 秒", + "custom-entry-unnamed": "未命名命令", + "custom-label": "自定义空闲命令", + "enable-description": "在一段时间不活动后,自动关闭屏幕、锁定或休眠。", + "enable-label": "启用空闲管理", + "fade-duration-description": "每次操作触发前,屏幕渐变为黑的动画持续时间(秒)。任何鼠标移动都会取消渐变。", + "fade-duration-label": "淡入淡出时长", + "lock-description": "锁屏激活前的非活动秒数。", + "lock-label": "锁定屏幕", + "resume-command-label": "恢复命令", + "screen-off-description": "显示器关闭前的不活动秒数。", + "screen-off-label": "关闭屏幕", + "status-description": "由混成器报告的空闲时间。", + "status-label": "空闲时间", + "suspend-description": "系统挂起前的非活动秒数。", + "tab-behavior": "行为", + "tab-custom": "自定义", + "timeouts-description": "设置为0以禁用一个阶段。保持唤醒激活时,超时会暂停。", + "timeouts-label": "超时", + "title": "空闲", + "unavailable": "此 Compositor 不支持原生空闲监控。" + }, + "indicator": { + "default-value": "默认值:{value}", + "system-default": "系统默认" + }, + "launcher": { + "clipboard-desc": "从启动器访问和管理你的剪贴板历史记录。", + "execute-desc": "配置应用程序的启动方式。", + "execute-title": "执行", + "settings-annotation-tool-description": "点击剪贴板历史记录中的注释按钮时要运行的命令。图像将被管道传输到此命令。", + "settings-annotation-tool-label": "标注工具", + "settings-annotation-tool-placeholder": "例如:'gradia','satty -f -'", + "settings-auto-paste-description": "自动粘贴选定的剪贴板项目。需要 wtype。", + "settings-auto-paste-label": "自动粘贴", + "settings-clip-chips-description": "显示一个标签栏,以按类型(图片、链接、文件、代码等)过滤剪贴板历史记录。", + "settings-clip-chips-label": "启用类别标签", + "settings-clip-preview-description": "在使用 >clip 命令时显示剪贴板内容的预览。", + "settings-clip-preview-label": "启用剪贴板预览", + "settings-clip-smart-icons-description": "为链接、文件、颜色和代码显示特定图标,而不是通用的剪贴板图标。", + "settings-clip-smart-icons-label": "启用智能图标", + "settings-clip-wrap-text-description": "将剪贴板列表中的文本换行显示,而不是截断。", + "settings-clip-wrap-text-label": "环绕剪贴板文本", + "settings-clipboard-history-description": "从启动器访问之前复制的项目。", + "settings-clipboard-history-label": "启用剪贴板历史记录", + "settings-clipboard-watch-image-description": "传递给 wl-paste 以进行图像更改的完整命令字符串(需要重启)。", + "settings-clipboard-watch-image-label": "图像监视命令", + "settings-clipboard-watch-text-description": "传递给 wl-paste 以进行文本更改的完整命令字符串(需要重启)。", + "settings-clipboard-watch-text-label": "文本监视命令", + "settings-custom-launch-prefix-description": "使用自定义启动器前缀命令(例如:'runapp'用于systemd集成)。", + "settings-custom-launch-prefix-enabled-description": "使用自定义前缀启动应用程序,而不是默认方法。", + "settings-custom-launch-prefix-enabled-label": "启用自定义启动前缀", + "settings-custom-launch-prefix-label": "自定义启动前缀", + "settings-density-description": "调整应用程序图标大小和 Launcher 密度。", + "settings-density-label": "密度", + "settings-desc": "自定义启动器的行为和外观。", + "settings-enable-session-search-description": "在 Launcher 中搜索时显示会话操作(锁定、关机、重启等)。", + "settings-enable-session-search-label": "启用会话搜索", + "settings-enable-settings-search-description": "在启动器中搜索时显示设置结果。", + "settings-enable-settings-search-label": "启用设置搜索", + "settings-enable-windows-search-description": "搜索并聚焦活动窗口。", + "settings-enable-windows-search-label": "启用窗口搜索", + "settings-grid-view-description": "以网格布局而非列表显示项目。", + "settings-icon-mode-description": "使用本地系统图标而不是 Tabler 图标。", + "settings-icon-mode-label": "使用本地图标", + "settings-ignore-mouse-input-description": "禁用启动器中的鼠标交互和滚轮。", + "settings-ignore-mouse-input-label": "忽略鼠标输入", + "settings-overlay-layer-description": "在覆盖层上显示启动器,位于全屏窗口之上。启用时,启动器不会附加到栏。", + "settings-overlay-layer-label": "在全屏上方显示", + "settings-position-description": "选择启动器面板出现的位置。", + "settings-show-categories-description": "显示用于筛选应用程序的类别标签。", + "settings-show-categories-label": "显示类别", + "settings-show-icon-background-description": "在图标后显示一个圆角矩形背景。", + "settings-show-icon-background-label": "显示图标背景", + "settings-sort-by-usage-description": "启用后,经常启动的应用程序将显示在列表首位。", + "settings-sort-by-usage-label": "按使用频率排序", + "settings-terminal-command-description": "启动终端的命令。例如:'kitty -e'或'gnome-terminal --'。", + "settings-terminal-command-label": "终端命令", + "settings-use-app2unit-description": "使用替代启动方法以更好地管理应用程序进程并防止问题。", + "settings-use-app2unit-label": "使用 App2Unit 启动应用程序", + "settings-view-mode-description": "选择启动器条目的布局。", + "settings-view-mode-label": "视图模式", + "title": "启动器" + }, + "location": { + "auto-locate-description": "使用您的 IP 地址定期自动检测您的位置。", + "auto-locate-label": "自动定位", + "calendar-cards-desc": "在日历面板中组织和启用/禁用卡片。", + "calendar-cards-title": "日历卡", + "calendar-header-label": "日历标题", + "calendar-month-label": "日历月", + "date-time-12hour-format-description": "在锁屏和日历上以 12 小时制格式显示时间。状态栏时钟有其自己的设置。", + "date-time-12hour-format-label": "使用12小时制时间格式", + "date-time-desc": "自定义日期和时间的显示方式。", + "date-time-first-day-of-week-automatic": "自动(使用系统区域设置)", + "date-time-first-day-of-week-description": "选择日历中一周的起始日。", + "date-time-first-day-of-week-label": "一周的第一天", + "date-time-show-events-description": "在日历面板中显示事件。", + "date-time-show-events-label": "显示日历事件", + "date-time-title": "日期和时间", + "date-time-use-analog-description": "在日历窗口上显示模拟时钟。", + "date-time-use-analog-label": "使用模拟时钟样式", + "date-time-week-numbers-description": "在日历中显示一年中的第几周(例如:第 38 周)。", + "date-time-week-numbers-label": "显示周数", + "geolocate-now-button": "立即定位", + "location-desc": "通过设置您的位置获取准确的天气和调度夜间模式。", + "location-search-description": "例如:多伦多, 安大略省", + "location-search-label": "搜索位置", + "location-search-placeholder": "输入位置名称", + "location-title": "您的位置", + "weather-desc": "选择您喜欢的温度单位。", + "weather-enabled-description": "在整个界面中显示天气信息并获取天气数据。禁用后,所有天气元素将被隐藏,不会进行网络请求。", + "weather-enabled-label": "启用天气", + "weather-fahrenheit-description": "以华氏度而非摄氏度显示温度。", + "weather-fahrenheit-label": "以华氏度显示温度 (°F)", + "weather-hide-city-description": "在整个界面中隐藏天气显示中的城市名称。", + "weather-hide-city-label": "隐藏城市名称", + "weather-hide-timezone-description": "在整个界面中,隐藏天气显示中的时区缩写。", + "weather-hide-timezone-label": "隐藏时区", + "weather-show-effects-description": "在天气卡片上显示额外的视觉效果(如雨、雪或闪电)。", + "weather-show-effects-label": "显示天气效果", + "weather-show-in-calendar-description": "直接在您的日历视图中显示每日天气预报。", + "weather-show-in-calendar-label": "在日历中显示天气", + "weather-talia-mascot-always-description": "始终使用 Talia emojis 而不是 tabler icons。", + "weather-talia-mascot-always-label": "始终显示Talia天气表情" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "当 fprintd(指纹认证)处于活动状态时,此选项允许您使用密码而非指纹登录。", + "allow-password-with-fprintd-label": "允许使用 fprintd 进行密码登录", + "auto-start-auth-description": "自动启动指纹验证,无需按键或单击按钮。", + "auto-start-auth-label": "自动启动身份验证", + "clock-format-description": "使用日期/时间语法令牌自定义时钟格式。", + "clock-format-label": "时钟格式", + "clock-style-analog": "模拟", + "clock-style-custom": "自定义", + "clock-style-description": "选择锁屏时钟的视觉样式。", + "clock-style-digital": "数字", + "clock-style-label": "时钟样式", + "compact-lockscreen-description": "仅显示登录输入和系统控制,隐藏天气和媒体小部件。", + "compact-lockscreen-label": "紧凑型锁屏", + "enable-lockscreen-media-controls-description": "在锁屏界面显示交互式媒体播放控件。", + "enable-lockscreen-media-controls-label": "锁屏媒体控件", + "lock-on-suspend-description": "系统挂起时自动锁定屏幕。", + "lock-on-suspend-label": "挂起时锁定", + "lock-screen-animations-description": "启用或禁用锁屏动画。", + "lock-screen-animations-label": "锁屏动画", + "lock-screen-blur-strength-description": "对锁屏壁纸应用模糊效果。", + "lock-screen-blur-strength-label": "锁屏模糊强度", + "lock-screen-tint-strength-description": "将色调叠加应用于锁屏壁纸。", + "lock-screen-tint-strength-label": "锁屏着色强度", + "monitors-desc": "在指定显示器上显示锁屏。如果未选择,则默认为所有显示器。", + "password-chars-description": "用于隐藏密码的可爱图标。", + "password-chars-label": "随机密码图标", + "show-hibernate-description": "在电源控制中显示'休眠'选项。", + "show-hibernate-label": "显示休眠", + "show-session-buttons-description": "允许从锁屏访问电源设置。", + "show-session-buttons-label": "电源控制", + "title": "锁屏" + }, + "network": { + "bluetooth-description": "启用蓝牙管理。", + "bluetooth-rssi-polling-description": "定期通过 bluetoothctl 采样已连接设备的 RSSI 值。可能并非适用于所有设备;启用后占用极少资源。", + "bluetooth-rssi-polling-label": "蓝牙信号轮询", + "desc": "管理 Wi-Fi 和蓝牙连接。", + "wifi-description": "管理无线网络(需要 NetworkManager)。" + }, + "notifications": { + "duration-critical-urgency-description": "高优先级通知保持可见的时间。", + "duration-critical-urgency-label": "高紧急度", + "duration-desc": "根据紧急级别配置通知保持可见的时间。", + "duration-low-urgency-description": "低优先级通知保持可见的时间。", + "duration-low-urgency-label": "低紧急度", + "duration-normal-urgency-description": "正常优先级通知保持可见的时间。", + "duration-normal-urgency-label": "正常紧急度", + "duration-respect-expire-description": "使用通知中设置的过期超时。", + "duration-respect-expire-label": "尊重过期超时", + "duration-title": "通知持续时间", + "history-clear-dismiss-description": "关闭时从历史记录中清除通知。", + "history-clear-dismiss-label": "关闭时清除", + "history-critical-urgency-description": "将关键优先级通知保存到历史记录。", + "history-critical-urgency-label": "将高紧急度保存到历史记录", + "history-desc": "根据紧急级别控制哪些通知保存到历史记录。", + "history-low-urgency-description": "将低优先级通知保存到历史记录。", + "history-low-urgency-label": "将低紧急度保存到历史记录", + "history-normal-urgency-description": "将正常优先级通知保存到历史记录。", + "history-normal-urgency-label": "将正常紧急度保存到历史记录", + "monitors-desc": "在特定显示器上显示通知。如果未选择,则默认为全部。", + "rules-action-block": "禁止", + "rules-action-block-desc": "完全禁止通知", + "rules-action-hide": "隐藏", + "rules-action-hide-desc": "无弹窗,无声音,添加到历史记录。", + "rules-action-label": "操作", + "rules-action-mute": "静音", + "rules-action-mute-desc": "无声音,仍显示弹窗并记录在历史中。", + "rules-add": "添加规则", + "rules-delete": "删除规则", + "rules-description": "匹配应用名称或内容。规则按顺序检查,并应用第一个匹配项。", + "rules-edit": "编辑规则", + "rules-label": "筛选规则", + "rules-pattern-label": "模式", + "rules-pattern-placeholder": "firefox, discord, 或 /regex/", + "rules-tab": "规则", + "settings-always-on-top-description": "在全屏窗口和其他图层上方显示通知。", + "settings-background-opacity-description": "调整通知背景的透明度。", + "settings-density-description": "选择通知卡密度。", + "settings-density-label": "密度", + "settings-desc": "配置通知的外观和行为。", + "settings-do-not-disturb-description": "启用后禁用所有通知弹出窗口。", + "settings-enabled-description": "启用或禁用通知守护进程,需要重启 Noctalia shell。", + "settings-enabled-label": "启用通知", + "settings-location-description": "通知在屏幕上出现的位置。", + "settings-markdown-description": "使用 Markdown 格式渲染通知内容。", + "settings-markdown-label": "启用 Markdown", + "sounds-desc": "配置通知音效和音量。", + "sounds-enabled-description": "启用收到通知时的音效。", + "sounds-enabled-label": "启用通知声音", + "sounds-excluded-apps-description": "对于自带内置音效的特定应用,跳过播放已配置的通知音效。", + "sounds-excluded-apps-label": "排除的应用程序", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "为高优先级通知播放的声音文件路径。", + "sounds-files-critical-label": "高紧急度声音", + "sounds-files-critical-select-title": "选择高紧急度声音文件", + "sounds-files-desc": "为不同的通知紧急级别配置自定义声音文件。", + "sounds-files-low-description": "为低优先级通知播放的声音文件路径。", + "sounds-files-low-label": "低紧急度声音", + "sounds-files-low-select-title": "选择低紧急度声音文件", + "sounds-files-normal-description": "为正常优先级通知播放的声音文件路径。", + "sounds-files-normal-label": "正常紧急度声音", + "sounds-files-normal-select-title": "选择正常紧急度声音文件", + "sounds-files-placeholder": "输入声音文件路径", + "sounds-files-select-file": "选择声音文件", + "sounds-files-title": "声音文件", + "sounds-files-unified-description": "为通知播放的声音文件路径。", + "sounds-files-unified-label": "通知声音", + "sounds-files-unified-select-title": "选择通知声音文件", + "sounds-separate-description": "为低、正常和高优先级通知使用不同的声音文件。", + "sounds-separate-label": "按优先级使用不同声音", + "sounds-title": "声音设置", + "sounds-unavailable-description": "安装 Qt6 Multimedia 以启用通知声音。", + "sounds-unavailable-label": "通知声音不可用", + "sounds-volume-description": "调整通知声音的音量级别。", + "sounds-volume-label": "声音音量", + "toast-battery-description": "当电池电量低于此百分比时显示警告。", + "toast-battery-label": "低电量警告", + "toast-desc": "配置气泡通知的外观和行为。", + "toast-keyboard-description": "键盘布局改变时显示一个 Toast。", + "toast-keyboard-label": "键盘布局", + "toast-media-description": "媒体播放状态改变时显示一个 Toast。", + "toast-media-label": "媒体" + }, + "osd": { + "always-on-top-description": "在全屏窗口和其他图层之上显示 OSD。", + "always-on-top-label": "始终置顶", + "background-opacity-description": "调整 OSD 背景的不透明度。", + "background-opacity-label": "背景不透明度", + "description": "配置用于在屏幕亮度、音量等状态发生变化时提供反馈的 OSD。", + "duration-auto-hide-description": "调整 OSD 消失前的时间。", + "duration-auto-hide-label": "自动隐藏超时", + "duration-desc": "屏幕显示自动隐藏前保持可见的时长。", + "duration-title": "自动隐藏超时", + "enabled-description": "实时显示音量与亮度变化。", + "enabled-label": "启用状态反馈(OSD)", + "events-desc": "选择哪些事件触发屏幕显示。", + "general-desc": "配置 OSD 的可见性和行为。", + "location-description": "OSD 出现的位置。", + "monitors-desc": "在特定显示器上显示 OSD。若未选择,则默认全部。", + "title": "状态反馈", + "types-brightness-description": "当屏幕亮度发生变化时显示 OSD。", + "types-brightness-label": "亮度", + "types-custom-text-description": "显示来自 IPC 的自定义文本消息的 OSD。", + "types-custom-text-label": "自定义文本", + "types-desc": "选择触发 OSD 的事件。", + "types-input-volume-description": "当麦克风音量发生变化时显示 OSD。", + "types-input-volume-label": "输入音量", + "types-lockkey-description": "当切换大写锁定、数字锁定或滚动锁定时显示 OSD。", + "types-lockkey-label": "锁定键", + "types-media-description": "当媒体播放状态改变(播放、暂停、跳过)时显示 OSD 。", + "types-media-label": "媒体播放", + "types-title": "OSD 触发事件", + "types-volume-description": "当音频输出音量发生变化时显示 OSD。", + "types-volume-label": "输出音量" + }, + "plugins": { + "auto-update": "自动更新插件", + "auto-update-description": "在 shell 启动时自动更新所有插件。", + "available-description": "浏览并安装来自已配置源的插件。", + "available-label": "可用插件", + "available-no-plugins-description": "检查你的插件源或刷新列表。", + "available-no-plugins-label": "没有可用的插件", + "check-for-updates": "检查更新", + "checking-for-updates": "正在检查更新...", + "collision-already-installed": "此插件已安装", + "collision-custom-version-exists": "来自“{source}”的自定义版本已安装", + "collision-official-version-exists": "此插件的官方版本已安装", + "development-disable": "禁用此插件的开发模式", + "development-enable": "为此插件启用开发模式。", + "filter-downloaded": "已下载", + "filter-not-downloaded": "未下载", + "filter-tags-description": "按类别或下载状态筛选插件。", + "filter-tags-label": "标签", + "hot-reload-description": "当插件文件发生更改时自动重新加载插件。对插件开发很有用。", + "hot-reload-label": "热重载(开发模式)", + "hot-reloaded": "已重新加载插件:{name}", + "install-error": "安装失败:{error}", + "install-incompatible": "{plugin} 需要 Noctalia v{version} 或更高版本", + "install-success": "成功安装 {plugin}", + "installed-description": "管理和配置所有本地安装的插件。", + "installed-label": "已安装插件", + "installed-no-plugins-description": "从“可用”部分安装插件。", + "installed-no-plugins-label": "未安装插件", + "installing": "正在安装 {plugin}...", + "notify-updates": "插件更新通知", + "notify-updates-description": "当有新的插件更新可用时,显示通知弹窗。", + "open-plugin-page": "打开插件页面", + "open-plugins-tab": "打开插件设置", + "plugin-settings-title": "{plugin} 设置", + "refresh-refreshing": "正在刷新插件列表...", + "refresh-tooltip": "刷新可用插件", + "settings-error-not-loaded": "插件未加载", + "settings-saved": "插件设置已保存", + "settings-tooltip": "插件设置", + "source-custom": "自定义源", + "sources-add-custom": "添加自定义存储库", + "sources-add-dialog-description": "添加一个 GitHub 仓库作为插件源。", + "sources-add-dialog-error": "无法添加插件源", + "sources-add-dialog-name": "仓库名称", + "sources-add-dialog-name-placeholder": "我的自定义插件", + "sources-add-dialog-success": "插件源添加成功", + "sources-add-dialog-title": "添加插件源", + "sources-add-dialog-url": "仓库 URL", + "sources-description": "管理插件仓库。", + "sources-placeholder": "我的酷仓库", + "sources-remove-tooltip": "移除插件源", + "title": "插件", + "translations-reloaded": "已重新加载翻译:{name}", + "uninstall-dialog-description": "你确定要卸载 {plugin} 吗? 这将移除所有插件数据。", + "uninstall-dialog-title": "卸载插件", + "uninstall-error": "卸载失败:{error}", + "uninstall-success": "成功卸载 {plugin}", + "uninstalling": "正在卸载 {plugin}...", + "update-all": "全部更新({count})", + "update-all-success": "所有插件已成功更新", + "update-available": "新插件更新可用", + "update-available-plural": "新插件更新可用({count}个)", + "update-error": "更新插件失败:{plugin}:{error}", + "update-pending": "v{current} → v{new} (需要 Noctalia v{required})", + "update-success": "已将 {plugin} 更新至 v{version}", + "update-version": "v{current} → v{new}", + "updating": "正在更新..." + }, + "region": { + "title": "地区" + }, + "session-menu": { + "countdown-duration-description": "设置倒计时计时器在执行电源操作之前持续多长时间。", + "countdown-duration-label": "倒计时持续时间", + "enable-countdown-description": "在执行电源操作之前显示倒计时计时器。", + "enable-countdown-label": "启用倒计时计时器", + "entries-desc": "自定义会话菜单中显示哪些电源操作及其顺序。", + "entries-title": "电源操作", + "entry-settings-command-description": "为此操作执行的自定义命令。留空则使用默认系统命令。", + "entry-settings-command-placeholder": "例如:systemctl poweroff", + "entry-settings-default-command-lock": "内部锁屏(无命令)", + "entry-settings-default-command-logout": "内部注销(无命令)", + "entry-settings-default-info-description": "如果没有指定自定义命令,将使用默认的系统命令。", + "entry-settings-default-info-label": "默认命令", + "entry-settings-keybind-description": "当会话菜单打开时,分配一个按键组合来触发此操作。", + "entry-settings-keybind-placeholder": "按下录制以分配一个键...", + "entry-settings-keybind-recording": "录制中... 按任意组合键", + "entry-settings-title": "配置 {entry}", + "entry-settings-tooltip": "配置命令", + "general-desc": "配置会话菜单面板的行为和外观。", + "large-buttons-layout-description": "选择会话菜单按钮的显示方式。", + "large-buttons-layout-label": "大按钮布局", + "large-buttons-style-description": "以网格布局显示带有大按钮的会话菜单。", + "large-buttons-style-label": "大按钮样式", + "position-description": "选择会话菜单面板打开时出现的位置。", + "show-header-description": "在会话菜单顶部显示标题和关闭按钮。", + "show-header-label": "显示标题", + "show-keybinds-description": "在会话选项上显示快捷键提示。", + "show-keybinds-label": "显示快捷键" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "在 Noctalia 性能模式启用时保持桌面小部件可见。", + "noctalia-performance-disable-desktop-widgets-label": "在性能模式下启用桌面小部件", + "noctalia-performance-disable-wallpaper-description": "在启用 Noctalia 性能模式时,保持桌面、概览和锁屏壁纸可见。", + "noctalia-performance-disable-wallpaper-label": "在性能模式下启用壁纸渲染", + "title": "系统" + }, + "system-monitor": { + "critical-color-label": "严重颜色", + "custom-highlight-colors-title-label": "自定义高亮颜色", + "disk-available-label": "可用磁盘空间", + "disk-section-label": "磁盘使用率", + "enable-dgpu-monitoring-description": "警告:这将唤醒您的独立显卡 (NVIDIA/AMD),这可能会显著影响配备混合显卡的笔记本电脑的电池续航时间。", + "enable-dgpu-monitoring-label": "启用独立显卡监控", + "external-monitor-description": "输入启动外部系统监视器应用程序时要执行的命令或应用程序路径。", + "external-monitor-label": "外部系统监视器命令", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "配置系统监视器的行为和外观。", + "gpu-section-label": "GPU温度", + "highlight-colors-section-label": "高亮颜色", + "polling-interval-label": "轮询间隔", + "polling-section-description": "配置每个系统指标的更新频率。", + "polling-section-label": "轮询间隔", + "threshold-critical": "严重", + "threshold-warning": "警告", + "thresholds-section-description": "为每个系统指标调整警告/严重阈值和轮询间隔。", + "use-custom-highlight-colors-description": "禁用时将使用主题默认高亮颜色。", + "use-custom-highlight-colors-label": "使用自定义高亮颜色", + "warning-color-label": "警告颜色" + }, + "user-interface": { + "allow-panels-without-bar-description": "启用后,面板可以在任何屏幕上打开。禁用后,面板将仅在有栏的屏幕上打开,这可以减少内存使用。", + "allow-panels-without-bar-label": "允许在没有栏的屏幕上显示面板", + "animation-disable-description": "禁用所有动画以获得更快、更流畅的体验。", + "animation-disable-label": "禁用 UI 动画", + "animation-speed-description": "调整全局动画速度。", + "animation-speed-label": "动画速度", + "animation-speed-reset": "重置动画速度", + "appearance-desc": "自定义视觉元素,如提示信息、边框和阴影。", + "blur-behind-description": "使用合成器模糊协议,模糊栏和面板后面的区域。", + "blur-behind-label": "背景模糊", + "box-border-description": "显示内容区域周围的轮廓。", + "box-border-label": "容器轮廓", + "box-border-radius-description": "调整主要布局部分(如侧边栏、卡片和内容面板)的圆角程度。", + "box-border-radius-label": "容器半径", + "control-border-radius-description": "控制交互元素的曲率,包括按钮、开关和文本字段。", + "control-border-radius-label": "输入半径", + "desc": "自定义界面的外观、感觉和行为。", + "dim-desktop-description": "当面板或菜单打开时,桌面变暗。", + "dim-desktop-label": "昏暗的桌面", + "dimmer-opacity-description": "设置桌面变暗时的不透明度。", + "dimmer-opacity-label": "变暗桌面不透明度", + "panel-background-opacity-description": "设置所有面板(栏、启动器、设置等)的背景不透明度。", + "panel-background-opacity-label": "面板背景不透明度", + "panels-attached-to-bar-description": "面板锁定到边栏和屏幕边缘,形成无缝外观,并具有时尚的倒角。", + "panels-attached-to-bar-label": "将面板对齐到边缘", + "panels-overlay-description": "确保面板和栏保持可见,即使在全屏应用程序上也是如此。", + "panels-overlay-label": "将面板和栏保持在顶部", + "scaling-description": "更改通用用户界面大小,不包括栏。", + "scaling-label": "界面缩放", + "scrollbar-always-visible-description": "当内容可滚动时,保持滚动条始终可见,而不是仅在悬停时显示。", + "scrollbar-always-visible-label": "始终显示滚动条", + "settings-panel-header": "设置面板", + "settings-panel-mode-description": "选择「设置」面板的布局(可能需要重新打开「设置」面板才会生效)。", + "settings-panel-mode-label": "「设置」面板模式", + "settings-panel-sidebar-card-style-description": "用圆角填充背景包裹设置侧边栏。", + "settings-panel-sidebar-card-style-label": "侧边栏卡片样式", + "shadows-description": "启用条形图和面板下的阴影。", + "shadows-direction-description": "选择阴影投射的位置。", + "shadows-direction-label": "阴影方向", + "shadows-label": "阴影", + "title": "用户界面", + "tooltips-description": "启用或禁用整个界面的提示信息。", + "tooltips-label": "显示提示信息(Tooltip)", + "translucent-widgets-description": "使面板内的按钮、选项卡和其他小部件半透明。", + "translucent-widgets-label": "半透明小部件" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "按字母顺序", + "automation-change-mode-description": "选择自动更改时如何选择壁纸。", + "automation-change-mode-label": "更改模式", + "automation-custom-interval-description": "以 HH:MM 格式输入时间(例如:01:30)。", + "automation-custom-interval-label": "自定义间隔", + "automation-interval-description": "自动更改壁纸的频率。", + "automation-interval-label": "壁纸间隔", + "automation-random-wallpaper-description": "按固定时间间隔随机更改壁纸。", + "automation-scheduled-change-description": "按固定时间间隔自动更改壁纸。", + "automation-scheduled-change-label": "定时更改", + "look-feel-edge-smoothness-description": "对过渡边缘应用柔和、羽化的效果。", + "look-feel-edge-smoothness-label": "软化过渡边缘", + "look-feel-fill-color-description": "选择可能出现在壁纸后面的填充颜色。", + "look-feel-fill-mode-description": "选择图像应如何缩放以匹配显示器的分辨率。", + "look-feel-fill-mode-label": "填充模式", + "look-feel-skip-startup-transition-description": "在 shell 启动时跳过壁纸动画。", + "look-feel-skip-startup-transition-label": "跳过启动过渡动画", + "look-feel-title": "外观与体验", + "look-feel-transition-duration-description": "过渡动画的持续时间(秒)。", + "look-feel-transition-duration-label": "过渡持续时间", + "look-feel-transition-type-description": "切换壁纸时的动画类型。", + "look-feel-transition-type-label": "过渡类型", + "settings-clear-cache-button": "清除缓存", + "settings-clear-cache-description": "清除缓存的已调整大小的壁纸以释放磁盘空间。", + "settings-clear-cache-label": "壁纸缓存", + "settings-clear-cache-toast": "壁纸缓存已清除", + "settings-desc": "控制壁纸的管理和显示方式。", + "settings-enable-management-description": "使用 Noctalia 管理壁纸。如果您更喜欢使用其他应用程序,请取消选中。", + "settings-enable-management-label": "启用壁纸管理", + "settings-enable-overview-description": "将模糊且变暗的壁纸应用于概览屏幕。", + "settings-enable-overview-label": "启用概览壁纸", + "settings-folder-description": "您的主壁纸文件夹路径。", + "settings-folder-label": "壁纸文件夹", + "settings-hide-wallpaper-filenames-tooltip-hide": "隐藏文件名", + "settings-hide-wallpaper-filenames-tooltip-show": "显示文件名", + "settings-monitor-specific-description": "为每个显示器设置不同的壁纸文件夹。", + "settings-monitor-specific-label": "显示器特定目录", + "settings-monitor-specific-tooltip": "显示器壁纸文件夹", + "settings-overview-blur-strength-description": "将模糊强度应用于概览。", + "settings-overview-blur-strength-label": "概览模糊强度", + "settings-overview-tint-description": "将着色强度应用到概览。", + "settings-overview-tint-label": "概览着色强度", + "settings-recursive-search-description": "同时在壁纸目录的子文件夹中搜索壁纸。", + "settings-recursive-search-label": "搜索子文件夹", + "settings-select-monitor-folder": "选择显示器壁纸文件夹", + "settings-selector-description": "选择你的壁纸。", + "settings-selector-position-description": "选择壁纸选择面板的显示位置。", + "settings-show-hidden-files-tooltip-hide": "不显示隐藏文件", + "settings-show-hidden-files-tooltip-show": "显示隐藏文件", + "settings-title": "壁纸设置", + "settings-use-original-images-description": "跳过在显示前调整壁纸大小。节省磁盘空间并减少壁纸更换时的 CPU 使用率,但对于非常大的图像可能会占用更多内存。", + "settings-use-original-images-label": "使用原始图片", + "settings-view-mode-description": "选择从您的目录显示壁纸的方式。", + "settings-view-mode-label": "查看模式", + "view-mode-browse": "浏览目录", + "view-mode-cycle-tooltip": "查看模式:{mode}(点击更改)", + "view-mode-recursive": "扁平化子目录", + "view-mode-single": "根目录" + } + }, + "placeholders": { + "add-new-keybind": "添加新键位绑定", + "command-example": "echo \"Hello World\"", + "enter-command": "输入要执行的命令(应用程序或自定义脚本)", + "enter-ipc-identifier": "输入 IPC 命令的唯一标识符", + "enter-path": "输入路径...", + "enter-text-to-collapse": "例如:“nothing is playing”。使用 /regex/ 表示匹配模式。", + "enter-tooltip": "输入提示信息", + "enter-width-pixels": "输入宽度(像素)", + "keybind-recording": "正在录制按键绑定...", + "search": "搜索...", + "search-icons": "例如:noctalia, niri, battery, cloud", + "search-launcher": "搜索条目...或使用 > 执行命令", + "search-wallhaven": "搜索 Wallhaven...", + "search-wallpapers": "输入以筛选壁纸..." + }, + "positions": { + "bottom": "底部", + "bottom-center": "底部居中", + "bottom-left": "左下", + "bottom-right": "右下", + "center": "居中", + "center-left": "左侧居中", + "center-right": "右侧居中", + "close-to-bar": "靠近栏按钮", + "follow-bar": "跟随栏", + "left": "左", + "right": "右", + "top": "顶部", + "top-center": "顶部居中", + "top-left": "左上", + "top-right": "右上" + }, + "session-menu": { + "action-in-seconds": "在 {seconds} 秒后将{action}...", + "cancel-timer": "取消定时器", + "title": "会话菜单" + }, + "setup": { + "all-done": "完成!", + "appearance": { + "subheader": "选择深色模式和颜色来源(壁纸或预设)。" + }, + "customize": { + "header": "自定义体验", + "subheader": "调整状态栏位置、密度、缩放等。" + }, + "skip-setup": "跳过设置", + "telemetry-wizard-done": "明白了!", + "telemetry-wizard-note": "尽在掌控——您随时可在设置中启用或禁用此功能", + "telemetry-wizard-subtitle": "我们添加了匿名分析功能以帮助改进 Noctalia", + "telemetry-wizard-title": "隐私权更新", + "wallpaper": { + "choose-dir": "选择包含壁纸图片的目录", + "dir-browse": "浏览文件夹", + "dir-description": "选择存放壁纸的文件夹。", + "dir-label": "壁纸目录", + "dir-select-title": "选择壁纸文件夹", + "header": "选择你的壁纸", + "no-dir": "未选择壁纸目录", + "no-valid": "在 {dir} 中未找到有效图片文件", + "none-in-dir": "目录中未找到壁纸", + "preview-error": "图片加载失败", + "select-prompt": "在下方选择一张壁纸", + "subheader": "用精美壁纸营造氛围。" + }, + "welcome": "欢迎", + "welcome-note": "先进行一些基础设置——更多选项可在“设置”中找到", + "welcome-subtitle": "让我们一起打造独一无二的桌面", + "welcome-title": "欢迎来到 Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "未检测到媒体播放器", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "未知应用", + "uptime": "系统运行时间:{uptime}", + "user-requested": "用户请求", + "welcome-back": "欢迎回来,", + "widget-settings-title": "{widget} 设置" + }, + "system-monitor": { + "core-usage": "核心 {id} 使用率", + "cpu-temp": "CPU 温度", + "cpu-usage": "CPU 使用率", + "disk": "磁盘", + "download-speed": "下载速度", + "gpu-temp": "GPU 温度", + "load-average": "平均负载", + "title": "系统监视器", + "upload-speed": "上传速度" + }, + "toast": { + "airplane-mode": { + "description": "禁用所有无线通信。", + "title": "飞行模式" + }, + "battery": { + "critical": "电量严重不足", + "critical-desc": "电量仅剩 {percent}% — 请立即连接充电器", + "low": "电池电量低", + "low-desc": "电量仅剩 {percent}% — 请连接充电器" + }, + "bluetooth": { + "address-copied": "地址已复制到剪贴板", + "auto-connect-disabled": "自动连接已禁用", + "auto-connect-enabled": "自动连接已启用", + "auto-connecting": "正在连接 {count} 个设备...", + "confirm-code": "在另一台设备上确认代码 {value}。", + "connect-failed": "无法连接到设备", + "disconnect-failed": "无法断开与设备的连接", + "discoverable-change-failed": "无法更改可发现状态", + "discoverable-disabled": "可发现性已禁用", + "discoverable-enabled": "已启用可发现性", + "display-code": "在另一台设备上输入代码 {value}。", + "forget-failed": "无法忘记设备", + "pair-failed": "配对设备失败", + "passkey-required": "设备需要通行密钥。", + "pincode-required": "设备需要PIN码。", + "state-change-failed": "无法更改蓝牙状态" + }, + "clipboard": { + "long-text": "长文本", + "unavailable": "剪贴板历史记录不可用", + "unavailable-desc": "未安装 'cliphist' 应用程序。请安装它以使用剪贴板历史记录功能" + }, + "custom-command-failed": { + "description": "命令失败: {command}\\n退出代码: {code}", + "title": "自定义命令失败" + }, + "do-not-disturb": { + "disabled": "'勿扰模式'已禁用", + "disabled-desc": "显示所有通知", + "enabled": "'勿扰模式'已启用", + "enabled-desc": "您可以在历史记录中找到这些通知" + }, + "donation-opened": "捐赠页面已在您的浏览器中打开", + "internet-limited": "已连接但无互联网", + "keyboard-layout": { + "changed": "键盘布局已更改为 {layout}", + "title": "键盘" + }, + "missing-control-center": { + "description": "控制中心小部件已从状态栏中移除。要再次从状态栏访问它,您需要重新添加小部件。您也可以通过右键点击状态栏来打开它", + "label": "最后一个控制中心小部件已移除" + }, + "night-light": { + "forced": "强制激活", + "normal": "正常模式", + "not-installed": "未安装 wlsunset" + }, + "noctalia-performance": { + "disabled": "性能模式已禁用", + "enabled": "已启用性能模式", + "label": "Noctalia 性能模式" + }, + "power-profile": { + "changed": "电源模式已更改", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "处理模板时遇到错误", + "title": "模板处理失败" + }, + "vpn": { + "connected": "已连接到“{name}”", + "disconnected": "已断开与“{name}”的连接" + }, + "wallpaper-colors": { + "disabled": "壁纸颜色已禁用", + "enabled": "壁纸颜色已启用", + "label": "壁纸颜色生成" + }, + "wifi": { + "connected": "已连接到 '{ssid}'", + "connection-failed": "连接失败", + "connection-timeout": "连接超时", + "disconnected": "已断开与 '{ssid}' 的连接", + "incorrect-password": "密码错误", + "network-not-found": "未找到网络" + } + }, + "tooltips": { + "add-widget": "添加小部件", + "auto-connect": "切换此设备的自动连接", + "bluetooth-auto-connect-off": "自动连接已禁用", + "bluetooth-auto-connect-on": "自动连接已启用", + "bluetooth-devices": "蓝牙设备", + "brightness-at": "亮度:{brightness}%", + "click-to-start-recording": "屏幕录制器(开始录制)", + "click-to-stop-recording": "屏幕录制器(停止录制)", + "collapse": "折叠侧边栏", + "copy-address": "复制地址", + "delete-notification": "删除通知", + "dismiss-notification": "关闭通知", + "do-not-disturb-enabled": "勿扰模式", + "expand": "展开侧边栏", + "forget-network": "忘记网络", + "grid-view": "网格视图", + "hidden-files-hide": "隐藏文件", + "hide-unnamed-devices": "隐藏未命名的设备", + "home": "主目录", + "input-muted": "切换输入静音", + "keep-awake": "保持唤醒", + "keyboard-layout": "{layout} 键盘布局", + "list-view": "列表视图", + "manage-vpn": "VPN 连接", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "已达到最大小部件数量", + "microphone-volume-at": "麦克风音量:{volume}%", + "move-to-section": "移至{section}", + "mute": "静音", + "next-media": "下一首", + "next-month": "下个月", + "night-light-not-installed": "夜间模式(不可用)", + "noctalia-performance-enabled": "Noctalia 性能模式", + "open-annotation-tool": "使用注释工具打开", + "open-control-center": "控制中心", + "open-notification-history-enable-dnd": "通知历史记录", + "open-settings": "设置", + "open-tray-dropdown": "系统托盘", + "output-muted": "切换输出静音", + "power-profile": "{profile} 电源模式", + "previous-media": "上一首", + "previous-month": "上个月", + "refresh-devices": "刷新设备", + "refresh-wallhaven": "刷新 Wallhaven 结果", + "refresh-wallpaper-list": "刷新壁纸列表", + "remove-widget": "移除小部件", + "search-close": "关闭搜索", + "session-menu": "会话菜单", + "show-all-devices": "显示所有设备", + "switch-to-dark-mode": "深色模式", + "switch-to-light-mode": "浅色模式", + "unmute": "取消静音", + "up": "上级目录", + "volume-at": "输出音量:{volume}%", + "wallpaper-selector": "壁纸选择器" + }, + "wallpaper": { + "browse": { + "empty-directory": "此目录为空。", + "go-root": "转到壁纸根目录", + "go-up": "转到父文件夹", + "go-up-hint": "使用后退按钮向上导航。" + }, + "configure-directory": "配置您的壁纸图片目录。", + "fill-modes": { + "crop": "裁剪(填充)", + "fit": "适应(包含)", + "repeat": "重复(平铺)", + "stretch": "拉伸" + }, + "no-match": "未找到匹配项。", + "no-wallpaper": "未找到壁纸。", + "panel": { + "apikey-help": "访问 NSFW 内容需要 API 密钥。", + "apikey-label": "API 密钥", + "apikey-managed-by-env": "通过 NOCTALIA_WALLHAVEN_API_KEY 环境变量进行管理。", + "apikey-placeholder": "输入您的 Wallhaven API 密钥", + "appearance-dark-tab": "深色", + "appearance-light-tab": "浅色", + "categories-anime": "动漫", + "categories-label": "分类", + "categories-people": "人物", + "color-extraction-disabled": "使用壁纸颜色提取", + "color-extraction-enabled": "使用预设配色方案", + "header-devices-apply-all-tooltip": "为每个显示器选择壁纸", + "header-devices-per-monitor-tooltip": "应用到所有显示器", + "header-link-light-dark-tooltip": "切换主题时保留壁纸", + "header-separate-light-dark-tooltip": "随主题自动切换壁纸", + "header-sun-linked-tooltip": "分离浅色和深色壁纸", + "header-sun-separate-tooltip": "明暗模式使用相同壁纸", + "order-asc": "升序", + "order-desc": "降序", + "order-label": "顺序", + "purity-label": "内容过滤器", + "purity-nsfw": "少儿不宜", + "purity-sfw": "安全", + "purity-sketchy": "可疑", + "ratios-all-portrait": "纵向", + "ratios-all-wide": "横向", + "ratios-any": "任意", + "ratios-label": "宽高比", + "resolution-atleast": "至少", + "resolution-exact": "精确", + "resolution-label": "分辨率", + "resolution-mode-label": "匹配模式", + "solid-color-tooltip": "纯色背景", + "sort-date-asc": "按最早排序", + "sort-date-desc": "按最新排序", + "sort-name-asc": "按名称排序 (A-Z)", + "sort-name-desc": "按名称排序 (Z-A)", + "sort-random": "随机排序", + "sorting-date-added": "上传日期", + "sorting-favorites": "收藏量", + "sorting-label": "排序方式", + "sorting-relevance": "相关性", + "sorting-toplist": "排行榜", + "sorting-views": "浏览量", + "source-label": "来源", + "source-wallhaven": "Wallhaven", + "title": "壁纸选择器", + "wallhaven-settings-title": "Wallhaven 设置" + }, + "transitions": { + "disc": "圆盘", + "fade": "淡入淡出", + "honeycomb": "蜂巢", + "pixelate": "像素化", + "stripes": "条纹", + "wipe": "擦除" + }, + "try-different-search": "尝试不同的搜索查询。", + "wallhaven": { + "loading": "正在加载壁纸...", + "no-results": "未找到壁纸。请尝试不同的搜索查询。", + "page": "{current} / {total}", + "page-prefix": "页面", + "page-suffix": "共 {total} 个" + } + }, + "weather": { + "clear-sky": "晴朗", + "drizzle": "毛毛雨", + "fog": "雾", + "mainly-clear": "基本晴朗", + "overcast": "阴天", + "partly-cloudy": "局部多云", + "rain-showers": "阵雨", + "snow": "雪", + "thunderstorm": "雷暴" + }, + "widgets": { + "color-picker": { + "palette-description": "从各种预定义颜色中选择。", + "palette-label": "调色板", + "palette-theme-colors": "快速访问您主题的调色板。", + "title": "颜色选择器" + }, + "datetime-tokens": { + "ampm-lowercase": "am/pm 小写", + "ampm-uppercase": "AM/PM 大写", + "common-12hour-time-minutes": "12小时制带分钟", + "common-24hour-time-minutes": "24小时制带分钟", + "common-24hour-time-seconds": "24小时制带秒", + "common-european-date": "欧洲日期格式", + "common-iso-date": "ISO 日期格式", + "common-us-date": "美国日期格式", + "common-weekday-date": "星期和日期", + "common-weekday-month-day": "星期、月份和日期", + "day-abbreviated": "星期名称缩写", + "day-full": "星期名称全称", + "day-leading-zero": "日期有前导零 (01-31)", + "day-no-leading-zero": "日期无前导零 (1-31)", + "hour-leading-zero": "小时有前导零 (00-23) — 24小时制", + "hour-no-leading-zero": "小时无前导零 (0-23) — 24小时制", + "minute-leading-zero": "分钟有前导零 (00-59)", + "minute-no-leading-zero": "分钟无前导零 (0-59)", + "month-abbreviated": "月份名称缩写", + "month-full": "月份名称全称", + "month-number-leading-zero": "月份数字有前导零 (01-12)", + "month-number-no-zero": "月份数字无前导零 (1-12)", + "second-leading-zero": "秒有前导零 (00-59)", + "second-no-leading-zero": "秒无前导零 (0-59)", + "timezone-abbreviation": "时区缩写", + "year-four-digit": "年份为四位数", + "year-two-digit": "年份为两位数 (00-99)" + }, + "file-picker": { + "item": "项目", + "items": "项目", + "search-placeholder": "搜索文件和文件夹...", + "select-current": "选择当前", + "select-file": "选择文件", + "select-folder": "选择文件夹", + "selected": "已选择:", + "title": "文件选择器" + }, + "file-picker-title": "选择文件", + "icon-picker": { + "title": "图标选择器" + }, + "tooltip-placeholder": "占位符" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "匿名身份", + "ca-cert": "CA 证书", + "eap-method": "EAP 方法", + "password": "用户密码", + "phase2-auth": "第二阶段认证", + "username": "身份 / 用户名" + }, + "panel": { + "action-required": "需要动作", + "add-hidden-network": "添加隐藏网络", + "add-network": "添加网络", + "available-interfaces": "可用接口", + "available-networks": "可用网络", + "disabled": "Wi-Fi 已禁用", + "disconnecting": "正在断开连接...", + "dns": "DNS", + "enable-message": "启用 Wi-Fi 以查看可用网络。", + "enter-password": "输入密码...", + "forget": "忘记", + "forget-network": "忘记此网络?", + "forgetting": "正在忘记...", + "hidden-network": "隐藏网络", + "interface": "网络接口", + "internet-connected": "已连接互联网", + "internet-limited": "无网络", + "internet-status": "互联网状态", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "已知网络", + "link-speed": "链路速度", + "network-name-ssid": "网络名称 (SSID)", + "no-ethernet-devices": "未检测到以太网设备", + "no-networks": "未找到网络", + "saved": "已保存", + "scan-again": "重新扫描", + "searching": "正在搜索附近网络...", + "security-open": "打开", + "security-wep": "WEP", + "security-wpa": "WPA/WPA2/WPA3 个人", + "security-wpa-ent": "WPA 企业版", + "security-wpa2-ent": "WPA2 企业版", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 企业版", + "show-password": "显示密码", + "title": "Wi-Fi" + }, + "signal": { + "excellent": "优秀", + "fair": "一般", + "good": "好", + "poor": "差", + "weak": "弱" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Translations/zh-TW.json b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/zh-TW.json new file mode 100644 index 0000000..9cdd000 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/Translations/zh-TW.json @@ -0,0 +1,2252 @@ +{ + "actions": { + "clear-history": "清除歷史", + "close-app": "關閉 {app}", + "connect-vpn": "連線至 {name}", + "cycle-visualizer": "切換視覺效果樣式", + "disable-bluetooth": "關閉藍牙", + "disable-dnd": "停用勿擾模式", + "disable-wifi": "關閉 Wi-Fi", + "disconnect-vpn": "自 {name} 中斷連線", + "dock-settings": "Dock 設定", + "enable-bluetooth": "啟用藍牙", + "enable-dnd": "啟用勿擾模式", + "enable-wifi": "啟用 Wi-Fi", + "launcher-settings": "啟動器設定", + "lower-to-bottom": "下移至底部", + "open-calendar": "開啟行事曆", + "open-display-settings": "顯示器設定", + "open-launcher": "打開啟動器", + "open-settings": "開啟設定", + "raise-to-top": "上移至頂部", + "random-wallpaper": "隨機桌布", + "run-custom-command": "執行自訂指令", + "show-active-only": "僅顯示作用中", + "show-all": "顯示全部", + "toggle-mute": "靜音開關", + "widget-settings": "小工具設定" + }, + "authentication": { + "error": "驗證錯誤", + "failed": "驗證失敗" + }, + "bar": { + "active-window": { + "colorize-icons-description": "將活動視窗的圖示套用主題用色", + "hide-mode-description": "控制小工具在沒有活動視窗時的行為", + "scrolling-mode-description": "控制視窗長標題時是否啟用文字捲動", + "show-app-icon-description": "在視窗標題旁顯示程式圖示", + "show-app-icon-label": "顯示應用程式的圖示", + "show-app-text-description": "顯示活動視窗的標題。", + "show-app-text-label": "顯示視窗標題" + }, + "audio-visualizer": { + "color-name-description": "選取視覺效果的顏色", + "color-name-label": "填充顏色", + "height-description": "自訂元件寬度。", + "hide-when-idle-description": "當啟用時, 如果沒有正在播放的媒體就隱藏視覺效果", + "hide-when-idle-label": "無任何媒體播放時隱藏", + "width-description": "自訂部件寬度" + }, + "battery": { + "device-default": "預設 (顯示裝置)", + "device-description": "選取要顯示哪個裝置的電池", + "device-label": "電池裝置", + "display-mode-description": "選擇電池在工具列中的顯示方式。", + "display-mode-graphic": "圖形化電池", + "display-mode-graphic-clean": "圖形電池(無%)", + "display-mode-icon-always": "圖示 - 始終顯示 %", + "display-mode-icon-hover": "圖示 - 懸停時顯示", + "display-mode-icon-only": "僅圖示", + "hide-if-idle-description": "當電池狀態為閒置時隱藏小工具", + "hide-if-idle-label": "閒置時隱藏", + "hide-if-not-detected-description": "如果系統沒有偵測到電池就隱藏小工具", + "hide-if-not-detected-label": "沒有偵測到時隱藏", + "low-battery-threshold-label": "低電量警告", + "show-noctalia-performance-description": "在電池面板內顯示 Noctalia 效能模式開關", + "show-noctalia-performance-label": "顯示 Noctalia 效能模式開關", + "show-power-profile-description": "在電池面板內顯示可切換的電源配置", + "show-power-profile-label": "顯示電源配置" + }, + "brightness": { + "apply-all-description": "使用滾輪時更改所有顯示器的亮度。", + "apply-all-label": "將捲動變更套用到所有螢幕" + }, + "clock": { + "clock-display-description": "利用下面列表的標記自訂你想要的時鐘格式; 如果想使用12小時制, 你必須要再增加 \"AP\" 標記", + "clock-display-label": "時鐘顯示", + "custom-font-description": "選取時鐘顯示使用的自訂字型", + "custom-font-label": "自訂字型", + "custom-font-placeholder": "選擇自訂字型...", + "custom-font-search-placeholder": "搜尋字型...", + "horizontal-bar-description": "提示: 可以利用 \\n 換行", + "horizontal-bar-label": "水平列顯示", + "preview": "預覽", + "tooltip-format-description": "在游標移過時提示框所顯示的格式, 如果留空則使用預設值", + "tooltip-format-label": "提示框格式", + "use-custom-font-description": "為時鐘強制套用自訂的字型", + "use-custom-font-label": "使用自訂字型", + "use-monospaced-font-description": "當啟用時, 時鐘會使用等寬字型顯示", + "use-monospaced-font-label": "使用等寬字型", + "vertical-bar-description": "使用空格把各個部件拆到下一行", + "vertical-bar-label": "垂直列顯示" + }, + "control-center": { + "browse-file": "瀏覽檔案", + "browse-library": "瀏覽圖示庫", + "color-selection-description": "將主題用色套用至圖示上", + "enable-colorization-description": "為圖示啟用著色,應用主題顏色。", + "icon-description": "自圖示庫或檔案選擇一個圖示", + "select-custom-icon": "選取一個自訂圖示", + "use-distro-logo-description": "使用你現在使用的發行版 Logo 作為圖示", + "use-distro-logo-label": "使用發行版圖示" + }, + "custom-button": { + "collapse-condition-description": "若輸出文字符合此值,按鈕將會摺疊。", + "collapse-condition-label": "摺疊條件", + "color-selection-description": "將主題用色套用至圖示及文字上", + "default-tooltip": "自訂按鈕,在設定中配置", + "display-command-output-description": "輸入一個想要定時重複執行的指令, 指令的第一行輸出將會作為顯示的文字", + "display-command-output-label": "顯示指令的輸出", + "display-command-output-stream-description": "輸入一個要持續執行的指令", + "dynamic-text": "動態文字", + "enable-colorization-description": "替自訂按鈕的圖示及文字啟用上色, 套用主題用色", + "enable-colorization-label": "啟用上色", + "general-tooltip-text-description": "在按鈕工具提示中顯示的自訂文字。", + "general-tooltip-text-label": "自訂工具提示文字", + "hide-mode-always-expanded": "總是展開", + "hide-mode-description": "控制當指令沒有輸出時,元件之可見性", + "hide-mode-expand-with-output": "有訊息輸出時展開", + "hide-mode-label": "隱藏模式", + "hide-mode-max-transparent": "最大展開但透明", + "icon-description": "從圖示庫選取一個圖示", + "ipc-identifier-description": "IPC 指令的唯一識別碼, 在使用 'qs -c noctalia-shell ipc call cb [動作] [識別碼]' 時利用此識別碼可透過 IPC 控制此按鈕。", + "ipc-identifier-label": "IPC 識別碼", + "left-click-description": "在點擊左鍵時要執行的指令", + "left-click-label": "點擊左鍵", + "left-click-update-text": "在點擊左鍵時更新顯示文字", + "max-text-length-horizontal-description": "在水平列所顯示的最大字元數量 (0為隱藏)", + "max-text-length-horizontal-label": "最大文字長度 (水平列)", + "max-text-length-vertical-description": "在垂直列所顯示的最大字元數量 (0為隱藏)", + "max-text-length-vertical-label": "最大文字長度 (垂直列)", + "middle-click-description": "在點擊中鍵時要執行的指令", + "middle-click-label": "點擊中鍵", + "middle-click-update-text": "在點擊中鍵時更新顯示文字", + "parse-json-description": "將執行的指令輸出為 JSON 物件來動態設置文字及圖示", + "parse-json-label": "將輸出解析為 JSON", + "refresh-interval-description": "間格 以毫秒 (ms) 計", + "refresh-interval-label": "重整間格", + "right-click-description": "在點擊右鍵時要執行的指令", + "right-click-label": "點擊右鍵", + "right-click-update-text": "在點擊右鍵時更新顯示文字", + "show-exec-tooltip-description": "顯示帶有指令詳細資訊的工具提示 (左鍵/右鍵/中鍵點擊,滾輪)。", + "show-exec-tooltip-label": "顯示指令工具提示", + "show-icon-description": "切換小工具圖示的能見性", + "show-icon-label": "顯示圖示", + "show-text-tooltip-description": "顯示帶有文字命令輸出的工具提示。", + "show-text-tooltip-label": "顯示動態文字工具提示", + "text-stream-description": "命令列的串流內容將以文字形式顯示在按鈕上。", + "text-stream-label": "串流", + "wheel-description": "在滾輪滾動時要執行的指令
在指令裡使用 $delta 表示指令中滾動多少的值", + "wheel-down": "滾輪 下", + "wheel-down-description": "在滾輪下滾時要執行的指令", + "wheel-down-label": "滾輪下滾", + "wheel-label": "滑鼠滾輪", + "wheel-mode-separate-description": "分別在滾輪上滾及下滾時執行不同指令", + "wheel-mode-separate-label": "分開設定滾輪指令", + "wheel-up": "滾輪 上", + "wheel-up-description": "在滾輪上滾時要執行的指令", + "wheel-up-label": "滾輪上滾", + "wheel-update-text": "在滾動滾輪時更新顯示文字" + }, + "keyboard-layout": { + "show-icon-description": "以圖示顯示鍵盤佈局" + }, + "lock-keys": { + "hide-when-off-description": "當按鍵不是啟動狀態時隱藏", + "hide-when-off-label": "停用時隱藏", + "show-caps-lock-description": "顯示 Caps Lock的狀態", + "show-caps-lock-label": "Caps Lock", + "show-num-lock-description": "顯示 Num Lock 的狀態", + "show-num-lock-label": "Num Lock", + "show-scroll-lock-description": "顯示 Scroll Lock 的狀態", + "show-scroll-lock-label": "Scroll Lock" + }, + "media-mini": { + "compact-mode-description": "替媒體播放面板換一個省空間的佈局", + "compact-mode-label": "緊湊模式", + "hide-mode-description": "控制當沒有媒體播放時小工具的行為", + "max-width-description": "設定小工具的最大水平寬度, 當標題簡短時小工具會跟著縮短", + "no-active-player": "沒有活動的媒體", + "panel-section-description": "控制媒體播放面板的外觀及行為", + "panel-section-label": "媒體播放面板", + "scrolling-mode-description": "控制歌曲長標題時是否啟用文字捲動", + "scrolling-mode-label": "捲動模式", + "show-album-art-description": "為現在播放的曲目顯示專輯封面", + "show-album-art-label": "顯示專輯封面", + "show-artist-first-description": "以 演出者 - 曲目 顯示而不是 曲目 - 演出者", + "show-artist-first-label": "演出者在前", + "show-progress-ring-description": "用一個進度圓圈顯示播放進度", + "show-progress-ring-label": "顯示進度圓圈", + "show-visualizer-description": "在音樂播放時為音訊顯示視覺效果", + "show-visualizer-label": "顯示視覺效果", + "use-fixed-width-description": "當啟用時, 小工具將總是使用最大寬度而不會配合標題調整", + "use-fixed-width-label": "使用固定寬度", + "visualizer-type-description": "選擇視覺效果的顯示風格", + "visualizer-type-label": "視覺效果樣式" + }, + "notification-history": { + "hide-widget-when-zero-description": "如果沒有未讀通知就隱藏通知圖示", + "hide-widget-when-zero-label": "沒有通知時隱藏圖示", + "hide-widget-when-zero-unread-description": "沒有未讀通知時隱藏通知圖示。", + "hide-widget-when-zero-unread-label": "沒有未讀通知時隱藏圖示", + "show-unread-badge-description": "顯示帶有數量的未讀標記", + "show-unread-badge-label": "顯示未讀標記", + "unread-badge-color-description": "選擇未讀通知徽章的顏色。", + "unread-badge-color-label": "未讀徽章顏色" + }, + "section-editor": { + "placeholder": "選取一個小工具來新增...", + "search-placeholder": "搜尋小工具..." + }, + "spacer": { + "width-description": "分隔間距 以像素 (px) 計" + }, + "system-monitor": { + "compact-mode-description": "用迷你長條來取代文字數值, 可以避免佈局跑版", + "compact-mode-label": "緊湊模式", + "cpu-cores-description": "個別顯示 CPU 核心使用率。", + "cpu-cores-label": "CPU 核心", + "cpu-frequency-description": "顯示目前 CPU 時脈速度(GHz)。", + "cpu-frequency-label": "顯示 CPU 頻率", + "cpu-temperature-description": "如果可用即顯示 CPU 溫度", + "cpu-temperature-label": "CPU 溫度", + "cpu-usage-description": "顯示現在 CPU 的使用百分比", + "cpu-usage-label": "CPU 使用率", + "disk-path-description": "選取要監視的磁碟掛載點", + "disk-path-label": "磁碟路徑", + "gpu-temperature-description": "如果可用即顯示 GPU 溫度", + "load-average-description": "顯示系統負載", + "load-average-label": "系統負載", + "memory-percentage-description": "顯示記憶體的使用百分比而不是數值表示", + "memory-percentage-label": "記憶體百分比", + "memory-usage-description": "顯示現在的記憶體使用量", + "memory-usage-label": "記憶體使用量", + "network-traffic-description": "顯示網路上傳及下載的速度", + "network-traffic-label": "網路流量", + "storage-as-percentage-description": "以百分比而非絕對值顯示磁碟空間。", + "storage-as-percentage-label": "磁碟百分比", + "storage-available-description": "顯示可用磁碟空間,而非已用空間。", + "storage-available-label": "可用磁碟空間", + "storage-usage-description": "顯示磁碟空間的使用量", + "storage-usage-label": "磁碟使用量", + "swap-usage-description": "顯示交換記憶體使用量", + "swap-usage-label": "交換記憶體使用量", + "use-monospace-font-description": "使用等寬字型可以達成一致的字元寬度", + "use-monospace-font-label": "等寬字型", + "use-padding-description": "用前導空格填充文字值,以防止佈局偏移。", + "use-padding-description-disabled-monospace-font": "此功能需要等寬字體。", + "use-padding-description-disabled-vertical": "垂直工作列不支援文字內邊距。", + "use-padding-label": "文字填充" + }, + "taskbar": { + "colorize-icons-description": "將工具列圖示套用主題用色", + "hide-mode-description": "當沒有符合的視窗時控制小工具的行為", + "hide-mode-label": "隱藏方式", + "icon-scale-description": "設定工具列圖示的縮放比例", + "icon-scale-label": "圖示比例", + "max-width-description": "工具列佔螢幕寬度百分比的最大寬度", + "max-width-label": "最大寬度", + "only-active-workspaces-description": "只顯示活躍中的工作區所執行的程式", + "only-active-workspaces-label": "來自現在活躍的工作區", + "only-same-monitor-description": "只顯示工具列所在的這個顯示器在執行的程式", + "only-same-monitor-label": "來自同個顯示器", + "show-pinned-apps-description": "顯示有在 Dock 標記為釘選的程式", + "show-pinned-apps-label": "顯示釘選的程式", + "show-title-description": "顯示視窗的標題", + "show-title-description-disabled": "當垂直顯示時不支援顯示標題", + "show-title-label": "顯示標題", + "smart-width-description": "基於視窗的多寡自動調整每個視窗佔用的寬度", + "smart-width-label": "智慧寬度", + "title-width-description": "設定工具列視窗標題的寬度 (以像素 (px) 計)", + "title-width-label": "標題寬度", + "title-width-reset-tooltip": "重設標題寬度" + }, + "tray": { + "chevron-color-description": "將主題顏色套用至抽屜的箭頭圖示。", + "chevron-color-label": "箭頭顏色", + "colorize-icons-description": "將系統夾圖示套用主題用色", + "colorize-icons-label": "為圖示上色", + "drawer-enabled-description": "當啟用時, 沒有釘選的系統匣圖示將會顯示在抽屜面板中
如果停用則所有的圖示都會在工具列裡顯示", + "drawer-enabled-label": "啟用抽屜", + "hide-passive-description": "當啟用時, 在 \"被動\" 狀態下的系統匣圖示將會被隱藏", + "hide-passive-label": "隱藏被動項目" + }, + "volume": { + "display-mode-description": "選擇你想如何顯示這項數值" + }, + "workspace": { + "character-count-description": "限制工作區所設定的名字要顯示幾個字元 (1-10)", + "character-count-label": "字元上限", + "empty-color-description": "設定空白 Workspace 的背景顏色。", + "empty-color-label": "空白工作區顏色", + "enable-scrollwheel-description": "利用滑鼠滾輪在工作區間切換", + "enable-scrollwheel-label": "捲輪切換工作區", + "focused-color-description": "設定焦點 Workspace 的背景顏色。", + "focused-color-label": "聚焦的工作區顏色", + "follow-focused-screen-description": "顯示正在聚焦的螢幕所擁有的工作區, 而不是工具列所在的螢幕擁有的工作區", + "follow-focused-screen-label": "跟隨焦點所在的螢幕", + "font-weight-description": "設定工作區內文字的視覺權重。", + "font-weight-label": "字體粗細", + "grouped-border-opacity-description": "設定工作區邊框的不透明度", + "grouped-border-opacity-label": "邊框不透明度", + "hide-unoccupied-description": "如果工作區沒有任何視窗則不要顯示", + "hide-unoccupied-label": "沒有佔用時隱藏", + "label-mode-description": "選擇工作區標籤該如何顯示", + "label-mode-label": "標籤樣式", + "occupied-color-description": "設定已佔用Workspace的背景顏色。", + "occupied-color-label": "已佔用工作區顏色", + "pill-size-description": "調整工作區指示器的大小 (50%-100%)。", + "pill-size-label": "膠囊大小", + "reverse-scrolling-description": "捲動時反轉工作區切換方向。", + "reverse-scrolling-label": "反轉捲動", + "show-applications-description": "顯示各個工作區的程式圖示", + "show-applications-hover-description": "當滑鼠懸停在工作區上時,在每個工作區內顯示應用程式圖示。", + "show-applications-hover-label": "滑鼠懸停時顯示應用程式", + "show-applications-label": "顯示應用程式", + "show-badge-description": "在群組模式下顯示工作區編號徽章。", + "show-badge-label": "顯示工作區標記", + "show-labels-only-when-occupied-description": "只在工作區有視窗時顯示工作區標籤", + "show-labels-only-when-occupied-label": "佔用時顯示標籤", + "unfocused-icons-opacity-description": "設定非焦點應用程式圖示的不透明度。", + "unfocused-icons-opacity-label": "非焦點圖示的不透明度" + } + }, + "battery": { + "all-batteries": "所有電池(組合)", + "battery-health": "電池健康度", + "battery-level": "電池電量", + "capacity-level": "容量:{level}", + "charging-rate": "充電速率: {rate} W", + "discharging-rate": "放電速率: {rate} W", + "health": "健康度: {percent}%", + "inhibit-idle-description": "保持系統喚醒", + "no-battery-detected": "沒有偵測到電池", + "plugged-in": "已插電", + "power-profile": "電源配置", + "time-left": "剩餘時間: {time}", + "time-until-full": "還有多久充滿: {time}" + }, + "bluetooth": { + "panel": { + "available-devices": "可用裝置", + "blocked": "已封鎖", + "connected-devices": "已連接的裝置", + "device-address": "裝置位址", + "disabled": "藍牙已關閉", + "discoverable": "可被偵測", + "enable-message": "啟用藍芽來檢視可用裝置", + "known-devices": "已知裝置", + "no-devices": "找不到可用裝置", + "paired-devices": "已配對裝置", + "pairing-mode": "確認你的裝置已經切至配對模式", + "scanning": "正在搜尋裝置...", + "signal-text-excellent": "訊號: 優秀", + "signal-text-fair": "訊號: 良好", + "signal-text-good": "訊號: 尚可", + "signal-text-poor": "訊號: 不佳", + "signal-text-unknown": "訊號: 未知", + "signal-text-very-poor": "訊號: 惡劣" + } + }, + "changelog": { + "error": { + "fetch-failed": "無法讀取更新內容, 請稍後再重試", + "rate-limit": "超過 GitHub API 的存取量限制, 請在數分鐘後再試一次" + }, + "panel": { + "buttons-discord": "加入我們的 Discord", + "buttons-dismiss": "OK", + "buttons-feedback": "建議與回饋", + "empty": "暫時沒有可用的發行說明", + "highlight-title": "更新亮點", + "released": "{date} 發布", + "subtitle-fresh": "謝謝你安裝 Noctalia! 這裡提供這個組建所包含的內容", + "subtitle-updated": "自 {previousVersion} 版更新", + "title": "{version} 版新鮮事", + "version": "{version} 版", + "version-new-user": "全新安裝" + } + }, + "common": { + "actions": "操作", + "active": "活躍", + "add": "新增", + "appearance": "外觀", + "apply": "套用", + "auto-connect": "自動連線", + "automation": "自動化", + "available": "可用", + "back": "返回", + "battery": "電池", + "behavior": "行為", + "bluetooth": "藍牙", + "brightness": "亮度", + "browse": "瀏覽", + "calendar": "行事曆", + "calendar-panel": "行事曆面板", + "cancel": "取消", + "cards": "卡片", + "charging": "正在充電", + "clear": "清空", + "clipboard": "剪貼簿", + "close": "關閉", + "color-muted": "已靜音", + "colors": "顏色", + "command": "指令", + "confirm": "確認", + "connect": "連接", + "connected": "已連接", + "connecting": "正在連線...", + "continue": "繼續", + "contributors": "貢獻者", + "copied-to-clipboard": "已複製到剪貼簿", + "countdown": "倒數", + "customize": "自訂", + "date": "日期", + "default": "預設", + "delete": "刪除", + "devices": "裝置", + "disabled": "已停用", + "discharging": "放電中", + "disconnect": "斷線", + "disconnected": "已斷線", + "disconnecting": "正在斷線...", + "display-mode": "顯示模式", + "documentation": "文件", + "download": "下載", + "duration": "持續時間", + "dysfunctional": "功能失調", + "edit": "編輯", + "enabled": "已啟用", + "error": "錯誤", + "ethernet": "乙太網路", + "events": "事件", + "execute": "執行", + "faithful": "忠實", + "focus": "關注", + "font-weight-bold": "粗體", + "font-weight-light": "細", + "font-weight-medium": "中", + "font-weight-regular": "一般", + "font-weight-semibold": "半粗體", + "frequency": "頻率", + "gateway": "網路閘道", + "general": "一般", + "height": "高度", + "hibernate": "睡眠", + "history": "歷史", + "icon": "圖示", + "idle": "閒置", + "info": "內容", + "input": "輸入", + "install": "安裝", + "installed": "已安裝", + "interface": "介面", + "internet": "網際網路", + "keybind": "按鍵綁定", + "language": "語言", + "loading": "正在載入...", + "local": "本機", + "location": "位置", + "lock": "鎖定", + "logout": "登出", + "look": "樣式", + "margins": "邊距", + "media": "媒體", + "media-player": "媒體播放器", + "memory": "記憶體", + "monitors": "顯示器", + "move-down": "下移", + "move-up": "向上移動", + "network": "網路", + "next": "下一首", + "night-light": "夜燈模式", + "no": "否", + "no-results": "沒有結果", + "no-summary": "沒有摘要", + "none": "無", + "not-found": "找不到", + "notifications": "通知", + "official": "官方", + "on-surface": "在表面上", + "output": "輸出", + "pair": "配對", + "paired": "已配對", + "pairing": "配對中...", + "panels": "面板", + "password": "密碼", + "pause": "暫停", + "performance": "效能", + "pin": "釘選", + "play": "播放", + "polling": "輪詢", + "position": "位置", + "previous": "上一首", + "primary": "主要的", + "random": "隨機", + "reboot": "重新啟動", + "reboot-to-uefi": "重新啟動至 UEFI", + "record": "開始錄製", + "refresh": "重新整理", + "required": "(必要)", + "reset": "重設", + "reset-to-default": "重設為預設值", + "result-count": "{count} 筆結果", + "result-count-plural": "{count} 筆結果", + "resume": "繼續", + "save": "儲存", + "scanning": "掃描中...", + "screen-corners": "畫面邊角", + "search": "搜尋", + "second": "{count} 秒", + "second-plural": "{count} 秒", + "secondary": "輔助", + "security": "安全", + "select": "選取", + "select-color": "選擇顏色", + "select-color-description": "套用主題顏色以示強調。", + "select-icon-color": "選擇圖示顏色", + "settings": "設定", + "shortcuts": "快捷鍵", + "shutdown": "關機", + "signal": "訊號", + "sound": "音效", + "sources": "來源", + "start": "開始", + "stop": "停止", + "supporters": "支持者", + "suspend": "暫停", + "templates": "模板", + "tertiary": "第三", + "test": "測試", + "thresholds": "門檻", + "title": "標題", + "toast": "氣泡訊息", + "trusted": "受信任的", + "uninstall": "移除", + "unknown": "未知", + "unpair": "解除配對", + "unpin": "解除釘選", + "update": "更新", + "upload": "上傳", + "username": "使用者名稱", + "userspace-reboot": "重新啟動使用者空間", + "version": "版本", + "vibrant": "鮮明", + "visualizer": "視覺效果", + "volume": "音量", + "volumes": "音量", + "wallpaper": "桌布", + "weather": "天氣", + "weather-loading": "正在載入天氣...", + "weather-no-location": "在設定中設定位置以查看天氣", + "week": "週", + "widgets": "小工具", + "width": "寬度", + "wifi": "Wi-Fi", + "windows": "視窗", + "yes": "是" + }, + "control-center": { + "power-profile": { + "tooltip-action": "電源配置", + "tooltip-disabled": "安裝 power-profiles-daemon 套件以使用電源配置" + }, + "wifi": { + "label-disconnected": "Wi-Fi 已斷線", + "label-ethernet": "乙太網路" + } + }, + "display-modes": { + "always-hide": "總是隱藏", + "always-show": "總是顯示", + "force-open": "強制開啟", + "on-hover": "游標移過時" + }, + "hide-modes": { + "auto-hide": "自動隱藏", + "hidden": "空白時隱藏", + "idle": "閒置時隱藏", + "non-exclusive": "非獨佔", + "transparent": "空白時變透明", + "visible": "始終可見" + }, + "launcher": { + "categories": { + "all": "所有項目", + "audiovideo": "音訊及影片", + "chat": "通訊聊天", + "development": "開發", + "education": "教育", + "emoji-activity": "活動", + "emoji-animals": "動物", + "emoji-flags": "旗幟", + "emoji-food": "食物及飲料", + "emoji-nature": "大自然", + "emoji-objects": "物品", + "emoji-people": "人物及肢體", + "emoji-recent": "最近使用", + "emoji-symbols": "符號", + "emoji-travel": "旅行及地點", + "game": "遊戲", + "graphics": "圖像", + "misc": "其他", + "office": "辦公", + "pinned": "釘選項目", + "system": "系統", + "webbrowser": "瀏覽器" + }, + "providers": { + "applications": "應用程式", + "calculator": "計算機", + "calculator-deprecated": ">calc 指令已經棄用, 很快就會被移除; 請直接在搜尋列輸入數學算式", + "calculator-description": "計算機 — 計算數學算式", + "calculator-enter-expression": "輸入一個數學算式", + "calculator-press-enter-to-copy": "按 Enter 複製計算結果", + "clipboard": "剪貼簿歷史", + "clipboard-clear-description": "清除所有剪貼簿歷史", + "clipboard-clear-description-full": "移除剪貼簿歷史的所有項目", + "clipboard-clear-history": "清除剪貼簿歷史", + "clipboard-delete": "刪除剪貼簿項目", + "clipboard-history-disabled": "停用剪貼簿歷史", + "clipboard-history-disabled-description": "在設定裡啟用剪貼簿歷史或者安裝 clipphist 套件", + "clipboard-loading": "剪貼簿歷史載入中...", + "clipboard-search-description": "搜尋剪貼簿歷史", + "command-description": "執行 shell 指令", + "emoji": "挑選表情符號", + "emoji-loading": "載入表情符號...", + "emoji-loading-description": "請稍候", + "emoji-no-recent": "還沒有最近使用的表情符號", + "emoji-search-description": "搜尋和複製表情符號", + "settings-search-description": "搜尋並前往設定", + "windows-search-description": "搜尋並聚焦開啟的視窗" + } + }, + "lock-screen": { + "authenticating": "驗證中...", + "password": "請輸入你的密碼...", + "restart": "重新啟動", + "shut-down": "關機" + }, + "notifications": { + "panel": { + "click-to-collapse": "點擊摺疊", + "click-to-expand": "點擊展開", + "description": "如果有收到通知就會顯示在這裡", + "no-notifications": "沒有任何通知" + }, + "range": { + "earlier": "更早以前", + "today": "今天", + "yesterday": "昨天" + }, + "time": { + "diff-d": "一天前", + "diff-dd": "{diff} 天前", + "diff-h": "一小時前", + "diff-hh": "{diff} 小時前", + "diff-m": "一分鐘前", + "diff-mm": "{diff} 分鐘前", + "now": "現在" + } + }, + "options": { + "bar": { + "density-comfortable": "餘裕", + "density-compact": "緊湊", + "density-default": "預設", + "density-mini": "迷你", + "density-spacious": "寬敞", + "display-mode-always-visible": "永遠可見", + "type-floating": "懸浮", + "type-framed": "邊框", + "type-simple": "簡約" + }, + "control-center": { + "quick-settings-style-classic": "古典", + "quick-settings-style-modern": "現代" + }, + "frame-rates-fps": "{fps} FPS", + "launcher-density": { + "comfortable": "舒適", + "compact": "緊湊", + "default": "預設" + }, + "launcher-view-mode": { + "grid": "網格", + "list": "列表" + }, + "notification-density": { + "compact": "緊湊", + "default": "預設" + }, + "scrolling-modes": { + "always": "總是捲動", + "hover": "游標移過時捲動", + "never": "從不捲動" + }, + "session-menu-grid-layout": { + "grid": "方格狀", + "single-row": "單列" + }, + "settings-panel-mode": { + "attached": "面板依附在工具列", + "centered": "居中面板", + "window": "分離視窗" + }, + "visualizer-types": { + "linear": "線條", + "mirrored": "鏡像", + "wave": "波形" + }, + "visualizer-visibility": { + "with-background": "只在有背景時" + }, + "workspace-labels": { + "index": "編號", + "index-and-name": "編號及名稱", + "name": "名稱" + } + }, + "panels": { + "about": { + "become-supporter": "成為支持者", + "changelog": "檢視更新日誌", + "changelog-on-startup": "更新時顯示變更日誌", + "changelog-on-startup-desc": "Noctalia 更新時自動顯示更新日誌。", + "contributors-description": "特別感謝我們這{count}位超讚的貢獻者!!", + "contributors-description-plural": "特別感謝我們這{count}位超讚的貢獻者!!", + "copy-info": "複製資訊", + "debug-disabled": "偵錯模式已停用", + "debug-enabled": "偵錯模式已啟用", + "info-copied": "資訊已複製至剪貼簿", + "noctalia-available": "可用:", + "noctalia-desc": "一個為 Wayland 精心打造, 兼具簡潔及流暢的桌面介面, 使用 Quickshell 建置", + "noctalia-git-commit": "Git commit:", + "noctalia-installed-version": "安裝版本:", + "noctalia-latest-version": "最新版本:", + "noctalia-qs-version": "Noctalia QS 版本:", + "noctalia-title": "Noctalia Shell", + "privacy-policy": "隱私權政策", + "support": "支持我們", + "supporter-badge": "支持者", + "supporters-desc": "非常感謝我們出色的支持者!", + "supporters-desc-plural": "非常感謝我們的{count}位出色支持者!", + "supporters-loading": "正在載入支持者...", + "system-board": "主機板:", + "system-cpu": "CPU:", + "system-disk": "磁碟:", + "system-gpu": "GPU:", + "system-host": "主機名稱:", + "system-install-hint": "安裝 fastfetch 套件即可檢視系統資訊", + "system-kernel": "核心:", + "system-loading": "正在載入系統資訊...", + "system-memory": "記憶體:", + "system-monitor": "顯示器:", + "system-not-installed": "尚未安裝 fastfetch", + "system-os": "作業系統:", + "system-packages": "套件數量:", + "system-product": "產品:", + "system-title": "系統資訊:", + "system-uptime": "開機時間:", + "system-wm": "視窗管理器:", + "telemetry-data-copied": "已複製匿名資訊至剪貼簿", + "telemetry-desc": "提供匿名化的系統資訊 (畫面解析度, 合成器, 發行版) 幫助 Noctalia 改進; 只在桌面啟動時執行, 沒有額外追蹤; 傳送的資料會在30天後自動刪除", + "telemetry-enabled": "傳送匿名化的系統資訊", + "telemetry-show-data": "檢視資料", + "telemetry-title": "隱私", + "title": "關於", + "up-to-date": "你已經在最新版了!!", + "update-available": "有更新版本", + "view-commit": "在 GitHub 上檢視 commit" + }, + "audio": { + "devices-desc": "設定音訊輸入及輸出裝置", + "devices-input-device-description": "選擇想用的音訊輸入裝置", + "devices-input-device-label": "輸入裝置", + "devices-output-device-description": "選擇想用的音訊輸出裝置", + "devices-output-device-label": "輸出裝置", + "devices-title": "音訊裝置", + "external-mixer-description": "輸入啟用外部混音器時所用的指令或是程式路徑", + "external-mixer-label": "外部混音器指令", + "external-mixer-placeholder": "pwvucontrol || pavucontrol", + "media-desc": "設定你偏好或想忽略的媒體播放程式", + "media-excluded-player-description": "輸入你想要系統忽略的播放器關鍵字, 不同關鍵字間應分行區隔", + "media-excluded-player-label": "排除的播放器", + "media-excluded-player-placeholder": "輸入播放器關鍵字後點 + 號", + "media-frame-rate-description": "頻率越高效果越滑順, 但也會用愈多資源", + "media-frame-rate-label": "幀數", + "media-primary-player-description": "輸入可以辨識你的常用播放器的關鍵字", + "media-primary-player-label": "首要播放器", + "media-primary-player-placeholder": "例如: spotify, vlc, mpv", + "media-scrolling-speed-description": "以秒為單位設定標題從頭捲動一輪的時間", + "media-scrolling-speed-label": "捲動速度", + "media-scrolling-title-description": "在碰到長媒體標題時啟用持續捲動", + "media-scrolling-title-label": "曲目捲動", + "media-title": "媒體播放器", + "on-middle-clicked-description": "在點擊中鍵時要執行的指令", + "panel-applications-empty": "沒有正在播放音訊的程式", + "spectrum-mirrored-description": "鏡像頻譜,使低頻位於中央。關閉時,低頻在左側,高頻在右側。", + "spectrum-mirrored-label": "鏡像頻譜", + "title": "音訊", + "visualizer-type-description": "選擇媒體播放時的視覺效果樣式", + "visualizer-type-label": "視覺效果樣式", + "volumes-desc": "調整音訊控制及音量", + "volumes-feedback-sound-file-description": "更改音量時播放的聲音檔案路徑。", + "volumes-feedback-sound-file-label": "音量回饋聲音檔案", + "volumes-feedback-sound-file-select-title": "選擇音量回饋聲音檔案", + "volumes-input-volume-description": "麥克風輸入音量", + "volumes-mute-input-description": "將預設音訊輸入 (麥克風) 靜音", + "volumes-mute-input-label": "輸入靜音", + "volumes-mute-output-description": "將系統的主要音訊輸出靜音", + "volumes-mute-output-label": "輸出靜音", + "volumes-output-volume-description": "系統全域音量", + "volumes-step-size-description": "微調音量調整一格的大小 (滾輪及鍵盤快捷鍵)", + "volumes-step-size-label": "音量步進大小", + "volumes-volume-feedback-description": "調整音量時播放回饋音效", + "volumes-volume-feedback-label": "播放音量回饋音效", + "volumes-volume-overdrive-description": "增強音量調整超過100%, 不是所有硬體都支援這個設定", + "volumes-volume-overdrive-label": "允許音量過載" + }, + "bar": { + "appearance-auto-hide-delay-description": "滑鼠離開後Bar隱藏所需時間", + "appearance-auto-hide-delay-label": "隱藏延遲", + "appearance-auto-hide-exclusive-note": "注意: 當自動隱藏處於活動狀態時,專屬區域會自動禁用。", + "appearance-auto-show-delay-description": "滑鼠進入邊緣時,欄顯示前的延遲時間。", + "appearance-auto-show-delay-label": "顯示延遲", + "appearance-background-opacity-description": "設定工具列背景的不透明度", + "appearance-background-opacity-label": "工具列背景不透明度", + "appearance-capsule-color-description": "為欄膠囊選擇一種顏色,或不使用任何顏色以採用預設表面顏色。", + "appearance-capsule-color-label": "膠囊顏色", + "appearance-capsule-opacity-description": "設定當啟用顯示小容器時小工具的背景不透明度", + "appearance-capsule-opacity-label": "小容器不透明度", + "appearance-content-padding-description": "調整列邊緣與小工具之間的內邊距。", + "appearance-content-padding-label": "內容內邊距", + "appearance-density-description": "調整工具列的內部間距來做出緊湊或寬敞的外觀", + "appearance-density-label": "工具列密度", + "appearance-desc": "自訂工具列的外觀及位置", + "appearance-display-mode-description": "選擇列何時可見", + "appearance-enable-exclusion-zone-inset-description": "減少排除區域1個實體像素,以便齊平的視窗能完美地延伸到工具列邊緣下方。", + "appearance-enable-exclusion-zone-inset-label": "內嵌排除區域", + "appearance-floating-description": "將工具列以懸浮的圓弧長條狀顯示", + "appearance-floating-label": "懸浮工具列", + "appearance-font-scale-description": "調整列中顯示的文字的字體大小比例。", + "appearance-font-scale-label": "字體縮放", + "appearance-frame-radius": "內圓角", + "appearance-frame-settings-description": "調整邊框粗細和內圓角半徑", + "appearance-frame-settings-label": "邊框設定", + "appearance-frame-thickness": "粗細", + "appearance-hide-on-overview-description": "當在合成器概覽時, 隱藏工具列並關閉面板", + "appearance-hide-on-overview-label": "概覽時隱藏工具列", + "appearance-margins-description": "調整懸浮工具列的邊距", + "appearance-margins-horizontal": "水平邊距", + "appearance-margins-vertical": "垂直邊距", + "appearance-outer-corners-description": "將工具列向外延伸顯示畫面圓角", + "appearance-outer-corners-label": "外側圓角", + "appearance-position-description": "選擇要把工具列放在螢幕哪裡", + "appearance-position-label": "工具列位置", + "appearance-show-capsule-description": "顯示小工具的填充背景", + "appearance-show-capsule-label": "顯示小容器", + "appearance-show-on-workspace-switch-description": "切換工作區時自動短暫顯示狀態列。", + "appearance-show-on-workspace-switch-label": "工作區切換時顯示狀態列", + "appearance-show-outline-description": "讓每個小工具都顯示可視邊框", + "appearance-show-outline-label": "顯示小工具外框", + "appearance-type-description": "選擇欄的樣式:簡約、懸浮或邊框", + "appearance-type-label": "欄類型", + "appearance-use-separate-opacity-description": "啟用後可針對工具列背景使用分開的不透明度數值", + "appearance-use-separate-opacity-label": "分開設定工具列不透明度", + "appearance-widget-spacing-description": "調整列中每個小工具之間的間距。", + "appearance-widget-spacing-label": "小工具間距", + "behavior-middle-click-command-description": "中鍵點擊時執行的指令。", + "behavior-middle-click-command-label": "中鍵點擊指令", + "behavior-middle-click-command-placeholder": "niri msg action 切換概覽", + "behavior-middle-click-description": "選擇在欄的空白區域中鍵點擊的操作。", + "behavior-middle-click-follow-mouse-description": "在游標位置開啟選定的中鍵點擊面板。", + "behavior-middle-click-follow-mouse-label": "中鍵點擊跟隨滑鼠", + "behavior-middle-click-label": "列中鍵點擊動作", + "behavior-right-click-command-description": "右鍵點擊時執行的命令。", + "behavior-right-click-command-label": "右鍵點擊命令", + "behavior-right-click-command-placeholder": "notify-send \"右鍵點擊\"", + "behavior-right-click-description": "選擇在列的空白區域右鍵點擊時執行的操作。", + "behavior-right-click-follow-mouse-description": "在游標位置開啟選定的右鍵面板。", + "behavior-right-click-follow-mouse-label": "右鍵跟隨滑鼠", + "behavior-right-click-label": "欄右鍵點擊操作", + "behavior-wheel-wrap-description": "啟用後,捲動會從最後一項繼續到第一項。", + "behavior-wheel-wrap-label": "循環", + "behavior-workspace-scroll-description": "選擇滑鼠滾輪在欄的空白區域執行什麼操作。", + "behavior-workspace-scroll-label": "列滑鼠滾輪動作", + "behavior-workspace-scroll-option-content": "內容", + "behavior-workspace-scroll-option-workspace": "工作區", + "monitor-configure-widgets": "設定小工具", + "monitor-override-settings": "覆寫全域設定", + "monitor-override-settings-description": "在這個顯示器上使用自訂設定", + "monitor-reset-all": "全部重設", + "monitor-widgets-title": "小工具設定 - {monitor}", + "monitors-desc": "只在特定的顯示器顯示工具列, 如果全部取消勾選則會顯示在所有顯示器", + "monitors-desc-new": "設定在哪些螢幕顯示工具列, 並自訂每個螢幕的設定", + "monitors-title": "在顯示器顯示", + "title": "工具列", + "tray-blacklist-description": "新增系統匣的排除項目, 支援萬用字元 (*)", + "tray-blacklist-label": "排除名單", + "tray-blacklist-placeholder": "例如: nm-applet, Fcitx*", + "tray-pin-application": "釘選程式", + "tray-unpin-application": "取消釘選程式", + "use-global-widgets": "使用全域小工具", + "widgets-desc": "拖曳小工具可以變更排列順序, 可利用右鍵選單在區塊間變動或直接解除它們", + "widgets-title": "排列小工具" + }, + "color-scheme": { + "color-source-use-wallpaper-colors-description": "從你的桌布產生主題配色, 自動從抽取的顏色建立一個連貫的主題", + "color-source-use-wallpaper-colors-label": "使用桌布配色", + "dark-mode-mode-description": "啟用自動切換淺色及暗色模式", + "dark-mode-mode-label": "深色模式", + "dark-mode-mode-manual": "手動排程", + "dark-mode-mode-off": "關閉", + "dark-mode-switch-description": "在夜間切換到深色主題眼睛可以觀看的更輕鬆", + "delete-error-description": "刪除 {scheme} 配色失敗", + "delete-error-title": "刪除失敗", + "delete-success-description": "成功刪除 {scheme} 配色", + "delete-success-title": "配色已刪除", + "download-button": "下載更多配色", + "download-downloading": "下載中...", + "download-empty": "沒有可用的配色", + "download-error-api-error": "API 錯誤: {status}", + "download-error-description": "下載 {scheme} 配色失敗", + "download-error-download-failed": "下載失敗, 錯誤碼: {code}", + "download-error-invalid-response": "無效的 API 回應格式", + "download-error-no-files": "找不到任何可作為配色的檔案", + "download-error-parse-failed": "解析 API 回應時發生錯誤: {error}", + "download-error-rate-limit": "已超過 GitHub API 的存取量限制", + "download-error-title": "下載失敗", + "download-fetching": "正在獲得可用的配色...", + "download-success-description": "成功下載 {scheme} 配色", + "download-success-title": "配色已下載", + "download-title": "下載配色", + "method-description": { + "content": "具有高保真色彩提取的 Material Design 方案,可與來源內容的實際色彩緊密匹配。", + "dysfunctional": "與 Faithful 類似,但選擇第二主導的顏色家族作為主要顏色。", + "faithful": "嘗試在生成和諧調色盤的同時,盡可能接近來源顏色。", + "fruit-salad": "Material Design 配色方案,可產生一個俏皮、充滿活力的調色盤,具有多樣且多變的色調。", + "monochrome": "使用單一色調灰階和最少色彩內容的 Material Design 方案。", + "muted": "產生更柔和、不飽和的色調,營造平靜而低調的外觀。", + "rainbow": "Material Design 方案,可在整個光譜中產生顏色,以實現最大的顏色多樣性。", + "tonal-spot": "Material Design 預設演算法。使用來源顏色的色調變化建立平衡的調色盤。", + "vibrant": "最大化色彩飽和度,打造大膽醒目的調色盤,呈現生動色調。" + }, + "predefined-desc": "從一批預先設計好的配色中選擇", + "predefined-generate-templates-label": "為預先設計的配色建立模板", + "predefined-title": "預先設計的配色", + "sync-gsettings-description": "將系統主題與活動的淺色或深色變體匹配。", + "sync-gsettings-label": "同步系統主題", + "templates-desc": "套用顏色至外部應用程式", + "templates-filter-description": "顯示特定類別的模板", + "templates-filter-label": "按類別篩選", + "templates-misc-description": "創建你自己的模板", + "templates-misc-label": "進階", + "templates-misc-user-templates-description": "只在你知道要做什麼時啟用 — 請參閱我們的線上文件", + "templates-misc-user-templates-label": "啟用使用者模板", + "templates-none-detected": "未偵測到", + "templates-write-path": "寫入: {filepath}", + "title": "主題配色", + "wallpaper-method-description": "選擇你喜歡的配色生成方法", + "wallpaper-method-label": "調色盤產生方法", + "wallpaper-monitor-source-description": "選取要用於擷取桌布顏色的螢幕。", + "wallpaper-monitor-source-label": "色彩產生來源" + }, + "connections": { + "authentication-required": "需要驗證", + "bluetooth-auto-connect-description": "當 Bluetooth 啟用時,自動連接到受信任的已配對裝置。", + "bluetooth-auto-connect-label": "自動連接已配對裝置", + "bluetooth-devices-unnamed": "未命名裝置不顯示。", + "bluetooth-discoverable": "當此設定分頁開啟時,此裝置可被發現為 {hostName}。", + "bluetooth-rssi-polling-description": "透過 bluetoothctl 定期取樣已連接裝置的 RSSI。可能並非所有裝置都可用;啟用時佔用最少資源。", + "bluetooth-rssi-polling-interval-description": "配置已連線裝置的訊號強度更新頻率。", + "bluetooth-rssi-polling-interval-label": "輪詢間隔", + "bluetooth-rssi-polling-label": "藍牙訊號輪詢", + "disable-discoverability-description": "將您的裝置從附近的 Bluetooth 裝置中隱藏。", + "disable-discoverability-label": "停用裝置可見性", + "hide-unnamed-devices-description": "隱藏僅顯示為 Bluetooth 位址的裝置。", + "hide-unnamed-devices-label": "隱藏未命名裝置", + "pin-instructions": "請輸入您裝置上顯示的PIN碼。", + "title": "連線", + "wifi-header-text": "使用 Wi-Fi 查看可用網路並連接到網際網路。" + }, + "control-center": { + "cards-desc": "自訂控制中心出現的控制項目及排序", + "desc": "設定控制中心面板的位置和行為", + "open-at-mouse-description": "當右鍵點擊欄的空白區域時,在游標附近開啟控制中心。若已停用,則改用上方的位置設定。", + "open-at-mouse-label": "右鍵點擊時在游標處開啟", + "position-description": "選擇當開啟控制中心時該從哪裡出現", + "shortcuts-custom-button-command-description": "在按鈕按下時要執行的命令", + "shortcuts-custom-button-enable-on-state-logic-description": "啟用第二個圖示與「熱區」狀態,並根據檢查命令判斷。", + "shortcuts-custom-button-enable-on-state-logic-label": "啟用狀態邏輯", + "shortcuts-custom-button-icon-description": "從圖庫中選擇一個圖示。", + "shortcuts-custom-button-on-clicked-label": "左鍵指令", + "shortcuts-custom-button-on-middle-clicked-description": "當按鈕被中鍵點擊時執行的命令。", + "shortcuts-custom-button-on-state-command-description": "用來檢查按鈕是否應處於「開啟」狀態的指令。傳回 0 表示應開啟,非 0 表示應關閉。", + "shortcuts-custom-button-on-state-command-label": "狀態檢查命令", + "shortcuts-custom-button-on-state-icon-description": "按鈕處於「開啓」狀態時的圖示。", + "shortcuts-custom-button-on-state-icon-label": "開啓狀態圖示", + "shortcuts-custom-button-state-checks-add": "新增狀態確認", + "shortcuts-custom-button-state-checks-command": "用於此狀態檢查的執行程式命令", + "shortcuts-custom-button-state-checks-label": "狀態確認", + "shortcuts-custom-button-state-checks-remove": "移除", + "shortcuts-custom-button-tooltip-description": "當游標移過按鈕時要顯示的提示框文字", + "shortcuts-custom-button-tooltip-label": "提示框文字", + "shortcuts-title": "快捷小工具", + "system-monitor-disk-path-description": "選擇控制中心裡的系統監視器卡片該監視哪一個磁碟掛載點", + "system-monitor-disk-path-label": "磁碟監視路徑", + "title": "控制中心" + }, + "desktop-widgets": { + "clock-enabled-description": "在桌面上顯示時鐘小工具", + "clock-enabled-label": "啟用時鐘小工具", + "clock-format-label": "格式", + "clock-height-description": "以像素 (px) 設定時鐘高度", + "clock-height-label": "高度", + "clock-minimal-mode-description": "使用顯示時間和日期的極簡時鐘", + "clock-minimal-mode-label": "極簡模式", + "clock-rounded-corners-description": "在小工具背景使用圓弧的角", + "clock-rounded-corners-label": "圓角", + "clock-show-background-description": "顯示時鐘小工具的填充背景", + "clock-show-background-label": "顯示背景", + "clock-show-date-description": "在時間下方顯示現在的日期", + "clock-show-date-label": "顯示日期", + "clock-show-seconds-description": "在時間顯示現在的秒鐘", + "clock-show-seconds-label": "顯示秒", + "clock-style-analog": "類比", + "clock-style-binary": "二進位", + "clock-style-description": "選擇時鐘顯示的樣式", + "clock-style-digital": "數位", + "clock-style-label": "時鐘樣式", + "clock-style-minimal": "極簡", + "clock-width-description": "以像素 (px) 設定時鐘寬度", + "cpu-intensive-note": "標有 ! 的Widget會佔用更多 CPU,請謹慎啟用。", + "edit-mode-button-label": "進入編輯模式", + "edit-mode-controls-explanation": "點擊左鍵及拖曳: 移動小工具及調整大小 點擊右鍵: 開啟右鍵選單", + "edit-mode-description": "啟用編輯模式以移動小工具; 當啟用時, 小工具會顯示一個可拖曳的外框以供移動.", + "edit-mode-exit-button": "離開編輯模式", + "edit-mode-grid-snap-label": "貼齊格線", + "edit-mode-grid-snap-scale-label": "格線吸附比例", + "edit-mode-label": "編輯模式", + "enabled-description": "啟用或完全停用桌面小工具", + "enabled-label": "啟用桌面小工具", + "general-desc": "設定顯示在你桌面上的小工具", + "general-title": "桌面小工具", + "media-player-enabled-description": "在桌面顯示一個媒體播放器小工具", + "media-player-enabled-label": "啟用媒體播放器小工具", + "media-player-rounded-corners-description": "在小工具的邊緣啟用圓角", + "media-player-show-album-art-description": "顯示專輯封面及歌曲資訊 (曲目及演出者)", + "media-player-show-album-art-label": "顯示專輯封面及曲目", + "media-player-show-background-description": "顯示填充背景", + "media-player-show-buttons-description": "顯示控制媒體的按鈕 (播放/暫停, 上一首, 下一首)", + "media-player-show-buttons-label": "顯示按鈕", + "media-player-show-visualizer-description": "顯示視覺效果", + "media-player-visualizer-type-description": "選擇視覺效果的樣式", + "overview-enabled-description": "當合成器概覽開啟時,顯示桌面小工具。", + "overview-enabled-label": "在概覽中顯示", + "system-stat-layout-bottom": "底部", + "system-stat-layout-description": "選擇圖例相對於圖表的顯示方式。", + "system-stat-layout-label": "佈局", + "system-stat-layout-side": "邊", + "system-stat-rounded-corners-description": "小工具背景使用圓角。", + "system-stat-rounded-corners-label": "圓角", + "system-stat-show-background-description": "顯示系統統計小工具的背景容器。", + "system-stat-show-background-label": "顯示背景", + "system-stat-stat-type-description": "選擇要顯示的系統統計資訊。", + "system-stat-stat-type-label": "統計類型", + "title": "桌面小工具", + "weather-enabled-description": "在桌面顯示一個天氣小工具", + "weather-enabled-label": "啟用天氣小工具", + "weather-show-background-description": "顯示天氣小工具的填充背景" + }, + "display": { + "monitors-backlight-device-auto-option": "預設", + "monitors-backlight-device-description": "為此輸出選擇一個背光裝置。", + "monitors-backlight-device-label": "背光裝置", + "monitors-brightness-step-description": "微調亮度調整一格的大小 (滾輪及鍵盤快捷鍵)", + "monitors-brightness-step-label": "亮度步進大小", + "monitors-brightness-unavailable-ddc-disabled": "無法使用亮度控制, 啟用 \"外部亮度調整支援\" 來控制這個顯示器的亮度", + "monitors-brightness-unavailable-generic": "無法在這個顯示器使用亮度控制", + "monitors-desc": "替個別的顯示器調整亮度", + "monitors-enforce-minimum-description": "解決某些顯示器在設置 0% 亮度時會完全關閉背光的問題", + "monitors-enforce-minimum-label": "強制最小 1% 亮度", + "monitors-external-brightness-description": "啟用 DDCUtil 支援以替外部顯示器使用 DDC/CI 協定控制亮度", + "monitors-external-brightness-label": "外部亮度調整支援", + "monitors-global-brightness-description": "同時調整所有顯示器的亮度。", + "monitors-global-brightness-label": "所有顯示器", + "monitors-title": "個別顯示器設定", + "night-light-auto-schedule-description": "基於在 {location} 日出日落的時間設定 — 推薦", + "night-light-auto-schedule-label": "自動排程", + "night-light-desc": "降低藍光照射可以幫助你的睡眠及眼睛疲勞", + "night-light-enable-description": "套用一個暖色濾鏡來降低藍光照射", + "night-light-enable-label": "啟用夜燈模式", + "night-light-force-activation-description": "忽略排程並立刻啟用夜燈模式", + "night-light-force-activation-label": "強制啟用", + "night-light-manual-schedule-description": "自訂日出及日落的時間", + "night-light-manual-schedule-label": "手動排程", + "night-light-manual-schedule-select-start": "選擇開始時間", + "night-light-manual-schedule-select-stop": "選擇結束時間", + "night-light-manual-schedule-sunrise": "日出時間", + "night-light-manual-schedule-sunset": "日落時間", + "night-light-temperature-day": "日間", + "night-light-temperature-day-description": "控制日間的顯示色溫", + "night-light-temperature-description": "設定日間及夜間的顯示色溫", + "night-light-temperature-label": "色溫", + "night-light-temperature-night": "夜間", + "night-light-temperature-night-description": "控制夜間的顯示色溫", + "title": "顯示器" + }, + "dock": { + "appearance-background-opacity-description": "調整 Dock 的背景不透明度", + "appearance-border-radius-description": "調整 Dock 的邊框圓角。", + "appearance-border-radius-label": "邊框圓角", + "appearance-colorize-icons-description": "將主題色彩套用至 Dock 上的應用程式圖示(僅非焦點應用程式)。", + "appearance-colorize-icons-label": "為圖示上色", + "appearance-dead-opacity-description": "調整未執行中應用程式圖示的不透明度。", + "appearance-dead-opacity-label": "非活躍應用程式不透明度", + "appearance-desc": "自訂 Dock 的外觀及行為", + "appearance-display-auto-hide": "自動隱藏", + "appearance-display-description": "選擇 Dock 的行為", + "appearance-display-exclusive": "獨占空間", + "appearance-dock-indicator-description": "當 Dock 隱藏時,顯示一個小指示器。", + "appearance-dock-indicator-label": "Dock 指示器", + "appearance-floating-distance-description": "設定 Dock 與螢幕邊緣相隔的距離", + "appearance-floating-distance-label": "Dock 懸浮間距", + "appearance-group-apps-description": "將同一應用程式的多個視窗分組到一個 Dock 項目中。", + "appearance-group-apps-label": "分組相同應用程式", + "appearance-group-click-action-cycle": "切換視窗", + "appearance-group-click-action-description": "選擇左鍵點擊對分組應用程式執行的操作。", + "appearance-group-click-action-label": "分組應用程式點擊動作", + "appearance-group-click-action-list": "開啟視窗列表", + "appearance-group-context-menu-mode-description": "選擇分組應用程式上下文選單的行為方式。", + "appearance-group-context-menu-mode-extended": "擴展", + "appearance-group-context-menu-mode-label": "分組應用程式選單模式", + "appearance-group-context-menu-mode-list": "視窗列表", + "appearance-group-indicator-style-description": "選擇分組執行指示器如何顯示焦點視窗狀態。", + "appearance-group-indicator-style-dots": "點", + "appearance-group-indicator-style-label": "分組指示器樣式", + "appearance-group-indicator-style-number": "數字", + "appearance-hide-show-speed-description": "調整 Dock 隱藏及浮出的動畫速度", + "appearance-hide-show-speed-label": "隱藏/浮出速度", + "appearance-icon-size-description": "調整 Dock 的整體大小", + "appearance-icon-size-label": "Dock 大小", + "appearance-inactive-indicators-description": "為所有應用程式顯示指示膠囊(而不僅是正在運作的程式)。", + "appearance-inactive-indicators-label": "運作指示器", + "appearance-indicator-color-description": "選擇隱藏 Dock 指示器的顏色。", + "appearance-indicator-color-label": "指示器顏色", + "appearance-indicator-opacity-description": "調整隱藏 Dock 指示器的不透明度。", + "appearance-indicator-opacity-label": "指示器不透明度", + "appearance-indicator-thickness-description": "啟用較粗的隱藏 Dock 指示器(6px 而非 3px)。", + "appearance-indicator-thickness-label": "較粗的指示器", + "appearance-launcher-icon-description": "選擇用於 Dock Launcher 的圖示。", + "appearance-launcher-icon-label": "啟動器圖示", + "appearance-launcher-position-description": "選擇啟動器圖示在停靠欄中的顯示位置。", + "appearance-launcher-position-end": "結束", + "appearance-launcher-position-label": "啟動器位置", + "appearance-launcher-position-start": "開始", + "appearance-launcher-use-distro-logo-description": "使用你的發行版標誌作為 Dock Launcher 圖示。", + "appearance-launcher-use-distro-logo-label": "使用發行版標誌而非圖示", + "appearance-pinned-static-description": "總是將釘選的程式圖示固定放在左邊", + "appearance-pinned-static-label": "固定放置釘選程式", + "appearance-position-description": "選擇 Dock 該放在畫面的哪裡", + "appearance-position-label": "位置", + "appearance-show-launcher-icon-description": "在 Dock 中顯示應用程式啟動器圖示。", + "appearance-show-launcher-icon-label": "顯示應用程式啟動器", + "appearance-sit-on-frame-description": "將停靠欄對齊在框架邊框內,而不是位於頂部。", + "appearance-sit-on-frame-label": "停靠欄位於框架上", + "appearance-type-attached": "停靠", + "appearance-type-description": "選擇浮動藥丸樣式或附著在邊緣的靜態列。", + "appearance-type-floating": "浮動", + "appearance-type-label": "Dock 樣式", + "enabled-description": "顯示或完全隱藏 Dock", + "enabled-label": "啟用 Dock", + "monitors-desc": "只在特定的顯示器顯示 Dock, 如果全部取消勾選則會顯示在所有顯示器", + "monitors-only-same-monitor-description": "只顯示與 Dock 在同一個顯示器的程式", + "monitors-only-same-monitor-label": "只在相同顯示器的程式", + "monitors-title": "在顯示器顯示", + "title": "Dock" + }, + "general": { + "copy-settings": "複製設定", + "fonts-default-description": "套用在整個介面的主要字型", + "fonts-default-label": "預設字型", + "fonts-default-placeholder": "選擇預設字型", + "fonts-default-scale-description": "增大或縮小預設的文字大小", + "fonts-default-scale-label": "預設文字大小", + "fonts-default-search-placeholder": "搜尋字型...", + "fonts-desc": "選擇要套用在整個介面的字型", + "fonts-monospace-description": "套用在數字及狀態顯示的等寬字型", + "fonts-monospace-label": "等寬字型", + "fonts-monospace-placeholder": "選擇等寬字型...", + "fonts-monospace-scale-description": "增大或縮小等寬文字的大小", + "fonts-monospace-scale-label": "等寬字型大小", + "fonts-monospace-search-placeholder": "搜尋等寬字型...", + "fonts-title": "字型", + "keybinds-conflict-description": "此按鍵組合已分配給 {action}。", + "keybinds-conflict-title": "按鍵綁定衝突", + "keybinds-description": "配置面板和啟動器的全域導航鍵。", + "keybinds-down": "下移", + "keybinds-enter": "確認 / 動作", + "keybinds-escape": "關閉 / 返回", + "keybinds-left": "向左移動", + "keybinds-modifier-description": "快捷鍵必須包含修飾鍵 (Ctrl 或 Alt)。", + "keybinds-modifier-title": "需要修飾鍵", + "keybinds-remove": "刪除", + "keybinds-right": "向右移動", + "keybinds-title": "導覽按鍵綁定", + "keybinds-up": "上移", + "language-desc": "選擇你偏好使用的語言", + "language-select-auto-detect": "自動選擇", + "language-select-description": "選擇要在應用程式所使用的介面語言", + "language-select-label": "介面語言", + "launch-setup-wizard": "設定精靈", + "profile-desc": "編輯你的使用者資訊及頭像", + "profile-picture-description": "你的頭像會在整個介面中出現", + "profile-picture-label": "個人資料圖片", + "profile-select-avatar": "選擇作為頭像的圖片", + "profile-title": "個人檔案", + "profile-tooltip": "個人頭像", + "reverse-scrolling-description": "反轉解讀的捲動方向", + "reverse-scrolling-label": "反轉捲動", + "screen-corners-desc": "自訂畫面邊角的弧度及視覺效果", + "screen-corners-radius-description": "調整畫面所使用的邊角半徑", + "screen-corners-radius-label": "畫面邊角半徑", + "screen-corners-radius-reset": "重設畫面邊角半徑", + "screen-corners-show-corners-description": "在畫面周邊顯示圓角", + "screen-corners-show-corners-label": "顯示畫面邊角", + "screen-corners-solid-black-description": "使用黑色而不是工具列背景色", + "screen-corners-solid-black-label": "黑色邊角", + "screen-corners-title": "畫面邊角", + "settings-copied": "已複製設定至剪貼簿", + "smooth-scrolling-description": "為列表捲動新增動畫,以獲得更流暢的滾輪體驗。", + "smooth-scrolling-label": "平滑捲動", + "tab-basics": "基礎", + "tab-keybinds": "按鍵綁定" + }, + "hooks": { + "color-generation-description": "顏色生成完成後執行的命令(例如,基於桌布的主題設定後)。", + "color-generation-label": "顏色已生成", + "color-generation-placeholder": "例如:notify-send \"顏色\" \"已生成\"", + "info-command-info-description": "• 指令將透過 shell (sh -lc) 執行
• 指令會在背景 (detached 狀態) 執行
• 測試按鈕會以當下填寫的內容執行", + "info-parameters-description": "• Wallpaper hook: $1 = 桌面背景路徑, $2 = 螢幕名稱, $3 = 主題 (深色/淺色)
• Theme toggle hook: $1 = true/false (深色模式狀態)
• Screen lock/unlock hooks: $1 = lock/unlock (螢幕鎖定狀態)
• Performance mode hooks: 無參數
• Session hook: $1 = action (shutdown/reboot)", + "info-parameters-label": "可用參數", + "noctalia-started-description": "Noctalia 完成載入時要執行的指令。", + "noctalia-started-label": "Noctalia 已啟動", + "noctalia-started-placeholder": "例如: notify-send 'Noctalia 已載入'", + "performance-mode-disabled-description": "Noctalia 效能模式停用時要執行的指令", + "performance-mode-disabled-label": "效能模式停用時", + "performance-mode-disabled-placeholder": "例如: notify-send \"Performance\" \"Mode disabled\"", + "performance-mode-enabled-description": "Noctalia 效能模式啟用時要執行的指令", + "performance-mode-enabled-label": "效能模式啟用時", + "performance-mode-enabled-placeholder": "例如: notify-send \"Performance\" \"Mode enabled\"", + "screen-lock-description": "在畫面鎖定時要執行的指令", + "screen-lock-label": "畫面鎖定時", + "screen-lock-placeholder": "例如: notify-send \"Screen\" \"Locked\"", + "screen-unlock-description": "在鎖定畫面解鎖時要執行的指令", + "screen-unlock-label": "畫面解鎖時", + "screen-unlock-placeholder": "例如: notify-send \"Screen\" \"Unlocked\"", + "session-description": "在關機或重新啟動前要執行的指令, 接收到的執行動作會視為 $1 (shutdown/reboot)", + "session-label": "工作階段結束時", + "session-placeholder": "例如: notify-send \"Session\" \"$1\"", + "system-hooks-desc": "設定當系統事件發生時要執行的指令", + "system-hooks-enable-description": "啟用或停用所有的 hook 指令", + "system-hooks-enable-label": "啟用 hook", + "system-hooks-title": "系統 hook", + "theme-changed-description": "在亮色及深色主題切換時要執行的指令", + "theme-changed-label": "主題切換時", + "theme-changed-placeholder": "例如: notify-send \"Theme\" \"Toggled\"", + "title": "Hooks", + "wallpaper-changed-description": "在桌布更換時要執行的指令", + "wallpaper-changed-label": "桌布更換時", + "wallpaper-changed-placeholder": "例如: notify-send \"Wallpaper\" \"Changed\"" + }, + "idle": { + "custom-add": "新增指令", + "custom-description": "在一段時間不活動後執行 shell 命令。", + "custom-entry-command": "命令", + "custom-entry-delete": "刪除", + "custom-entry-edit": "編輯自訂命令", + "custom-entry-name": "名稱", + "custom-entry-name-placeholder": "例如:關燈", + "custom-entry-new": "新增自訂命令", + "custom-entry-timeout": "閒置時間", + "custom-entry-timeout-format": "{count} 秒", + "custom-entry-timeout-format-plural": "{count} 秒", + "custom-entry-unnamed": "未命名命令", + "custom-label": "自訂閒置指令", + "enable-description": "在一段時間不活動後,自動關閉螢幕、鎖定或暫停。", + "enable-label": "啟用閒置管理", + "fade-duration-description": "每次操作觸發前,螢幕漸變為黑的動畫持續時間(秒)。任何滑鼠移動都會取消漸變。", + "fade-duration-label": "淡入淡出時長", + "lock-description": "鎖定畫面啟用前的閒置秒數。", + "lock-label": "鎖定螢幕", + "resume-command-label": "恢復命令", + "screen-off-description": "顯示器關閉前的不活動秒數。", + "screen-off-label": "關閉螢幕", + "status-description": "由混成器回報的閒置時間。", + "status-label": "閒置時間", + "suspend-description": "系統暫停前的閒置秒數。", + "tab-behavior": "行為", + "tab-custom": "自訂", + "timeouts-description": "設定為0以停用一個階段。保持喚醒啟用時,逾時會暫停。", + "timeouts-label": "逾時", + "title": "閒置", + "unavailable": "此 Compositor 不支援原生閒置監控。" + }, + "indicator": { + "default-value": "預設值: {value}", + "system-default": "系統預設" + }, + "launcher": { + "clipboard-desc": "在啟動器存取及整理你的剪貼簿歷史", + "execute-desc": "設定應用程式該如何啟動", + "execute-title": "執行", + "settings-annotation-tool-description": "點擊剪貼簿歷史記錄中的註記按鈕時要執行的命令。圖片將會透過管線傳送至此命令。", + "settings-annotation-tool-label": "註記工具", + "settings-annotation-tool-placeholder": "例如: gradia', 'satty -f -'", + "settings-auto-paste-description": "自動貼上選取的剪貼簿項目, 需要 wtype 套件", + "settings-auto-paste-label": "自動貼上", + "settings-clip-chips-description": "顯示一個分頁列,以按類型(圖片、連結、檔案、程式碼等)篩選剪貼簿歷史記錄。", + "settings-clip-chips-label": "啟用類別標籤", + "settings-clip-preview-description": "使用 >clip 命令時顯示剪貼簿內容的預覽。", + "settings-clip-preview-label": "啟用剪貼簿預覽", + "settings-clip-smart-icons-description": "為連結、檔案、顏色和程式碼顯示特定圖示,而不是通用的剪貼簿圖示。", + "settings-clip-smart-icons-label": "啟用智慧圖示", + "settings-clip-wrap-text-description": "將剪貼簿的文字換行顯示而不是截掉", + "settings-clip-wrap-text-label": "剪貼簿文字換行", + "settings-clipboard-history-description": "在啟動器存取先前所複製的項目", + "settings-clipboard-history-label": "啟用剪貼簿歷史", + "settings-clipboard-watch-image-description": "傳遞給 wl-paste 以進行圖像更改的完整命令字串(需要重新啟動)。", + "settings-clipboard-watch-image-label": "圖像監控命令", + "settings-clipboard-watch-text-description": "傳遞給 wl-paste 以進行文字變更的完整命令字串(需要重新啟動)。", + "settings-clipboard-watch-text-label": "文字監看指令", + "settings-custom-launch-prefix-description": "在命令前加上自訂啟動器前綴(例如: 用「runapp」來整合 systemd)。", + "settings-custom-launch-prefix-enabled-description": "使用自訂前綴而不是預設的方式來啟動應用程式", + "settings-custom-launch-prefix-enabled-label": "啟用自訂的程式啟動前綴", + "settings-custom-launch-prefix-label": "自訂啟動前綴", + "settings-density-description": "調整應用程式圖示大小和 Launcher 密度。", + "settings-density-label": "密度", + "settings-desc": "自訂啟動器的外觀及行為", + "settings-enable-session-search-description": "在 Launcher 中搜尋時顯示工作階段動作(鎖定、關機、重新啟動等)。", + "settings-enable-session-search-label": "啟用工作階段搜尋", + "settings-enable-settings-search-description": "在啟動器中搜尋時顯示設定結果。", + "settings-enable-settings-search-label": "啟用設定搜尋", + "settings-enable-windows-search-description": "搜尋並聚焦作用中視窗。", + "settings-enable-windows-search-label": "啟用視窗搜尋", + "settings-grid-view-description": "將項目以方格排列而不是列表顯示", + "settings-icon-mode-description": "使用系統原生的圖示而不是自 Tabler 圖示包帶入", + "settings-icon-mode-label": "使用原生圖示", + "settings-ignore-mouse-input-description": "對啟動器停用滑鼠互動及滾輪捲動", + "settings-ignore-mouse-input-label": "忽略滑鼠輸入", + "settings-overlay-layer-description": "在覆蓋層上顯示啟動器,位於全螢幕視窗之上。啟用時,啟動器不會附加到列。", + "settings-overlay-layer-label": "在全螢幕上方顯示", + "settings-position-description": "選擇啟動器面板要出現的地方", + "settings-show-categories-description": "顯示分類標籤來篩選應用程式", + "settings-show-categories-label": "顯示分類", + "settings-show-icon-background-description": "在圖示後面顯示一個圓角矩形為背景", + "settings-show-icon-background-label": "顯示圖示背景", + "settings-sort-by-usage-description": "當啟用時, 最常使用的程式會出現在列表的上面", + "settings-sort-by-usage-label": "按最常使用排序", + "settings-terminal-command-description": "打開終端機的指令, 例如: 'kitty -e'或 'gnome-terminal --'", + "settings-terminal-command-label": "終端機指令", + "settings-use-app2unit-description": "使用替代的啟動方式,以更好地管理應用程式程序並避免問題。", + "settings-use-app2unit-label": "使用 App2Unit 啟動應用程式", + "settings-view-mode-description": "選擇啟動器項目的版面配置。", + "settings-view-mode-label": "檢視模式", + "title": "啟動器" + }, + "location": { + "auto-locate-description": "使用您的 IP 位址定期自動偵測您的位置。", + "auto-locate-label": "自動定位", + "calendar-cards-desc": "整理及啟用/停用在行事曆面板使用的卡片", + "calendar-cards-title": "行事曆卡片", + "calendar-header-label": "行事曆抬頭", + "calendar-month-label": "行事曆月曆", + "date-time-12hour-format-description": "在鎖定畫面及行事曆以12小時制來顯示時間, 工具列的時鐘是個別設定的", + "date-time-12hour-format-label": "使用12小時制", + "date-time-desc": "自訂日期與時間該如何顯示", + "date-time-first-day-of-week-automatic": "自動 (使用系統區域設定)", + "date-time-first-day-of-week-description": "選擇行事曆的每一週自星期幾開始", + "date-time-first-day-of-week-label": "每週的第一天", + "date-time-show-events-description": "在行事曆面板裡顯示活動", + "date-time-show-events-label": "顯示行事曆活動", + "date-time-title": "日期與時間", + "date-time-use-analog-description": "在日曆視窗上顯示類比時鐘。", + "date-time-use-analog-label": "使用類比時鐘", + "date-time-week-numbers-description": "在行事曆顯示今年當週的週數 (例如: 第38週)", + "date-time-week-numbers-label": "顯示週數", + "geolocate-now-button": "立即定位", + "location-desc": "設定你的所在地以取得準確的天氣及夜光模式時間", + "location-search-description": "例如: Taipei (英文)", + "location-search-label": "搜尋位置", + "location-search-placeholder": "輸入位置名稱", + "location-title": "你的所在地", + "weather-desc": "選擇你偏好的溫度單位", + "weather-enabled-description": "獲取並在整個介面中顯示現在天氣; 當停用時, 所有的天氣元素都會被隱藏而且不會做出網路請求", + "weather-enabled-label": "啟用天氣", + "weather-fahrenheit-description": "以華氏 (°F) 而不是攝氏 (°C) 顯示溫度", + "weather-fahrenheit-label": "以華氏溫度 (°F) 顯示", + "weather-hide-city-description": "自各個介面中的天氣區塊隱藏城市名稱", + "weather-hide-city-label": "隱藏城市名稱", + "weather-hide-timezone-description": "自各個介面中的天氣區塊隱藏所在時區", + "weather-hide-timezone-label": "隱藏時區", + "weather-show-effects-description": "在氣象卡片顯示額外的視覺效果 (像是下雨, 下雪或閃電)", + "weather-show-effects-label": "顯示天氣特效", + "weather-show-in-calendar-description": "在檢視行事曆時一同顯示每日天氣", + "weather-show-in-calendar-label": "在行事曆中顯示天氣", + "weather-talia-mascot-always-description": "始終使用 Talia emojis 而不是 tabler icons。", + "weather-talia-mascot-always-label": "始終顯示Talia天氣表情符號" + }, + "lock-screen": { + "allow-password-with-fprintd-description": "當 fprintd(指紋認證)處於活動狀態時,此選項允許您使用密碼而非指紋登入。", + "allow-password-with-fprintd-label": "允許使用 fprintd 進行密碼登入", + "auto-start-auth-description": "自動啟動指紋驗證,無需按鍵或點擊按鈕。", + "auto-start-auth-label": "自動啟動驗證", + "clock-format-description": "使用日期/時間語法令牌自訂時鐘格式。", + "clock-format-label": "時鐘格式", + "clock-style-analog": "類比", + "clock-style-custom": "自訂", + "clock-style-description": "選擇鎖定畫面時鐘的視覺樣式。", + "clock-style-digital": "數位", + "clock-style-label": "時鐘樣式", + "compact-lockscreen-description": "只顯示登入欄位及電源控制, 隱藏天氣及媒體小工具", + "compact-lockscreen-label": "精簡鎖定畫面", + "enable-lockscreen-media-controls-description": "在鎖定畫面顯示互動式媒體播放控制項。", + "enable-lockscreen-media-controls-label": "鎖定畫面媒體控制項", + "lock-on-suspend-description": "在暫停系統時自動鎖定畫面", + "lock-on-suspend-label": "暫停時鎖定", + "lock-screen-animations-description": "啟用或停用鎖定畫面動畫。", + "lock-screen-animations-label": "鎖定畫面動畫", + "lock-screen-blur-strength-description": "對鎖定畫面桌布套用模糊效果。", + "lock-screen-blur-strength-label": "鎖定螢幕模糊強度", + "lock-screen-tint-strength-description": "將色調疊加套用到鎖定畫面桌布。", + "lock-screen-tint-strength-label": "鎖定畫面著色強度", + "monitors-desc": "在特定顯示器上顯示鎖定畫面。如果未選擇,則預設為所有顯示器。", + "password-chars-description": "用於隱藏密碼的可愛圖示。", + "password-chars-label": "隨機密碼圖示", + "show-hibernate-description": "在電源及工作階段選單顯示睡眠", + "show-hibernate-label": "顯示睡眠", + "show-session-buttons-description": "允許在鎖定畫面存取電源控制", + "show-session-buttons-label": "電源控制", + "title": "鎖定畫面" + }, + "network": { + "bluetooth-description": "啟用藍牙管理", + "bluetooth-rssi-polling-description": "透過 bluetoothctl 定期取樣連接裝置的 RSSI 值; 不一定每個裝置都可用; 啟用時會使用最少資源", + "bluetooth-rssi-polling-label": "回報藍牙訊號", + "desc": "管理 Wi-Fi 及藍牙連線", + "wifi-description": "管理無線網路 (需要 NetworkManager 套件)" + }, + "notifications": { + "duration-critical-urgency-description": "設定非常危急的通知該顯示多久", + "duration-critical-urgency-label": "非常危急", + "duration-desc": "設定通知該基於它們的優先級顯示多久", + "duration-low-urgency-description": "設定低急迫性的通知該顯示多久", + "duration-low-urgency-label": "低急迫性", + "duration-normal-urgency-description": "設定普通急迫的通知該顯示多久", + "duration-normal-urgency-label": "普通急迫", + "duration-respect-expire-description": "使用通知中設定的到期逾時時間。", + "duration-respect-expire-label": "遵循到期逾時設定", + "duration-title": "通知顯示時長", + "history-clear-dismiss-description": "關閉時從歷史記錄中清除通知。", + "history-clear-dismiss-label": "關閉時清除", + "history-critical-urgency-description": "將非常危急的通知存放到歷史通知", + "history-critical-urgency-label": "儲存非常危急到歷史通知", + "history-desc": "根據通知的緊急程度,控制要將哪些通知儲存至紀錄。", + "history-low-urgency-description": "將低急迫性的通知存放到歷史通知", + "history-low-urgency-label": "儲存低急迫性到歷史通知", + "history-normal-urgency-description": "將普通急迫的通知存放到歷史通知", + "history-normal-urgency-label": "儲存普通急迫到歷史通知", + "monitors-desc": "只在特定的顯示器顯示通知, 如果全部取消勾選則會顯示在所有顯示器", + "rules-action-block": "區塊", + "rules-action-block-desc": "完全跳過。", + "rules-action-hide": "隱藏", + "rules-action-hide-desc": "無彈窗,無聲音,新增至歷史記錄。", + "rules-action-label": "動作", + "rules-action-mute": "靜音", + "rules-action-mute-desc": "無聲音,仍顯示彈出視窗並記錄在歷史中。", + "rules-add": "新增規則", + "rules-delete": "刪除規則", + "rules-description": "匹配應用程式名稱或內容。規則會依序檢查,並套用第一個匹配項。", + "rules-edit": "編輯規則", + "rules-label": "篩選規則", + "rules-pattern-label": "模式", + "rules-pattern-placeholder": "firefox, discord, 或 /regex/", + "rules-tab": "規則", + "settings-always-on-top-description": "在全螢幕視窗與其他層級上方顯示通知。", + "settings-background-opacity-description": "調整通知背景的不透明度", + "settings-density-description": "選擇通知卡密度。", + "settings-density-label": "密度", + "settings-desc": "設定通知的外觀及行為", + "settings-do-not-disturb-description": "當啟用時, 停止彈出所有通知", + "settings-enabled-description": "啟用或完全停用通知的 daemon, 需要重新啟動 Noctalia 介面", + "settings-enabled-label": "啟用通知", + "settings-location-description": "通知該在畫面的哪裡出現", + "settings-markdown-description": "使用 Markdown 格式渲染通知內容。", + "settings-markdown-label": "啟用 Markdown", + "sounds-desc": "設定通知所使用的音效及音量", + "sounds-enabled-description": "在有新通知時啟用音效", + "sounds-enabled-label": "啟用通知音效", + "sounds-excluded-apps-description": "針對特定具有內建音效的應用程式,略過在此設定的通知音效。", + "sounds-excluded-apps-label": "排除的應用程式", + "sounds-excluded-apps-placeholder": "discord,firefox,chrome,chromium,edge", + "sounds-files-critical-description": "作為非常危急的通知音效檔案所在的路徑", + "sounds-files-critical-label": "非常危急的音效", + "sounds-files-critical-select-title": "選擇相當危急音效檔", + "sounds-files-desc": "為不同的通知緊急程度等級設定自訂音效檔案。", + "sounds-files-low-description": "作為低急迫性的的通知音效檔案所在的路徑", + "sounds-files-low-label": "低急迫性的音效", + "sounds-files-low-select-title": "選擇低急迫性音效檔", + "sounds-files-normal-description": "作為普通急迫的通知音效檔案所在的路徑", + "sounds-files-normal-label": "普通急迫的音效", + "sounds-files-normal-select-title": "選擇普通急迫音效檔", + "sounds-files-placeholder": "輸入音效檔所在的路徑", + "sounds-files-select-file": "選擇音效檔", + "sounds-files-title": "音效檔", + "sounds-files-unified-description": "作為通知音效的檔案所在的路徑", + "sounds-files-unified-label": "通知音效", + "sounds-files-unified-select-title": "選擇通知音效檔", + "sounds-separate-description": "分別為低急迫性, 普通急迫及相當危急通知使用不同的音效檔", + "sounds-separate-label": "為不同優先級使用不同音效", + "sounds-title": "音效設定", + "sounds-unavailable-description": "安裝 qt6 multimedia 套件以啟用通知音效", + "sounds-unavailable-label": "通知音效無法使用", + "sounds-volume-description": "調整通知音效的音量大小", + "sounds-volume-label": "音效音量", + "toast-battery-description": "當電池電量低於此百分比時顯示警告。", + "toast-battery-label": "低電量提醒", + "toast-desc": "設定氣泡訊息的外觀及行為", + "toast-keyboard-description": "當鍵盤佈局改變時顯示氣泡訊息", + "toast-keyboard-label": "鍵盤佈局", + "toast-media-description": "媒體播放狀態變更時顯示氣泡訊息", + "toast-media-label": "媒體" + }, + "osd": { + "always-on-top-description": "將螢幕顯示在全螢幕視窗與其他層疊之上", + "always-on-top-label": "總是置頂", + "background-opacity-description": "控制螢幕顯示背景的不透明度", + "background-opacity-label": "背景不透明度", + "description": "設定螢幕顯示的指示項目, 如音量及亮度", + "duration-auto-hide-description": "調整螢幕顯示消失的時間", + "duration-auto-hide-label": "在時間後隱藏", + "duration-desc": "設定螢幕顯示在自動消失前要顯示多久", + "duration-title": "自動隱藏時間", + "enabled-description": "音量及亮度改變時即時顯示", + "enabled-label": "啟用螢幕顯示", + "events-desc": "選擇要哪些事件觸發螢幕顯示", + "general-desc": "設定螢幕顯示的外觀及行為", + "location-description": "設定螢幕顯示在哪裡出現", + "monitors-desc": "只在特定的顯示器顯示OSD, 如果全部取消勾選則會顯示在所有顯示器", + "title": "螢幕顯示 (OSD)", + "types-brightness-description": "當亮度改變時秀出螢幕顯示", + "types-brightness-label": "亮度", + "types-custom-text-description": "顯示從 IPC 傳入的自訂文字訊息", + "types-custom-text-label": "自訂文字", + "types-desc": "選取會觸發螢幕顯示的事件", + "types-input-volume-description": "當麥克風改變時秀出螢幕顯示", + "types-input-volume-label": "輸入音量", + "types-lockkey-description": "當 Caps Lock, Num Lock, Scroll Lock 切換時秀出螢幕顯示", + "types-lockkey-label": "Lock 鍵", + "types-media-description": "媒體播放狀態變更時秀出螢幕顯示 (播放, 暫停, 跳過)", + "types-media-label": "媒體播放", + "types-title": "螢幕顯示的觸發事件", + "types-volume-description": "當輸出音量改變時秀出螢幕顯示", + "types-volume-label": "輸出音量" + }, + "plugins": { + "auto-update": "自動更新外掛程式", + "auto-update-description": "在 shell 啟動時自動更新所有外掛程式。", + "available-description": "從設定的來源檢視及安裝外掛模組", + "available-label": "可用的外掛模組", + "available-no-plugins-description": "檢查你的模組來源或者重新整理列表", + "available-no-plugins-label": "沒有可用的外掛模組", + "check-for-updates": "檢查更新", + "checking-for-updates": "正在檢查更新...", + "collision-already-installed": "這個模組已經安裝過了", + "collision-custom-version-exists": "已經安裝了來自 \"{source}\" 的自訂版本", + "collision-official-version-exists": "已經安裝了這個模組的官方版本", + "development-disable": "停用此插件的開發者模式", + "development-enable": "為此外掛程式啟用開發模式。", + "filter-downloaded": "已下載", + "filter-not-downloaded": "未下載", + "filter-tags-description": "以分類或下載狀態篩選外掛模組", + "filter-tags-label": "標籤", + "hot-reload-description": "當模組使用的檔案變動時自動重新載入, 在模組開發時特別有用", + "hot-reload-label": "熱重載 (開發模式)", + "hot-reloaded": "重新載入模組: {name}", + "install-error": "安裝失敗: {error}", + "install-incompatible": "{plugin} 需要 Noctalia v{version} 或以上", + "install-success": "成功安裝 {plugin}", + "installed-description": "設定及管理所有本機已安裝的外掛模組", + "installed-label": "已安裝的外掛模組", + "installed-no-plugins-description": "從 \"可用\"頁面來安裝外掛模組", + "installed-no-plugins-label": "沒有安裝外掛模組", + "installing": "正在安裝 {plugin}...", + "notify-updates": "外掛程式更新通知", + "notify-updates-description": "當有新的外掛程式更新可用時,顯示通知彈出視窗。", + "open-plugin-page": "打開模組頁面", + "open-plugins-tab": "開啟外掛模組設定", + "plugin-settings-title": "{plugin} 設定", + "refresh-refreshing": "正在重新整理模組清單...", + "refresh-tooltip": "重新整理可用的外掛模組", + "settings-error-not-loaded": "沒有載入外掛模組", + "settings-saved": "已儲存模組設定", + "settings-tooltip": "外掛模組設定", + "source-custom": "自訂來源", + "sources-add-custom": "新增自訂存儲庫", + "sources-add-dialog-description": "新增一個 GitHub 儲存庫作為模組來源", + "sources-add-dialog-error": "新增模組來源失敗", + "sources-add-dialog-name": "儲存庫名稱", + "sources-add-dialog-name-placeholder": "我的自訂外掛模組", + "sources-add-dialog-success": "成功新增外掛模組來源", + "sources-add-dialog-title": "新增外掛模組來源", + "sources-add-dialog-url": "儲存庫 URL", + "sources-description": "管理外掛套件存儲庫。", + "sources-placeholder": "我的超酷儲存庫", + "sources-remove-tooltip": "移除外掛模組來源", + "title": "外掛模組 (Plugins)", + "translations-reloaded": "已重新載入翻譯:{name}", + "uninstall-dialog-description": "你確定想要移除 {plugin}? 這樣會移除所有模組的資料", + "uninstall-dialog-title": "移除外掛模組", + "uninstall-error": "移除失敗: {error}", + "uninstall-success": "成功移除 {plugin}", + "uninstalling": "正在移除 {plugin}...", + "update-all": "將全部 {count} 個都更新", + "update-all-success": "已成功更新所有的外掛模組", + "update-available": "有新的外掛模組更新", + "update-available-plural": "有外掛模組可更新 ({count}個)", + "update-error": "更新模組失敗: {plugin}: {error}", + "update-pending": "v{current} → v{new} (需要 Noctalia v{required})", + "update-success": "已更新 {plugin} 至 v{version}", + "update-version": "v{current} → v{new}", + "updating": "正在更新..." + }, + "region": { + "title": "地區" + }, + "session-menu": { + "countdown-duration-description": "設定在執行電源操作前要倒數計時多久", + "countdown-duration-label": "倒數長度", + "enable-countdown-description": "在執行電源操作前先顯示一個倒數計時器", + "enable-countdown-label": "啟用倒數計時", + "entries-desc": "自訂要出現在工作階段選單中的項目及順序", + "entries-title": "電源操作", + "entry-settings-command-description": "自訂執行這個動作所執行的指令, 留空則使用系統的預設指令", + "entry-settings-command-placeholder": "例如: systemctl poweroff", + "entry-settings-default-command-lock": "內建鎖定畫面 (沒有指令)", + "entry-settings-default-command-logout": "內建登出 (沒有指令)", + "entry-settings-default-info-description": "如果沒有指定自訂指令則使用系統的預設指令", + "entry-settings-default-info-label": "預設指令", + "entry-settings-keybind-description": "當會話選單打開時,分配一個按鍵組合來觸發此操作。", + "entry-settings-keybind-placeholder": "按下錄製以分配一個鍵...", + "entry-settings-keybind-recording": "錄製中... 按任意組合鍵", + "entry-settings-title": "設定{entry}", + "entry-settings-tooltip": "設定指令", + "general-desc": "設定工作階段選單的外觀和行為", + "large-buttons-layout-description": "選擇工作階段選單的按鈕該如何顯示", + "large-buttons-layout-label": "大按鈕佈局", + "large-buttons-style-description": "以方格排列的大按鈕顯示工作階段選單", + "large-buttons-style-label": "以大按鈕顯示", + "position-description": "選擇工作階段選單在哪裡顯示", + "show-header-description": "在工作階段選單顯示標題及關閉按鈕", + "show-header-label": "顯示標題列", + "show-keybinds-description": "在工作階段選項上顯示快捷鍵提示。", + "show-keybinds-label": "顯示快捷鍵" + }, + "system": { + "noctalia-performance-disable-desktop-widgets-description": "當 Noctalia 效能模式啟用時保持桌面小工具可見。", + "noctalia-performance-disable-desktop-widgets-label": "在效能模式下啟用桌面小工具", + "noctalia-performance-disable-wallpaper-description": "在啟用 Noctalia 效能模式時,保持桌面、概覽和鎖定畫面桌布可見。", + "noctalia-performance-disable-wallpaper-label": "在效能模式下啟用桌布渲染", + "title": "系統" + }, + "system-monitor": { + "critical-color-label": "危急顏色", + "custom-highlight-colors-title-label": "自訂突出色", + "disk-available-label": "可用磁碟空間", + "disk-section-label": "磁碟使用量", + "enable-dgpu-monitoring-description": "注意: 這個選項會喚醒你的 GPU (NVIDIA/AMD), 在混合顯卡的筆電上可能會嚴重影響電池續航力", + "enable-dgpu-monitoring-label": "啟用監視獨立 GPU", + "external-monitor-description": "輸入當啟用外部系統監視器時想要執行的指令或應用程式路徑", + "external-monitor-label": "外部系統監視器指令", + "external-monitor-placeholder": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor", + "general-desc": "設定系統監視器的外觀及行為", + "gpu-section-label": "GPU 溫度", + "highlight-colors-section-label": "突出色", + "polling-interval-label": "輪詢間隔", + "polling-section-description": "設定該多久更新一次系統計量值", + "polling-section-label": "輪詢間隔", + "threshold-critical": "嚴重", + "threshold-warning": "警告", + "thresholds-section-description": "調整每個系統量測值的警告/危急門檻", + "use-custom-highlight-colors-description": "當停用時, 將會使用主題預設的突出色", + "use-custom-highlight-colors-label": "使用自訂的突出色", + "warning-color-label": "警告顏色" + }, + "user-interface": { + "allow-panels-without-bar-description": "當啟用時, 面板可以在任何螢幕上打開; 當停用時面板就只會在有工具列的螢幕上打開, 也可以減少記憶體使用量", + "allow-panels-without-bar-label": "允許面板在沒有工具列的畫面顯示", + "animation-disable-description": "停用所有動畫以獲得更快, 不拖泥帶水的體驗", + "animation-disable-label": "停用介面動畫", + "animation-speed-description": "調整介面全域的動畫速度", + "animation-speed-label": "動畫速度", + "animation-speed-reset": "重設動畫速度", + "appearance-desc": "自訂視覺元素, 如提示框, 邊框及陰影", + "blur-behind-description": "使用合成器模糊協定,模糊欄位和面板後方的區域。", + "blur-behind-label": "背景模糊", + "box-border-description": "在內容區塊的外圍顯示外框", + "box-border-label": "頁面容器外框", + "box-border-radius-description": "調整主要版面區塊(例如側邊欄、卡片與內容面板)的圓角弧度。", + "box-border-radius-label": "頁面容器半徑", + "control-border-radius-description": "控制互動元素(包括按鈕、切換開關與文字欄位)的圓角弧度。", + "control-border-radius-label": "控制項半徑", + "desc": "自訂介面顯示的行為, 外觀及風格", + "dim-desktop-description": "當功能表或面板打開時調暗桌布", + "dim-desktop-label": "調暗桌布", + "dimmer-opacity-description": "設定調暗桌布所使用的不透明度", + "dimmer-opacity-label": "調暗桌面不透明度", + "panel-background-opacity-description": "設定所有面板的背景不透明度 (工具列, 啟動器, 設定等...)", + "panel-background-opacity-label": "面板背景不透明度", + "panels-attached-to-bar-description": "將面板鎖定在工具列及畫面邊緣, 配合倒角可做出更一致的效果", + "panels-attached-to-bar-label": "面板貼齊邊緣", + "panels-overlay-description": "確保工具列及面板始終可見, 甚至疊過全螢幕的程式", + "panels-overlay-label": "維持工具列及面板在最上層", + "scaling-description": "調整除了工具列以外的整體介面大小", + "scaling-label": "介面比例", + "scrollbar-always-visible-description": "當內容可捲動時,保持捲軸始終可見,而不是僅在懸停時顯示。", + "scrollbar-always-visible-label": "永遠顯示捲軸", + "settings-panel-header": "設定面板", + "settings-panel-mode-description": "選擇介面設定出現所用的佈局 (可能需要重新打開)", + "settings-panel-mode-label": "設定面板顯示", + "settings-panel-sidebar-card-style-description": "用圓角填充背景包裹設定側邊欄。", + "settings-panel-sidebar-card-style-label": "側邊欄卡片樣式", + "shadows-description": "為工具列及面板啟用陰影", + "shadows-direction-description": "選擇陰影投射的位置。", + "shadows-direction-label": "陰影方向", + "shadows-label": "陰影", + "title": "使用者介面", + "tooltips-description": "在整個介面啟用或停用提示框", + "tooltips-label": "顯示提示框", + "translucent-widgets-description": "使面板內的按鈕、分頁和其他小部件半透明。", + "translucent-widgets-label": "半透明小工具" + }, + "wallpaper": { + "automation-change-mode-alphabetical": "照字母排序", + "automation-change-mode-description": "當自動更換時選擇以什麼規則選擇桌布", + "automation-change-mode-label": "更換模式", + "automation-custom-interval-description": "用 HH:MM 格式輸入時間 (例如: 01:30)", + "automation-custom-interval-label": "自訂間格", + "automation-interval-description": "桌布該多久自動更換", + "automation-interval-label": "桌布更換間格", + "automation-random-wallpaper-description": "在固定的間格隨機更換桌布", + "automation-scheduled-change-description": "在固定的間格自動更換桌布", + "automation-scheduled-change-label": "排程更換", + "look-feel-edge-smoothness-description": "在轉場邊緣套用柔和的羽化效果。", + "look-feel-edge-smoothness-label": "柔化轉場邊緣", + "look-feel-fill-color-description": "挑選一個桌布底下可能會顯示的填充實色", + "look-feel-fill-mode-description": "選擇圖像該以什麼方式縮放來符合顯示器的解析度", + "look-feel-fill-mode-label": "填充模式", + "look-feel-skip-startup-transition-description": "在 shell 啟動時跳過桌布動畫。", + "look-feel-skip-startup-transition-label": "跳過啟動過場動畫", + "look-feel-title": "外觀及風格", + "look-feel-transition-duration-description": "設定轉場動畫使用的秒數", + "look-feel-transition-duration-label": "轉場長度", + "look-feel-transition-type-description": "切換桌布時的轉場樣式", + "look-feel-transition-type-label": "轉場樣式", + "settings-clear-cache-button": "清除快取", + "settings-clear-cache-description": "清除快取中已調整大小的桌布以釋放磁碟空間。", + "settings-clear-cache-label": "桌面背景快取", + "settings-clear-cache-toast": "背景圖片快取已清除", + "settings-desc": "控制桌布的顯示和管理", + "settings-enable-management-description": "使用 Noctalia 管理桌布, 如果你偏好使用其他程式設定則取消", + "settings-enable-management-label": "啟用桌布管理", + "settings-enable-overview-description": "在總覽畫面套用一個模糊調暗過的桌布", + "settings-enable-overview-label": "啟用總覽桌布", + "settings-folder-description": "你主要存放桌布的文件夾路徑", + "settings-folder-label": "桌布文件夾", + "settings-hide-wallpaper-filenames-tooltip-hide": "隱藏檔案名稱", + "settings-hide-wallpaper-filenames-tooltip-show": "顯示檔案名稱", + "settings-monitor-specific-description": "替個別顯示器設定不同的桌布文件夾", + "settings-monitor-specific-label": "給特定顯示器所使用的文件夾", + "settings-monitor-specific-tooltip": "顯示器桌布文件夾", + "settings-overview-blur-strength-description": "將模糊強度應用於概覽。", + "settings-overview-blur-strength-label": "概覽模糊強度", + "settings-overview-tint-description": "將著色強度套用至概覽。", + "settings-overview-tint-label": "概覽著色強度", + "settings-recursive-search-description": "也同時搜尋桌布目錄底下的子文件夾", + "settings-recursive-search-label": "搜尋子文件夾", + "settings-select-monitor-folder": "選擇顯示器所使用的桌布文件夾", + "settings-selector-description": "選擇你的桌布", + "settings-selector-position-description": "選擇挑選桌布面板要在哪裡出現", + "settings-show-hidden-files-tooltip-hide": "隱藏隱藏檔案", + "settings-show-hidden-files-tooltip-show": "顯示隱藏檔案", + "settings-title": "桌布設定", + "settings-use-original-images-description": "跳過在顯示前調整桌布大小。節省磁碟空間並減少桌布更換時的 CPU 使用率,但對於非常大的圖像可能會佔用更多記憶體。", + "settings-use-original-images-label": "使用原始圖片", + "settings-view-mode-description": "選擇如何從您的目錄顯示桌布。", + "settings-view-mode-label": "檢視模式", + "view-mode-browse": "瀏覽目錄", + "view-mode-cycle-tooltip": "檢視模式:{mode} (點擊以變更)", + "view-mode-recursive": "扁平化的子目錄", + "view-mode-single": "根目錄" + } + }, + "placeholders": { + "add-new-keybind": "新增按鍵綁定", + "command-example": "echo \"Hello World\"", + "enter-command": "輸入要執行的指令 (程式或自訂腳本)", + "enter-ipc-identifier": "輸入 IPC 命令的唯一識別碼", + "enter-path": "輸入路徑...", + "enter-text-to-collapse": "例如: 'nothing is playing'. 也可用正規表示式 /regex/ 指定格式", + "enter-tooltip": "輸入提示框文字", + "enter-width-pixels": "以像素 (px) 輸入寬度", + "keybind-recording": "正在錄製按鍵綁定...", + "search": "在這裡搜尋...", + "search-icons": "例如: noctalia, niri, battery, cloud", + "search-launcher": "搜尋所有項目... 或輸入 > 使用指令", + "search-wallhaven": "搜尋 Wallhaven...", + "search-wallpapers": "輸入來篩選桌布..." + }, + "positions": { + "bottom": "底部", + "bottom-center": "底部置中", + "bottom-left": "左下角", + "bottom-right": "右下角", + "center": "居中", + "center-left": "左邊置中", + "center-right": "右邊置中", + "close-to-bar": "靠近工具列按鈕", + "follow-bar": "緊跟工具列", + "left": "左邊", + "right": "右邊", + "top": "頂部", + "top-center": "頂部置中", + "top-left": "左上角", + "top-right": "右上角" + }, + "session-menu": { + "action-in-seconds": "將在{seconds}秒後{action}...", + "cancel-timer": "取消計時器", + "title": "工作階段選單" + }, + "setup": { + "all-done": "完成!", + "appearance": { + "subheader": "選擇深色模式和顏色來源(桌布或預設)。" + }, + "customize": { + "header": "客製你的使用體驗", + "subheader": "調整工具列的位置, 密度, 比例等等" + }, + "skip-setup": "跳過設定", + "telemetry-wizard-done": "了解!", + "telemetry-wizard-note": "盡在掌控——您隨時可在設定中啟用或停用此功能", + "telemetry-wizard-subtitle": "我們新增了匿名分析功能以協助改善 Noctalia", + "telemetry-wizard-title": "隱私權更新", + "wallpaper": { + "choose-dir": "選擇一個存放你桌布的目錄", + "dir-browse": "瀏覽桌布文件夾", + "dir-description": "選擇放置你桌布的文件夾", + "dir-label": "桌布目錄", + "dir-select-title": "選擇桌布文件夾", + "header": "選擇你的桌布", + "no-dir": "沒有選擇任何桌布目錄", + "no-valid": "在 {dir} 找不到任何圖像檔案", + "none-in-dir": "在目錄裡找不到任何桌布", + "preview-error": "載入圖像失敗", + "select-prompt": "在下面選擇一個桌布", + "subheader": "用一個漂亮的背景營造氛圍" + }, + "welcome": "歡迎", + "welcome-note": "幾個基本的設定讓你簡單上手 — 所有的選項可以在設定裡調整", + "welcome-subtitle": "讓你的桌面獨一無二", + "welcome-title": "歡迎來到 Noctalia!" + }, + "system": { + "cpu-temperature": "{temp}°C", + "disk-usage": "{percent}%", + "location-display": "{name} ({coordinates})", + "monitor-description": "{model} ({width}x{height} @ {scale}x)", + "no-media-player-detected": "沒有偵測到媒體播放器", + "scaling-percentage": "{percentage}%", + "signal-strength": "{signal}%", + "unknown-app": "未知的程式", + "uptime": "開機時間: {uptime}", + "user-requested": "使用者請求", + "welcome-back": "歡迎回來,", + "widget-settings-title": "{widget} 設定" + }, + "system-monitor": { + "core-usage": "核心 {id} 使用率", + "cpu-temp": "CPU 溫度", + "cpu-usage": "CPU 使用率", + "disk": "磁碟已用", + "download-speed": "下載速度", + "gpu-temp": "GPU 溫度", + "load-average": "平均負載", + "title": "系統監視器", + "upload-speed": "上傳速度" + }, + "toast": { + "airplane-mode": { + "description": "停用所有無線通訊。", + "title": "飛航模式" + }, + "battery": { + "critical": "電量嚴重不足", + "critical-desc": "電量剩餘 {percent}% — 請立即連接充電器", + "low": "電量過低", + "low-desc": "電量剩餘 {percent}% — 請連接充電器" + }, + "bluetooth": { + "address-copied": "已複製位址至剪貼簿", + "auto-connect-disabled": "自動連線已停用", + "auto-connect-enabled": "自動連線已啟用", + "auto-connecting": "正在連接 {count} 個裝置...", + "confirm-code": "在另一個裝置上確認配對碼: {value}", + "connect-failed": "裝置連接失敗", + "disconnect-failed": "裝置中斷連接失敗", + "discoverable-change-failed": "切換被偵測狀態失敗", + "discoverable-disabled": "現在不會被偵測", + "discoverable-enabled": "現在已可被偵測", + "display-code": "在另一個裝置上輸入配對碼: {value}", + "forget-failed": "忘記裝置失敗", + "pair-failed": "裝置配對失敗", + "passkey-required": "配對的裝置需要 Passkey", + "pincode-required": "配對的裝置需要 PIN 碼", + "state-change-failed": "變更藍牙狀態失敗" + }, + "clipboard": { + "long-text": "長串文字", + "unavailable": "無法使用剪貼簿歷史", + "unavailable-desc": "尚未安裝 cliphist 套件, 要使用剪貼簿歷史請安裝套件" + }, + "custom-command-failed": { + "description": "指令失敗: {command}\\n結束代碼: {code}", + "title": "自訂指令失敗" + }, + "do-not-disturb": { + "disabled": "勿擾模式已停用", + "disabled-desc": "顯示所有通知", + "enabled": "勿擾模式已啟用", + "enabled-desc": "你會在歷史通知找到這些通知" + }, + "donation-opened": "捐款頁面已在您的瀏覽器中開啟", + "internet-limited": "已連接但沒有網際網路", + "keyboard-layout": { + "changed": "切換鍵盤佈局為 {layout}", + "title": "鍵盤" + }, + "missing-control-center": { + "description": "控制中心小工具已經從工具列移除, 如果想重新自工具列存取, 你必須把小工具加回來; 你也可以在工具列直接點擊右鍵打開", + "label": "最後一個控制中心小工具已解除" + }, + "night-light": { + "forced": "強制開啟", + "normal": "一般模式", + "not-installed": "尚未安裝 wlsunset 套件" + }, + "noctalia-performance": { + "disabled": "效能模式已停用", + "enabled": "效能模式已啟用", + "label": "Noctalia 效能模式" + }, + "power-profile": { + "changed": "電源配置已變更", + "profile-name": "{profile}" + }, + "theming-processor-failed": { + "desc-generic": "在處理模板時發生錯誤", + "title": "範本處理失敗" + }, + "vpn": { + "connected": "已連至 '{name}'", + "disconnected": "已自 '{name}' 斷開連線" + }, + "wallpaper-colors": { + "disabled": "桌布配色已停用", + "enabled": "桌布配色已啟用", + "label": "桌布配色" + }, + "wifi": { + "connected": "已連上 '{ssid}'", + "connection-failed": "連線失敗", + "connection-timeout": "連線逾時", + "disconnected": "已自 '{ssid}' 斷開連線", + "incorrect-password": "密碼錯誤", + "network-not-found": "找不到網路" + } + }, + "tooltips": { + "add-widget": "新增小工具", + "auto-connect": "切換此裝置的自動連線", + "bluetooth-auto-connect-off": "自動連線已停用", + "bluetooth-auto-connect-on": "自動連線已啟用", + "bluetooth-devices": "藍牙裝置", + "brightness-at": "亮度: {brightness}%", + "click-to-start-recording": "螢幕錄影 (開始錄製)", + "click-to-stop-recording": "螢幕錄影 (停止錄製)", + "collapse": "收起側邊欄", + "copy-address": "複製位址", + "delete-notification": "刪除通知", + "dismiss-notification": "關閉通知", + "do-not-disturb-enabled": "勿擾模式", + "expand": "展開側邊欄", + "forget-network": "忘記網路設定", + "grid-view": "方格檢視", + "hidden-files-hide": "隱藏檔案", + "hide-unnamed-devices": "隱藏不具名裝置", + "home": "家目錄", + "input-muted": "開關輸入靜音", + "keep-awake": "保持喚醒", + "keyboard-layout": "鍵盤佈局: {layout}", + "list-view": "列表檢視", + "manage-vpn": "VPN 連線", + "manage-wifi": "Wi-Fi", + "max-widgets-reached": "已達到小工具數量上限", + "microphone-volume-at": "麥克風音量: {volume}%", + "move-to-section": "移至{section}", + "mute": "靜音", + "next-media": "下一首", + "next-month": "下個月", + "night-light-not-installed": "夜燈模式 (無法使用)", + "noctalia-performance-enabled": "Noctalia 效能模式", + "open-annotation-tool": "使用註記工具開啟", + "open-control-center": "控制中心", + "open-notification-history-enable-dnd": "歷史通知", + "open-settings": "設定", + "open-tray-dropdown": "系統匣", + "output-muted": "開關輸出靜音", + "power-profile": "電源配置: {profile} ", + "previous-media": "上一首", + "previous-month": "上個月", + "refresh-devices": "重新整理裝置", + "refresh-wallhaven": "重新整理 Wallhaven 結果", + "refresh-wallpaper-list": "重新整理桌布列表", + "remove-widget": "解除小工具", + "search-close": "關閉搜尋", + "session-menu": "工作階段選單", + "show-all-devices": "顯示所有裝置", + "switch-to-dark-mode": "深色模式", + "switch-to-light-mode": "淺色模式", + "unmute": "解除靜音", + "up": "上層目錄", + "volume-at": "輸出音量: {volume}%", + "wallpaper-selector": "挑選桌布" + }, + "wallpaper": { + "browse": { + "empty-directory": "這個目錄是空的。", + "go-root": "前往桌布根目錄", + "go-up": "前往上層資料夾", + "go-up-hint": "使用返回按鈕向上導航。" + }, + "configure-directory": "設定你的桌布目錄", + "fill-modes": { + "crop": "裁切 (填滿)", + "fit": "調整 (收容)", + "repeat": "重複 (平鋪)", + "stretch": "延展" + }, + "no-match": "沒有相關結果", + "no-wallpaper": "找不到桌布", + "panel": { + "apikey-help": "需要 API key 以存取工作不宜 (NSFW) 的內容", + "apikey-label": "API key", + "apikey-managed-by-env": "已透過 NOCTALIA_WALLHAVEN_API_KEY 這個環境變數管理", + "apikey-placeholder": "輸入你的 Wallhaven API Key", + "appearance-dark-tab": "深色", + "appearance-light-tab": "淺色", + "categories-anime": "動漫", + "categories-label": "分類", + "categories-people": "人物", + "color-extraction-disabled": "使用桌布顏色擷取", + "color-extraction-enabled": "使用預設配色方案", + "header-devices-apply-all-tooltip": "為每個螢幕選擇桌布", + "header-devices-per-monitor-tooltip": "套用到所有顯示器", + "header-link-light-dark-tooltip": "切換主題時保留桌布", + "header-separate-light-dark-tooltip": "隨主題自動切換桌布", + "header-sun-linked-tooltip": "分離淺色和深色桌布", + "header-sun-separate-tooltip": "明暗模式使用相同桌布", + "order-asc": "升序", + "order-desc": "降序", + "order-label": "排序", + "purity-label": "內容過濾", + "purity-nsfw": "工作不宜 (NSFW)", + "purity-sfw": "安全題材", + "purity-sketchy": "擦邊題材", + "ratios-all-portrait": "全部直式", + "ratios-all-wide": "所有寬螢幕", + "ratios-any": "任何", + "ratios-label": "長寬比", + "resolution-atleast": "至少有", + "resolution-exact": "符合", + "resolution-label": "解析度", + "resolution-mode-label": "選項", + "solid-color-tooltip": "實色背景", + "sort-date-asc": "按最早排序", + "sort-date-desc": "按最新排序", + "sort-name-asc": "按名稱排序 (A-Z)", + "sort-name-desc": "按名稱排序 (Z-A)", + "sort-random": "隨機排序", + "sorting-date-added": "新增日期", + "sorting-favorites": "最愛", + "sorting-label": "按... 排序", + "sorting-relevance": "相關度", + "sorting-toplist": "Toplist", + "sorting-views": "觀看數", + "source-label": "來源", + "source-wallhaven": "Wallhaven", + "title": "挑選桌布", + "wallhaven-settings-title": "Wallhaven 設定" + }, + "transitions": { + "disc": "圓盤", + "fade": "淡入", + "honeycomb": "蜂巢", + "pixelate": "像素化", + "stripes": "條紋", + "wipe": "擦除" + }, + "try-different-search": "試著用不同的搜尋字串", + "wallhaven": { + "loading": "正在載入桌布...", + "no-results": "找不到桌布, 試著用不同的搜尋字串", + "page": "第 {current} / {total} 頁", + "page-prefix": "頁面", + "page-suffix": "共 {total} 個" + } + }, + "weather": { + "clear-sky": "晴朗", + "drizzle": "毛毛雨", + "fog": "起霧", + "mainly-clear": "晴時多雲", + "overcast": "陰天", + "partly-cloudy": "局部多雲", + "rain-showers": "陣雨", + "snow": "下雪", + "thunderstorm": "閃電暴雨" + }, + "widgets": { + "color-picker": { + "palette-description": "從各種預先設定的顏色中挑選", + "palette-label": "調色盤", + "palette-theme-colors": "快速存取你的主題所使用的配色", + "title": "挑選顏色" + }, + "datetime-tokens": { + "ampm-lowercase": "小寫的 am/pm", + "ampm-uppercase": "大寫的 AM/PM", + "common-12hour-time-minutes": "帶分鐘的12小時制時間", + "common-24hour-time-minutes": "帶分鐘的24小時制時間", + "common-24hour-time-seconds": "帶秒鐘的24小時制時間", + "common-european-date": "歐洲格式日期", + "common-iso-date": "ISO 格式日期", + "common-us-date": "美國格式日期", + "common-weekday-date": "帶星期的日期", + "common-weekday-month-day": "星期, 月份及日", + "day-abbreviated": "縮寫的月份", + "day-full": "完整的星期", + "day-leading-zero": "帶零的日 (01-31)", + "day-no-leading-zero": "不帶零的日 (01-31)", + "hour-leading-zero": "帶零的小時 (00-23) — 24小時制", + "hour-no-leading-zero": "不帶零的小時 (0-23) — 24小時制", + "minute-leading-zero": "帶零的分鐘 (00-59)", + "minute-no-leading-zero": "不帶零的分鐘 (00-59)", + "month-abbreviated": "縮寫的月份", + "month-full": "完整的月份", + "month-number-leading-zero": "以帶零數字表記的月份 (01-12)", + "month-number-no-zero": "以不帶零數字表記的月份 (1-12)", + "second-leading-zero": "帶零的秒鐘 (00-59)", + "second-no-leading-zero": "不帶零的秒鐘 (0-59)", + "timezone-abbreviation": "時區縮寫", + "year-four-digit": "四位數的西曆年份", + "year-two-digit": "二位數的西曆年份 (00-99)" + }, + "file-picker": { + "item": "項目", + "items": "項目", + "search-placeholder": "選擇檔案及文件夾...", + "select-current": "選擇此項", + "select-file": "選擇檔案", + "select-folder": "選擇文件夾", + "selected": "已選取:", + "title": "檔案選取器" + }, + "file-picker-title": "選擇一個檔案", + "icon-picker": { + "title": "圖示選取器" + }, + "tooltip-placeholder": "暫用文字" + }, + "wifi": { + "enterprise": { + "anonymous-identity": "匿名身份", + "ca-cert": "CA 憑證", + "eap-method": "EAP 方法", + "password": "使用者密碼", + "phase2-auth": "第二階段認證", + "username": "身分 / 使用者名稱" + }, + "panel": { + "action-required": "需要額外操作", + "add-hidden-network": "新增隱藏網路", + "add-network": "新增網路", + "available-interfaces": "可用的介面", + "available-networks": "可用的網路", + "disabled": "Wi-Fi 已關閉", + "disconnecting": "正在斷線...", + "dns": "DNS", + "enable-message": "啟用 Wi-Fi 來查看可用網路", + "enter-password": "輸入 Wi-Fi 密碼...", + "forget": "忘記", + "forget-network": "忘記這個網路設定", + "forgetting": "正在忘記...", + "hidden-network": "隱藏網路", + "interface": "網路介面", + "internet-connected": "已連接網際網路", + "internet-limited": "沒有網際網路", + "internet-status": "上線狀態", + "ipv4": "IPv4", + "ipv6": "IPv6", + "known-networks": "已知的網路", + "link-speed": "連結速度", + "network-name-ssid": "網路名稱 (SSID)", + "no-ethernet-devices": "沒有偵測到任何乙太網路裝置", + "no-networks": "找不到 Wi-Fi 網路", + "saved": "已儲存", + "scan-again": "再次掃描", + "searching": "正在搜尋網路...", + "security-open": "開啟", + "security-wep": "WEP", + "security-wpa": "WPA/WPA2/WPA3 個人", + "security-wpa-ent": "WPA 企業版", + "security-wpa2-ent": "WPA2 企業版", + "security-wpa23": "WPA2/WPA3", + "security-wpa3": "WPA3", + "security-wpa3-ent": "WPA3 企業版", + "show-password": "顯示密碼", + "title": "Wi‑Fi" + }, + "signal": { + "excellent": "優秀", + "fair": "良好", + "good": "尚可", + "poor": "差", + "weak": "弱" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/Wallpaper/noctalia.png b/fedora/.config/quickshell/noctalia-shell/Assets/Wallpaper/noctalia.png new file mode 100644 index 0000000..809881b Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Assets/Wallpaper/noctalia.png differ diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/noctalia.svg b/fedora/.config/quickshell/noctalia-shell/Assets/noctalia.svg new file mode 100644 index 0000000..1064622 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/noctalia.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/settings-default.json b/fedora/.config/quickshell/noctalia-shell/Assets/settings-default.json new file mode 100644 index 0000000..262d6a7 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/settings-default.json @@ -0,0 +1,563 @@ +{ + "settingsVersion": 0, + "bar": { + "barType": "simple", + "position": "top", + "monitors": [], + "density": "default", + "showOutline": false, + "showCapsule": true, + "capsuleOpacity": 1, + "capsuleColorKey": "none", + "widgetSpacing": 6, + "contentPadding": 2, + "fontScale": 1, + "enableExclusionZoneInset": true, + "backgroundOpacity": 0.93, + "useSeparateOpacity": false, + "marginVertical": 4, + "marginHorizontal": 4, + "frameThickness": 8, + "frameRadius": 12, + "outerCorners": true, + "hideOnOverview": false, + "displayMode": "always_visible", + "autoHideDelay": 500, + "autoShowDelay": 150, + "showOnWorkspaceSwitch": true, + "widgets": { + "left": [ + { + "id": "Launcher" + }, + { + "id": "Clock" + }, + { + "id": "SystemMonitor" + }, + { + "id": "ActiveWindow" + }, + { + "id": "MediaMini" + } + ], + "center": [ + { + "id": "Workspace" + } + ], + "right": [ + { + "id": "Tray" + }, + { + "id": "NotificationHistory" + }, + { + "id": "Battery" + }, + { + "id": "Volume" + }, + { + "id": "Brightness" + }, + { + "id": "ControlCenter" + } + ] + }, + "mouseWheelAction": "none", + "reverseScroll": false, + "mouseWheelWrap": true, + "middleClickAction": "none", + "middleClickFollowMouse": false, + "middleClickCommand": "", + "rightClickAction": "controlCenter", + "rightClickFollowMouse": true, + "rightClickCommand": "", + "screenOverrides": [] + }, + "general": { + "avatarImage": "", + "dimmerOpacity": 0.2, + "showScreenCorners": false, + "forceBlackScreenCorners": false, + "scaleRatio": 1, + "radiusRatio": 1, + "iRadiusRatio": 1, + "boxRadiusRatio": 1, + "screenRadiusRatio": 1, + "animationSpeed": 1, + "animationDisabled": false, + "compactLockScreen": false, + "lockScreenAnimations": false, + "lockOnSuspend": true, + "showSessionButtonsOnLockScreen": true, + "showHibernateOnLockScreen": false, + "enableLockScreenMediaControls": false, + "enableShadows": true, + "enableBlurBehind": true, + "shadowDirection": "bottom_right", + "shadowOffsetX": 2, + "shadowOffsetY": 3, + "language": "", + "allowPanelsOnScreenWithoutBar": true, + "showChangelogOnStartup": true, + "telemetryEnabled": false, + "enableLockScreenCountdown": true, + "lockScreenCountdownDuration": 10000, + "autoStartAuth": false, + "allowPasswordWithFprintd": false, + "clockStyle": "custom", + "clockFormat": "hh\\nmm", + "passwordChars": false, + "lockScreenMonitors": [], + "lockScreenBlur": 0, + "lockScreenTint": 0, + "keybinds": { + "keyUp": [ + "Up" + ], + "keyDown": [ + "Down" + ], + "keyLeft": [ + "Left" + ], + "keyRight": [ + "Right" + ], + "keyEnter": [ + "Return", + "Enter" + ], + "keyEscape": [ + "Esc" + ], + "keyRemove": [ + "Del" + ] + }, + "reverseScroll": false, + "smoothScrollEnabled": true + }, + "ui": { + "fontDefault": "", + "fontFixed": "", + "fontDefaultScale": 1, + "fontFixedScale": 1, + "tooltipsEnabled": true, + "scrollbarAlwaysVisible": true, + "boxBorderEnabled": false, + "panelBackgroundOpacity": 0.93, + "translucentWidgets": false, + "panelsAttachedToBar": true, + "settingsPanelMode": "attached", + "settingsPanelSideBarCardStyle": false + }, + "location": { + "name": "", + "weatherEnabled": true, + "weatherShowEffects": true, + "weatherTaliaMascotAlways": false, + "useFahrenheit": false, + "use12hourFormat": false, + "showWeekNumberInCalendar": false, + "showCalendarEvents": true, + "showCalendarWeather": true, + "analogClockInCalendar": false, + "firstDayOfWeek": -1, + "hideWeatherTimezone": false, + "hideWeatherCityName": false, + "autoLocate": true + }, + "calendar": { + "cards": [ + { + "enabled": true, + "id": "calendar-header-card" + }, + { + "enabled": true, + "id": "calendar-month-card" + }, + { + "enabled": true, + "id": "weather-card" + } + ] + }, + "wallpaper": { + "enabled": true, + "overviewEnabled": false, + "directory": "", + "monitorDirectories": [], + "enableMultiMonitorDirectories": false, + "showHiddenFiles": false, + "viewMode": "single", + "setWallpaperOnAllMonitors": true, + "linkLightAndDarkWallpapers": true, + "fillMode": "crop", + "fillColor": "#000000", + "useSolidColor": false, + "solidColor": "#1a1a2e", + "automationEnabled": false, + "wallpaperChangeMode": "random", + "randomIntervalSec": 300, + "transitionDuration": 1500, + "transitionType": [ + "fade", + "disc", + "stripes", + "wipe", + "pixelate", + "honeycomb" + ], + "skipStartupTransition": false, + "transitionEdgeSmoothness": 0.05, + "panelPosition": "follow_bar", + "hideWallpaperFilenames": false, + "useOriginalImages": false, + "overviewBlur": 0.4, + "overviewTint": 0.6, + "useWallhaven": false, + "wallhavenQuery": "", + "wallhavenSorting": "relevance", + "wallhavenOrder": "desc", + "wallhavenCategories": "111", + "wallhavenPurity": "100", + "wallhavenRatios": "", + "wallhavenApiKey": "", + "wallhavenResolutionMode": "atleast", + "wallhavenResolutionWidth": "", + "wallhavenResolutionHeight": "", + "sortOrder": "name", + "favorites": [] + }, + "appLauncher": { + "enableClipboardHistory": false, + "autoPasteClipboard": false, + "enableClipPreview": true, + "clipboardWrapText": true, + "enableClipboardSmartIcons": true, + "enableClipboardChips": true, + "clipboardWatchTextCommand": "wl-paste --type text --watch cliphist store", + "clipboardWatchImageCommand": "wl-paste --type image --watch cliphist store", + "position": "center", + "pinnedApps": [], + "sortByMostUsed": true, + "terminalCommand": "alacritty -e", + "customLaunchPrefixEnabled": false, + "customLaunchPrefix": "", + "viewMode": "list", + "showCategories": true, + "iconMode": "tabler", + "showIconBackground": false, + "enableSettingsSearch": true, + "enableWindowsSearch": true, + "enableSessionSearch": true, + "ignoreMouseInput": false, + "screenshotAnnotationTool": "", + "overviewLayer": false, + "density": "default" + }, + "controlCenter": { + "position": "close_to_bar_button", + "diskPath": "/", + "shortcuts": { + "left": [ + { + "id": "Network" + }, + { + "id": "Bluetooth" + }, + { + "id": "WallpaperSelector" + }, + { + "id": "NoctaliaPerformance" + } + ], + "right": [ + { + "id": "Notifications" + }, + { + "id": "PowerProfile" + }, + { + "id": "KeepAwake" + }, + { + "id": "NightLight" + } + ] + }, + "cards": [ + { + "enabled": true, + "id": "profile-card" + }, + { + "enabled": true, + "id": "shortcuts-card" + }, + { + "enabled": true, + "id": "audio-card" + }, + { + "enabled": false, + "id": "brightness-card" + }, + { + "enabled": true, + "id": "weather-card" + }, + { + "enabled": true, + "id": "media-sysmon-card" + } + ] + }, + "systemMonitor": { + "cpuWarningThreshold": 80, + "cpuCriticalThreshold": 90, + "tempWarningThreshold": 80, + "tempCriticalThreshold": 90, + "gpuWarningThreshold": 80, + "gpuCriticalThreshold": 90, + "memWarningThreshold": 80, + "memCriticalThreshold": 90, + "swapWarningThreshold": 80, + "swapCriticalThreshold": 90, + "diskWarningThreshold": 80, + "diskCriticalThreshold": 90, + "diskAvailWarningThreshold": 20, + "diskAvailCriticalThreshold": 10, + "batteryWarningThreshold": 20, + "batteryCriticalThreshold": 5, + "enableDgpuMonitoring": false, + "useCustomColors": false, + "warningColor": "", + "criticalColor": "", + "externalMonitor": "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor" + }, + "noctaliaPerformance": { + "disableWallpaper": true, + "disableDesktopWidgets": true + }, + "dock": { + "enabled": true, + "position": "bottom", + "displayMode": "auto_hide", + "dockType": "floating", + "backgroundOpacity": 1, + "floatingRatio": 1, + "size": 1, + "onlySameOutput": true, + "monitors": [], + "pinnedApps": [], + "colorizeIcons": false, + "showLauncherIcon": false, + "launcherPosition": "end", + "launcherUseDistroLogo": false, + "launcherIcon": "", + "launcherIconColor": "none", + "pinnedStatic": false, + "inactiveIndicators": false, + "groupApps": false, + "groupContextMenuMode": "extended", + "groupClickAction": "cycle", + "groupIndicatorStyle": "dots", + "deadOpacity": 0.6, + "animationSpeed": 1, + "sitOnFrame": false, + "showDockIndicator": false, + "indicatorThickness": 3, + "indicatorColor": "primary", + "indicatorOpacity": 0.6 + }, + "network": { + "bluetoothRssiPollingEnabled": false, + "bluetoothRssiPollIntervalMs": 60000, + "networkPanelView": "wifi", + "wifiDetailsViewMode": "grid", + "bluetoothDetailsViewMode": "grid", + "bluetoothHideUnnamedDevices": false, + "disableDiscoverability": false, + "bluetoothAutoConnect": true + }, + "sessionMenu": { + "enableCountdown": true, + "countdownDuration": 10000, + "position": "center", + "showHeader": true, + "showKeybinds": true, + "largeButtonsStyle": true, + "largeButtonsLayout": "single-row", + "powerOptions": [ + { + "action": "lock", + "enabled": true, + "keybind": "1" + }, + { + "action": "suspend", + "enabled": true, + "keybind": "2" + }, + { + "action": "hibernate", + "enabled": true, + "keybind": "3" + }, + { + "action": "reboot", + "enabled": true, + "keybind": "4" + }, + { + "action": "logout", + "enabled": true, + "keybind": "5" + }, + { + "action": "shutdown", + "enabled": true, + "keybind": "6" + }, + { + "action": "rebootToUefi", + "enabled": true, + "keybind": "7" + } + ] + }, + "notifications": { + "enabled": true, + "enableMarkdown": false, + "density": "default", + "monitors": [], + "location": "top_right", + "overlayLayer": true, + "backgroundOpacity": 1, + "respectExpireTimeout": false, + "lowUrgencyDuration": 3, + "normalUrgencyDuration": 8, + "criticalUrgencyDuration": 15, + "clearDismissed": true, + "saveToHistory": { + "low": true, + "normal": true, + "critical": true + }, + "sounds": { + "enabled": false, + "volume": 0.5, + "separateSounds": false, + "criticalSoundFile": "", + "normalSoundFile": "", + "lowSoundFile": "", + "excludedApps": "discord,firefox,chrome,chromium,edge" + }, + "enableMediaToast": false, + "enableKeyboardLayoutToast": true, + "enableBatteryToast": true + }, + "osd": { + "enabled": true, + "location": "top_right", + "autoHideMs": 2000, + "overlayLayer": true, + "backgroundOpacity": 1, + "enabledTypes": [ + 0, + 1, + 2 + ], + "monitors": [] + }, + "audio": { + "volumeStep": 5, + "volumeOverdrive": false, + "spectrumFrameRate": 30, + "visualizerType": "linear", + "spectrumMirrored": true, + "mprisBlacklist": [], + "preferredPlayer": "", + "volumeFeedback": false, + "volumeFeedbackSoundFile": "" + }, + "brightness": { + "brightnessStep": 5, + "enforceMinimum": true, + "enableDdcSupport": false, + "backlightDeviceMappings": [] + }, + "colorSchemes": { + "useWallpaperColors": false, + "predefinedScheme": "Noctalia (default)", + "darkMode": true, + "schedulingMode": "off", + "manualSunrise": "06:30", + "manualSunset": "18:30", + "generationMethod": "tonal-spot", + "monitorForColors": "", + "syncGsettings": true + }, + "templates": { + "activeTemplates": [], + "enableUserTheming": false + }, + "nightLight": { + "enabled": false, + "forced": false, + "autoSchedule": true, + "nightTemp": "4000", + "dayTemp": "6500", + "manualSunrise": "06:30", + "manualSunset": "18:30" + }, + "hooks": { + "enabled": false, + "wallpaperChange": "", + "darkModeChange": "", + "screenLock": "", + "screenUnlock": "", + "performanceModeEnabled": "", + "performanceModeDisabled": "", + "startup": "", + "session": "", + "colorGeneration": "" + }, + "plugins": { + "autoUpdate": false, + "notifyUpdates": true + }, + "idle": { + "enabled": false, + "screenOffTimeout": 600, + "lockTimeout": 660, + "suspendTimeout": 1800, + "fadeDuration": 5, + "screenOffCommand": "", + "lockCommand": "", + "suspendCommand": "", + "resumeScreenOffCommand": "", + "resumeLockCommand": "", + "resumeSuspendCommand": "", + "customCommands": "[]" + }, + "desktopWidgets": { + "enabled": false, + "overviewEnabled": true, + "gridSnap": false, + "gridSnapScale": false, + "monitorWidgets": [] + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/settings-search-index.json b/fedora/.config/quickshell/noctalia-shell/Assets/settings-search-index.json new file mode 100644 index 0000000..7a05103 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/settings-search-index.json @@ -0,0 +1,2728 @@ +[ + { + "labelKey": "panels.about.changelog-on-startup", + "descriptionKey": "panels.about.changelog-on-startup-desc", + "widget": "NToggle", + "tab": 21, + "tabLabel": "panels.about.title", + "subTab": 0, + "subTabLabel": "common.info" + }, + { + "labelKey": "panels.about.telemetry-enabled", + "descriptionKey": "panels.about.telemetry-desc", + "widget": "NToggle", + "tab": 21, + "tabLabel": "panels.about.title", + "subTab": 0, + "subTabLabel": "common.info" + }, + { + "labelKey": "panels.audio.devices-output-device-label", + "descriptionKey": "panels.audio.devices-output-device-description", + "widget": "NLabel", + "tab": 14, + "tabLabel": "panels.audio.title", + "subTab": 1, + "subTabLabel": "common.devices" + }, + { + "labelKey": "panels.audio.devices-input-device-label", + "descriptionKey": "panels.audio.devices-input-device-description", + "widget": "NLabel", + "tab": 14, + "tabLabel": "panels.audio.title", + "subTab": 1, + "subTabLabel": "common.devices" + }, + { + "labelKey": "panels.audio.media-primary-player-label", + "descriptionKey": "panels.audio.media-primary-player-description", + "widget": "NTextInput", + "tab": 14, + "tabLabel": "panels.audio.title", + "subTab": 2, + "subTabLabel": "common.media" + }, + { + "labelKey": "panels.audio.media-excluded-player-label", + "descriptionKey": "panels.audio.media-excluded-player-description", + "widget": "NTextInputButton", + "tab": 14, + "tabLabel": "panels.audio.title", + "subTab": 2, + "subTabLabel": "common.media" + }, + { + "labelKey": "panels.audio.visualizer-type-label", + "descriptionKey": "panels.audio.visualizer-type-description", + "widget": "NComboBox", + "tab": 14, + "tabLabel": "panels.audio.title", + "subTab": 3, + "subTabLabel": "common.visualizer" + }, + { + "labelKey": "panels.audio.spectrum-mirrored-label", + "descriptionKey": "panels.audio.spectrum-mirrored-description", + "widget": "NToggle", + "tab": 14, + "tabLabel": "panels.audio.title", + "subTab": 3, + "subTabLabel": "common.visualizer" + }, + { + "labelKey": "panels.audio.media-frame-rate-label", + "descriptionKey": "panels.audio.media-frame-rate-description", + "widget": "NComboBox", + "tab": 14, + "tabLabel": "panels.audio.title", + "subTab": 3, + "subTabLabel": "common.visualizer" + }, + { + "labelKey": "panels.osd.types-volume-label", + "descriptionKey": "panels.audio.volumes-output-volume-description", + "widget": "NValueSlider", + "tab": 14, + "tabLabel": "panels.audio.title", + "subTab": 0, + "subTabLabel": "common.volumes" + }, + { + "labelKey": "panels.audio.volumes-mute-output-label", + "descriptionKey": "panels.audio.volumes-mute-output-description", + "widget": "NToggle", + "tab": 14, + "tabLabel": "panels.audio.title", + "subTab": 0, + "subTabLabel": "common.volumes" + }, + { + "labelKey": "panels.audio.volumes-volume-feedback-label", + "descriptionKey": "panels.audio.volumes-volume-feedback-description", + "widget": "NToggle", + "tab": 14, + "tabLabel": "panels.audio.title", + "subTab": 0, + "subTabLabel": "common.volumes" + }, + { + "labelKey": "panels.audio.volumes-feedback-sound-file-label", + "descriptionKey": "panels.audio.volumes-feedback-sound-file-description", + "widget": "NLabel", + "tab": 14, + "tabLabel": "panels.audio.title", + "subTab": 0, + "subTabLabel": "common.volumes" + }, + { + "labelKey": "panels.osd.types-input-volume-label", + "descriptionKey": "panels.audio.volumes-input-volume-description", + "widget": "NValueSlider", + "tab": 14, + "tabLabel": "panels.audio.title", + "subTab": 0, + "subTabLabel": "common.volumes" + }, + { + "labelKey": "panels.audio.volumes-mute-input-label", + "descriptionKey": "panels.audio.volumes-mute-input-description", + "widget": "NToggle", + "tab": 14, + "tabLabel": "panels.audio.title", + "subTab": 0, + "subTabLabel": "common.volumes" + }, + { + "labelKey": "panels.audio.volumes-step-size-label", + "descriptionKey": "panels.audio.volumes-step-size-description", + "widget": "NSpinBox", + "tab": 14, + "tabLabel": "panels.audio.title", + "subTab": 0, + "subTabLabel": "common.volumes" + }, + { + "labelKey": "panels.audio.volumes-volume-overdrive-label", + "descriptionKey": "panels.audio.volumes-volume-overdrive-description", + "widget": "NToggle", + "tab": 14, + "tabLabel": "panels.audio.title", + "subTab": 0, + "subTabLabel": "common.volumes" + }, + { + "labelKey": "panels.bar.appearance-position-label", + "descriptionKey": "panels.bar.appearance-position-description", + "widget": "NComboBox", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.bar.appearance-density-label", + "descriptionKey": "panels.bar.appearance-density-description", + "widget": "NComboBox", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.bar.appearance-type-label", + "descriptionKey": "panels.bar.appearance-type-description", + "widget": "NComboBox", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "common.display-mode", + "descriptionKey": "panels.bar.appearance-display-mode-description", + "widget": "NComboBox", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.bar.appearance-use-separate-opacity-label", + "descriptionKey": "panels.bar.appearance-use-separate-opacity-description", + "widget": "NToggle", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.bar.appearance-background-opacity-label", + "descriptionKey": "panels.bar.appearance-background-opacity-description", + "widget": "NValueSlider", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.useSeparateOpacity" + ] + }, + { + "labelKey": "panels.bar.appearance-font-scale-label", + "descriptionKey": "panels.bar.appearance-font-scale-description", + "widget": "NValueSlider", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.bar.appearance-widget-spacing-label", + "descriptionKey": "panels.bar.appearance-widget-spacing-description", + "widget": "NValueSlider", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.bar.appearance-content-padding-label", + "descriptionKey": "panels.bar.appearance-content-padding-description", + "widget": "NValueSlider", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.bar.appearance-show-outline-label", + "descriptionKey": "panels.bar.appearance-show-outline-description", + "widget": "NToggle", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.bar.appearance-show-capsule-label", + "descriptionKey": "panels.bar.appearance-show-capsule-description", + "widget": "NToggle", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.bar.appearance-capsule-color-label", + "descriptionKey": "panels.bar.appearance-capsule-color-description", + "widget": "NColorChoice", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.showCapsule" + ] + }, + { + "labelKey": "panels.bar.appearance-capsule-opacity-label", + "descriptionKey": "panels.bar.appearance-capsule-opacity-description", + "widget": "NValueSlider", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.showCapsule" + ] + }, + { + "labelKey": "panels.bar.appearance-enable-exclusion-zone-inset-label", + "descriptionKey": "panels.bar.appearance-enable-exclusion-zone-inset-description", + "widget": "NToggle", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.bar.appearance-hide-on-overview-label", + "descriptionKey": "panels.bar.appearance-hide-on-overview-description", + "widget": "NToggle", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "CompositorService.isNiri" + ] + }, + { + "labelKey": "panels.bar.appearance-outer-corners-label", + "descriptionKey": "panels.bar.appearance-outer-corners-description", + "widget": "NToggle", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.barType === \"simple\"" + ] + }, + { + "labelKey": "panels.bar.appearance-frame-settings-label", + "descriptionKey": "panels.bar.appearance-frame-settings-description", + "widget": "NLabel", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.barType === \"framed\"" + ] + }, + { + "labelKey": "panels.bar.appearance-frame-thickness", + "descriptionKey": null, + "widget": "NValueSlider", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.barType === \"framed\"" + ] + }, + { + "labelKey": "panels.bar.appearance-frame-radius", + "descriptionKey": null, + "widget": "NValueSlider", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.barType === \"framed\"" + ] + }, + { + "labelKey": "panels.bar.appearance-margins-vertical", + "descriptionKey": "panels.bar.appearance-margins-description", + "widget": "NSpinBox", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.barType === \"floating\"" + ] + }, + { + "labelKey": "panels.bar.appearance-margins-horizontal", + "descriptionKey": "panels.bar.appearance-margins-description", + "widget": "NSpinBox", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.barType === \"floating\"" + ] + }, + { + "labelKey": "panels.bar.appearance-auto-hide-delay-label", + "descriptionKey": "panels.bar.appearance-auto-hide-delay-description", + "widget": "NValueSlider", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.displayMode === \"auto_hide\"" + ] + }, + { + "labelKey": "panels.bar.appearance-auto-show-delay-label", + "descriptionKey": "panels.bar.appearance-auto-show-delay-description", + "widget": "NValueSlider", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.displayMode === \"auto_hide\"" + ] + }, + { + "labelKey": "panels.bar.appearance-show-on-workspace-switch-label", + "descriptionKey": "panels.bar.appearance-show-on-workspace-switch-description", + "widget": "NToggle", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.bar.displayMode === \"auto_hide\"" + ] + }, + { + "labelKey": "panels.bar.behavior-workspace-scroll-label", + "descriptionKey": "panels.bar.behavior-workspace-scroll-description", + "widget": "NComboBox", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 2, + "subTabLabel": "common.behavior" + }, + { + "labelKey": "panels.general.reverse-scrolling-label", + "descriptionKey": "panels.general.reverse-scrolling-description", + "widget": "NToggle", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 2, + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.bar.mouseWheelAction !== \"none\"" + ] + }, + { + "labelKey": "panels.bar.behavior-wheel-wrap-label", + "descriptionKey": "panels.bar.behavior-wheel-wrap-description", + "widget": "NToggle", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 2, + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.bar.mouseWheelAction === \"workspace\"" + ] + }, + { + "labelKey": "panels.bar.behavior-middle-click-label", + "descriptionKey": "panels.bar.behavior-middle-click-description", + "widget": "NComboBox", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 2, + "subTabLabel": "common.behavior" + }, + { + "labelKey": "panels.bar.behavior-middle-click-command-label", + "descriptionKey": "panels.bar.behavior-middle-click-command-description", + "widget": "NTextInput", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 2, + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.bar.middleClickAction === \"command\"" + ] + }, + { + "labelKey": "panels.bar.behavior-middle-click-follow-mouse-label", + "descriptionKey": "panels.bar.behavior-middle-click-follow-mouse-description", + "widget": "NToggle", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 2, + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.bar.middleClickAction !== \"none\" && Settings.data.bar.middleClickAction !== \"command\" && !(Settings.data.bar.middleClickAction === \"settings\" && Settings.data.ui.settingsPanelMode === \"window\")" + ] + }, + { + "labelKey": "panels.bar.behavior-right-click-label", + "descriptionKey": "panels.bar.behavior-right-click-description", + "widget": "NComboBox", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 2, + "subTabLabel": "common.behavior" + }, + { + "labelKey": "panels.bar.behavior-right-click-command-label", + "descriptionKey": "panels.bar.behavior-right-click-command-description", + "widget": "NTextInput", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 2, + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.bar.rightClickAction === \"command\"" + ] + }, + { + "labelKey": "panels.bar.behavior-right-click-follow-mouse-label", + "descriptionKey": "panels.bar.behavior-right-click-follow-mouse-description", + "widget": "NToggle", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 2, + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.bar.rightClickAction !== \"none\" && Settings.data.bar.rightClickAction !== \"command\" && !(Settings.data.bar.rightClickAction === \"settings\" && Settings.data.ui.settingsPanelMode === \"window\")" + ] + }, + { + "labelKey": "panels.bar.monitor-override-settings", + "descriptionKey": "panels.bar.monitor-override-settings-description", + "widget": "NToggle", + "tab": 4, + "tabLabel": "panels.bar.title", + "subTab": 3, + "subTabLabel": "common.monitors" + }, + { + "labelKey": "tooltips.switch-to-dark-mode", + "descriptionKey": "panels.color-scheme.dark-mode-switch-description", + "widget": "NToggle", + "tab": 2, + "tabLabel": "panels.color-scheme.title", + "subTab": 0, + "subTabLabel": "common.colors" + }, + { + "labelKey": "panels.color-scheme.sync-gsettings-label", + "descriptionKey": "panels.color-scheme.sync-gsettings-description", + "widget": "NToggle", + "tab": 2, + "tabLabel": "panels.color-scheme.title", + "subTab": 0, + "subTabLabel": "common.colors" + }, + { + "labelKey": "panels.color-scheme.dark-mode-mode-label", + "descriptionKey": "panels.color-scheme.dark-mode-mode-description", + "widget": "NComboBox", + "tab": 2, + "tabLabel": "panels.color-scheme.title", + "subTab": 0, + "subTabLabel": "common.colors" + }, + { + "labelKey": "panels.display.night-light-manual-schedule-label", + "descriptionKey": "panels.display.night-light-manual-schedule-description", + "widget": "NLabel", + "tab": 2, + "tabLabel": "panels.color-scheme.title", + "subTab": 0, + "subTabLabel": "common.colors", + "visibleWhen": [ + "Settings.data.colorSchemes.schedulingMode === \"manual\"" + ] + }, + { + "labelKey": "panels.color-scheme.color-source-use-wallpaper-colors-label", + "descriptionKey": "panels.color-scheme.color-source-use-wallpaper-colors-description", + "widget": "NToggle", + "tab": 2, + "tabLabel": "panels.color-scheme.title", + "subTab": 0, + "subTabLabel": "common.colors" + }, + { + "labelKey": "panels.color-scheme.wallpaper-monitor-source-label", + "descriptionKey": "panels.color-scheme.wallpaper-monitor-source-description", + "widget": "NComboBox", + "tab": 2, + "tabLabel": "panels.color-scheme.title", + "subTab": 0, + "subTabLabel": "common.colors" + }, + { + "labelKey": "panels.color-scheme.wallpaper-method-label", + "descriptionKey": "panels.color-scheme.wallpaper-method-description", + "widget": "NComboBox", + "tab": 2, + "tabLabel": "panels.color-scheme.title", + "subTab": 0, + "subTabLabel": "common.colors" + }, + { + "labelKey": "panels.color-scheme.templates-misc-user-templates-label", + "descriptionKey": "panels.color-scheme.templates-misc-user-templates-description", + "widget": "NCheckbox", + "tab": 2, + "tabLabel": "panels.color-scheme.title", + "subTab": 1, + "subTabLabel": "common.templates" + }, + { + "labelKey": "common.bluetooth", + "descriptionKey": null, + "widget": "NToggle", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 1, + "subTabLabel": "common.bluetooth" + }, + { + "labelKey": "bluetooth.panel.connected-devices", + "descriptionKey": null, + "widget": "NLabel", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 1, + "subTabLabel": "common.bluetooth" + }, + { + "labelKey": "bluetooth.panel.paired-devices", + "descriptionKey": null, + "widget": "NLabel", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 1, + "subTabLabel": "common.bluetooth" + }, + { + "labelKey": "bluetooth.panel.available-devices", + "descriptionKey": null, + "widget": "NLabel", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 1, + "subTabLabel": "common.bluetooth" + }, + { + "labelKey": "panels.connections.bluetooth-auto-connect-label", + "descriptionKey": "panels.connections.bluetooth-auto-connect-description", + "widget": "NToggle", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 1, + "subTabLabel": "common.bluetooth" + }, + { + "labelKey": "panels.connections.hide-unnamed-devices-label", + "descriptionKey": "panels.connections.hide-unnamed-devices-description", + "widget": "NToggle", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 1, + "subTabLabel": "common.bluetooth" + }, + { + "labelKey": "panels.connections.disable-discoverability-label", + "descriptionKey": "panels.connections.disable-discoverability-description", + "widget": "NToggle", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 1, + "subTabLabel": "common.bluetooth" + }, + { + "labelKey": "panels.connections.bluetooth-rssi-polling-label", + "descriptionKey": "panels.connections.bluetooth-rssi-polling-description", + "widget": "NToggle", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 1, + "subTabLabel": "common.bluetooth" + }, + { + "labelKey": "panels.connections.bluetooth-rssi-polling-interval-label", + "descriptionKey": "panels.connections.bluetooth-rssi-polling-interval-description", + "widget": "NSpinBox", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 1, + "subTabLabel": "common.bluetooth", + "visibleWhen": [ + "Settings.data.network.bluetoothRssiPollingEnabled" + ] + }, + { + "labelKey": "common.auto-connect", + "descriptionKey": null, + "widget": "NCheckbox", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 1, + "subTabLabel": "common.bluetooth", + "visibleWhen": [ + "Settings.data.network.bluetoothAutoConnect" + ] + }, + { + "labelKey": "common.wifi", + "descriptionKey": null, + "widget": "NToggle", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 0, + "subTabLabel": "common.wifi" + }, + { + "labelKey": "common.connected", + "descriptionKey": null, + "widget": "NLabel", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 0, + "subTabLabel": "common.wifi" + }, + { + "labelKey": "wifi.panel.known-networks", + "descriptionKey": null, + "widget": "NLabel", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 0, + "subTabLabel": "common.wifi" + }, + { + "labelKey": "wifi.panel.available-networks", + "descriptionKey": null, + "widget": "NLabel", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 0, + "subTabLabel": "common.wifi" + }, + { + "labelKey": "toast.airplane-mode.title", + "descriptionKey": "toast.airplane-mode.description", + "widget": "NToggle", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 0, + "subTabLabel": "common.wifi" + }, + { + "labelKey": "wifi.panel.network-name-ssid", + "descriptionKey": null, + "widget": "NTextInput", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 0, + "subTabLabel": "common.wifi", + "visibleWhen": [ + "false" + ] + }, + { + "labelKey": "wifi.enterprise.eap-method", + "descriptionKey": null, + "widget": "NComboBox", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 0, + "subTabLabel": "common.wifi", + "visibleWhen": [ + "false" + ] + }, + { + "labelKey": "wifi.enterprise.phase2-auth", + "descriptionKey": null, + "widget": "NComboBox", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 0, + "subTabLabel": "common.wifi", + "visibleWhen": [ + "false" + ] + }, + { + "labelKey": "wifi.enterprise.anonymous-identity", + "descriptionKey": null, + "widget": "NTextInput", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 0, + "subTabLabel": "common.wifi", + "visibleWhen": [ + "false" + ] + }, + { + "labelKey": "wifi.enterprise.ca-cert", + "descriptionKey": null, + "widget": "NTextInput", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 0, + "subTabLabel": "common.wifi", + "visibleWhen": [ + "false" + ] + }, + { + "labelKey": "wifi.enterprise.username", + "descriptionKey": null, + "widget": "NTextInput", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 0, + "subTabLabel": "common.wifi", + "visibleWhen": [ + "false" + ] + }, + { + "labelKey": "common.password", + "descriptionKey": null, + "widget": "NTextInput", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 0, + "subTabLabel": "common.wifi", + "visibleWhen": [ + "false" + ] + }, + { + "labelKey": "wifi.panel.show-password", + "descriptionKey": null, + "widget": "NCheckbox", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 0, + "subTabLabel": "common.wifi", + "visibleWhen": [ + "false" + ] + }, + { + "labelKey": "wifi.panel.hidden-network", + "descriptionKey": null, + "widget": "NCheckbox", + "tab": 16, + "tabLabel": "panels.connections.title", + "subTab": 0, + "subTabLabel": "common.wifi", + "visibleWhen": [ + "false" + ] + }, + { + "labelKey": "common.position", + "descriptionKey": "panels.control-center.position-description", + "widget": "NComboBox", + "tab": 7, + "tabLabel": "panels.control-center.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.control-center.system-monitor-disk-path-label", + "descriptionKey": "panels.control-center.system-monitor-disk-path-description", + "widget": "NComboBox", + "tab": 7, + "tabLabel": "panels.control-center.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.desktop-widgets.enabled-label", + "descriptionKey": "panels.desktop-widgets.enabled-description", + "widget": "NToggle", + "tab": 6, + "tabLabel": "panels.desktop-widgets.title", + "subTab": null + }, + { + "labelKey": "panels.desktop-widgets.overview-enabled-label", + "descriptionKey": "panels.desktop-widgets.overview-enabled-description", + "widget": "NToggle", + "tab": 6, + "tabLabel": "panels.desktop-widgets.title", + "subTab": null + }, + { + "labelKey": "panels.display.monitors-backlight-device-label", + "descriptionKey": "panels.display.monitors-backlight-device-description", + "widget": "NComboBox", + "tab": 15, + "tabLabel": "panels.display.title", + "subTab": 0, + "subTabLabel": "common.brightness" + }, + { + "labelKey": "panels.display.monitors-brightness-step-label", + "descriptionKey": "panels.display.monitors-brightness-step-description", + "widget": "NSpinBox", + "tab": 15, + "tabLabel": "panels.display.title", + "subTab": 0, + "subTabLabel": "common.brightness" + }, + { + "labelKey": "panels.display.monitors-enforce-minimum-label", + "descriptionKey": "panels.display.monitors-enforce-minimum-description", + "widget": "NToggle", + "tab": 15, + "tabLabel": "panels.display.title", + "subTab": 0, + "subTabLabel": "common.brightness" + }, + { + "labelKey": "panels.display.monitors-external-brightness-label", + "descriptionKey": "panels.display.monitors-external-brightness-description", + "widget": "NToggle", + "tab": 15, + "tabLabel": "panels.display.title", + "subTab": 0, + "subTabLabel": "common.brightness" + }, + { + "labelKey": "panels.display.night-light-enable-label", + "descriptionKey": "panels.display.night-light-enable-description", + "widget": "NToggle", + "tab": 15, + "tabLabel": "panels.display.title", + "subTab": 1, + "subTabLabel": "common.night-light" + }, + { + "labelKey": "panels.display.night-light-temperature-night", + "descriptionKey": "panels.display.night-light-temperature-night-description", + "widget": "NLabel", + "tab": 15, + "tabLabel": "panels.display.title", + "subTab": 1, + "subTabLabel": "common.night-light" + }, + { + "labelKey": "panels.display.night-light-temperature-day", + "descriptionKey": "panels.display.night-light-temperature-day-description", + "widget": "NLabel", + "tab": 15, + "tabLabel": "panels.display.title", + "subTab": 1, + "subTabLabel": "common.night-light" + }, + { + "labelKey": "panels.display.night-light-auto-schedule-label", + "descriptionKey": "panels.display.night-light-auto-schedule-description", + "widget": "NToggle", + "tab": 15, + "tabLabel": "panels.display.title", + "subTab": 1, + "subTabLabel": "common.night-light" + }, + { + "labelKey": "panels.display.night-light-force-activation-label", + "descriptionKey": "panels.display.night-light-force-activation-description", + "widget": "NToggle", + "tab": 15, + "tabLabel": "panels.display.title", + "subTab": 1, + "subTabLabel": "common.night-light" + }, + { + "labelKey": "panels.dock.enabled-label", + "descriptionKey": "panels.dock.enabled-description", + "widget": "NToggle", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.dock.appearance-position-label", + "descriptionKey": "panels.dock.appearance-position-description", + "widget": "NComboBox", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.dock.appearance-type-label", + "descriptionKey": "panels.dock.appearance-type-description", + "widget": "NComboBox", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.display.title", + "descriptionKey": "panels.dock.appearance-display-description", + "widget": "NComboBox", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.dockType === \"floating\"" + ] + }, + { + "labelKey": "panels.dock.appearance-sit-on-frame-label", + "descriptionKey": "panels.dock.appearance-sit-on-frame-description", + "widget": "NToggle", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.dockType === \"attached\" && Settings.data.bar.barType === \"framed\"" + ] + }, + { + "labelKey": "panels.dock.appearance-dock-indicator-label", + "descriptionKey": "panels.dock.appearance-dock-indicator-description", + "widget": "NToggle", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.dock.appearance-indicator-thickness-label", + "descriptionKey": "panels.dock.appearance-indicator-thickness-description", + "widget": "NToggle", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.showDockIndicator" + ] + }, + { + "labelKey": "panels.dock.appearance-indicator-color-label", + "descriptionKey": "panels.dock.appearance-indicator-color-description", + "widget": "NColorChoice", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.showDockIndicator" + ] + }, + { + "labelKey": "panels.dock.appearance-indicator-opacity-label", + "descriptionKey": "panels.dock.appearance-indicator-opacity-description", + "widget": "NValueSlider", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.showDockIndicator" + ] + }, + { + "labelKey": "panels.osd.background-opacity-label", + "descriptionKey": "panels.dock.appearance-background-opacity-description", + "widget": "NValueSlider", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.dock.appearance-dead-opacity-label", + "descriptionKey": "panels.dock.appearance-dead-opacity-description", + "widget": "NValueSlider", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.dock.appearance-floating-distance-label", + "descriptionKey": "panels.dock.appearance-floating-distance-description", + "widget": "NValueSlider", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.dockType === \"floating\"" + ] + }, + { + "labelKey": "panels.dock.appearance-icon-size-label", + "descriptionKey": "panels.dock.appearance-icon-size-description", + "widget": "NValueSlider", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.dock.appearance-hide-show-speed-label", + "descriptionKey": "panels.dock.appearance-hide-show-speed-description", + "widget": "NValueSlider", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.dockType === \"floating\" && Settings.data.dock.displayMode === \"auto_hide\"" + ] + }, + { + "labelKey": "panels.dock.appearance-inactive-indicators-label", + "descriptionKey": "panels.dock.appearance-inactive-indicators-description", + "widget": "NToggle", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.dock.appearance-pinned-static-label", + "descriptionKey": "panels.dock.appearance-pinned-static-description", + "widget": "NToggle", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.dock.appearance-group-apps-label", + "descriptionKey": "panels.dock.appearance-group-apps-description", + "widget": "NToggle", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.dock.appearance-group-click-action-label", + "descriptionKey": "panels.dock.appearance-group-click-action-description", + "widget": "NComboBox", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.groupApps" + ] + }, + { + "labelKey": "panels.dock.appearance-group-context-menu-mode-label", + "descriptionKey": "panels.dock.appearance-group-context-menu-mode-description", + "widget": "NComboBox", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.groupApps" + ] + }, + { + "labelKey": "panels.dock.appearance-group-indicator-style-label", + "descriptionKey": "panels.dock.appearance-group-indicator-style-description", + "widget": "NComboBox", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.groupApps" + ] + }, + { + "labelKey": "panels.dock.monitors-only-same-monitor-label", + "descriptionKey": "panels.dock.monitors-only-same-monitor-description", + "widget": "NToggle", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.dock.appearance-colorize-icons-label", + "descriptionKey": "panels.dock.appearance-colorize-icons-description", + "widget": "NToggle", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.dock.appearance-show-launcher-icon-label", + "descriptionKey": "panels.dock.appearance-show-launcher-icon-description", + "widget": "NToggle", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.dock.appearance-launcher-position-label", + "descriptionKey": "panels.dock.appearance-launcher-position-description", + "widget": "NComboBox", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.showLauncherIcon" + ] + }, + { + "labelKey": "panels.dock.appearance-launcher-use-distro-logo-label", + "descriptionKey": "panels.dock.appearance-launcher-use-distro-logo-description", + "widget": "NToggle", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.showLauncherIcon" + ] + }, + { + "labelKey": "panels.dock.appearance-launcher-icon-label", + "descriptionKey": "panels.dock.appearance-launcher-icon-description", + "widget": "NLabel", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.showLauncherIcon" + ] + }, + { + "labelKey": "common.select-icon-color", + "descriptionKey": null, + "widget": "NColorChoice", + "tab": 5, + "tabLabel": "panels.dock.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.dock.showLauncherIcon" + ] + }, + { + "labelKey": "panels.general.profile-picture-label", + "descriptionKey": "panels.general.profile-picture-description", + "widget": "NTextInputButton", + "tab": 0, + "tabLabel": "common.general", + "subTab": null + }, + { + "labelKey": "panels.general.fonts-default-label", + "descriptionKey": "panels.general.fonts-default-description", + "widget": "NSearchableComboBox", + "tab": 0, + "tabLabel": "common.general", + "subTab": null + }, + { + "labelKey": "panels.general.fonts-monospace-label", + "descriptionKey": "panels.general.fonts-monospace-description", + "widget": "NSearchableComboBox", + "tab": 0, + "tabLabel": "common.general", + "subTab": null + }, + { + "labelKey": "panels.general.fonts-default-scale-label", + "descriptionKey": "panels.general.fonts-default-scale-description", + "widget": "NValueSlider", + "tab": 0, + "tabLabel": "common.general", + "subTab": null + }, + { + "labelKey": "panels.general.fonts-monospace-scale-label", + "descriptionKey": "panels.general.fonts-monospace-scale-description", + "widget": "NValueSlider", + "tab": 0, + "tabLabel": "common.general", + "subTab": null + }, + { + "labelKey": "panels.general.smooth-scrolling-label", + "descriptionKey": "panels.general.smooth-scrolling-description", + "widget": "NToggle", + "tab": 0, + "tabLabel": "common.general", + "subTab": null + }, + { + "labelKey": "panels.general.keybinds-title", + "descriptionKey": "panels.general.keybinds-description", + "widget": "NLabel", + "tab": 0, + "tabLabel": "common.general", + "subTab": null + }, + { + "labelKey": "panels.hooks.system-hooks-enable-label", + "descriptionKey": "panels.hooks.system-hooks-enable-description", + "widget": "NToggle", + "tab": 20, + "tabLabel": "panels.hooks.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.hooks.info-parameters-label", + "descriptionKey": "panels.hooks.info-parameters-description", + "widget": "NLabel", + "tab": 20, + "tabLabel": "panels.hooks.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.hooks.noctalia-started-label", + "descriptionKey": "panels.hooks.noctalia-started-description", + "widget": "HookRow", + "tab": 20, + "tabLabel": "panels.hooks.title", + "subTab": 1, + "subTabLabel": "panels.hooks.title" + }, + { + "labelKey": "panels.hooks.wallpaper-changed-label", + "descriptionKey": "panels.hooks.wallpaper-changed-description", + "widget": "HookRow", + "tab": 20, + "tabLabel": "panels.hooks.title", + "subTab": 1, + "subTabLabel": "panels.hooks.title" + }, + { + "labelKey": "panels.hooks.color-generation-label", + "descriptionKey": "panels.hooks.color-generation-description", + "widget": "HookRow", + "tab": 20, + "tabLabel": "panels.hooks.title", + "subTab": 1, + "subTabLabel": "panels.hooks.title" + }, + { + "labelKey": "panels.hooks.theme-changed-label", + "descriptionKey": "panels.hooks.theme-changed-description", + "widget": "HookRow", + "tab": 20, + "tabLabel": "panels.hooks.title", + "subTab": 1, + "subTabLabel": "panels.hooks.title" + }, + { + "labelKey": "panels.hooks.screen-lock-label", + "descriptionKey": "panels.hooks.screen-lock-description", + "widget": "HookRow", + "tab": 20, + "tabLabel": "panels.hooks.title", + "subTab": 1, + "subTabLabel": "panels.hooks.title" + }, + { + "labelKey": "panels.hooks.screen-unlock-label", + "descriptionKey": "panels.hooks.screen-unlock-description", + "widget": "HookRow", + "tab": 20, + "tabLabel": "panels.hooks.title", + "subTab": 1, + "subTabLabel": "panels.hooks.title" + }, + { + "labelKey": "panels.hooks.performance-mode-enabled-label", + "descriptionKey": "panels.hooks.performance-mode-enabled-description", + "widget": "HookRow", + "tab": 20, + "tabLabel": "panels.hooks.title", + "subTab": 1, + "subTabLabel": "panels.hooks.title" + }, + { + "labelKey": "panels.hooks.performance-mode-disabled-label", + "descriptionKey": "panels.hooks.performance-mode-disabled-description", + "widget": "HookRow", + "tab": 20, + "tabLabel": "panels.hooks.title", + "subTab": 1, + "subTabLabel": "panels.hooks.title" + }, + { + "labelKey": "panels.hooks.session-label", + "descriptionKey": "panels.hooks.session-description", + "widget": "HookRow", + "tab": 20, + "tabLabel": "panels.hooks.title", + "subTab": 1, + "subTabLabel": "panels.hooks.title" + }, + { + "labelKey": "panels.idle.enable-label", + "descriptionKey": "panels.idle.enable-description", + "widget": "NToggle", + "tab": 13, + "tabLabel": "panels.idle.title", + "subTab": 0, + "subTabLabel": "panels.idle.tab-behavior" + }, + { + "labelKey": "panels.idle.status-label", + "descriptionKey": "panels.idle.status-description", + "widget": "NLabel", + "tab": 13, + "tabLabel": "panels.idle.title", + "subTab": 0, + "subTabLabel": "panels.idle.tab-behavior", + "visibleWhen": [ + "IdleService.nativeIdleMonitorAvailable" + ] + }, + { + "labelKey": "panels.idle.timeouts-label", + "descriptionKey": "panels.idle.timeouts-description", + "widget": "NLabel", + "tab": 13, + "tabLabel": "panels.idle.title", + "subTab": 0, + "subTabLabel": "panels.idle.tab-behavior" + }, + { + "labelKey": "panels.idle.fade-duration-label", + "descriptionKey": "panels.idle.fade-duration-description", + "widget": "NSpinBox", + "tab": 13, + "tabLabel": "panels.idle.title", + "subTab": 0, + "subTabLabel": "panels.idle.tab-behavior" + }, + { + "labelKey": "panels.idle.custom-label", + "descriptionKey": "panels.idle.custom-description", + "widget": "NLabel", + "tab": 13, + "tabLabel": "panels.idle.title", + "subTab": 1, + "subTabLabel": "panels.idle.tab-custom" + }, + { + "labelKey": "panels.launcher.settings-clipboard-history-label", + "descriptionKey": "panels.launcher.settings-clipboard-history-description", + "widget": "NToggle", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 1, + "subTabLabel": "common.clipboard" + }, + { + "labelKey": "panels.launcher.settings-clip-preview-label", + "descriptionKey": "panels.launcher.settings-clip-preview-description", + "widget": "NToggle", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 1, + "subTabLabel": "common.clipboard" + }, + { + "labelKey": "panels.launcher.settings-clip-wrap-text-label", + "descriptionKey": "panels.launcher.settings-clip-wrap-text-description", + "widget": "NToggle", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 1, + "subTabLabel": "common.clipboard" + }, + { + "labelKey": "panels.launcher.settings-auto-paste-label", + "descriptionKey": "panels.launcher.settings-auto-paste-description", + "widget": "NToggle", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 1, + "subTabLabel": "common.clipboard" + }, + { + "labelKey": "panels.launcher.settings-clip-smart-icons-label", + "descriptionKey": "panels.launcher.settings-clip-smart-icons-description", + "widget": "NToggle", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 1, + "subTabLabel": "common.clipboard" + }, + { + "labelKey": "panels.launcher.settings-clip-chips-label", + "descriptionKey": "panels.launcher.settings-clip-chips-description", + "widget": "NToggle", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 1, + "subTabLabel": "common.clipboard" + }, + { + "labelKey": "panels.launcher.settings-clipboard-watch-text-label", + "descriptionKey": "panels.launcher.settings-clipboard-watch-text-description", + "widget": "NTextInput", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 1, + "subTabLabel": "common.clipboard", + "visibleWhen": [ + "Settings.data.appLauncher.enableClipboardHistory" + ] + }, + { + "labelKey": "panels.launcher.settings-clipboard-watch-image-label", + "descriptionKey": "panels.launcher.settings-clipboard-watch-image-description", + "widget": "NTextInput", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 1, + "subTabLabel": "common.clipboard", + "visibleWhen": [ + "Settings.data.appLauncher.enableClipboardHistory" + ] + }, + { + "labelKey": "panels.launcher.settings-terminal-command-label", + "descriptionKey": "panels.launcher.settings-terminal-command-description", + "widget": "NTextInput", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 2, + "subTabLabel": "common.execute" + }, + { + "labelKey": "panels.launcher.settings-custom-launch-prefix-enabled-label", + "descriptionKey": "panels.launcher.settings-custom-launch-prefix-enabled-description", + "widget": "NToggle", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 2, + "subTabLabel": "common.execute" + }, + { + "labelKey": "panels.launcher.settings-custom-launch-prefix-label", + "descriptionKey": "panels.launcher.settings-custom-launch-prefix-description", + "widget": "NTextInput", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 2, + "subTabLabel": "common.execute", + "visibleWhen": [ + "Settings.data.appLauncher.customLaunchPrefixEnabled" + ] + }, + { + "labelKey": "panels.launcher.settings-annotation-tool-label", + "descriptionKey": "panels.launcher.settings-annotation-tool-description", + "widget": "NTextInput", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 2, + "subTabLabel": "common.execute" + }, + { + "labelKey": "panels.launcher.settings-overlay-layer-label", + "descriptionKey": "panels.launcher.settings-overlay-layer-description", + "widget": "NToggle", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.launcher.settings-view-mode-label", + "descriptionKey": "panels.launcher.settings-view-mode-description", + "widget": "NComboBox", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.launcher.settings-density-label", + "descriptionKey": "panels.launcher.settings-density-description", + "widget": "NComboBox", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.launcher.settings-show-categories-label", + "descriptionKey": "panels.launcher.settings-show-categories-description", + "widget": "NToggle", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.launcher.settings-icon-mode-label", + "descriptionKey": "panels.launcher.settings-icon-mode-description", + "widget": "NToggle", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.launcher.settings-show-icon-background-label", + "descriptionKey": "panels.launcher.settings-show-icon-background-description", + "widget": "NToggle", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.launcher.settings-sort-by-usage-label", + "descriptionKey": "panels.launcher.settings-sort-by-usage-description", + "widget": "NToggle", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.launcher.settings-enable-settings-search-label", + "descriptionKey": "panels.launcher.settings-enable-settings-search-description", + "widget": "NToggle", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.launcher.settings-enable-windows-search-label", + "descriptionKey": "panels.launcher.settings-enable-windows-search-description", + "widget": "NToggle", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.launcher.settings-enable-session-search-label", + "descriptionKey": "panels.launcher.settings-enable-session-search-description", + "widget": "NToggle", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.launcher.settings-ignore-mouse-input-label", + "descriptionKey": "panels.launcher.settings-ignore-mouse-input-description", + "widget": "NToggle", + "tab": 8, + "tabLabel": "panels.launcher.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.lock-screen.clock-style-label", + "descriptionKey": "panels.lock-screen.clock-style-description", + "widget": "NComboBox", + "tab": 11, + "tabLabel": "panels.lock-screen.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.lock-screen.clock-format-label", + "descriptionKey": "panels.lock-screen.clock-format-description", + "widget": "NTextInput", + "tab": 11, + "tabLabel": "panels.lock-screen.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.general.clockStyle === \"custom\"" + ] + }, + { + "labelKey": "panels.lock-screen.password-chars-label", + "descriptionKey": "panels.lock-screen.password-chars-description", + "widget": "NToggle", + "tab": 11, + "tabLabel": "panels.lock-screen.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.lock-screen.compact-lockscreen-label", + "descriptionKey": "panels.lock-screen.compact-lockscreen-description", + "widget": "NToggle", + "tab": 11, + "tabLabel": "panels.lock-screen.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.lock-screen.enable-lockscreen-media-controls-label", + "descriptionKey": "panels.lock-screen.enable-lockscreen-media-controls-description", + "widget": "NToggle", + "tab": 11, + "tabLabel": "panels.lock-screen.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "!Settings.data.general.compactLockScreen" + ] + }, + { + "labelKey": "panels.lock-screen.lock-screen-animations-label", + "descriptionKey": "panels.lock-screen.lock-screen-animations-description", + "widget": "NToggle", + "tab": 11, + "tabLabel": "panels.lock-screen.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.lock-screen.lock-screen-blur-strength-label", + "descriptionKey": "panels.lock-screen.lock-screen-blur-strength-description", + "widget": "NValueSlider", + "tab": 11, + "tabLabel": "panels.lock-screen.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.lock-screen.lock-screen-tint-strength-label", + "descriptionKey": "panels.lock-screen.lock-screen-tint-strength-description", + "widget": "NValueSlider", + "tab": 11, + "tabLabel": "panels.lock-screen.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.lock-screen.lock-on-suspend-label", + "descriptionKey": "panels.lock-screen.lock-on-suspend-description", + "widget": "NToggle", + "tab": 11, + "tabLabel": "panels.lock-screen.title", + "subTab": 1, + "subTabLabel": "common.behavior" + }, + { + "labelKey": "panels.lock-screen.auto-start-auth-label", + "descriptionKey": "panels.lock-screen.auto-start-auth-description", + "widget": "NToggle", + "tab": 11, + "tabLabel": "panels.lock-screen.title", + "subTab": 1, + "subTabLabel": "common.behavior" + }, + { + "labelKey": "panels.lock-screen.allow-password-with-fprintd-label", + "descriptionKey": "panels.lock-screen.allow-password-with-fprintd-description", + "widget": "NToggle", + "tab": 11, + "tabLabel": "panels.lock-screen.title", + "subTab": 1, + "subTabLabel": "common.behavior" + }, + { + "labelKey": "panels.lock-screen.show-session-buttons-label", + "descriptionKey": "panels.lock-screen.show-session-buttons-description", + "widget": "NToggle", + "tab": 11, + "tabLabel": "panels.lock-screen.title", + "subTab": 1, + "subTabLabel": "common.behavior" + }, + { + "labelKey": "panels.lock-screen.show-hibernate-label", + "descriptionKey": "panels.lock-screen.show-hibernate-description", + "widget": "NToggle", + "tab": 11, + "tabLabel": "panels.lock-screen.title", + "subTab": 1, + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.general.showSessionButtonsOnLockScreen" + ] + }, + { + "labelKey": "panels.session-menu.enable-countdown-label", + "descriptionKey": "panels.session-menu.enable-countdown-description", + "widget": "NToggle", + "tab": 11, + "tabLabel": "panels.lock-screen.title", + "subTab": 1, + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.general.showSessionButtonsOnLockScreen" + ] + }, + { + "labelKey": "panels.session-menu.countdown-duration-label", + "descriptionKey": "panels.session-menu.countdown-duration-description", + "widget": "NValueSlider", + "tab": 11, + "tabLabel": "panels.lock-screen.title", + "subTab": 1, + "subTabLabel": "common.behavior", + "visibleWhen": [ + "Settings.data.general.showSessionButtonsOnLockScreen && Settings.data.general.enableLockScreenCountdown" + ] + }, + { + "labelKey": "panels.notifications.duration-respect-expire-label", + "descriptionKey": "panels.notifications.duration-respect-expire-description", + "widget": "NToggle", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 1, + "subTabLabel": "common.duration" + }, + { + "labelKey": "panels.notifications.duration-low-urgency-label", + "descriptionKey": "panels.notifications.duration-low-urgency-description", + "widget": "NValueSlider", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 1, + "subTabLabel": "common.duration" + }, + { + "labelKey": "panels.notifications.duration-normal-urgency-label", + "descriptionKey": "panels.notifications.duration-normal-urgency-description", + "widget": "NValueSlider", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 1, + "subTabLabel": "common.duration" + }, + { + "labelKey": "panels.notifications.duration-critical-urgency-label", + "descriptionKey": "panels.notifications.duration-critical-urgency-description", + "widget": "NValueSlider", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 1, + "subTabLabel": "common.duration" + }, + { + "labelKey": "panels.notifications.settings-enabled-label", + "descriptionKey": "panels.notifications.settings-enabled-description", + "widget": "NToggle", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.notifications.settings-density-label", + "descriptionKey": "panels.notifications.settings-density-description", + "widget": "NComboBox", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "tooltips.do-not-disturb-enabled", + "descriptionKey": "panels.notifications.settings-do-not-disturb-description", + "widget": "NToggle", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.osd.always-on-top-label", + "descriptionKey": "panels.notifications.settings-always-on-top-description", + "widget": "NToggle", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.notifications.history-clear-dismiss-label", + "descriptionKey": "panels.notifications.history-clear-dismiss-description", + "widget": "NToggle", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 2, + "subTabLabel": "common.history" + }, + { + "labelKey": "panels.notifications.settings-markdown-label", + "descriptionKey": "panels.notifications.settings-markdown-description", + "widget": "NToggle", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 2, + "subTabLabel": "common.history" + }, + { + "labelKey": "panels.notifications.history-low-urgency-label", + "descriptionKey": "panels.notifications.history-low-urgency-description", + "widget": "NToggle", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 2, + "subTabLabel": "common.history" + }, + { + "labelKey": "panels.notifications.history-normal-urgency-label", + "descriptionKey": "panels.notifications.history-normal-urgency-description", + "widget": "NToggle", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 2, + "subTabLabel": "common.history" + }, + { + "labelKey": "panels.notifications.history-critical-urgency-label", + "descriptionKey": "panels.notifications.history-critical-urgency-description", + "widget": "NToggle", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 2, + "subTabLabel": "common.history" + }, + { + "labelKey": "panels.notifications.rules-label", + "descriptionKey": "panels.notifications.rules-description", + "widget": "NLabel", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 5, + "subTabLabel": "panels.notifications.rules-tab" + }, + { + "labelKey": "panels.notifications.sounds-unavailable-label", + "descriptionKey": "panels.notifications.sounds-unavailable-description", + "widget": "NLabel", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 3, + "subTabLabel": "common.sound", + "visibleWhen": [ + "!SoundService.multimediaAvailable" + ] + }, + { + "labelKey": "panels.notifications.sounds-enabled-label", + "descriptionKey": "panels.notifications.sounds-enabled-description", + "widget": "NToggle", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 3, + "subTabLabel": "common.sound" + }, + { + "labelKey": "panels.notifications.sounds-volume-label", + "descriptionKey": "panels.notifications.sounds-volume-description", + "widget": "NValueSlider", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 3, + "subTabLabel": "common.sound" + }, + { + "labelKey": "panels.notifications.sounds-separate-label", + "descriptionKey": "panels.notifications.sounds-separate-description", + "widget": "NToggle", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 3, + "subTabLabel": "common.sound" + }, + { + "labelKey": "panels.notifications.sounds-files-unified-label", + "descriptionKey": "panels.notifications.sounds-files-unified-description", + "widget": "NLabel", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 3, + "subTabLabel": "common.sound", + "visibleWhen": [ + "!(Settings.data.notifications?.sounds?.separateSounds ?? false)" + ] + }, + { + "labelKey": "panels.notifications.sounds-files-low-label", + "descriptionKey": "panels.notifications.sounds-files-low-description", + "widget": "NLabel", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 3, + "subTabLabel": "common.sound", + "visibleWhen": [ + "SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) && (Settings.data.notifications?.sounds?.separateSounds ?? false)" + ] + }, + { + "labelKey": "panels.notifications.sounds-files-normal-label", + "descriptionKey": "panels.notifications.sounds-files-normal-description", + "widget": "NLabel", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 3, + "subTabLabel": "common.sound", + "visibleWhen": [ + "SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) && (Settings.data.notifications?.sounds?.separateSounds ?? false)" + ] + }, + { + "labelKey": "panels.notifications.sounds-files-critical-label", + "descriptionKey": "panels.notifications.sounds-files-critical-description", + "widget": "NLabel", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 3, + "subTabLabel": "common.sound", + "visibleWhen": [ + "SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) && (Settings.data.notifications?.sounds?.separateSounds ?? false)" + ] + }, + { + "labelKey": "panels.notifications.sounds-excluded-apps-label", + "descriptionKey": "panels.notifications.sounds-excluded-apps-description", + "widget": "NLabel", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 3, + "subTabLabel": "common.sound" + }, + { + "labelKey": "panels.notifications.toast-media-label", + "descriptionKey": "panels.notifications.toast-media-description", + "widget": "NCheckbox", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 4, + "subTabLabel": "common.toast" + }, + { + "labelKey": "panels.notifications.toast-keyboard-label", + "descriptionKey": "panels.notifications.toast-keyboard-description", + "widget": "NCheckbox", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 4, + "subTabLabel": "common.toast" + }, + { + "labelKey": "panels.notifications.toast-battery-label", + "descriptionKey": "panels.notifications.toast-battery-description", + "widget": "NCheckbox", + "tab": 9, + "tabLabel": "common.notifications", + "subTab": 4, + "subTabLabel": "common.toast" + }, + { + "labelKey": "panels.osd.enabled-label", + "descriptionKey": "panels.osd.enabled-description", + "widget": "NToggle", + "tab": 10, + "tabLabel": "panels.osd.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.osd.duration-auto-hide-label", + "descriptionKey": "panels.osd.duration-auto-hide-description", + "widget": "NValueSlider", + "tab": 10, + "tabLabel": "panels.osd.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.plugins.available-no-plugins-label", + "descriptionKey": "panels.plugins.available-no-plugins-description", + "widget": "NLabel", + "tab": 19, + "tabLabel": "panels.plugins.title", + "subTab": 1, + "subTabLabel": "common.available" + }, + { + "labelKey": "panels.plugins.auto-update", + "descriptionKey": "panels.plugins.auto-update-description", + "widget": "NToggle", + "tab": 19, + "tabLabel": "panels.plugins.title", + "subTab": 0, + "subTabLabel": "common.installed" + }, + { + "labelKey": "panels.plugins.notify-updates", + "descriptionKey": "panels.plugins.notify-updates-description", + "widget": "NToggle", + "tab": 19, + "tabLabel": "panels.plugins.title", + "subTab": 0, + "subTabLabel": "common.installed" + }, + { + "labelKey": "panels.plugins.installed-no-plugins-label", + "descriptionKey": "panels.plugins.installed-no-plugins-description", + "widget": "NLabel", + "tab": 19, + "tabLabel": "panels.plugins.title", + "subTab": 0, + "subTabLabel": "common.installed" + }, + { + "labelKey": "panels.plugins.sources-add-dialog-name", + "descriptionKey": null, + "widget": "NTextInput", + "tab": 19, + "tabLabel": "panels.plugins.title", + "subTab": 2, + "subTabLabel": "common.sources" + }, + { + "labelKey": "panels.plugins.sources-add-dialog-url", + "descriptionKey": null, + "widget": "NTextInput", + "tab": 19, + "tabLabel": "panels.plugins.title", + "subTab": 2, + "subTabLabel": "common.sources" + }, + { + "labelKey": "panels.location.date-time-use-analog-label", + "descriptionKey": "panels.location.date-time-use-analog-description", + "widget": "NToggle", + "tab": 17, + "tabLabel": "panels.region.title", + "subTab": 2, + "subTabLabel": "common.calendar-panel" + }, + { + "labelKey": "panels.location.date-time-week-numbers-label", + "descriptionKey": "panels.location.date-time-week-numbers-description", + "widget": "NToggle", + "tab": 17, + "tabLabel": "panels.region.title", + "subTab": 2, + "subTabLabel": "common.calendar-panel" + }, + { + "labelKey": "panels.location.date-time-show-events-label", + "descriptionKey": "panels.location.date-time-show-events-description", + "widget": "NToggle", + "tab": 17, + "tabLabel": "panels.region.title", + "subTab": 2, + "subTabLabel": "common.calendar-panel" + }, + { + "labelKey": "panels.location.date-time-12hour-format-label", + "descriptionKey": "panels.location.date-time-12hour-format-description", + "widget": "NToggle", + "tab": 17, + "tabLabel": "panels.region.title", + "subTab": 1, + "subTabLabel": "common.date" + }, + { + "labelKey": "panels.location.date-time-first-day-of-week-label", + "descriptionKey": "panels.location.date-time-first-day-of-week-description", + "widget": "NComboBox", + "tab": 17, + "tabLabel": "panels.region.title", + "subTab": 1, + "subTabLabel": "common.date" + }, + { + "labelKey": "panels.general.language-select-label", + "descriptionKey": "panels.general.language-select-description", + "widget": "NComboBox", + "tab": 17, + "tabLabel": "panels.region.title", + "subTab": 0, + "subTabLabel": "common.location" + }, + { + "labelKey": "panels.location.auto-locate-label", + "descriptionKey": "panels.location.auto-locate-description", + "widget": "NToggle", + "tab": 17, + "tabLabel": "panels.region.title", + "subTab": 0, + "subTabLabel": "common.location" + }, + { + "labelKey": "panels.location.location-search-label", + "descriptionKey": "panels.location.location-search-description", + "widget": "NTextInput", + "tab": 17, + "tabLabel": "panels.region.title", + "subTab": 0, + "subTabLabel": "common.location", + "visibleWhen": [ + "!Settings.data.location.autoLocate" + ] + }, + { + "labelKey": "panels.location.weather-enabled-label", + "descriptionKey": "panels.location.weather-enabled-description", + "widget": "NToggle", + "tab": 17, + "tabLabel": "panels.region.title", + "subTab": 0, + "subTabLabel": "common.location" + }, + { + "labelKey": "panels.location.weather-fahrenheit-label", + "descriptionKey": "panels.location.weather-fahrenheit-description", + "widget": "NToggle", + "tab": 17, + "tabLabel": "panels.region.title", + "subTab": 0, + "subTabLabel": "common.location" + }, + { + "labelKey": "panels.location.weather-show-effects-label", + "descriptionKey": "panels.location.weather-show-effects-description", + "widget": "NToggle", + "tab": 17, + "tabLabel": "panels.region.title", + "subTab": 0, + "subTabLabel": "common.location" + }, + { + "labelKey": "panels.location.weather-talia-mascot-always-label", + "descriptionKey": "panels.location.weather-talia-mascot-always-description", + "widget": "NToggle", + "tab": 17, + "tabLabel": "panels.region.title", + "subTab": 0, + "subTabLabel": "common.location" + }, + { + "labelKey": "panels.location.weather-hide-city-label", + "descriptionKey": "panels.location.weather-hide-city-description", + "widget": "NToggle", + "tab": 17, + "tabLabel": "panels.region.title", + "subTab": 0, + "subTabLabel": "common.location" + }, + { + "labelKey": "panels.location.weather-hide-timezone-label", + "descriptionKey": "panels.location.weather-hide-timezone-description", + "widget": "NToggle", + "tab": 17, + "tabLabel": "panels.region.title", + "subTab": 0, + "subTabLabel": "common.location" + }, + { + "labelKey": "panels.session-menu.large-buttons-style-label", + "descriptionKey": "panels.session-menu.large-buttons-style-description", + "widget": "NToggle", + "tab": 12, + "tabLabel": "session-menu.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.session-menu.large-buttons-layout-label", + "descriptionKey": "panels.session-menu.large-buttons-layout-description", + "widget": "NComboBox", + "tab": 12, + "tabLabel": "session-menu.title", + "subTab": 0, + "subTabLabel": "common.general", + "visibleWhen": [ + "Settings.data.sessionMenu.largeButtonsStyle" + ] + }, + { + "labelKey": "panels.session-menu.show-header-label", + "descriptionKey": "panels.session-menu.show-header-description", + "widget": "NToggle", + "tab": 12, + "tabLabel": "session-menu.title", + "subTab": 0, + "subTabLabel": "common.general", + "visibleWhen": [ + "!Settings.data.sessionMenu.largeButtonsStyle" + ] + }, + { + "labelKey": "panels.session-menu.show-keybinds-label", + "descriptionKey": "panels.session-menu.show-keybinds-description", + "widget": "NToggle", + "tab": 12, + "tabLabel": "session-menu.title", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.system-monitor.enable-dgpu-monitoring-label", + "descriptionKey": "panels.system-monitor.enable-dgpu-monitoring-description", + "widget": "NToggle", + "tab": 18, + "tabLabel": "panels.system.title", + "subTab": 0, + "subTabLabel": "system-monitor.title" + }, + { + "labelKey": "panels.system-monitor.use-custom-highlight-colors-label", + "descriptionKey": "panels.system-monitor.use-custom-highlight-colors-description", + "widget": "NToggle", + "tab": 18, + "tabLabel": "panels.system.title", + "subTab": 0, + "subTabLabel": "system-monitor.title" + }, + { + "labelKey": "panels.system-monitor.external-monitor-label", + "descriptionKey": "panels.system-monitor.external-monitor-description", + "widget": "NTextInput", + "tab": 18, + "tabLabel": "panels.system.title", + "subTab": 0, + "subTabLabel": "system-monitor.title" + }, + { + "labelKey": "panels.system.noctalia-performance-disable-wallpaper-label", + "descriptionKey": "panels.system.noctalia-performance-disable-wallpaper-description", + "widget": "NToggle", + "tab": 18, + "tabLabel": "panels.system.title", + "subTab": 2, + "subTabLabel": "common.performance" + }, + { + "labelKey": "panels.system.noctalia-performance-disable-desktop-widgets-label", + "descriptionKey": "panels.system.noctalia-performance-disable-desktop-widgets-description", + "widget": "NToggle", + "tab": 18, + "tabLabel": "panels.system.title", + "subTab": 2, + "subTabLabel": "common.performance" + }, + { + "labelKey": "panels.user-interface.tooltips-label", + "descriptionKey": "panels.user-interface.tooltips-description", + "widget": "NToggle", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.user-interface.box-border-label", + "descriptionKey": "panels.user-interface.box-border-description", + "widget": "NToggle", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.user-interface.scrollbar-always-visible-label", + "descriptionKey": "panels.user-interface.scrollbar-always-visible-description", + "widget": "NToggle", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.user-interface.shadows-label", + "descriptionKey": "panels.user-interface.shadows-description", + "widget": "NToggle", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.user-interface.blur-behind-label", + "descriptionKey": "panels.user-interface.blur-behind-description", + "widget": "NToggle", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.user-interface.translucent-widgets-label", + "descriptionKey": "panels.user-interface.translucent-widgets-description", + "widget": "NToggle", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.user-interface.shadows-direction-label", + "descriptionKey": "panels.user-interface.shadows-direction-description", + "widget": "NComboBox", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "Settings.data.general.enableShadows" + ] + }, + { + "labelKey": "panels.user-interface.scaling-label", + "descriptionKey": "panels.user-interface.scaling-description", + "widget": "NValueSlider", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.user-interface.box-border-radius-label", + "descriptionKey": "panels.user-interface.box-border-radius-description", + "widget": "NValueSlider", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.user-interface.control-border-radius-label", + "descriptionKey": "panels.user-interface.control-border-radius-description", + "widget": "NValueSlider", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.user-interface.animation-disable-label", + "descriptionKey": "panels.user-interface.animation-disable-description", + "widget": "NToggle", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 0, + "subTabLabel": "common.appearance" + }, + { + "labelKey": "panels.user-interface.animation-speed-label", + "descriptionKey": "panels.user-interface.animation-speed-description", + "widget": "NValueSlider", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 0, + "subTabLabel": "common.appearance", + "visibleWhen": [ + "!Settings.data.general.animationDisabled" + ] + }, + { + "labelKey": "panels.user-interface.panels-attached-to-bar-label", + "descriptionKey": "panels.user-interface.panels-attached-to-bar-description", + "widget": "NToggle", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 1, + "subTabLabel": "common.panels" + }, + { + "labelKey": "panels.user-interface.allow-panels-without-bar-label", + "descriptionKey": "panels.user-interface.allow-panels-without-bar-description", + "widget": "NToggle", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 1, + "subTabLabel": "common.panels", + "visibleWhen": [ + "(Quickshell.screens.length > 1)" + ] + }, + { + "labelKey": "panels.user-interface.panel-background-opacity-label", + "descriptionKey": "panels.user-interface.panel-background-opacity-description", + "widget": "NValueSlider", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 1, + "subTabLabel": "common.panels" + }, + { + "labelKey": "panels.user-interface.dimmer-opacity-label", + "descriptionKey": "panels.user-interface.dimmer-opacity-description", + "widget": "NValueSlider", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 1, + "subTabLabel": "common.panels" + }, + { + "labelKey": "panels.user-interface.settings-panel-mode-label", + "descriptionKey": "panels.user-interface.settings-panel-mode-description", + "widget": "NComboBox", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 1, + "subTabLabel": "common.panels" + }, + { + "labelKey": "panels.user-interface.settings-panel-sidebar-card-style-label", + "descriptionKey": "panels.user-interface.settings-panel-sidebar-card-style-description", + "widget": "NToggle", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 1, + "subTabLabel": "common.panels" + }, + { + "labelKey": "panels.general.screen-corners-show-corners-label", + "descriptionKey": "panels.general.screen-corners-show-corners-description", + "widget": "NToggle", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 2, + "subTabLabel": "common.screen-corners" + }, + { + "labelKey": "panels.general.screen-corners-solid-black-label", + "descriptionKey": "panels.general.screen-corners-solid-black-description", + "widget": "NToggle", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 2, + "subTabLabel": "common.screen-corners" + }, + { + "labelKey": "panels.general.screen-corners-radius-label", + "descriptionKey": "panels.general.screen-corners-radius-description", + "widget": "NValueSlider", + "tab": 1, + "tabLabel": "panels.user-interface.title", + "subTab": 2, + "subTabLabel": "common.screen-corners" + }, + { + "labelKey": "panels.wallpaper.automation-scheduled-change-label", + "descriptionKey": "panels.wallpaper.automation-scheduled-change-description", + "widget": "NToggle", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 2, + "subTabLabel": "common.automation" + }, + { + "labelKey": "panels.wallpaper.automation-change-mode-label", + "descriptionKey": "panels.wallpaper.automation-change-mode-description", + "widget": "NComboBox", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 2, + "subTabLabel": "common.automation" + }, + { + "labelKey": "panels.wallpaper.automation-interval-label", + "descriptionKey": "panels.wallpaper.automation-interval-description", + "widget": "NSpinBox", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 2, + "subTabLabel": "common.automation" + }, + { + "labelKey": "panels.wallpaper.settings-enable-management-label", + "descriptionKey": "panels.wallpaper.settings-enable-management-description", + "widget": "NToggle", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "tooltips.wallpaper-selector", + "descriptionKey": "panels.wallpaper.settings-selector-description", + "widget": "NLabel", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.wallpaper.settings-view-mode-label", + "descriptionKey": "panels.wallpaper.settings-view-mode-description", + "widget": "NComboBox", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.wallpaper.settings-folder-label", + "descriptionKey": "panels.wallpaper.settings-folder-description", + "widget": "NTextInputButton", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.wallpaper.settings-monitor-specific-label", + "descriptionKey": "panels.wallpaper.settings-monitor-specific-description", + "widget": "NToggle", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.wallpaper.settings-use-original-images-label", + "descriptionKey": "panels.wallpaper.settings-use-original-images-description", + "widget": "NToggle", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.wallpaper.settings-clear-cache-label", + "descriptionKey": "panels.wallpaper.settings-clear-cache-description", + "widget": "NLabel", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 0, + "subTabLabel": "common.general" + }, + { + "labelKey": "panels.wallpaper.settings-enable-overview-label", + "descriptionKey": "panels.wallpaper.settings-enable-overview-description", + "widget": "NToggle", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 0, + "subTabLabel": "common.general", + "visibleWhen": [ + "CompositorService.isNiri" + ] + }, + { + "labelKey": "panels.wallpaper.settings-overview-blur-strength-label", + "descriptionKey": "panels.wallpaper.settings-overview-blur-strength-description", + "widget": "NValueSlider", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 0, + "subTabLabel": "common.general", + "visibleWhen": [ + "CompositorService.isNiri", + "Settings.data.wallpaper.overviewEnabled" + ] + }, + { + "labelKey": "panels.wallpaper.settings-overview-tint-label", + "descriptionKey": "panels.wallpaper.settings-overview-tint-description", + "widget": "NValueSlider", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 0, + "subTabLabel": "common.general", + "visibleWhen": [ + "CompositorService.isNiri", + "Settings.data.wallpaper.overviewEnabled" + ] + }, + { + "labelKey": "panels.wallpaper.look-feel-fill-mode-label", + "descriptionKey": "panels.wallpaper.look-feel-fill-mode-description", + "widget": "NComboBox", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 1, + "subTabLabel": "common.look" + }, + { + "labelKey": "bar.audio-visualizer.color-name-label", + "descriptionKey": "panels.wallpaper.look-feel-fill-color-description", + "widget": "NLabel", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 1, + "subTabLabel": "common.look" + }, + { + "labelKey": "panels.wallpaper.look-feel-transition-type-label", + "descriptionKey": "panels.wallpaper.look-feel-transition-type-description", + "widget": "NLabel", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 1, + "subTabLabel": "common.look" + }, + { + "labelKey": "panels.wallpaper.look-feel-skip-startup-transition-label", + "descriptionKey": "panels.wallpaper.look-feel-skip-startup-transition-description", + "widget": "NToggle", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 1, + "subTabLabel": "common.look" + }, + { + "labelKey": "panels.wallpaper.look-feel-transition-duration-label", + "descriptionKey": "panels.wallpaper.look-feel-transition-duration-description", + "widget": "NValueSlider", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 1, + "subTabLabel": "common.look" + }, + { + "labelKey": "panels.wallpaper.look-feel-edge-smoothness-label", + "descriptionKey": "panels.wallpaper.look-feel-edge-smoothness-description", + "widget": "NValueSlider", + "tab": 3, + "tabLabel": "common.wallpaper", + "subTab": 1, + "subTabLabel": "common.look" + } +] \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Assets/settings-widgets-default.json b/fedora/.config/quickshell/noctalia-shell/Assets/settings-widgets-default.json new file mode 100644 index 0000000..df518b5 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Assets/settings-widgets-default.json @@ -0,0 +1,295 @@ +{ + "bar": { + "ActiveWindow": { + "showText": true, + "showIcon": true, + "hideMode": "hidden", + "scrollingMode": "hover", + "maxWidth": 145, + "useFixedWidth": false, + "colorizeIcons": false, + "textColor": "none" + }, + "AudioVisualizer": { + "width": 200, + "colorName": "primary", + "hideWhenIdle": false + }, + "Battery": { + "displayMode": "graphic-clean", + "deviceNativePath": "__default__", + "showPowerProfiles": false, + "showNoctaliaPerformance": false, + "hideIfNotDetected": true, + "hideIfIdle": false + }, + "Bluetooth": { + "displayMode": "onhover", + "iconColor": "none", + "textColor": "none" + }, + "Brightness": { + "displayMode": "onhover", + "iconColor": "none", + "textColor": "none", + "applyToAllMonitors": false + }, + "Clock": { + "clockColor": "none", + "useCustomFont": false, + "customFont": "", + "formatHorizontal": "HH:mm ddd, MMM dd", + "formatVertical": "HH mm - dd MM", + "tooltipFormat": "HH:mm ddd, MMM dd" + }, + "ControlCenter": { + "useDistroLogo": false, + "icon": "noctalia", + "customIconPath": "", + "colorizeDistroLogo": false, + "colorizeSystemIcon": "none", + "enableColorization": false + }, + "CustomButton": { + "icon": "heart", + "showIcon": true, + "showExecTooltip": true, + "showTextTooltip": true, + "generalTooltipText": "", + "hideMode": "alwaysExpanded", + "leftClickExec": "", + "leftClickUpdateText": false, + "rightClickExec": "", + "rightClickUpdateText": false, + "middleClickExec": "", + "middleClickUpdateText": false, + "textCommand": "", + "textStream": false, + "textIntervalMs": 3000, + "textCollapse": "", + "parseJson": false, + "wheelExec": "", + "wheelUpExec": "", + "wheelDownExec": "", + "wheelMode": "unified", + "wheelUpdateText": false, + "wheelUpUpdateText": false, + "wheelDownUpdateText": false, + "maxTextLength": { + "horizontal": 10, + "vertical": 10 + }, + "enableColorization": false, + "colorizeSystemIcon": "none", + "ipcIdentifier": "" + }, + "DarkMode": { + "iconColor": "none" + }, + "KeepAwake": { + "iconColor": "none", + "textColor": "none" + }, + "KeyboardLayout": { + "displayMode": "onhover", + "showIcon": true, + "iconColor": "none", + "textColor": "none" + }, + "LockKeys": { + "showCapsLock": true, + "showNumLock": true, + "showScrollLock": true, + "capsLockIcon": "letter-c", + "numLockIcon": "letter-n", + "scrollLockIcon": "letter-s", + "hideWhenOff": false + }, + "Launcher": { + "useDistroLogo": false, + "icon": "rocket", + "customIconPath": "", + "colorizeSystemIcon": "none", + "enableColorization": false, + "iconColor": "none" + }, + "MediaMini": { + "hideMode": "hidden", + "scrollingMode": "hover", + "maxWidth": 145, + "useFixedWidth": false, + "hideWhenIdle": false, + "showAlbumArt": true, + "showArtistFirst": true, + "showVisualizer": false, + "showProgressRing": true, + "visualizerType": "linear", + "textColor": "none", + "compactMode": false, + "panelShowAlbumArt": true + }, + "Microphone": { + "displayMode": "onhover", + "middleClickCommand": "pwvucontrol || pavucontrol", + "iconColor": "none", + "textColor": "none" + }, + "NotificationHistory": { + "showUnreadBadge": true, + "hideWhenZero": false, + "hideWhenZeroUnread": false, + "unreadBadgeColor": "primary", + "iconColor": "none" + }, + "SessionMenu": { + "iconColor": "error" + }, + "Settings": { + "iconColor": "none" + }, + "Spacer": { + "width": 20 + }, + "SystemMonitor": { + "compactMode": true, + "iconColor": "none", + "textColor": "none", + "useMonospaceFont": true, + "usePadding": false, + "showCpuUsage": true, + "showCpuCores": false, + "showCpuFreq": false, + "showCpuTemp": true, + "showGpuTemp": false, + "showLoadAverage": false, + "showMemoryUsage": true, + "showMemoryAsPercent": false, + "showSwapUsage": false, + "showNetworkStats": false, + "showDiskUsage": false, + "showDiskUsageAsPercent": false, + "showDiskAvailable": false, + "diskPath": "/" + }, + "Taskbar": { + "onlySameOutput": true, + "onlyActiveWorkspaces": true, + "hideMode": "hidden", + "colorizeIcons": false, + "showTitle": false, + "titleWidth": 120, + "showPinnedApps": true, + "smartWidth": true, + "maxTaskbarWidth": 40, + "iconScale": 0.8 + }, + "Tray": { + "blacklist": [], + "colorizeIcons": false, + "chevronColor": "none", + "pinned": [], + "drawerEnabled": true, + "hidePassive": false + }, + "VPN": { + "displayMode": "onhover", + "iconColor": "none", + "textColor": "none" + }, + "Network": { + "displayMode": "onhover", + "iconColor": "none", + "textColor": "none" + }, + "NightLight": { + "iconColor": "none" + }, + "NoctaliaPerformance": { + "iconColor": "none" + }, + "PowerProfile": { + "iconColor": "none" + }, + "Workspace": { + "labelMode": "index", + "followFocusedScreen": false, + "hideUnoccupied": false, + "characterCount": 2, + "showApplications": false, + "showApplicationsHover": false, + "showLabelsOnlyWhenOccupied": true, + "colorizeIcons": false, + "unfocusedIconsOpacity": 1, + "groupedBorderOpacity": 1, + "enableScrollWheel": true, + "iconScale": 0.8, + "focusedColor": "primary", + "occupiedColor": "secondary", + "emptyColor": "secondary", + "showBadge": true, + "pillSize": 0.6, + "fontWeight": "bold" + }, + "Volume": { + "displayMode": "onhover", + "middleClickCommand": "pwvucontrol || pavucontrol", + "iconColor": "none", + "textColor": "none" + }, + "WallpaperSelector": { + "iconColor": "none" + } + }, + "controlCenter": { + "CustomButton": { + "icon": "heart", + "onClicked": "", + "onRightClicked": "", + "onMiddleClicked": "", + "stateChecksJson": "[]", + "generalTooltipText": "", + "enableOnStateLogic": false, + "showExecTooltip": true + } + }, + "desktop": { + "Clock": { + "showBackground": true, + "roundedCorners": true, + "clockStyle": "digital", + "clockColor": "none", + "useCustomFont": false, + "customFont": "", + "format": "HH:mm\\nd MMMM yyyy" + }, + "MediaPlayer": { + "showBackground": true, + "roundedCorners": true, + "visualizerType": "linear", + "hideMode": "visible", + "showButtons": true, + "showAlbumArt": true, + "showVisualizer": true + }, + "Weather": { + "showBackground": true, + "roundedCorners": true + }, + "SystemStat": { + "showBackground": true, + "roundedCorners": true, + "statType": "CPU", + "diskPath": "/", + "layout": "bottom" + }, + "AudioVisualizer": { + "showBackground": true, + "roundedCorners": true, + "width": 320, + "height": 72, + "visualizerType": "linear", + "hideWhenIdle": false, + "colorName": "primary" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/CREDITS.md b/fedora/.config/quickshell/noctalia-shell/CREDITS.md new file mode 100644 index 0000000..daaa0f2 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/CREDITS.md @@ -0,0 +1,48 @@ +# Credits + +Noctalia Shell is made possible by the incredible work of many open-source projects and contributors. + +## Design & Branding + +- **MrDowntempo** - Creator of the Noctalia Owl and moon logo +- **[SaberJ2X](https://www.reddit.com/user/SaberJ64/)** - Creator of Talia, the Noctalia mascot + +## Core Framework + +- **[Quickshell](https://github.com/outfoxxed/quickshell)** - The Qt/QML-based Wayland shell framework that powers Noctalia + +## Runtime Dependencies + +### System Integration +- **[brightnessctl](https://github.com/Hummer12007/brightnessctl)** - Screen brightness control +- **[wlsunset](https://sr.ht/~kennylevinsen/wlsunset/)** - Night light and blue light filter support +- **[wl-clipboard](https://github.com/bugaevc/wl-clipboard)** - Wayland clipboard utilities +- **[ddcutil](https://www.ddcutil.com/)** - External display brightness control +- **[power-profiles-daemon](https://gitlab.freedesktop.org/upower/power-profiles-daemon)** - Power profile management + +### Media & Audio +- **[gpu-screen-recorder](https://git.dec05eba.com/gpu-screen-recorder/about/)** - Hardware-accelerated screen recording +- **[Cava](https://github.com/karlstav/cava)** - Audio visualizer component + +### Utilities +- **[cliphist](https://github.com/sentriz/cliphist)** - Clipboard history support + +## Icons +- **[Tabler Icons](https://tabler.io/icons)** - Icon set used throughout the shell +- **[Riyan Resdian on Noun Project](https://thenounproject.com/creator/yaicon/)** - Plug icon + +## Audio Assets +- **[Universfield on Pixabay](https://pixabay.com/users/universfield-28281460/)** - Notification sound effect +- **[DrNI on Freesound](https://freesound.org/people/DrNI/sounds/34562/)** - Timer's alarm sound effect +- **[Lucas McCallister on Freesound](http://www.freesound.org/samplesViewSingle.php?id=67091)** - Volume change feedback sound effect + + +## Special Thanks +- The **Wayland** community for building the future of Linux desktop graphics +- The **Niri**, **Hyprland**, **Sway**, **Labwc**, and **MangoWC** teams for their excellent Wayland compositors +- All the contributors and users who have helped make Noctalia better + +## License +Noctalia Shell is licensed under the MIT License. See [LICENSE](LICENSE) for details. + +Each dependency listed above is governed by its own respective license. Please refer to their individual projects for licensing information. diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Color.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Color.qml new file mode 100644 index 0000000..6e940f5 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Color.qml @@ -0,0 +1,473 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +/* +Noctalia is not strictly a Material Design project, it supports both some predefined +color schemes and dynamic color generation from the wallpaper. + +We ultimately decided to use a restricted set of colors that follows the +Material Design 3 naming convention. + +NOTE: All color names are prefixed with 'm' (e.g., mPrimary) to prevent QML from +misinterpreting them as signals (e.g., the 'onPrimary' property name). +*/ +Singleton { + id: root + + property bool reloadColors: false + + // Suppress transition animations until the first colors.json load completes + property bool skipTransition: true + + // Flag indicating theme colors are currently transitioning (for widgets to disable their own animations) + property bool isTransitioning: false + + // Timer to reset isTransitioning after animation completes + Timer { + id: transitionTimer + interval: Style.animationSlowest + 50 // Small buffer after animation + onTriggered: root.isTransitioning = false + } + + // --- Key Colors: These are the main accent colors that define your app's style + property color mPrimary: defaultColors.mPrimary + property color mOnPrimary: defaultColors.mOnPrimary + property color mSecondary: defaultColors.mSecondary + property color mOnSecondary: defaultColors.mOnSecondary + property color mTertiary: defaultColors.mTertiary + property color mOnTertiary: defaultColors.mOnTertiary + + // --- Utility Colors: These colors serve specific, universal purposes like indicating errors + property color mError: defaultColors.mError + property color mOnError: defaultColors.mOnError + + // --- Surface and Variant Colors: These provide additional options for surfaces and their contents, creating visual hierarchy + property color mSurface: defaultColors.mSurface + property color mOnSurface: defaultColors.mOnSurface + + property color mSurfaceVariant: defaultColors.mSurfaceVariant + property color mOnSurfaceVariant: defaultColors.mOnSurfaceVariant + + property color mOutline: defaultColors.mOutline + property color mShadow: defaultColors.mShadow + + property color mHover: defaultColors.mHover + property color mOnHover: defaultColors.mOnHover + + // --- Color transition animations --- + Behavior on mPrimary { + enabled: !root.skipTransition + ColorAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutCubic + } + } + Behavior on mOnPrimary { + enabled: !root.skipTransition + ColorAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutCubic + } + } + Behavior on mSecondary { + enabled: !root.skipTransition + ColorAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutCubic + } + } + Behavior on mOnSecondary { + enabled: !root.skipTransition + ColorAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutCubic + } + } + Behavior on mTertiary { + enabled: !root.skipTransition + ColorAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutCubic + } + } + Behavior on mOnTertiary { + enabled: !root.skipTransition + ColorAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutCubic + } + } + Behavior on mError { + enabled: !root.skipTransition + ColorAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutCubic + } + } + Behavior on mOnError { + enabled: !root.skipTransition + ColorAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutCubic + } + } + Behavior on mSurface { + enabled: !root.skipTransition + ColorAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutCubic + } + } + Behavior on mOnSurface { + enabled: !root.skipTransition + ColorAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutCubic + } + } + Behavior on mSurfaceVariant { + enabled: !root.skipTransition + ColorAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutCubic + } + } + Behavior on mOnSurfaceVariant { + enabled: !root.skipTransition + ColorAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutCubic + } + } + Behavior on mOutline { + enabled: !root.skipTransition + ColorAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutCubic + } + } + Behavior on mShadow { + enabled: !root.skipTransition + ColorAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutCubic + } + } + Behavior on mHover { + enabled: !root.skipTransition + ColorAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutCubic + } + } + Behavior on mOnHover { + enabled: !root.skipTransition + ColorAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutCubic + } + } + + // Helper to start transition and update a color + function startTransition() { + root.isTransitioning = true; + transitionTimer.restart(); + } + + // Update colors when customColorsData changes (imperative assignment enables Behavior animations) + Connections { + target: customColorsData + function onMPrimaryChanged() { + if (!root.skipTransition) { + startTransition(); + } + root.mPrimary = customColorsData.mPrimary; + } + function onMOnPrimaryChanged() { + if (!root.skipTransition) { + startTransition(); + } + root.mOnPrimary = customColorsData.mOnPrimary; + } + function onMSecondaryChanged() { + if (!root.skipTransition) { + startTransition(); + } + root.mSecondary = customColorsData.mSecondary; + } + function onMOnSecondaryChanged() { + if (!root.skipTransition) { + startTransition(); + } + root.mOnSecondary = customColorsData.mOnSecondary; + } + function onMTertiaryChanged() { + if (!root.skipTransition) { + startTransition(); + } + root.mTertiary = customColorsData.mTertiary; + } + function onMOnTertiaryChanged() { + if (!root.skipTransition) { + startTransition(); + } + root.mOnTertiary = customColorsData.mOnTertiary; + } + function onMErrorChanged() { + if (!root.skipTransition) { + startTransition(); + } + root.mError = customColorsData.mError; + } + function onMOnErrorChanged() { + if (!root.skipTransition) { + startTransition(); + } + root.mOnError = customColorsData.mOnError; + } + function onMSurfaceChanged() { + if (!root.skipTransition) { + startTransition(); + } + root.mSurface = customColorsData.mSurface; + } + function onMOnSurfaceChanged() { + if (!root.skipTransition) { + startTransition(); + } + root.mOnSurface = customColorsData.mOnSurface; + } + function onMSurfaceVariantChanged() { + if (!root.skipTransition) { + startTransition(); + } + root.mSurfaceVariant = customColorsData.mSurfaceVariant; + } + function onMOnSurfaceVariantChanged() { + if (!root.skipTransition) { + startTransition(); + } + root.mOnSurfaceVariant = customColorsData.mOnSurfaceVariant; + } + function onMOutlineChanged() { + if (!root.skipTransition) { + startTransition(); + } + root.mOutline = customColorsData.mOutline; + } + function onMShadowChanged() { + if (!root.skipTransition) { + startTransition(); + } + root.mShadow = customColorsData.mShadow; + } + function onMHoverChanged() { + if (!root.skipTransition) { + startTransition(); + } + root.mHover = customColorsData.mHover; + } + function onMOnHoverChanged() { + if (!root.skipTransition) { + startTransition(); + } + root.mOnHover = customColorsData.mOnHover; + } + } + + function resolveColorKey(key) { + switch (key) { + case "primary": + return root.mPrimary; + case "secondary": + return root.mSecondary; + case "tertiary": + return root.mTertiary; + case "error": + return root.mError; + default: + return root.mOnSurface; + } + } + + function resolveOnColorKey(key) { + switch (key) { + case "primary": + return root.mOnPrimary; + case "secondary": + return root.mOnSecondary; + case "tertiary": + return root.mOnTertiary; + case "error": + return root.mOnError; + default: + return root.mSurface; + } + } + + function resolveColorKeyOptional(key) { + switch (key) { + case "primary": + return root.mPrimary; + case "secondary": + return root.mSecondary; + case "tertiary": + return root.mTertiary; + case "error": + return root.mError; + default: + return "transparent"; + } + } + + // Adaptive opacity calculation: automatically makes light mode more transparent + function adaptiveOpacity(baseOpacity) { + return Settings.data.colorSchemes.darkMode ? baseOpacity : Math.pow(baseOpacity, 1.5); + } + + function smartAlpha(baseColor, minAlpha = 0.4) { + if (!Settings.data.ui.translucentWidgets) + return baseColor; + + let alpha = Math.max(adaptiveOpacity(Settings.data.ui.panelBackgroundOpacity), minAlpha); + + // Combine with the base color's existing alpha + let resultAlpha = Math.max(0, baseColor.a - (1.0 - alpha)); + return Qt.alpha(baseColor, resultAlpha); + } + + readonly property var colorKeyModel: [ + { + "key": "none", + "name": I18n.tr("common.none") + }, + { + "key": "primary", + "name": I18n.tr("common.primary") + }, + { + "key": "secondary", + "name": I18n.tr("common.secondary") + }, + { + "key": "tertiary", + "name": I18n.tr("common.tertiary") + }, + { + "key": "error", + "name": I18n.tr("common.error") + } + ] + + // -------------------------------- + // Default colors: Noctalia (default) dark — must match Assets/ColorScheme/Noctalia-default + QtObject { + id: defaultColors + + readonly property color mPrimary: "#fff59b" + readonly property color mOnPrimary: "#0e0e43" + + readonly property color mSecondary: "#a9aefe" + readonly property color mOnSecondary: "#0e0e43" + + readonly property color mTertiary: "#9BFECE" + readonly property color mOnTertiary: "#0e0e43" + + readonly property color mError: "#FD4663" + readonly property color mOnError: "#0e0e43" + + readonly property color mSurface: "#070722" + readonly property color mOnSurface: "#f3edf7" + + readonly property color mSurfaceVariant: "#11112d" + readonly property color mOnSurfaceVariant: "#7c80b4" + + readonly property color mOutline: "#21215F" + readonly property color mShadow: "#070722" + + readonly property color mHover: "#9BFECE" + readonly property color mOnHover: "#0e0e43" + } + + // ---------------------------------------------------------------- + // FileView to load custom colors data from colors.json + FileView { + id: customColorsFile + path: Settings.directoriesCreated ? (Settings.configDir + "colors.json") : undefined + printErrors: false + watchChanges: true + onFileChanged: { + Logger.d("Color", "Reloading colors from disk"); + reloadColors = true; + reload(); + } + onAdapterUpdated: { + Logger.d("Color", "Writing colors to disk"); + writeAdapter(); + } + + onLoaded: { + if (root.skipTransition) { + Qt.callLater(function () { + root.skipTransition = false; + }); + } + } + + // Trigger initial load when path changes from empty to actual path + onPathChanged: { + if (path !== undefined) { + reload(); + } + } + onLoadFailed: function (error) { + if (reloadColors) { + reloadColors = false; + return; + } + + if (root.skipTransition) { + Qt.callLater(function () { + root.skipTransition = false; + }); + } + + // Error code 2 = ENOENT (No such file or directory) + if (error === 2 || error.toString().includes("No such file")) { + // File doesn't exist, create it with default values + writeAdapter(); + } + } + JsonAdapter { + id: customColorsData + + property color mPrimary: defaultColors.mPrimary + property color mOnPrimary: defaultColors.mOnPrimary + + property color mSecondary: defaultColors.mSecondary + property color mOnSecondary: defaultColors.mOnSecondary + + property color mTertiary: defaultColors.mTertiary + property color mOnTertiary: defaultColors.mOnTertiary + + property color mError: defaultColors.mError + property color mOnError: defaultColors.mOnError + + property color mSurface: defaultColors.mSurface + property color mOnSurface: defaultColors.mOnSurface + + property color mSurfaceVariant: defaultColors.mSurfaceVariant + property color mOnSurfaceVariant: defaultColors.mOnSurfaceVariant + + property color mOutline: defaultColors.mOutline + property color mShadow: defaultColors.mShadow + + property color mHover: defaultColors.mHover + property color mOnHover: defaultColors.mOnHover + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/FuzzySort.qml b/fedora/.config/quickshell/noctalia-shell/Commons/FuzzySort.qml new file mode 100644 index 0000000..d729e2a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/FuzzySort.qml @@ -0,0 +1,883 @@ +pragma Singleton + +import QtQuick +import Quickshell + +Singleton { + id: root + + // Public API + function go(search, targets, options) { + return _go(search, targets, options); + } + + function single(search, target) { + return _single(search, target); + } + + function highlight(result, open, close) { + if (open === undefined) + open = ''; + if (close === undefined) + close = ''; + return _highlight(result, open, close); + } + + function prepare(target) { + return _prepare(target); + } + + function cleanup() { + return _cleanup(); + } + + // Internal implementation + readonly property var _INFINITY: Infinity + readonly property var _NEGATIVE_INFINITY: -Infinity + readonly property var _NULL: null + property var _noResults: { + let r = []; + r.total = 0; + return r; + } + property var _noTarget: _prepare('') + + property var _preparedCache: new Map() + property var _preparedSearchCache: new Map() + + property var _matchesSimple: [] + property var _matchesStrict: [] + property var _nextBeginningIndexesChanges: [] + property var _keysSpacesBestScores: [] + property var _allowPartialMatchScores: [] + property var _tmpTargets: [] + property var _tmpResults: [] + property var _q: _fastpriorityqueue() + + function _fastpriorityqueue() { + var e = [], o = 0, a = {}; + var v = function (r) { + for (var a = 0, vc = e[a], c = 1; c < o; ) { + var s = c + 1; + a = c; + if (s < o && e[s]._score < e[c]._score) + a = s; + e[a - 1 >> 1] = e[a]; + c = 1 + (a << 1); + } + for (var f = a - 1 >> 1; a > 0 && vc._score < e[f]._score; f = (a = f) - 1 >> 1) + e[a] = e[f]; + e[a] = vc; + }; + a.add = function (r) { + var ac = o; + e[o++] = r; + for (var vc = ac - 1 >> 1; ac > 0 && r._score < e[vc]._score; vc = (ac = vc) - 1 >> 1) + e[ac] = e[vc]; + e[ac] = r; + }; + a.poll = function () { + if (o !== 0) { + var ac = e[0]; + e[0] = e[--o]; + v(); + return ac; + } + }; + a.peek = function () { + if (o !== 0) + return e[0]; + }; + a.replaceTop = function (r) { + e[0] = r; + v(); + }; + return a; + } + + function _createResult() { + return { + target: '', + obj: _NULL, + _score: _NEGATIVE_INFINITY, + _indexes: [], + _targetLower: '', + _targetLowerCodes: _NULL, + _nextBeginningIndexes: _NULL, + _bitflags: 0, + get indexes() { + return this._indexes.slice(0, this._indexes.len).sort((a, b) => a - b); + }, + set indexes(idx) { + this._indexes = idx; + }, + highlight: function (open, close) { + return root._highlight(this, open, close); + }, + get score() { + return root._normalizeScore(this._score); + }, + set score(s) { + this._score = root._denormalizeScore(s); + } + }; + } + + function _createKeysResult(len) { + var arr = new Array(len); + arr._score = _NEGATIVE_INFINITY; + arr.obj = _NULL; + Object.defineProperty(arr, 'score', { + get: function () { + return root._normalizeScore(this._score); + }, + set: function (s) { + this._score = root._denormalizeScore(s); + } + }); + return arr; + } + + function _new_result(target, options) { + var result = _createResult(); + result.target = target; + result.obj = options.obj ?? _NULL; + result._score = options._score ?? _NEGATIVE_INFINITY; + result._indexes = options._indexes ?? []; + result._targetLower = options._targetLower ?? ''; + result._targetLowerCodes = options._targetLowerCodes ?? _NULL; + result._nextBeginningIndexes = options._nextBeginningIndexes ?? _NULL; + result._bitflags = options._bitflags ?? 0; + return result; + } + + function _normalizeScore(score) { + if (score === _NEGATIVE_INFINITY) + return 0; + if (score > 1) + return score; + return Math.E ** (((-score + 1) ** 0.04307 - 1) * -2); + } + + function _denormalizeScore(normalizedScore) { + if (normalizedScore === 0) + return _NEGATIVE_INFINITY; + if (normalizedScore > 1) + return normalizedScore; + return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307); + } + + function _remove_accents(str) { + return str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, ''); + } + + function _prepareLowerInfo(str) { + str = _remove_accents(str); + var strLen = str.length; + var lower = str.toLowerCase(); + var lowerCodes = []; + var bitflags = 0; + var containsSpace = false; + + for (var i = 0; i < strLen; ++i) { + var lowerCode = lowerCodes[i] = lower.charCodeAt(i); + if (lowerCode === 32) { + containsSpace = true; + continue; + } + var bit = lowerCode >= 97 && lowerCode <= 122 ? lowerCode - 97 : lowerCode >= 48 && lowerCode <= 57 ? 26 : lowerCode <= 127 ? 30 : 31; + bitflags |= 1 << bit; + } + return { + lowerCodes: lowerCodes, + bitflags: bitflags, + containsSpace: containsSpace, + _lower: lower + }; + } + + function _prepareBeginningIndexes(target) { + var targetLen = target.length; + var beginningIndexes = []; + var beginningIndexesLen = 0; + var wasUpper = false; + var wasAlphanum = false; + for (var i = 0; i < targetLen; ++i) { + var targetCode = target.charCodeAt(i); + var isUpper = targetCode >= 65 && targetCode <= 90; + var isAlphanum = isUpper || targetCode >= 97 && targetCode <= 122 || targetCode >= 48 && targetCode <= 57; + var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum; + wasUpper = isUpper; + wasAlphanum = isAlphanum; + if (isBeginning) + beginningIndexes[beginningIndexesLen++] = i; + } + return beginningIndexes; + } + + function _prepareNextBeginningIndexes(target) { + target = _remove_accents(target); + var targetLen = target.length; + var beginningIndexes = _prepareBeginningIndexes(target); + var nextBeginningIndexes = []; + var lastIsBeginning = beginningIndexes[0]; + var lastIsBeginningI = 0; + for (var i = 0; i < targetLen; ++i) { + if (lastIsBeginning > i) { + nextBeginningIndexes[i] = lastIsBeginning; + } else { + lastIsBeginning = beginningIndexes[++lastIsBeginningI]; + nextBeginningIndexes[i] = lastIsBeginning === undefined ? targetLen : lastIsBeginning; + } + } + return nextBeginningIndexes; + } + + function _prepareSearch(search) { + if (typeof search === 'number') + search = '' + search; + else if (typeof search !== 'string') + search = ''; + search = search.trim(); + var info = _prepareLowerInfo(search); + + var spaceSearches = []; + if (info.containsSpace) { + var searches = search.split(/\s+/); + searches = [...new Set(searches)]; + for (var i = 0; i < searches.length; i++) { + if (searches[i] === '') + continue; + var _info = _prepareLowerInfo(searches[i]); + spaceSearches.push({ + lowerCodes: _info.lowerCodes, + _lower: searches[i].toLowerCase(), + containsSpace: false + }); + } + } + return { + lowerCodes: info.lowerCodes, + _lower: info._lower, + containsSpace: info.containsSpace, + bitflags: info.bitflags, + spaceSearches: spaceSearches + }; + } + + function _prepare(target) { + if (typeof target === 'number') + target = '' + target; + else if (typeof target !== 'string') + target = ''; + var info = _prepareLowerInfo(target); + return _new_result(target, { + _targetLower: info._lower, + _targetLowerCodes: info.lowerCodes, + _bitflags: info.bitflags + }); + } + + function _cleanup() { + _preparedCache.clear(); + _preparedSearchCache.clear(); + } + + function _isPrepared(x) { + return typeof x === 'object' && typeof x._bitflags === 'number'; + } + + function _getPrepared(target) { + if (target.length > 999) + return _prepare(target); + var targetPrepared = _preparedCache.get(target); + if (targetPrepared !== undefined) + return targetPrepared; + targetPrepared = _prepare(target); + _preparedCache.set(target, targetPrepared); + return targetPrepared; + } + + function _getPreparedSearch(search) { + if (search.length > 999) + return _prepareSearch(search); + var searchPrepared = _preparedSearchCache.get(search); + if (searchPrepared !== undefined) + return searchPrepared; + searchPrepared = _prepareSearch(search); + _preparedSearchCache.set(search, searchPrepared); + return searchPrepared; + } + + function _getValue(obj, prop) { + var tmp = obj[prop]; + if (tmp !== undefined) + return tmp; + if (typeof prop === 'function') + return prop(obj); + var segs = prop; + if (!Array.isArray(prop)) + segs = prop.split('.'); + var len = segs.length; + var i = -1; + while (obj && (++i < len)) + obj = obj[segs[i]]; + return obj; + } + + function _single(search, target) { + if (!search || !target) + return _NULL; + var preparedSearch = _getPreparedSearch(search); + if (!_isPrepared(target)) + target = _getPrepared(target); + var searchBitflags = preparedSearch.bitflags; + if ((searchBitflags & target._bitflags) !== searchBitflags) + return _NULL; + return _algorithm(preparedSearch, target); + } + + function _highlight(result, open, close) { + if (open === undefined) + open = ''; + if (close === undefined) + close = ''; + var callback = typeof open === 'function' ? open : undefined; + + var target = result.target; + var targetLen = target.length; + var indexes = result.indexes; + var highlighted = ''; + var matchI = 0; + var indexesI = 0; + var opened = false; + var parts = []; + + for (var i = 0; i < targetLen; ++i) { + var ch = target[i]; + if (indexes[indexesI] === i) { + ++indexesI; + if (!opened) { + opened = true; + if (callback) { + parts.push(highlighted); + highlighted = ''; + } else { + highlighted += open; + } + } + if (indexesI === indexes.length) { + if (callback) { + highlighted += ch; + parts.push(callback(highlighted, matchI++)); + highlighted = ''; + parts.push(target.substr(i + 1)); + } else { + highlighted += ch + close + target.substr(i + 1); + } + break; + } + } else { + if (opened) { + opened = false; + if (callback) { + parts.push(callback(highlighted, matchI++)); + highlighted = ''; + } else { + highlighted += close; + } + } + } + highlighted += ch; + } + return callback ? parts : highlighted; + } + + function _all(targets, options) { + var results = []; + results.total = targets.length; + var limit = options?.limit || _INFINITY; + + if (options?.key) { + for (var i = 0; i < targets.length; i++) { + var obj = targets[i]; + var target = _getValue(obj, options.key); + if (target == _NULL) + continue; + if (!_isPrepared(target)) + target = _getPrepared(target); + var result = _new_result(target.target, { + _score: target._score, + obj: obj + }); + results.push(result); + if (results.length >= limit) + return results; + } + } else if (options?.keys) { + for (var i = 0; i < targets.length; i++) { + var obj = targets[i]; + var objResults = _createKeysResult(options.keys.length); + for (var keyI = options.keys.length - 1; keyI >= 0; --keyI) { + var target = _getValue(obj, options.keys[keyI]); + if (!target) { + objResults[keyI] = _noTarget; + continue; + } + if (!_isPrepared(target)) + target = _getPrepared(target); + target._score = _NEGATIVE_INFINITY; + target._indexes.len = 0; + objResults[keyI] = target; + } + objResults.obj = obj; + objResults._score = _NEGATIVE_INFINITY; + results.push(objResults); + if (results.length >= limit) + return results; + } + } else { + for (var i = 0; i < targets.length; i++) { + var target = targets[i]; + if (target == _NULL) + continue; + if (!_isPrepared(target)) + target = _getPrepared(target); + target._score = _NEGATIVE_INFINITY; + target._indexes.len = 0; + results.push(target); + if (results.length >= limit) + return results; + } + } + return results; + } + + function _algorithm(preparedSearch, prepared, allowSpaces, allowPartialMatch) { + if (allowSpaces === undefined) + allowSpaces = false; + if (allowPartialMatch === undefined) + allowPartialMatch = false; + + if (allowSpaces === false && preparedSearch.containsSpace) + return _algorithmSpaces(preparedSearch, prepared, allowPartialMatch); + + var searchLower = preparedSearch._lower; + var searchLowerCodes = preparedSearch.lowerCodes; + var searchLowerCode = searchLowerCodes[0]; + var targetLowerCodes = prepared._targetLowerCodes; + var searchLen = searchLowerCodes.length; + var targetLen = targetLowerCodes.length; + var searchI = 0; + var targetI = 0; + var matchesSimpleLen = 0; + + for (; ; ) { + var isMatch = searchLowerCode === targetLowerCodes[targetI]; + if (isMatch) { + _matchesSimple[matchesSimpleLen++] = targetI; + ++searchI; + if (searchI === searchLen) + break; + searchLowerCode = searchLowerCodes[searchI]; + } + ++targetI; + if (targetI >= targetLen) + return _NULL; + } + + searchI = 0; + var successStrict = false; + var matchesStrictLen = 0; + + var nextBeginningIndexes = prepared._nextBeginningIndexes; + if (nextBeginningIndexes === _NULL) + nextBeginningIndexes = prepared._nextBeginningIndexes = _prepareNextBeginningIndexes(prepared.target); + targetI = _matchesSimple[0] === 0 ? 0 : nextBeginningIndexes[_matchesSimple[0] - 1]; + + var backtrackCount = 0; + if (targetI !== targetLen) + for (; ; ) { + if (targetI >= targetLen) { + if (searchI <= 0) + break; + ++backtrackCount; + if (backtrackCount > 200) + break; + --searchI; + var lastMatch = _matchesStrict[--matchesStrictLen]; + targetI = nextBeginningIndexes[lastMatch]; + } else { + var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]; + if (isMatch) { + _matchesStrict[matchesStrictLen++] = targetI; + ++searchI; + if (searchI === searchLen) { + successStrict = true; + break; + } + ++targetI; + } else { + targetI = nextBeginningIndexes[targetI]; + } + } + } + + var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, _matchesSimple[0]); + var isSubstring = !!~substringIndex; + var isSubstringBeginning = !isSubstring ? false : substringIndex === 0 || prepared._nextBeginningIndexes[substringIndex - 1] === substringIndex; + + if (isSubstring && !isSubstringBeginning) { + for (var i = 0; i < nextBeginningIndexes.length; i = nextBeginningIndexes[i]) { + if (i <= substringIndex) + continue; + for (var s = 0; s < searchLen; s++) + if (searchLowerCodes[s] !== prepared._targetLowerCodes[i + s]) + break; + if (s === searchLen) { + substringIndex = i; + isSubstringBeginning = true; + break; + } + } + } + + var calculateScore = function (matches) { + var score = 0; + var extraMatchGroupCount = 0; + for (var i = 1; i < searchLen; ++i) { + if (matches[i] - matches[i - 1] !== 1) { + score -= matches[i]; + ++extraMatchGroupCount; + } + } + var unmatchedDistance = matches[searchLen - 1] - matches[0] - (searchLen - 1); + score -= (12 + unmatchedDistance) * extraMatchGroupCount; + if (matches[0] !== 0) + score -= matches[0] * matches[0] * 0.2; + if (!successStrict) { + score *= 1000; + } else { + var uniqueBeginningIndexes = 1; + for (var i = nextBeginningIndexes[0]; i < targetLen; i = nextBeginningIndexes[i]) + ++uniqueBeginningIndexes; + if (uniqueBeginningIndexes > 24) + score *= (uniqueBeginningIndexes - 24) * 10; + } + score -= (targetLen - searchLen) / 2; + if (isSubstring) + score /= 1 + searchLen * searchLen * 1; + if (isSubstringBeginning) + score /= 1 + searchLen * searchLen * 1; + score -= (targetLen - searchLen) / 2; + return score; + }; + + var matchesBest, score; + if (!successStrict) { + if (isSubstring) + for (var i = 0; i < searchLen; ++i) + _matchesSimple[i] = substringIndex + i; + matchesBest = _matchesSimple; + score = calculateScore(matchesBest); + } else { + if (isSubstringBeginning) { + for (var i = 0; i < searchLen; ++i) + _matchesSimple[i] = substringIndex + i; + matchesBest = _matchesSimple; + score = calculateScore(_matchesSimple); + } else { + matchesBest = _matchesStrict; + score = calculateScore(_matchesStrict); + } + } + + prepared._score = score; + for (var i = 0; i < searchLen; ++i) + prepared._indexes[i] = matchesBest[i]; + prepared._indexes.len = searchLen; + + var result = _createResult(); + result.target = prepared.target; + result._score = prepared._score; + result._indexes = prepared._indexes; + return result; + } + + function _algorithmSpaces(preparedSearch, target, allowPartialMatch) { + var seen_indexes = new Set(); + var score = 0; + var result = _NULL; + + var first_seen_index_last_search = 0; + var searches = preparedSearch.spaceSearches; + var searchesLen = searches.length; + var changeslen = 0; + + var resetNextBeginningIndexes = function () { + for (let i = changeslen - 1; i >= 0; i--) + target._nextBeginningIndexes[_nextBeginningIndexesChanges[i * 2 + 0]] = _nextBeginningIndexesChanges[i * 2 + 1]; + }; + + var hasAtLeast1Match = false; + for (var i = 0; i < searchesLen; ++i) { + _allowPartialMatchScores[i] = _NEGATIVE_INFINITY; + var search = searches[i]; + result = _algorithm(search, target); + + if (allowPartialMatch) { + if (result === _NULL) + continue; + hasAtLeast1Match = true; + } else { + if (result === _NULL) { + resetNextBeginningIndexes(); + return _NULL; + } + } + + var isTheLastSearch = i === searchesLen - 1; + if (!isTheLastSearch) { + var indexes = result._indexes; + var indexesIsConsecutiveSubstring = true; + for (let j = 0; j < indexes.len - 1; j++) { + if (indexes[j + 1] - indexes[j] !== 1) { + indexesIsConsecutiveSubstring = false; + break; + } + } + + if (indexesIsConsecutiveSubstring) { + var newBeginningIndex = indexes[indexes.len - 1] + 1; + var toReplace = target._nextBeginningIndexes[newBeginningIndex - 1]; + for (let j = newBeginningIndex - 1; j >= 0; j--) { + if (toReplace !== target._nextBeginningIndexes[j]) + break; + target._nextBeginningIndexes[j] = newBeginningIndex; + _nextBeginningIndexesChanges[changeslen * 2 + 0] = j; + _nextBeginningIndexesChanges[changeslen * 2 + 1] = toReplace; + changeslen++; + } + } + } + + score += result._score / searchesLen; + _allowPartialMatchScores[i] = result._score / searchesLen; + + if (result._indexes[0] < first_seen_index_last_search) { + score -= (first_seen_index_last_search - result._indexes[0]) * 2; + } + first_seen_index_last_search = result._indexes[0]; + + for (var j = 0; j < result._indexes.len; ++j) + seen_indexes.add(result._indexes[j]); + } + + if (allowPartialMatch && !hasAtLeast1Match) + return _NULL; + + resetNextBeginningIndexes(); + + var allowSpacesResult = _algorithm(preparedSearch, target, true); + if (allowSpacesResult !== _NULL && allowSpacesResult._score > score) { + if (allowPartialMatch) { + for (var i = 0; i < searchesLen; ++i) { + _allowPartialMatchScores[i] = allowSpacesResult._score / searchesLen; + } + } + return allowSpacesResult; + } + + if (allowPartialMatch) + result = target; + result._score = score; + + var idx = 0; + for (let index of seen_indexes) + result._indexes[idx++] = index; + result._indexes.len = idx; + + return result; + } + + function _go(search, targets, options) { + if (!search) + return options?.all ? _all(targets, options) : _noResults; + + var preparedSearch = _getPreparedSearch(search); + var searchBitflags = preparedSearch.bitflags; + var containsSpace = preparedSearch.containsSpace; + + var threshold = _denormalizeScore(options?.threshold ?? 0.35); + var limit = options?.limit || _INFINITY; + + var resultsLen = 0; + var limitedCount = 0; + var targetsLen = targets.length; + + function push_result(result) { + if (resultsLen < limit) { + _q.add(result); + ++resultsLen; + } else { + ++limitedCount; + if (result._score > _q.peek()._score) + _q.replaceTop(result); + } + } + + if (options?.key) { + var key = options.key; + for (var i = 0; i < targetsLen; ++i) { + var obj = targets[i]; + var target = _getValue(obj, key); + if (!target) + continue; + if (!_isPrepared(target)) + target = _getPrepared(target); + if ((searchBitflags & target._bitflags) !== searchBitflags) + continue; + var result = _algorithm(preparedSearch, target); + if (result === _NULL) + continue; + if (result._score < threshold) + continue; + result.obj = obj; + push_result(result); + } + } else if (options?.keys) { + var keys = options.keys; + var keysLen = keys.length; + + outer: for (var i = 0; i < targetsLen; ++i) { + var obj = targets[i]; + var keysBitflags = 0; + for (var keyI = 0; keyI < keysLen; ++keyI) { + var key = keys[keyI]; + var target = _getValue(obj, key); + if (!target) { + _tmpTargets[keyI] = _noTarget; + continue; + } + if (!_isPrepared(target)) + target = _getPrepared(target); + _tmpTargets[keyI] = target; + keysBitflags |= target._bitflags; + } + + if ((searchBitflags & keysBitflags) !== searchBitflags) + continue; + + if (containsSpace) + for (let j = 0; j < preparedSearch.spaceSearches.length; j++) + _keysSpacesBestScores[j] = _NEGATIVE_INFINITY; + + for (var keyI = 0; keyI < keysLen; ++keyI) { + target = _tmpTargets[keyI]; + if (target === _noTarget) { + _tmpResults[keyI] = _noTarget; + continue; + } + + _tmpResults[keyI] = _algorithm(preparedSearch, target, false, containsSpace); + if (_tmpResults[keyI] === _NULL) { + _tmpResults[keyI] = _noTarget; + continue; + } + + if (containsSpace) + for (let j = 0; j < preparedSearch.spaceSearches.length; j++) { + if (_allowPartialMatchScores[j] > -1000) { + if (_keysSpacesBestScores[j] > _NEGATIVE_INFINITY) { + var tmp = (_keysSpacesBestScores[j] + _allowPartialMatchScores[j]) / 4; + if (tmp > _keysSpacesBestScores[j]) + _keysSpacesBestScores[j] = tmp; + } + } + if (_allowPartialMatchScores[j] > _keysSpacesBestScores[j]) + _keysSpacesBestScores[j] = _allowPartialMatchScores[j]; + } + } + + if (containsSpace) { + for (let j = 0; j < preparedSearch.spaceSearches.length; j++) + if (_keysSpacesBestScores[j] === _NEGATIVE_INFINITY) + continue outer; + } else { + var hasAtLeast1Match = false; + for (let j = 0; j < keysLen; j++) + if (_tmpResults[j]._score !== _NEGATIVE_INFINITY) { + hasAtLeast1Match = true; + break; + } + if (!hasAtLeast1Match) + continue; + } + + var objResults = _createKeysResult(keysLen); + for (let j = 0; j < keysLen; j++) + objResults[j] = _tmpResults[j]; + + var score; + if (containsSpace) { + score = 0; + for (let j = 0; j < preparedSearch.spaceSearches.length; j++) + score += _keysSpacesBestScores[j]; + } else { + score = _NEGATIVE_INFINITY; + for (let j = 0; j < keysLen; j++) { + var res = objResults[j]; + if (res._score > -1000) { + if (score > _NEGATIVE_INFINITY) { + var tmp = (score + res._score) / 4; + if (tmp > score) + score = tmp; + } + } + if (res._score > score) + score = res._score; + } + } + + objResults.obj = obj; + objResults._score = score; + + if (options?.scoreFn) { + score = options.scoreFn(objResults); + if (!score) + continue; + score = _denormalizeScore(score); + objResults._score = score; + } + + if (score < threshold) + continue; + push_result(objResults); + } + } else { + for (var i = 0; i < targetsLen; ++i) { + var target = targets[i]; + if (!target) + continue; + if (!_isPrepared(target)) + target = _getPrepared(target); + if ((searchBitflags & target._bitflags) !== searchBitflags) + continue; + var result = _algorithm(preparedSearch, target); + if (result === _NULL) + continue; + if (result._score < threshold) + continue; + push_result(result); + } + } + + if (resultsLen === 0) + return _noResults; + var results = new Array(resultsLen); + for (var i = resultsLen - 1; i >= 0; --i) + results[i] = _q.poll(); + results.total = resultsLen + limitedCount; + return results; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/I18n.qml b/fedora/.config/quickshell/noctalia-shell/Commons/I18n.qml new file mode 100644 index 0000000..8248adc --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/I18n.qml @@ -0,0 +1,373 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +Singleton { + id: root + + property bool isLoaded: false + property string langCode: "" + property var locale: Qt.locale() + property string systemDetectedLangCode: "" + property string fullLocaleCode: "" // Preserves regional locale variants + property var translations: ({}) + property var fallbackTranslations: ({}) + + // Static list of available translations — update when adding/removing translation files + property var availableLanguages: ["en", "en-GB", "cs", "de", "es", "fr", "hu", "it", "ja", "ko-KR", "ku", "nl", "nn-HN", "nn-NO", "pl", "pt", "ru", "sv", "tr", "uk-UA", "vi", "zh-CN", "zh-TW"] + + // Default date format per language (used by lock screen, etc.) + readonly property var dateFormats: ({ + "de": "dddd, d. MMMM", + "en": "dddd, MMMM d", + "es": "dddd, d 'de' MMMM", + "fr": "dddd d MMMM", + "hu": "dddd, MMMM d.", + "it": "dddd d MMMM", + "ja": "yyyy年M月d日 dddd", + "ko": "yyyy년 M월 d일 dddd", + "ku": "dddd, dê MMMM", + "nl": "dddd d MMMM", + "nn": "dddd d. MMMM", + "pl": "dddd, d MMMM", + "pt": "dddd, d 'de' MMMM", + "ru": "dddd, d MMMM", + "sv": "dddd d MMMM", + "tr": "dddd, d MMMM", + "uk": "dddd, d MMMM", + "vi": "dddd, d MMMM", + "zh": "yyyy年M月d日 dddd" + }) + + // Signals for reactive updates + signal languageChanged(string newLanguage) + signal translationsLoaded + + // FileView to load translation files + property FileView translationFile: FileView { + id: fileView + printErrors: false + watchChanges: true + onFileChanged: reload() + onLoaded: { + try { + var data = JSON.parse(text()); + root.translations = data; + Logger.i("I18n", `Loaded translations for "${root.langCode}"`); + + root.isLoaded = true; + root.translationsLoaded(); + + // Load English fallback for non-English languages (only after main file succeeds) + if (root.langCode !== "en") { + fallbackFileView.path = `file://${Quickshell.shellDir}/Assets/Translations/en.json`; + } + } catch (e) { + Logger.e("I18n", `Failed to parse translation file: ${e}`); + setLanguage("en"); + } + } + onLoadFailed: function (error) { + if (root.langCode === "en") { + Logger.e("I18n", `Failed to load English translation file: ${error}`); + // English also failed - still emit signal to unblock startup + root.isLoaded = true; + root.translationsLoaded(); + return; + } + + // Qt.callLater is needed because FileView doesn't re-trigger when path + // is changed inside its own onLoadFailed handler + var strippedCode = stripScript(root.langCode); + if (strippedCode !== root.langCode) { + Logger.d("I18n", `Translation file for "${root.langCode}" not found, trying "${strippedCode}"`); + root.langCode = strippedCode; + Qt.callLater(loadTranslations); + } else { + // Try language-only code (e.g. "zh-CN" → "zh") + var shortCode = root.langCode.substring(0, 2); + if (shortCode !== root.langCode) { + Logger.d("I18n", `Translation file for "${root.langCode}" not found, trying "${shortCode}"`); + root.langCode = shortCode; + Qt.callLater(loadTranslations); + } else { + Logger.w("I18n", `Translation file for "${root.langCode}" not found, falling back to English`); + root.langCode = "en"; + root.fullLocaleCode = "en"; + root.locale = Qt.locale("en"); + Qt.callLater(loadTranslations); + } + } + } + } + + // FileView to load fallback translation files + property FileView fallbackTranslationFile: FileView { + id: fallbackFileView + watchChanges: true + onFileChanged: reload() + onLoaded: { + try { + var data = JSON.parse(text()); + root.fallbackTranslations = data; + Logger.d("I18n", `Loaded english fallback translations`); + } catch (e) { + Logger.e("I18n", `Failed to parse fallback translation file: ${e}`); + } + } + onLoadFailed: function (error) { + Logger.e("I18n", `Failed to load fallback translation file: ${error}`); + } + } + + // Correct language when settings finish loading from disk (or user changes it) + Connections { + target: Settings.data.general + function onLanguageChanged() { + var userLang = Settings.data.general.language; + if (userLang !== "" && userLang !== root.langCode && availableLanguages.includes(userLang)) { + Logger.i("I18n", `Applying user language preference: "${userLang}"`); + setLanguage(userLang); + } else if (userLang === "" && root.systemDetectedLangCode !== "" && root.systemDetectedLangCode !== root.langCode) { + Logger.i("I18n", `Language reset to default, reverting to system language: "${root.systemDetectedLangCode}"`); + setLanguage(root.systemDetectedLangCode); + } + } + } + + Component.onCompleted: { + Logger.i("I18n", "Service started"); + + var lang = determineFastLanguage(); + langCode = lang.code; + fullLocaleCode = lang.fullLocale; + locale = Qt.locale(lang.fullLocale); + systemDetectedLangCode = lang.code; + Logger.i("I18n", `Loading "${lang.code}" (locale: "${lang.fullLocale}")`); + loadTranslations(); + } + + // Strip 4-letter script subtag from a BCP 47 tag (e.g. "fr-Latn-FR" → "fr-FR") + function stripScript(tag) { + var parts = tag.split("-"); + var result = []; + for (var i = 0; i < parts.length; i++) { + if (parts[i].length === 4 && /^[A-Za-z]{4}$/.test(parts[i])) { + continue; + } + result.push(parts[i]); + } + return result.join("-"); + } + + // Determine the best language match against availableLanguages + function determineFastLanguage() { + // User preference from Settings (defaults to "" if not yet loaded from disk) + var userLang = Settings.data.general.language; + if (userLang !== "" && availableLanguages.includes(userLang)) { + return { + code: userLang, + fullLocale: userLang + }; + } + + // Match system locale against available translations + for (var i = 0; i < Qt.locale().uiLanguages.length; i++) { + var fullLang = Qt.locale().uiLanguages[i]; + + // Exact match (e.g. "zh-CN") + if (availableLanguages.includes(fullLang)) { + return { + code: fullLang, + fullLocale: fullLang + }; + } + + // Script-stripped match (e.g. "zh-Hans-CN" → "zh-CN") + var stripped = stripScript(fullLang); + if (stripped !== fullLang && availableLanguages.includes(stripped)) { + return { + code: stripped, + fullLocale: fullLang + }; + } + + // Language-only match (e.g. "fr-FR" → "fr") + var short_ = fullLang.substring(0, 2); + if (availableLanguages.includes(short_)) { + return { + code: short_, + fullLocale: fullLang + }; + } + } + + return { + code: "en", + fullLocale: "en" + }; + } + + function dateFormat() { + var lang = langCode.split("-")[0]; + return dateFormats[lang] || "dddd, d MMMM"; + } + + // ------------------------------------------- + function setLanguage(newLangCode, fullLocale) { + if (typeof fullLocale === "undefined") { + fullLocale = newLangCode; + } + + if (newLangCode !== langCode && availableLanguages.includes(newLangCode)) { + langCode = newLangCode; + fullLocaleCode = fullLocale; + locale = Qt.locale(fullLocale); + Logger.i("I18n", `Language set to "${langCode}" with locale "${fullLocale}"`); + languageChanged(langCode); + loadTranslations(); + } else if (!availableLanguages.includes(newLangCode)) { + Logger.w("I18n", `Language "${newLangCode}" is not available`); + } + } + + // ------------------------------------------- + function loadTranslations() { + if (langCode === "") + return; + const filePath = `file://${Quickshell.shellDir}/Assets/Translations/${langCode}.json`; + fileView.path = filePath; + isLoaded = false; + } + + // ------------------------------------------- + // Check if a translation exists + function hasTranslation(key) { + if (!isLoaded) + return false; + + const keys = key.split("."); + var value = translations; + + for (var i = 0; i < keys.length; i++) { + if (value && typeof value === "object" && keys[i] in value) { + value = value[keys[i]]; + } else { + return false; + } + } + + return typeof value === "string"; + } + + // ------------------------------------------- + // Get all translation keys (useful for debugging) + function getAllKeys(obj, prefix) { + if (typeof obj === "undefined") + obj = translations; + if (typeof prefix === "undefined") + prefix = ""; + + var keys = []; + for (var key in (obj || {})) { + const value = obj[key]; + const fullKey = prefix ? `${prefix}.${key}` : key; + if (typeof value === "object" && value !== null) { + keys = keys.concat(getAllKeys(value, fullKey)); + } else if (typeof value === "string") { + keys.push(fullKey); + } + } + return keys; + } + + // ------------------------------------------- + // Reload translations (useful for development) + function reload() { + Logger.d("I18n", "Reloading translations"); + loadTranslations(); + } + + // ------------------------------------------- + // Main translation function + function tr(key, interpolations) { + if (typeof interpolations === "undefined") + interpolations = {}; + + if (!isLoaded) { + //Logger.d("I18n", "Translations not loaded yet") + return key; + } + + // Navigate nested keys (e.g., "menu.file.open") + const keys = key.split("."); + + // Look-up translation in the active language + var value = translations; + var notFound = false; + for (var i = 0; i < keys.length; i++) { + if (value && typeof value === "object" && keys[i] in value) { + value = value[keys[i]]; + } else { + Logger.d("I18n", `Translation key "${key}" not found at part "${keys[i]}"`); + Logger.d("I18n", `Available keys: ${Object.keys(value || {}).join(", ")}`); + notFound = true; + break; + } + } + + // Fallback to english if not found + if (notFound && availableLanguages.includes("en") && langCode !== "en") { + value = fallbackTranslations; + for (var i = 0; i < keys.length; i++) { + if (value && typeof value === "object" && keys[i] in value) { + value = value[keys[i]]; + } else { + // Indicate this key does not even exists in the english fallback + return `!!${key}!!`; + } + } + } else if (notFound) { + // No fallback available + return `!!${key}!!`; + } + + if (typeof value !== "string") { + Logger.d("I18n", `Translation key "${key}" is not a string`); + return key; + } + + // Handle interpolations (e.g., "Hello {name}!") + var result = value; + for (var placeholder in interpolations) { + const regex = new RegExp(`\\{${placeholder}\\}`, 'g'); + result = result.replace(regex, interpolations[placeholder]); + } + + return result; + } + + // ------------------------------------------- + // Plural translation function + function trp(key, count, interpolations) { + if (typeof interpolations === "undefined") { + interpolations = {}; + } + + // Use key for singular, key-plural for plural + const realKey = count === 1 ? key : `${key}-plural`; + + // Merge interpolations with count + var finalInterpolations = { + "count": count + }; + for (var prop in interpolations) { + finalInterpolations[prop] = interpolations[prop]; + } + + return tr(realKey, finalInterpolations); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Icons.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Icons.qml new file mode 100644 index 0000000..1e8752d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Icons.qml @@ -0,0 +1,85 @@ +pragma Singleton + +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Commons + +Singleton { + id: root + + // Expose the font family name for easy access + readonly property string fontFamily: currentFontLoader ? currentFontLoader.name : "" + readonly property string defaultIcon: IconsTabler.defaultIcon + readonly property var icons: IconsTabler.icons + readonly property var aliases: IconsTabler.aliases + readonly property string fontPath: "/Assets/Fonts/tabler/noctalia-tabler-icons.ttf" + + // Current active font loader + property FontLoader currentFontLoader: null + property int fontVersion: 0 + + // Create a unique cache-busting path + readonly property string cacheBustingPath: Quickshell.shellDir + fontPath + "?v=" + fontVersion + "&t=" + Date.now() + + // Signal emitted when font is reloaded + signal fontReloaded + + Component.onCompleted: { + Logger.i("Icons", "Service started"); + loadFontWithCacheBusting(); + } + + Connections { + target: Quickshell + function onReloadCompleted() { + Logger.d("Icons", "Quickshell reload completed - forcing font reload"); + reloadFont(); + } + } + + // --------------------------------------- + function get(iconName) { + // Check in aliases first + if (aliases[iconName] !== undefined) { + iconName = aliases[iconName]; + } + + // Find the appropriate codepoint + return icons[iconName]; + } + + function loadFontWithCacheBusting() { + Logger.d("Icons", "Loading font with cache busting"); + + // Destroy old loader first + if (currentFontLoader) { + currentFontLoader.destroy(); + currentFontLoader = null; + } + + // Create new loader with cache-busting URL + currentFontLoader = Qt.createQmlObject(` + import QtQuick + FontLoader { + source: "${cacheBustingPath}" + } + `, root, "dynamicFontLoader_" + fontVersion); + + // Connect to the new loader's status changes + currentFontLoader.statusChanged.connect(function () { + if (currentFontLoader.status === FontLoader.Ready) { + Logger.d("Icons", "Font loaded successfully:", currentFontLoader.name, "(version " + fontVersion + ")"); + fontReloaded(); + } else if (currentFontLoader.status === FontLoader.Error) { + Logger.e("Icons", "Font failed to load (version " + fontVersion + ")"); + } + }); + } + + function reloadFont() { + Logger.d("Icons", "Forcing font reload..."); + fontVersion++; + loadFontWithCacheBusting(); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/IconsTabler.qml b/fedora/.config/quickshell/noctalia-shell/Commons/IconsTabler.qml new file mode 100644 index 0000000..5efc5f5 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/IconsTabler.qml @@ -0,0 +1,6204 @@ +pragma Singleton + +import QtQuick +import Quickshell + +Singleton { + id: root + + readonly property string defaultIcon: "skull" + + readonly property var aliases: { + "close": "x", + "check": "check", + "settings": "settings", + "refresh": "refresh", + "add": "plus", + "trash": "trash", + "menu": "menu-2", + "person": "user", + "folder-open": "folder-open", + "download": "download", + "toast-notice": "circle-check", + "toast-warning": "alert-circle", + "toast-error": "circle-x", + "question-mark": "question-mark", + "search": "search", + "warning": "exclamation-circle", + "stop": "player-stop-filled", + "busy": "hourglass-empty", + "media-pause": "player-pause-filled", + "media-play": "player-play-filled", + "media-prev": "player-skip-back-filled", + "media-next": "player-skip-forward-filled", + "download-speed": "download", + "upload-speed": "upload", + "cpu-intensive": "alert-octagon", + "cpu-usage": "brand-speedtest", + "cpu-temperature": "flame", + "gpu-temperature": "device-desktop", + "memory": "cpu", + "performance": "gauge", + "balanced": "scale", + "powersaver": "leaf", + "storage": "database", + "keyboard": "keyboard", + "shutdown": "power", + "lock": "lock", + "lock-pause": "lock-pause", + "logout": "logout", + "reboot": "refresh", + "suspend": "player-pause", + "hibernate": "zzz", + "nightlight-on": "moon", + "nightlight-off": "moon-off", + "nightlight-forced": "moon-stars", + "bell": "bell", + "bell-off": "bell-off", + "keep-awake-on": "mug", + "keep-awake-off": "mug-off", + "disc": "disc-filled", + "eye": "eye", + "pin": "pin", + "unpin": "pinned-off", + "image": "photo", + "dark-mode": "contrast-filled", + "camera-video": "video", + "wallpaper-selector": "library-photo", + "color-picker": "color-picker", + "chevron-left": "chevron-left", + "chevron-right": "chevron-right", + "chevron-up": "chevron-up", + "chevron-down": "chevron-down", + "caret-up": "caret-up-filled", + "caret-down": "caret-down-filled", + "caret-left": "caret-left-filled", + "caret-right": "caret-right-filled", + "star": "star", + "star-off": "star-off", + "battery-exclamation": "battery-exclamation", + "battery-charging": "battery-charging", + "battery-charging-2": "battery-charging-2", + "battery-4": "battery-4", + "battery-3": "battery-3", + "battery-2": "battery-2", + "battery-1": "battery-1", + "battery": "battery", + "battery-off": "battery-off", + "wifi-0": "wifi-0", + "wifi-1": "wifi-1", + "wifi-2": "wifi-2", + "wifi-off": "wifi-off", + "wifi": "wifi", + "microphone": "microphone", + "microphone-mute": "microphone-off", + "volume-mute": "volume-off", + "volume-x": "volume-3", + "volume-zero": "volume-3", + "volume-low": "volume-2", + "volume-high": "volume", + "weather-sun": "sun", + "weather-moon": "moon", + "weather-moon-stars": "moon-stars", + "weather-cloud-off": "cloud-off", + "weather-cloud": "cloud", + "weather-cloud-haze": "cloud-fog", + "weather-cloud-lightning": "cloud-bolt", + "weather-cloud-rain": "cloud-rain", + "weather-cloud-snow": "cloud-snow", + "weather-cloud-sun": "cloud-sun", + "brightness-low": "brightness-down-filled", + "brightness-high": "brightness-up-filled", + "settings-general": "adjustments-horizontal", + "settings-bar": "crop-16-9", + "settings-user-interface": "layout-board", + "settings-control-center": "adjustments-horizontal", + "settings-dock": "layout-bottombar", + "settings-launcher": "rocket", + "settings-audio": "device-speaker", + "settings-display": "device-desktop", + "settings-network": "circles-relation", + "settings-brightness": "brightness-up", + "settings-location": "world-pin", + "settings-color-scheme": "palette", + "settings-wallpaper": "paint", + "settings-wallpaper-selector": "library-photo", + "settings-hooks": "link", + "settings-notifications": "bell", + "settings-osd": "picture-in-picture", + "settings-about": "info-square-rounded", + "settings-idle": "moon", + "settings-lock-screen": "lock", + "settings-session-menu": "power", + "settings-system-monitor": "activity", + "bluetooth": "bluetooth", + "bt-device-generic": "bluetooth", + "bt-device-gamepad": "device-gamepad-2", + "bt-device-microphone": "microphone", + "bt-device-headset": "headset", + "bt-device-earbuds": "device-airpods", + "bt-device-headphones": "headphones", + "bt-device-mouse": "mouse-2", + "bt-device-keyboard": "bluetooth", + "bt-device-phone": "device-mobile", + "bt-device-watch": "device-watch", + "bt-device-speaker": "device-speaker", + "bt-device-tv": "device-tv", + "antenna-bars-1": "antenna-bars-1", + "antenna-bars-2": "antenna-bars-2", + "antenna-bars-3": "antenna-bars-3", + "antenna-bars-4": "antenna-bars-4", + "antenna-bars-5": "antenna-bars-5", + "antenna-bars-off": "antenna-bars-off", + "noctalia": "noctalia", + "hyprland": "hyprland", + "filepicker-folder": "folder", + "filepicker-refresh": "refresh", + "filepicker-close": "x", + "filepicker-arrow-left": "arrow-left", + "filepicker-arrow-up": "arrow-up", + "filepicker-home": "home", + "filepicker-layout-grid": "layout-grid", + "filepicker-list": "list", + "filepicker-search": "search", + "filepicker-x": "x", + "filepicker-photo": "photo", + "filepicker-check": "check", + "filepicker-file-text": "file-text", + "filepicker-video": "video", + "filepicker-music": "music", + "filepicker-archive": "archive", + "filepicker-table": "table", + "filepicker-presentation": "presentation", + "filepicker-code": "code", + "filepicker-settings": "settings", + "filepicker-file": "file", + "filepicker-text": "file-text", + "filepicker-eye": "eye", + "filepicker-eye-off": "eye-off", + "filepicker-folder-current": "checks", + "plugin": "plug-connected", + "info": "file-description", + "official-plugin": "shield-filled" + } + + // Fonts Codepoints - do not change! + // Some icons have been disabled because Qt's text rendering engine recognizes + // some ranges as special Unicode characters at a very low level. + // ex: fe00-fe2f + readonly property var icons: { + "123": "\u{f554}", + "360": "\u{f62f}", + "12-hours": "\u{fc53}", + "24-hours": "\u{f5e7}", + "2fa": "\u{eca0}", + "360-view": "\u{f566}", + "3d-cube-sphere": "\u{ecd7}", + "3d-cube-sphere-off": "\u{f3b5}", + "3d-rotate": "\u{f020}", + "a-b": "\u{ec36}", + "a-b-2": "\u{f25f}", + "a-b-off": "\u{f0a6}", + "abacus": "\u{f05c}", + "abacus-off": "\u{f3b6}", + "abc": "\u{f567}", + "access-point": "\u{ed1b}", + "access-point-off": "\u{ed1a}", + "accessible": "\u{eba9}", + "accessible-filled": "\u{f6ea}", + "accessible-off": "\u{f0a7}", + "activity": "\u{ed23}", + "activity-heartbeat": "\u{f0db}", + "ad": "\u{ea02}", + "ad-2": "\u{ef1f}", + "ad-circle": "\u{f79e}", + "ad-circle-filled": "\u{f7d3}", + "ad-circle-off": "\u{f79d}", + "ad-filled": "\u{f6eb}", + "ad-off": "\u{f3b7}", + "address-book": "\u{f021}", + "address-book-off": "\u{f3b8}", + "adjustments": "\u{ea03}", + "adjustments-alt": "\u{ec37}", + "adjustments-bolt": "\u{f7fb}", + "adjustments-cancel": "\u{f7fc}", + "adjustments-check": "\u{f7fd}", + "adjustments-code": "\u{f7fe}", + "adjustments-cog": "\u{f7ff}", + "adjustments-dollar": "\u{f800}", + "adjustments-down": "\u{f801}", + "adjustments-exclamation": "\u{f802}", + "adjustments-filled": "\u{f6ec}", + "adjustments-heart": "\u{f803}", + "adjustments-horizontal": "\u{ec38}", + "adjustments-minus": "\u{f804}", + "adjustments-off": "\u{f0a8}", + "adjustments-pause": "\u{f805}", + "adjustments-pin": "\u{f806}", + "adjustments-plus": "\u{f807}", + "adjustments-question": "\u{f808}", + "adjustments-search": "\u{f809}", + "adjustments-share": "\u{f80a}", + "adjustments-spark": "\u{ffbe}", + "adjustments-star": "\u{f80b}", + "adjustments-up": "\u{f80c}", + "adjustments-x": "\u{f80d}", + "aerial-lift": "\u{edfe}", + "aerial-lift-filled": "\u{10101}", + "affiliate": "\u{edff}", + "affiliate-filled": "\u{f6ed}", + "ai": "\u{fee7}", + "air-balloon": "\u{f4a6}", + "air-balloon-filled": "\u{10100}", + "air-conditioning": "\u{f3a2}", + "air-conditioning-disabled": "\u{f542}", + "air-traffic-control": "\u{fb01}", + "alarm": "\u{ea04}", + "alarm-average": "\u{fc9e}", + "alarm-filled": "\u{f709}", + "alarm-minus": "\u{f630}", + "alarm-minus-filled": "\u{f70a}", + "alarm-off": "\u{f0a9}", + "alarm-plus": "\u{f631}", + "alarm-plus-filled": "\u{f70b}", + "alarm-smoke": "\u{100b6}", + "alarm-snooze": "\u{f632}", + "alarm-snooze-filled": "\u{f70c}", + "album": "\u{f022}", + "album-off": "\u{f3b9}", + "alert-circle": "\u{ea05}", + "alert-circle-filled": "\u{f6ee}", + "alert-circle-off": "\u{fc65}", + "alert-hexagon": "\u{f80e}", + "alert-hexagon-filled": "\u{fa34}", + "alert-hexagon-off": "\u{fc66}", + "alert-octagon": "\u{ecc6}", + "alert-octagon-filled": "\u{f6ef}", + "alert-small": "\u{f80f}", + "alert-small-off": "\u{fc67}", + "alert-square": "\u{f811}", + "alert-square-filled": "\u{fa35}", + "alert-square-rounded": "\u{f810}", + "alert-square-rounded-filled": "\u{fa36}", + "alert-square-rounded-off": "\u{fc68}", + "alert-triangle": "\u{ea06}", + "alert-triangle-filled": "\u{f6f0}", + "alert-triangle-off": "\u{fc69}", + "alien": "\u{ebde}", + "alien-filled": "\u{f70d}", + "align-box-bottom-center": "\u{f530}", + "align-box-bottom-center-filled": "\u{f70e}", + "align-box-bottom-left": "\u{f531}", + "align-box-bottom-left-filled": "\u{f70f}", + "align-box-bottom-right": "\u{f532}", + "align-box-bottom-right-filled": "\u{f710}", + "align-box-center-bottom": "\u{facb}", + "align-box-center-middle": "\u{f79f}", + "align-box-center-middle-filled": "\u{f7d4}", + "align-box-center-stretch": "\u{facc}", + "align-box-center-top": "\u{facd}", + "align-box-left-bottom": "\u{f533}", + "align-box-left-bottom-filled": "\u{f711}", + "align-box-left-middle": "\u{f534}", + "align-box-left-middle-filled": "\u{f712}", + "align-box-left-stretch": "\u{face}", + "align-box-left-top": "\u{f535}", + "align-box-left-top-filled": "\u{f713}", + "align-box-right-bottom": "\u{f536}", + "align-box-right-bottom-filled": "\u{f714}", + "align-box-right-middle": "\u{f537}", + "align-box-right-middle-filled": "\u{f7d5}", + "align-box-right-stretch": "\u{facf}", + "align-box-right-top": "\u{f538}", + "align-box-right-top-filled": "\u{f715}", + "align-box-top-center": "\u{f539}", + "align-box-top-center-filled": "\u{f716}", + "align-box-top-left": "\u{f53a}", + "align-box-top-left-filled": "\u{f717}", + "align-box-top-right": "\u{f53b}", + "align-box-top-right-filled": "\u{f718}", + "align-center": "\u{ea07}", + "align-justified": "\u{ea08}", + "align-left": "\u{ea09}", + "align-left-2": "\u{ff00}", + "align-right": "\u{ea0a}", + //"align-right-2": "\u{feff}", + "alpha": "\u{f543}", + "alphabet-arabic": "\u{ff2f}", + "alphabet-bangla": "\u{ff2e}", + "alphabet-cyrillic": "\u{f1df}", + "alphabet-greek": "\u{f1e0}", + "alphabet-hebrew": "\u{ff2d}", + "alphabet-korean": "\u{ff2c}", + "alphabet-latin": "\u{f1e1}", + "alphabet-thai": "\u{ff2b}", + "alt": "\u{fc54}", + "ambulance": "\u{ebf5}", + "ampersand": "\u{f229}", + "analyze": "\u{f3a3}", + "analyze-filled": "\u{f719}", + "analyze-off": "\u{f3ba}", + "anchor": "\u{eb76}", + "anchor-off": "\u{f0f7}", + "angle": "\u{ef20}", + "ankh": "\u{f1cd}", + "antenna": "\u{f094}", + "antenna-bars-1": "\u{ecc7}", + "antenna-bars-2": "\u{ecc8}", + "antenna-bars-3": "\u{ecc9}", + "antenna-bars-4": "\u{ecca}", + "antenna-bars-5": "\u{eccb}", + "antenna-bars-off": "\u{f0aa}", + "antenna-off": "\u{f3bb}", + "aperture": "\u{eb58}", + "aperture-off": "\u{f3bc}", + "api": "\u{effd}", + "api-app": "\u{effc}", + "api-app-off": "\u{f0ab}", + "api-off": "\u{f0f8}", + "app-window": "\u{efe6}", + "app-window-filled": "\u{f71a}", + "apple": "\u{ef21}", + "apple-filled": "\u{10017}", + "apps": "\u{ebb6}", + "apps-filled": "\u{f6f1}", + "apps-off": "\u{f0ac}", + "archery-arrow": "\u{fc55}", + "archive": "\u{ea0b}", + "archive-filled": "\u{fa82}", + "archive-off": "\u{f0ad}", + "armchair": "\u{ef9e}", + "armchair-2": "\u{efe7}", + "armchair-2-off": "\u{f3bd}", + "armchair-off": "\u{f3be}", + "arrow-autofit-content": "\u{ef31}", + "arrow-autofit-content-filled": "\u{f6f2}", + "arrow-autofit-down": "\u{ef32}", + "arrow-autofit-down-filled": "\u{10113}", + "arrow-autofit-height": "\u{ef33}", + "arrow-autofit-height-filled": "\u{10112}", + "arrow-autofit-left": "\u{ef34}", + "arrow-autofit-left-filled": "\u{10111}", + "arrow-autofit-right": "\u{ef35}", + "arrow-autofit-right-filled": "\u{10110}", + "arrow-autofit-up": "\u{ef36}", + "arrow-autofit-up-filled": "\u{1010f}", + "arrow-autofit-width": "\u{ef37}", + "arrow-autofit-width-filled": "\u{1010e}", + "arrow-back": "\u{ea0c}", + "arrow-back-up": "\u{eb77}", + "arrow-back-up-double": "\u{f9ec}", + "arrow-badge-down": "\u{f60b}", + "arrow-badge-down-filled": "\u{f7d6}", + "arrow-badge-left": "\u{f60c}", + "arrow-badge-left-filled": "\u{f7d7}", + "arrow-badge-right": "\u{f60d}", + "arrow-badge-right-filled": "\u{f7d8}", + "arrow-badge-up": "\u{f60e}", + "arrow-badge-up-filled": "\u{f7d9}", + "arrow-bar-both": "\u{fadd}", + "arrow-bar-down": "\u{ea0d}", + "arrow-bar-left": "\u{ea0e}", + "arrow-bar-right": "\u{ea0f}", + "arrow-bar-to-down": "\u{ec88}", + "arrow-bar-to-down-dashed": "\u{10164}", + "arrow-bar-to-left": "\u{ec89}", + "arrow-bar-to-left-dashed": "\u{10163}", + "arrow-bar-to-right": "\u{ec8a}", + "arrow-bar-to-right-dashed": "\u{10162}", + "arrow-bar-to-up": "\u{ec8b}", + "arrow-bar-to-up-dashed": "\u{10161}", + "arrow-bar-up": "\u{ea10}", + "arrow-bear-left": "\u{f045}", + "arrow-bear-left-2": "\u{f044}", + "arrow-bear-right": "\u{f047}", + "arrow-bear-right-2": "\u{f046}", + "arrow-big-down": "\u{edda}", + "arrow-big-down-filled": "\u{f6c6}", + "arrow-big-down-line": "\u{efe8}", + "arrow-big-down-line-filled": "\u{f6c7}", + "arrow-big-down-lines": "\u{efe9}", + "arrow-big-down-lines-filled": "\u{f6c8}", + "arrow-big-left": "\u{eddb}", + "arrow-big-left-filled": "\u{f6c9}", + "arrow-big-left-line": "\u{efea}", + "arrow-big-left-line-filled": "\u{f6ca}", + "arrow-big-left-lines": "\u{efeb}", + "arrow-big-left-lines-filled": "\u{f6cb}", + "arrow-big-right": "\u{eddc}", + "arrow-big-right-filled": "\u{f6cc}", + "arrow-big-right-line": "\u{efec}", + "arrow-big-right-line-filled": "\u{f6cd}", + "arrow-big-right-lines": "\u{efed}", + "arrow-big-right-lines-filled": "\u{f6ce}", + "arrow-big-up": "\u{eddd}", + "arrow-big-up-filled": "\u{f6cf}", + "arrow-big-up-line": "\u{efee}", + "arrow-big-up-line-filled": "\u{f6d0}", + "arrow-big-up-lines": "\u{efef}", + "arrow-big-up-lines-filled": "\u{f6d1}", + "arrow-bounce": "\u{f3a4}", + "arrow-capsule": "\u{fade}", + "arrow-curve-left": "\u{f048}", + "arrow-curve-right": "\u{f049}", + "arrow-down": "\u{ea16}", + "arrow-down-bar": "\u{ed98}", + "arrow-down-circle": "\u{ea11}", + "arrow-down-circle-filled": "\u{1003b}", + "arrow-down-dashed": "\u{1006a}", + "arrow-down-from-arc": "\u{fd86}", + "arrow-down-left": "\u{ea13}", + "arrow-down-left-circle": "\u{ea12}", + "arrow-down-rhombus": "\u{f61d}", + "arrow-down-rhombus-filled": "\u{1003a}", + "arrow-down-right": "\u{ea15}", + "arrow-down-right-circle": "\u{ea14}", + "arrow-down-square": "\u{ed9a}", + "arrow-down-square-filled": "\u{10039}", + "arrow-down-tail": "\u{ed9b}", + "arrow-down-to-arc": "\u{fd87}", + "arrow-elbow-left": "\u{f9ed}", + "arrow-elbow-right": "\u{f9ee}", + "arrow-fork": "\u{f04a}", + "arrow-forward": "\u{ea17}", + "arrow-forward-up": "\u{eb78}", + "arrow-forward-up-double": "\u{f9ef}", + "arrow-guide": "\u{f22a}", + "arrow-guide-filled": "\u{10038}", + "arrow-iteration": "\u{f578}", + "arrow-left": "\u{ea19}", + "arrow-left-bar": "\u{ed9c}", + "arrow-left-circle": "\u{ea18}", + "arrow-left-circle-filled": "\u{10037}", + "arrow-left-dashed": "\u{10069}", + "arrow-left-from-arc": "\u{fd88}", + "arrow-left-rhombus": "\u{f61e}", + "arrow-left-rhombus-filled": "\u{10036}", + "arrow-left-right": "\u{f04b}", + "arrow-left-square": "\u{ed9d}", + "arrow-left-square-filled": "\u{10035}", + "arrow-left-tail": "\u{ed9e}", + "arrow-left-to-arc": "\u{fd89}", + "arrow-loop-left": "\u{ed9f}", + "arrow-loop-left-2": "\u{f04c}", + "arrow-loop-right": "\u{eda0}", + "arrow-loop-right-2": "\u{f04d}", + "arrow-merge": "\u{f04e}", + "arrow-merge-alt-left": "\u{fc9f}", + "arrow-merge-alt-right": "\u{fca0}", + "arrow-merge-both": "\u{f23b}", + "arrow-merge-left": "\u{f23c}", + "arrow-merge-right": "\u{f23d}", + "arrow-move-down": "\u{f2ba}", + "arrow-move-down-filled": "\u{10034}", + "arrow-move-left": "\u{f2bb}", + "arrow-move-left-filled": "\u{10033}", + "arrow-move-right": "\u{f2bc}", + "arrow-move-right-filled": "\u{10032}", + "arrow-move-up": "\u{f2bd}", + "arrow-move-up-filled": "\u{10031}", + "arrow-narrow-down": "\u{ea1a}", + "arrow-narrow-down-dashed": "\u{10068}", + "arrow-narrow-left": "\u{ea1b}", + "arrow-narrow-left-dashed": "\u{10067}", + "arrow-narrow-right": "\u{ea1c}", + "arrow-narrow-right-dashed": "\u{10066}", + "arrow-narrow-up": "\u{ea1d}", + "arrow-narrow-up-dashed": "\u{10065}", + "arrow-ramp-left": "\u{ed3c}", + "arrow-ramp-left-2": "\u{f04f}", + "arrow-ramp-left-3": "\u{f050}", + "arrow-ramp-right": "\u{ed3d}", + "arrow-ramp-right-2": "\u{f051}", + "arrow-ramp-right-3": "\u{f052}", + "arrow-right": "\u{ea1f}", + "arrow-right-bar": "\u{eda1}", + "arrow-right-circle": "\u{ea1e}", + "arrow-right-circle-filled": "\u{10030}", + "arrow-right-dashed": "\u{10064}", + "arrow-right-from-arc": "\u{fd8a}", + "arrow-right-rhombus": "\u{f61f}", + "arrow-right-rhombus-filled": "\u{1002f}", + "arrow-right-square": "\u{eda2}", + "arrow-right-square-filled": "\u{1002e}", + "arrow-right-tail": "\u{eda3}", + "arrow-right-to-arc": "\u{fd8b}", + "arrow-rotary-first-left": "\u{f053}", + "arrow-rotary-first-right": "\u{f054}", + "arrow-rotary-last-left": "\u{f055}", + "arrow-rotary-last-right": "\u{f056}", + "arrow-rotary-left": "\u{f057}", + "arrow-rotary-right": "\u{f058}", + "arrow-rotary-straight": "\u{f059}", + "arrow-roundabout-left": "\u{f22b}", + "arrow-roundabout-right": "\u{f22c}", + "arrow-sharp-turn-left": "\u{f05a}", + "arrow-sharp-turn-right": "\u{f05b}", + "arrow-up": "\u{ea25}", + "arrow-up-bar": "\u{eda4}", + "arrow-up-circle": "\u{ea20}", + "arrow-up-circle-filled": "\u{1002d}", + "arrow-up-dashed": "\u{10063}", + "arrow-up-from-arc": "\u{fd8c}", + "arrow-up-left": "\u{ea22}", + "arrow-up-left-circle": "\u{ea21}", + "arrow-up-rhombus": "\u{f620}", + "arrow-up-rhombus-filled": "\u{1002c}", + "arrow-up-right": "\u{ea24}", + "arrow-up-right-circle": "\u{ea23}", + "arrow-up-square": "\u{eda6}", + "arrow-up-square-filled": "\u{1002b}", + "arrow-up-tail": "\u{eda7}", + "arrow-up-to-arc": "\u{fd8d}", + "arrow-wave-left-down": "\u{eda8}", + "arrow-wave-left-up": "\u{eda9}", + "arrow-wave-right-down": "\u{edaa}", + "arrow-wave-right-up": "\u{edab}", + "arrow-zig-zag": "\u{f4a7}", + "arrows-cross": "\u{effe}", + "arrows-diagonal": "\u{ea27}", + "arrows-diagonal-2": "\u{ea26}", + "arrows-diagonal-minimize": "\u{ef39}", + "arrows-diagonal-minimize-2": "\u{ef38}", + "arrows-diff": "\u{f296}", + "arrows-double-ne-sw": "\u{edde}", + "arrows-double-nw-se": "\u{eddf}", + "arrows-double-se-nw": "\u{ede0}", + "arrows-double-sw-ne": "\u{ede1}", + "arrows-down": "\u{edad}", + "arrows-down-up": "\u{edac}", + "arrows-exchange": "\u{f1f4}", + "arrows-exchange-2": "\u{f1f3}", + "arrows-horizontal": "\u{eb59}", + "arrows-join": "\u{edaf}", + "arrows-join-2": "\u{edae}", + "arrows-left": "\u{edb1}", + "arrows-left-down": "\u{ee00}", + "arrows-left-right": "\u{edb0}", + "arrows-maximize": "\u{ea28}", + "arrows-minimize": "\u{ea29}", + "arrows-move": "\u{f22f}", + "arrows-move-horizontal": "\u{f22d}", + "arrows-move-vertical": "\u{f22e}", + "arrows-random": "\u{f095}", + "arrows-right": "\u{edb3}", + "arrows-right-down": "\u{ee01}", + "arrows-right-left": "\u{edb2}", + "arrows-shuffle": "\u{f000}", + "arrows-shuffle-2": "\u{efff}", + "arrows-sort": "\u{eb5a}", + "arrows-split": "\u{edb5}", + "arrows-split-2": "\u{edb4}", + "arrows-transfer-down": "\u{f2cc}", + "arrows-transfer-up": "\u{f2cd}", + "arrows-transfer-up-down": "\u{ffac}", + "arrows-up": "\u{edb7}", + "arrows-up-down": "\u{edb6}", + "arrows-up-left": "\u{ee02}", + "arrows-up-right": "\u{ee03}", + "arrows-vertical": "\u{eb5b}", + "artboard": "\u{ea2a}", + "artboard-filled": "\u{fa83}", + "artboard-off": "\u{f0ae}", + "article": "\u{f1e2}", + "article-filled": "\u{f7da}", + "article-off": "\u{f3bf}", + "aspect-ratio": "\u{ed30}", + "aspect-ratio-filled": "\u{f7db}", + "aspect-ratio-off": "\u{f0af}", + "assembly": "\u{f24d}", + "assembly-filled": "\u{fe9e}", + "assembly-off": "\u{f3c0}", + "asset": "\u{f1ce}", + "asset-filled": "\u{fe9d}", + "asterisk": "\u{efd5}", + "asterisk-simple": "\u{efd4}", + "at": "\u{ea2b}", + "at-off": "\u{f0b0}", + "atom": "\u{eb79}", + "atom-2": "\u{ebdf}", + "atom-2-filled": "\u{f71b}", + "atom-off": "\u{f0f9}", + "augmented-reality": "\u{f023}", + "augmented-reality-2": "\u{f37e}", + "augmented-reality-off": "\u{f3c1}", + "auth-2fa": "\u{eca0}", + "automatic-gearbox": "\u{fc89}", + "automatic-gearbox-filled": "\u{1002a}", + "automation": "\u{fef8}", + "avocado": "\u{fd8e}", + "award": "\u{ea2c}", + "award-filled": "\u{f71c}", + "award-off": "\u{f0fa}", + "axe": "\u{ef9f}", + "axis-x": "\u{ef45}", + "axis-y": "\u{ef46}", + "baby-bottle": "\u{f5d2}", + "baby-carriage": "\u{f05d}", + "baby-carriage-filled": "\u{fe9c}", + "background": "\u{fd2c}", + "backhoe": "\u{ed86}", + "backpack": "\u{ef47}", + "backpack-off": "\u{f3c2}", + "backslash": "\u{fab9}", + "backspace": "\u{ea2d}", + "backspace-filled": "\u{f7dc}", + "badge": "\u{efc2}", + "badge-2k": "\u{100b5}", + "badge-3d": "\u{f555}", + "badge-3d-filled": "\u{fe9b}", + "badge-3k": "\u{100b4}", + "badge-4k": "\u{f556}", + "badge-4k-filled": "\u{fe9a}", + "badge-5k": "\u{100b3}", + "badge-8k": "\u{f557}", + "badge-8k-filled": "\u{fe99}", + "badge-ad": "\u{f558}", + "badge-ad-filled": "\u{fe98}", + "badge-ad-off": "\u{fd8f}", + "badge-ar": "\u{f559}", + "badge-ar-filled": "\u{fe97}", + "badge-cc": "\u{f55a}", + "badge-cc-filled": "\u{fe96}", + "badge-filled": "\u{f667}", + "badge-hd": "\u{f55b}", + "badge-hd-filled": "\u{fe95}", + "badge-off": "\u{f0fb}", + "badge-sd": "\u{f55c}", + "badge-sd-filled": "\u{fe94}", + "badge-tm": "\u{f55d}", + "badge-tm-filled": "\u{fe93}", + "badge-vo": "\u{f55e}", + "badge-vo-filled": "\u{fe92}", + "badge-vr": "\u{f55f}", + "badge-vr-filled": "\u{fe91}", + "badge-wc": "\u{f560}", + "badge-wc-filled": "\u{fe90}", + "badges": "\u{efc3}", + "badges-filled": "\u{f7dd}", + "badges-off": "\u{f0fc}", + "baguette": "\u{f3a5}", + "ball-american-football": "\u{ee04}", + "ball-american-football-off": "\u{f3c3}", + "ball-baseball": "\u{efa0}", + "ball-basketball": "\u{ec28}", + "ball-bowling": "\u{ec29}", + "ball-football": "\u{ee06}", + "ball-football-off": "\u{ee05}", + "ball-tennis": "\u{ec2a}", + "ball-volleyball": "\u{ec2b}", + "balloon": "\u{ef3a}", + "balloon-filled": "\u{fa84}", + "balloon-off": "\u{f0fd}", + "ballpen": "\u{f06e}", + "ballpen-filled": "\u{fa85}", + "ballpen-off": "\u{f0b1}", + "ban": "\u{ea2e}", + "bandage": "\u{eb7a}", + "bandage-filled": "\u{f7de}", + "bandage-off": "\u{f3c4}", + "barbell": "\u{eff0}", + "barbell-filled": "\u{fe8f}", + "barbell-off": "\u{f0b2}", + "barcode": "\u{ebc6}", + "barcode-off": "\u{f0b3}", + "barrel": "\u{f0b4}", + "barrel-off": "\u{f0fe}", + "barrier-block": "\u{f00e}", + "barrier-block-filled": "\u{fe8e}", + "barrier-block-off": "\u{f0b5}", + "baseline": "\u{f024}", + "baseline-density-large": "\u{f9f0}", + "baseline-density-medium": "\u{f9f1}", + "baseline-density-small": "\u{f9f2}", + "basket": "\u{ebe1}", + "basket-bolt": "\u{fb43}", + "basket-cancel": "\u{fb44}", + "basket-check": "\u{fb45}", + "basket-code": "\u{fb46}", + "basket-cog": "\u{fb47}", + "basket-discount": "\u{fb48}", + "basket-dollar": "\u{fb49}", + "basket-down": "\u{fb4a}", + "basket-exclamation": "\u{fb4b}", + "basket-filled": "\u{f7df}", + "basket-heart": "\u{fb4c}", + "basket-minus": "\u{fb4d}", + "basket-off": "\u{f0b6}", + "basket-pause": "\u{fb4e}", + "basket-pin": "\u{fb4f}", + "basket-plus": "\u{fb50}", + "basket-question": "\u{fb51}", + "basket-search": "\u{fb52}", + "basket-share": "\u{fb53}", + "basket-star": "\u{fb54}", + "basket-up": "\u{fb55}", + "basket-x": "\u{fb56}", + "bat": "\u{f284}", + "bath": "\u{ef48}", + "bath-filled": "\u{f71d}", + "bath-off": "\u{f0ff}", + "battery": "\u{ea34}", + "battery-1": "\u{ea2f}", + "battery-1-filled": "\u{f71e}", + "battery-2": "\u{ea30}", + "battery-2-filled": "\u{f71f}", + "battery-3": "\u{ea31}", + "battery-3-filled": "\u{f720}", + "battery-4": "\u{ea32}", + "battery-4-filled": "\u{f721}", + "battery-automotive": "\u{ee07}", + "battery-automotive-filled": "\u{10029}", + "battery-charging": "\u{ea33}", + "battery-charging-2": "\u{ef3b}", + "battery-eco": "\u{ef3c}", + "battery-exclamation": "\u{ff1d}", + "battery-filled": "\u{f668}", + "battery-off": "\u{ed1c}", + "battery-spark": "\u{ffbd}", + "battery-vertical": "\u{ff13}", + "battery-vertical-1": "\u{ff1c}", + "battery-vertical-1-filled": "\u{10028}", + "battery-vertical-2": "\u{ff1b}", + "battery-vertical-2-filled": "\u{10027}", + "battery-vertical-3": "\u{ff1a}", + "battery-vertical-3-filled": "\u{10026}", + "battery-vertical-4": "\u{ff19}", + "battery-vertical-4-filled": "\u{10025}", + "battery-vertical-charging": "\u{ff17}", + "battery-vertical-charging-2": "\u{ff18}", + "battery-vertical-eco": "\u{ff16}", + "battery-vertical-exclamation": "\u{ff15}", + "battery-vertical-filled": "\u{10024}", + "battery-vertical-off": "\u{ff14}", + "beach": "\u{ef3d}", + "beach-off": "\u{f0b7}", + "bed": "\u{eb5c}", + "bed-filled": "\u{f7e0}", + "bed-flat": "\u{fca1}", + "bed-flat-filled": "\u{fe8d}", + "bed-off": "\u{f100}", + "beer": "\u{efa1}", + "beer-filled": "\u{f7e1}", + "beer-off": "\u{f101}", + "bell": "\u{ea35}", + "bell-bolt": "\u{f812}", + "bell-cancel": "\u{f813}", + "bell-check": "\u{f814}", + "bell-code": "\u{f815}", + "bell-cog": "\u{f816}", + "bell-dollar": "\u{f817}", + "bell-down": "\u{f818}", + "bell-exclamation": "\u{f819}", + "bell-filled": "\u{f669}", + "bell-heart": "\u{f81a}", + "bell-minus": "\u{ede2}", + "bell-minus-filled": "\u{f722}", + "bell-off": "\u{ece9}", + "bell-pause": "\u{f81b}", + "bell-pin": "\u{f81c}", + "bell-plus": "\u{ede3}", + "bell-plus-filled": "\u{f723}", + "bell-question": "\u{f81d}", + "bell-ringing": "\u{ed07}", + "bell-ringing-2": "\u{ede4}", + "bell-ringing-2-filled": "\u{f724}", + "bell-ringing-filled": "\u{f725}", + "bell-school": "\u{f05e}", + "bell-search": "\u{f81e}", + "bell-share": "\u{f81f}", + "bell-star": "\u{f820}", + "bell-up": "\u{f821}", + "bell-x": "\u{ede5}", + "bell-x-filled": "\u{f726}", + "bell-z": "\u{eff1}", + "bell-z-filled": "\u{f727}", + "beta": "\u{f544}", + "bible": "\u{efc4}", + "bike": "\u{ea36}", + "bike-filled": "\u{10023}", + "bike-off": "\u{f0b8}", + "binary": "\u{ee08}", + "binary-off": "\u{f3c5}", + "binary-tree": "\u{f5d4}", + "binary-tree-2": "\u{f5d3}", + "binary-tree-2-filled": "\u{ff65}", + "binary-tree-filled": "\u{ff64}", + "binoculars": "\u{fefe}", + "binoculars-filled": "\u{ff0b}", + "biohazard": "\u{ecb8}", + "biohazard-filled": "\u{fe8c}", + "biohazard-off": "\u{f0b9}", + "blade": "\u{f4bd}", + "blade-filled": "\u{f7e2}", + "bleach": "\u{f2f3}", + "bleach-chlorine": "\u{f2f0}", + "bleach-no-chlorine": "\u{f2f1}", + "bleach-off": "\u{f2f2}", + "blend-mode": "\u{feb0}", + "blender": "\u{fca2}", + "blender-filled": "\u{10022}", + "blob": "\u{feaf}", + "blob-filled": "\u{feb1}", + "blockquote": "\u{ee09}", + "blocks": "\u{100b2}", + "bluetooth": "\u{ea37}", + "bluetooth-connected": "\u{ecea}", + "bluetooth-off": "\u{eceb}", + "bluetooth-x": "\u{f081}", + "blur": "\u{ef8c}", + "blur-off": "\u{f3c6}", + "bmp": "\u{f3a6}", + "body-scan": "\u{fca3}", + "bold": "\u{eb7b}", + "bold-off": "\u{f0ba}", + "bolt": "\u{ea38}", + "bolt-filled": "\u{10021}", + "bolt-off": "\u{ecec}", + "bomb": "\u{f59c}", + "bomb-filled": "\u{fa86}", + "bone": "\u{edb8}", + "bone-filled": "\u{fe8b}", + "bone-off": "\u{f0bb}", + "bong": "\u{f3a7}", + "bong-filled": "\u{10020}", + "bong-off": "\u{f3c7}", + "book": "\u{ea39}", + "book-2": "\u{efc5}", + "book-download": "\u{f070}", + "book-filled": "\u{fa87}", + "book-off": "\u{f0bc}", + "book-upload": "\u{f071}", + "bookmark": "\u{ea3a}", + "bookmark-ai": "\u{fc8a}", + "bookmark-edit": "\u{fa5e}", + "bookmark-filled": "\u{fa88}", + "bookmark-minus": "\u{fa5f}", + "bookmark-off": "\u{eced}", + "bookmark-plus": "\u{fa60}", + "bookmark-question": "\u{fa61}", + "bookmarks": "\u{ed08}", + "bookmarks-filled": "\u{fb1f}", + "bookmarks-off": "\u{f0bd}", + "books": "\u{eff2}", + "books-off": "\u{f0be}", + "boom": "\u{fdbe}", + "boom-filled": "\u{fe8a}", + "border-all": "\u{ea3b}", + "border-bottom": "\u{ea3c}", + "border-bottom-plus": "\u{fdbd}", + "border-corner-ios": "\u{fd98}", + "border-corner-pill": "\u{fd62}", + "border-corner-rounded": "\u{fd63}", + "border-corner-square": "\u{fd64}", + "border-corners": "\u{f7a0}", + "border-horizontal": "\u{ea3d}", + "border-inner": "\u{ea3e}", + "border-left": "\u{ea3f}", + "border-left-plus": "\u{fdbc}", + "border-none": "\u{ea40}", + "border-outer": "\u{ea41}", + "border-radius": "\u{eb7c}", + "border-right": "\u{ea42}", + "border-right-plus": "\u{fdbb}", + "border-sides": "\u{f7a1}", + "border-style": "\u{ee0a}", + "border-style-2": "\u{ef22}", + "border-top": "\u{ea43}", + "border-top-plus": "\u{fdba}", + "border-vertical": "\u{ea44}", + "bottle": "\u{ef0b}", + "bottle-filled": "\u{fa89}", + "bottle-off": "\u{f3c8}", + "bounce-left": "\u{f59d}", + "bounce-left-filled": "\u{fb20}", + "bounce-right": "\u{f59e}", + "bounce-right-filled": "\u{fb21}", + "bow": "\u{f096}", + "bow-filled": "\u{fe89}", + "bowl": "\u{f4fa}", + "bowl-chopsticks": "\u{fd90}", + "bowl-chopsticks-filled": "\u{fe88}", + "bowl-filled": "\u{fb22}", + "bowl-spoon": "\u{fd91}", + "bowl-spoon-filled": "\u{fe87}", + "bowling": "\u{100b1}", + "box": "\u{ea45}", + "box-align-bottom": "\u{f2a8}", + "box-align-bottom-filled": "\u{fa8a}", + "box-align-bottom-left": "\u{f2ce}", + "box-align-bottom-left-filled": "\u{fa8b}", + "box-align-bottom-right": "\u{f2cf}", + "box-align-bottom-right-filled": "\u{fa8c}", + "box-align-left": "\u{f2a9}", + "box-align-left-filled": "\u{fa8d}", + "box-align-right": "\u{f2aa}", + "box-align-right-filled": "\u{fa8e}", + "box-align-top": "\u{f2ab}", + "box-align-top-filled": "\u{fa8f}", + "box-align-top-left": "\u{f2d0}", + "box-align-top-left-filled": "\u{fa90}", + "box-align-top-right": "\u{f2d1}", + "box-align-top-right-filled": "\u{fa91}", + "box-margin": "\u{ee0b}", + "box-model": "\u{ee0c}", + "box-model-2": "\u{ef23}", + "box-model-2-off": "\u{f3c9}", + "box-model-off": "\u{f3ca}", + "box-multiple": "\u{ee17}", + "box-multiple-0": "\u{ee0d}", + "box-multiple-1": "\u{ee0e}", + "box-multiple-2": "\u{ee0f}", + "box-multiple-3": "\u{ee10}", + "box-multiple-4": "\u{ee11}", + "box-multiple-5": "\u{ee12}", + "box-multiple-6": "\u{ee13}", + "box-multiple-7": "\u{ee14}", + "box-multiple-8": "\u{ee15}", + "box-multiple-9": "\u{ee16}", + "box-multiple-filled": "\u{1001f}", + "box-off": "\u{f102}", + "box-padding": "\u{ee18}", + "box-seam": "\u{eaff}", + "braces": "\u{ebcc}", + "braces-off": "\u{f0bf}", + "brackets": "\u{ebcd}", + "brackets-angle": "\u{fcb2}", + "brackets-angle-off": "\u{fcb1}", + "brackets-contain": "\u{f1e5}", + "brackets-contain-end": "\u{f1e3}", + "brackets-contain-start": "\u{f1e4}", + "brackets-off": "\u{f0c0}", + "braille": "\u{f545}", + "brain": "\u{f59f}", + "brand-4chan": "\u{f494}", + "brand-abstract": "\u{f495}", + "brand-adobe": "\u{f0dc}", + "brand-adobe-after-effect": "\u{ff2a}", + "brand-adobe-illustrator": "\u{ff29}", + "brand-adobe-indesign": "\u{ff28}", + "brand-adobe-photoshop": "\u{ff27}", + "brand-adobe-premier": "\u{ff26}", + "brand-adobe-xd": "\u{ff25}", + "brand-adonis-js": "\u{f496}", + "brand-airbnb": "\u{ed68}", + "brand-airtable": "\u{ef6a}", + "brand-algolia": "\u{f390}", + "brand-alipay": "\u{f7a2}", + "brand-alpine-js": "\u{f324}", + "brand-amazon": "\u{f230}", + "brand-amd": "\u{f653}", + "brand-amie": "\u{ffab}", + "brand-amigo": "\u{f5f9}", + "brand-among-us": "\u{f205}", + "brand-android": "\u{ec16}", + "brand-angular": "\u{ef6b}", + "brand-angular-filled": "\u{10095}", + "brand-ansible": "\u{fa70}", + "brand-ao3": "\u{f5e8}", + "brand-appgallery": "\u{f231}", + "brand-apple": "\u{ec17}", + "brand-apple-arcade": "\u{ed69}", + "brand-apple-filled": "\u{fd74}", + "brand-apple-news": "\u{ff24}", + "brand-apple-podcast": "\u{f1e6}", + "brand-appstore": "\u{ed24}", + "brand-arc": "\u{feae}", + "brand-asana": "\u{edc5}", + "brand-astro": "\u{fdb9}", + "brand-auth0": "\u{fcb3}", + "brand-aws": "\u{fa4c}", + "brand-azure": "\u{fa4d}", + "brand-backbone": "\u{f325}", + "brand-badoo": "\u{f206}", + "brand-baidu": "\u{f5e9}", + "brand-bandcamp": "\u{f207}", + "brand-bandlab": "\u{f5fa}", + "brand-beats": "\u{f208}", + "brand-bebo": "\u{ffaa}", + "brand-behance": "\u{ec6e}", + "brand-bilibili": "\u{f6d2}", + "brand-binance": "\u{f5a0}", + "brand-bing": "\u{edc6}", + "brand-bitbucket": "\u{edc7}", + "brand-bitbucket-filled": "\u{100cb}", + "brand-blackberry": "\u{f568}", + "brand-blender": "\u{f326}", + "brand-blogger": "\u{f35a}", + "brand-bluesky": "\u{fd75}", + "brand-booking": "\u{edc8}", + "brand-bootstrap": "\u{ef3e}", + "brand-bulma": "\u{f327}", + "brand-bumble": "\u{f5fb}", + "brand-bunpo": "\u{f4cf}", + "brand-c-sharp": "\u{f003}", + "brand-cake": "\u{f7a3}", + "brand-cakephp": "\u{f7af}", + "brand-campaignmonitor": "\u{f328}", + "brand-carbon": "\u{f348}", + "brand-cashapp": "\u{f391}", + "brand-chrome": "\u{ec18}", + "brand-cinema-4d": "\u{fa71}", + "brand-citymapper": "\u{f5fc}", + "brand-cloudflare": "\u{fa4e}", + "brand-codecov": "\u{f329}", + "brand-codepen": "\u{ec6f}", + "brand-codesandbox": "\u{ed6a}", + "brand-cohost": "\u{f5d5}", + "brand-coinbase": "\u{f209}", + "brand-comedy-central": "\u{f217}", + "brand-coreos": "\u{f5fd}", + "brand-couchdb": "\u{f60f}", + "brand-couchsurfing": "\u{f392}", + "brand-cpp": "\u{f5fe}", + "brand-craft": "\u{fa72}", + "brand-crunchbase": "\u{f7e3}", + "brand-css3": "\u{ed6b}", + "brand-ctemplar": "\u{f4d0}", + "brand-cucumber": "\u{ef6c}", + "brand-cupra": "\u{f4d1}", + "brand-cypress": "\u{f333}", + "brand-d3": "\u{f24e}", + "brand-databricks": "\u{fc41}", + "brand-days-counter": "\u{f4d2}", + "brand-dcos": "\u{f32a}", + "brand-debian": "\u{ef57}", + "brand-deezer": "\u{f78b}", + "brand-deliveroo": "\u{f4d3}", + "brand-deno": "\u{f24f}", + "brand-denodo": "\u{f610}", + "brand-deviantart": "\u{ecfb}", + "brand-digg": "\u{fa73}", + "brand-dingtalk": "\u{f5ea}", + "brand-discord": "\u{ece3}", + "brand-discord-filled": "\u{f7e4}", + "brand-disney": "\u{f20a}", + "brand-disqus": "\u{edc9}", + "brand-django": "\u{f349}", + "brand-docker": "\u{edca}", + "brand-doctrine": "\u{ef6d}", + "brand-dolby-digital": "\u{f4d4}", + "brand-douban": "\u{f5ff}", + "brand-dribbble": "\u{ec19}", + "brand-dribbble-filled": "\u{f7e5}", + "brand-dropbox": "\u{1018a}", + "brand-drops": "\u{f4d5}", + "brand-drupal": "\u{f393}", + "brand-edge": "\u{ecfc}", + "brand-elastic": "\u{f611}", + "brand-electronic-arts": "\u{fa74}", + "brand-ember": "\u{f497}", + "brand-envato": "\u{f394}", + "brand-etsy": "\u{f654}", + "brand-evernote": "\u{f600}", + "brand-facebook": "\u{ec1a}", + "brand-facebook-filled": "\u{f7e6}", + "brand-feedly": "\u{fa75}", + "brand-figma": "\u{ec93}", + "brand-filezilla": "\u{fa76}", + "brand-finder": "\u{f218}", + "brand-firebase": "\u{ef6e}", + "brand-firefox": "\u{ecfd}", + "brand-fiverr": "\u{f7a4}", + "brand-flickr": "\u{ecfe}", + "brand-flightradar24": "\u{f4d6}", + "brand-flipboard": "\u{f20b}", + "brand-flutter": "\u{f395}", + "brand-fortnite": "\u{f260}", + "brand-foursquare": "\u{ecff}", + "brand-framer": "\u{ec1b}", + "brand-framer-motion": "\u{f78c}", + "brand-funimation": "\u{f655}", + "brand-gatsby": "\u{f396}", + "brand-git": "\u{ef6f}", + "brand-github": "\u{ec1c}", + "brand-github-copilot": "\u{f4a8}", + "brand-github-filled": "\u{f7e7}", + "brand-gitlab": "\u{ec1d}", + "brand-gmail": "\u{efa2}", + "brand-golang": "\u{f78d}", + "brand-google": "\u{ec1f}", + "brand-google-analytics": "\u{edcb}", + "brand-google-big-query": "\u{f612}", + "brand-google-drive": "\u{ec1e}", + "brand-google-filled": "\u{fd1a}", + "brand-google-fit": "\u{f297}", + "brand-google-home": "\u{f601}", + "brand-google-maps": "\u{fa4f}", + "brand-google-one": "\u{f232}", + "brand-google-photos": "\u{f20c}", + "brand-google-play": "\u{ed25}", + "brand-google-podcasts": "\u{f656}", + "brand-grammarly": "\u{f32b}", + "brand-graphql": "\u{f32c}", + "brand-gravatar": "\u{edcc}", + "brand-grindr": "\u{f20d}", + "brand-guardian": "\u{f4fb}", + "brand-gumroad": "\u{f5d6}", + "brand-hackerrank": "\u{ff23}", + "brand-hbo": "\u{f657}", + "brand-headlessui": "\u{f32d}", + "brand-hexo": "\u{fa50}", + "brand-hipchat": "\u{edcd}", + "brand-html5": "\u{ed6c}", + "brand-inertia": "\u{f34a}", + "brand-instagram": "\u{ec20}", + "brand-instagram-filled": "\u{10094}", + "brand-intercom": "\u{f1cf}", + "brand-itch": "\u{fa22}", + "brand-javascript": "\u{ef0c}", + "brand-juejin": "\u{f7b0}", + "brand-kako-talk": "\u{fd2d}", + "brand-kbin": "\u{fad0}", + "brand-kick": "\u{fa23}", + "brand-kick-filled": "\u{10093}", + "brand-kickstarter": "\u{edce}", + "brand-kotlin": "\u{ed6d}", + "brand-laravel": "\u{f34b}", + "brand-lastfm": "\u{f001}", + "brand-leetcode": "\u{fa51}", + "brand-letterboxd": "\u{fa24}", + "brand-line": "\u{f7e8}", + "brand-linkedin": "\u{ec8c}", + "brand-linkedin-filled": "\u{10092}", + "brand-linktree": "\u{f1e7}", + "brand-linqpad": "\u{f562}", + "brand-livewire": "\u{fd76}", + "brand-loom": "\u{ef70}", + "brand-mailgun": "\u{f32e}", + "brand-mantine": "\u{f32f}", + "brand-mastercard": "\u{ef49}", + "brand-mastodon": "\u{f250}", + "brand-matrix": "\u{f5eb}", + "brand-mcdonalds": "\u{f251}", + "brand-medium": "\u{ec70}", + "brand-meetup": "\u{fc6a}", + "brand-mercedes": "\u{f072}", + "brand-messenger": "\u{ec71}", + "brand-messenger-filled": "\u{100a7}", + "brand-meta": "\u{efb0}", + "brand-metabrainz": "\u{ff12}", + "brand-minecraft": "\u{faef}", + "brand-miniprogram": "\u{f602}", + "brand-mixpanel": "\u{f397}", + "brand-monday": "\u{f219}", + "brand-mongodb": "\u{f613}", + "brand-my-oppo": "\u{f4d7}", + "brand-mysql": "\u{f614}", + "brand-national-geographic": "\u{f603}", + "brand-nem": "\u{f5a1}", + "brand-netbeans": "\u{ef71}", + "brand-netease-music": "\u{f604}", + "brand-netflix": "\u{edcf}", + "brand-nexo": "\u{f5a2}", + "brand-nextcloud": "\u{f4d8}", + "brand-nextjs": "\u{f0dd}", + "brand-nodejs": "\u{fae0}", + "brand-nord-vpn": "\u{f37f}", + "brand-notion": "\u{ef7b}", + "brand-npm": "\u{f569}", + "brand-nuxt": "\u{f0de}", + "brand-nytimes": "\u{ef8d}", + "brand-oauth": "\u{fa52}", + "brand-office": "\u{f398}", + "brand-ok-ru": "\u{f399}", + "brand-onedrive": "\u{f5d7}", + "brand-onlyfans": "\u{f605}", + "brand-open-source": "\u{edd0}", + "brand-open-source-filled": "\u{10091}", + "brand-openai": "\u{f78e}", + "brand-openvpn": "\u{f39a}", + "brand-opera": "\u{ec21}", + "brand-opera-filled": "\u{10090}", + "brand-pagekit": "\u{edd1}", + "brand-parsinta": "\u{fc42}", + "brand-patreon": "\u{edd2}", + "brand-patreon-filled": "\u{fcff}", + "brand-paypal": "\u{ec22}", + "brand-paypal-filled": "\u{f7e9}", + "brand-paypay": "\u{f5ec}", + "brand-peanut": "\u{f39b}", + "brand-pepsi": "\u{f261}", + "brand-php": "\u{ef72}", + "brand-picsart": "\u{f4d9}", + "brand-pinterest": "\u{ec8d}", + "brand-pinterest-filled": "\u{1008f}", + "brand-planetscale": "\u{f78f}", + "brand-pnpm": "\u{fd77}", + "brand-pocket": "\u{ed00}", + "brand-polymer": "\u{f498}", + "brand-powershell": "\u{f5ed}", + "brand-printables": "\u{fd1b}", + "brand-prisma": "\u{f499}", + "brand-producthunt": "\u{edd3}", + "brand-pushbullet": "\u{f330}", + "brand-pushover": "\u{f20e}", + "brand-python": "\u{ed01}", + "brand-qq": "\u{f606}", + "brand-radix-ui": "\u{f790}", + "brand-react": "\u{f34c}", + "brand-react-native": "\u{ef73}", + "brand-reason": "\u{f49a}", + "brand-reddit": "\u{ec8e}", + "brand-redhat": "\u{f331}", + "brand-redux": "\u{f3a8}", + "brand-revolut": "\u{f4da}", + "brand-rumble": "\u{fad1}", + "brand-rust": "\u{fa53}", + "brand-safari": "\u{ec23}", + "brand-samsungpass": "\u{f4db}", + "brand-sass": "\u{edd4}", + "brand-sentry": "\u{edd5}", + "brand-sharik": "\u{f4dc}", + "brand-shazam": "\u{edd6}", + "brand-shopee": "\u{f252}", + "brand-sketch": "\u{ec24}", + "brand-sketch-filled": "\u{1008e}", + "brand-skype": "\u{ed02}", + "brand-slack": "\u{ec72}", + "brand-snapchat": "\u{ec25}", + "brand-snapchat-filled": "\u{1008d}", + "brand-snapseed": "\u{f253}", + "brand-snowflake": "\u{f615}", + "brand-socket-io": "\u{f49b}", + "brand-solidjs": "\u{f5ee}", + "brand-soundcloud": "\u{ed6e}", + "brand-spacehey": "\u{f4fc}", + "brand-speedtest": "\u{fa77}", + "brand-spotify": "\u{ed03}", + "brand-spotify-filled": "\u{fe86}", + "brand-stackoverflow": "\u{ef58}", + "brand-stackshare": "\u{f607}", + "brand-steam": "\u{ed6f}", + "brand-steam-filled": "\u{1008c}", + "brand-stocktwits": "\u{fd78}", + "brand-storj": "\u{fa54}", + "brand-storybook": "\u{f332}", + "brand-storytel": "\u{f608}", + "brand-strava": "\u{f254}", + "brand-stripe": "\u{edd7}", + "brand-stripe-filled": "\u{1008b}", + "brand-sublime-text": "\u{ef74}", + "brand-sugarizer": "\u{f7a5}", + "brand-supabase": "\u{f6d3}", + "brand-superhuman": "\u{f50c}", + "brand-supernova": "\u{f49c}", + "brand-surfshark": "\u{f255}", + "brand-svelte": "\u{f0df}", + "brand-swift": "\u{fa55}", + "brand-symfony": "\u{f616}", + "brand-tabler": "\u{ec8f}", + "brand-tabler-filled": "\u{1008a}", + "brand-tailwind": "\u{eca1}", + "brand-taobao": "\u{f5ef}", + "brand-teams": "\u{fadf}", + "brand-ted": "\u{f658}", + "brand-telegram": "\u{ec26}", + "brand-terraform": "\u{fa56}", + "brand-tesla": "\u{10099}", + "brand-tether": "\u{f5a3}", + "brand-thingiverse": "\u{fd1c}", + "brand-threads": "\u{fb02}", + "brand-threejs": "\u{f5f0}", + "brand-tidal": "\u{ed70}", + "brand-tiktok": "\u{ec73}", + "brand-tiktok-filled": "\u{f7ea}", + "brand-tinder": "\u{ed71}", + "brand-tinder-filled": "\u{10089}", + "brand-topbuzz": "\u{f50d}", + "brand-torchain": "\u{f5a4}", + "brand-toyota": "\u{f262}", + "brand-trello": "\u{f39d}", + "brand-tripadvisor": "\u{f002}", + "brand-tumblr": "\u{ed04}", + "brand-tumblr-filled": "\u{10088}", + "brand-twilio": "\u{f617}", + "brand-twitch": "\u{ed05}", + "brand-twitter": "\u{ec27}", + "brand-twitter-filled": "\u{f7eb}", + "brand-typescript": "\u{f5f1}", + "brand-uber": "\u{ef75}", + "brand-ubuntu": "\u{ef59}", + "brand-unity": "\u{f49d}", + "brand-unsplash": "\u{edd8}", + "brand-upwork": "\u{f39e}", + "brand-valorant": "\u{f39f}", + "brand-vercel": "\u{ef24}", + "brand-vercel-filled": "\u{10087}", + "brand-vimeo": "\u{ed06}", + "brand-vimeo-filled": "\u{10086}", + "brand-vinted": "\u{f20f}", + "brand-visa": "\u{f380}", + "brand-visual-studio": "\u{ef76}", + "brand-vite": "\u{f5f2}", + "brand-vivaldi": "\u{f210}", + "brand-vk": "\u{ed72}", + "brand-vlc": "\u{fa78}", + "brand-volkswagen": "\u{f50e}", + "brand-vsco": "\u{f334}", + "brand-vscode": "\u{f3a0}", + "brand-vue": "\u{f0e0}", + "brand-walmart": "\u{f211}", + "brand-waze": "\u{f5d8}", + "brand-webflow": "\u{f2d2}", + "brand-wechat": "\u{f5f3}", + "brand-weibo": "\u{f609}", + "brand-weibo-filled": "\u{100a6}", + "brand-whatsapp": "\u{ec74}", + "brand-whatsapp-filled": "\u{10085}", + "brand-wikipedia": "\u{fa79}", + "brand-windows": "\u{ecd8}", + "brand-windows-filled": "\u{10084}", + "brand-windy": "\u{f4dd}", + "brand-wish": "\u{f212}", + "brand-wix": "\u{f3a1}", + "brand-wordpress": "\u{f2d3}", + "brand-x": "\u{fc0f}", + "brand-x-filled": "\u{fc21}", + "brand-xamarin": "\u{fa7a}", + "brand-xbox": "\u{f298}", + "brand-xdeep": "\u{fc10}", + "brand-xing": "\u{f21a}", + "brand-yahoo": "\u{ed73}", + "brand-yandex": "\u{fae1}", + "brand-yarn": "\u{fd79}", + "brand-yatse": "\u{f213}", + "brand-ycombinator": "\u{edd9}", + "brand-youtube": "\u{ec90}", + "brand-youtube-filled": "\u{fc22}", + "brand-youtube-kids": "\u{f214}", + "brand-zalando": "\u{f49e}", + "brand-zapier": "\u{f49f}", + "brand-zeit": "\u{f335}", + "brand-zhihu": "\u{f60a}", + "brand-zoom": "\u{f215}", + "brand-zulip": "\u{f4de}", + "brand-zwift": "\u{f216}", + "bread": "\u{efa3}", + "bread-filled": "\u{fe85}", + "bread-off": "\u{f3cb}", + "briefcase": "\u{ea46}", + "briefcase-2": "\u{fb03}", + "briefcase-2-filled": "\u{fe84}", + "briefcase-filled": "\u{fd00}", + "briefcase-off": "\u{f3cc}", + "brightness": "\u{eb7f}", + "brightness-2": "\u{ee19}", + "brightness-auto": "\u{fd99}", + "brightness-auto-filled": "\u{fe83}", + "brightness-down": "\u{eb7d}", + "brightness-down-filled": "\u{fb23}", + "brightness-filled": "\u{fe82}", + "brightness-half": "\u{ee1a}", + "brightness-off": "\u{f3cd}", + "brightness-up": "\u{eb7e}", + "brightness-up-filled": "\u{fb24}", + "broadcast": "\u{f1e9}", + "broadcast-off": "\u{f1e8}", + "browser": "\u{ebb7}", + "browser-check": "\u{efd6}", + "browser-maximize": "\u{100b0}", + "browser-minus": "\u{100af}", + "browser-off": "\u{f0c1}", + "browser-plus": "\u{efd7}", + "browser-share": "\u{100ae}", + "browser-x": "\u{efd8}", + "brush": "\u{ebb8}", + "brush-off": "\u{f0c2}", + "bubble": "\u{feba}", + "bubble-filled": "\u{fec3}", + "bubble-minus": "\u{febe}", + "bubble-plus": "\u{febd}", + "bubble-tea": "\u{ff51}", + "bubble-tea-2": "\u{ff52}", + "bubble-text": "\u{febc}", + "bubble-text-filled": "\u{100a5}", + "bubble-x": "\u{febb}", + "bucket": "\u{ea47}", + "bucket-droplet": "\u{f56a}", + "bucket-off": "\u{f103}", + "bug": "\u{ea48}", + "bug-filled": "\u{fd01}", + "bug-off": "\u{f0c3}", + "building": "\u{ea4f}", + "building-airport": "\u{ffa9}", + "building-arch": "\u{ea49}", + "building-bank": "\u{ebe2}", + "building-bridge": "\u{ea4b}", + "building-bridge-2": "\u{ea4a}", + "building-bridge-2-filled": "\u{10189}", + "building-broadcast-tower": "\u{f4be}", + "building-broadcast-tower-filled": "\u{fe81}", + "building-burj-al-arab": "\u{ff50}", + "building-carousel": "\u{ed87}", + "building-castle": "\u{ed88}", + "building-church": "\u{ea4c}", + "building-circus": "\u{f4bf}", + "building-cog": "\u{10062}", + "building-community": "\u{ebf6}", + "building-cottage": "\u{ee1b}", + "building-estate": "\u{f5a5}", + "building-factory": "\u{ee1c}", + "building-factory-2": "\u{f082}", + "building-fortress": "\u{ed89}", + "building-hospital": "\u{ea4d}", + "building-lighthouse": "\u{ed8a}", + "building-minus": "\u{10061}", + "building-monument": "\u{ed26}", + "building-mosque": "\u{fa57}", + "building-off": "\u{fefd}", + "building-pavilion": "\u{ebf7}", + "building-plus": "\u{10060}", + "building-skyscraper": "\u{ec39}", + "building-stadium": "\u{f641}", + "building-store": "\u{ea4e}", + "building-tunnel": "\u{f5a6}", + "building-warehouse": "\u{ebe3}", + "building-wind-turbine": "\u{f4c0}", + "buildings": "\u{ff40}", + "bulb": "\u{ea51}", + "bulb-filled": "\u{f66a}", + "bulb-off": "\u{ea50}", + "bulldozer": "\u{ee1d}", + "burger": "\u{fcb4}", + "bus": "\u{ebe4}", + "bus-filled": "\u{100ff}", + "bus-off": "\u{f3ce}", + "bus-stop": "\u{f2d4}", + "businessplan": "\u{ee1e}", + "butterfly": "\u{efd9}", + "butterfly-filled": "\u{10016}", + "cactus": "\u{f21b}", + "cactus-filled": "\u{fb25}", + "cactus-off": "\u{f3cf}", + "cake": "\u{f00f}", + "cake-off": "\u{f104}", + "cake-roll": "\u{100bd}", + "calculator": "\u{eb80}", + "calculator-filled": "\u{fb26}", + "calculator-off": "\u{f0c4}", + "calendar": "\u{ea53}", + "calendar-bolt": "\u{f822}", + "calendar-cancel": "\u{f823}", + "calendar-check": "\u{f824}", + "calendar-clock": "\u{fd2e}", + "calendar-code": "\u{f825}", + "calendar-cog": "\u{f826}", + "calendar-dollar": "\u{f827}", + "calendar-dot": "\u{fd3e}", + "calendar-down": "\u{f828}", + "calendar-due": "\u{f621}", + "calendar-event": "\u{ea52}", + "calendar-event-filled": "\u{100b9}", + "calendar-exclamation": "\u{f829}", + "calendar-filled": "\u{fb27}", + "calendar-heart": "\u{f82a}", + "calendar-minus": "\u{ebb9}", + "calendar-month": "\u{fd2f}", + "calendar-month-filled": "\u{100b8}", + "calendar-off": "\u{ee1f}", + "calendar-pause": "\u{f82b}", + "calendar-pin": "\u{f82c}", + "calendar-plus": "\u{ebba}", + "calendar-question": "\u{f82d}", + "calendar-repeat": "\u{fad2}", + "calendar-sad": "\u{fd1d}", + "calendar-search": "\u{f82e}", + "calendar-share": "\u{f82f}", + "calendar-smile": "\u{fd1e}", + "calendar-star": "\u{f830}", + "calendar-stats": "\u{ee20}", + "calendar-time": "\u{ee21}", + "calendar-up": "\u{f831}", + "calendar-user": "\u{fd1f}", + "calendar-week": "\u{fd30}", + "calendar-week-filled": "\u{100b7}", + "calendar-x": "\u{f832}", + "camera": "\u{ea54}", + "camera-ai": "\u{ffa8}", + "camera-bitcoin": "\u{ffa7}", + "camera-bolt": "\u{f833}", + "camera-cancel": "\u{f834}", + "camera-check": "\u{f835}", + "camera-code": "\u{f836}", + "camera-cog": "\u{f837}", + "camera-dollar": "\u{f838}", + "camera-down": "\u{f839}", + "camera-exclamation": "\u{f83a}", + "camera-filled": "\u{fa37}", + "camera-heart": "\u{f83b}", + "camera-minus": "\u{ec3a}", + "camera-moon": "\u{ffa6}", + "camera-off": "\u{ecee}", + "camera-pause": "\u{f83c}", + "camera-pin": "\u{f83d}", + "camera-plus": "\u{ec3b}", + "camera-question": "\u{f83e}", + "camera-rotate": "\u{ee22}", + "camera-search": "\u{f83f}", + "camera-selfie": "\u{ee23}", + "camera-share": "\u{f840}", + "camera-spark": "\u{ffbc}", + "camera-star": "\u{f841}", + "camera-up": "\u{f842}", + "camera-x": "\u{f843}", + "camper": "\u{fa25}", + "campfire": "\u{f5a7}", + "campfire-filled": "\u{fb28}", + "cancel": "\u{ff11}", + "candle": "\u{efc6}", + "candle-filled": "\u{fc23}", + "candy": "\u{ef0d}", + "candy-off": "\u{f0c5}", + "cane": "\u{f50f}", + "cannabis": "\u{f4c1}", + "cannabis-filled": "\u{10015}", + "cap-projecting": "\u{ff22}", + "cap-rounded": "\u{ff21}", + "cap-straight": "\u{ff20}", + "capsule": "\u{fae3}", + "capsule-filled": "\u{fc24}", + "capsule-horizontal": "\u{fae2}", + "capsule-horizontal-filled": "\u{fc25}", + "capture": "\u{ec3c}", + "capture-filled": "\u{fb29}", + "capture-off": "\u{f0c6}", + "car": "\u{ebbb}", + "car-4wd": "\u{fdb8}", + "car-4wd-filled": "\u{1001e}", + "car-crane": "\u{ef25}", + "car-crane-filled": "\u{100fe}", + "car-crash": "\u{efa4}", + "car-fan": "\u{fdb3}", + "car-fan-1": "\u{fdb7}", + "car-fan-2": "\u{fdb6}", + "car-fan-3": "\u{fdb5}", + "car-fan-auto": "\u{fdb4}", + "car-fan-filled": "\u{1001d}", + "car-filled": "\u{1004c}", + "car-garage": "\u{fc77}", + "car-off": "\u{f0c7}", + "car-suv": "\u{fc8b}", + "car-suv-filled": "\u{1004d}", + "car-turbine": "\u{f4fd}", + "carambola": "\u{feb9}", + "carambola-filled": "\u{10014}", + "caravan": "\u{ec7c}", + "caravan-filled": "\u{100fd}", + "cardboards": "\u{ed74}", + "cardboards-filled": "\u{1001c}", + "cardboards-off": "\u{f0c8}", + "cards": "\u{f510}", + "cards-filled": "\u{fc26}", + "caret-down": "\u{eb5d}", + "caret-down-filled": "\u{fb2a}", + "caret-left": "\u{eb5e}", + "caret-left-filled": "\u{fb2b}", + "caret-left-right": "\u{fc43}", + "caret-left-right-filled": "\u{fd02}", + "caret-right": "\u{eb5f}", + "caret-right-filled": "\u{fb2c}", + "caret-up": "\u{eb60}", + "caret-up-down": "\u{fc44}", + "caret-up-down-filled": "\u{fd03}", + "caret-up-filled": "\u{fb2d}", + "carousel-horizontal": "\u{f659}", + "carousel-horizontal-filled": "\u{fa92}", + "carousel-vertical": "\u{f65a}", + "carousel-vertical-filled": "\u{fa93}", + "carrot": "\u{f21c}", + "carrot-off": "\u{f3d0}", + "cash": "\u{ea55}", + "cash-banknote": "\u{ee25}", + "cash-banknote-edit": "\u{10149}", + "cash-banknote-filled": "\u{fe80}", + "cash-banknote-heart": "\u{10148}", + "cash-banknote-minus": "\u{10147}", + "cash-banknote-move": "\u{10145}", + "cash-banknote-move-back": "\u{10146}", + "cash-banknote-off": "\u{ee24}", + "cash-banknote-plus": "\u{10144}", + "cash-edit": "\u{10143}", + "cash-heart": "\u{10142}", + "cash-minus": "\u{10141}", + "cash-move": "\u{1013f}", + "cash-move-back": "\u{10140}", + "cash-off": "\u{f105}", + "cash-plus": "\u{1013e}", + "cash-register": "\u{fee6}", + "cast": "\u{ea56}", + "cast-off": "\u{f0c9}", + "cat": "\u{f65b}", + "category": "\u{f1f6}", + "category-2": "\u{f1f5}", + "category-filled": "\u{fb2e}", + "category-minus": "\u{fd20}", + "category-plus": "\u{fd21}", + "ce": "\u{ed75}", + "ce-off": "\u{f0ca}", + "cell": "\u{f05f}", + "cell-signal-1": "\u{f083}", + "cell-signal-2": "\u{f084}", + "cell-signal-3": "\u{f085}", + "cell-signal-4": "\u{f086}", + "cell-signal-5": "\u{f087}", + "cell-signal-off": "\u{f088}", + "certificate": "\u{ed76}", + "certificate-2": "\u{f073}", + "certificate-2-off": "\u{f0cb}", + "certificate-off": "\u{f0cc}", + "chair-director": "\u{f2d5}", + "chalkboard": "\u{f34d}", + "chalkboard-off": "\u{f3d1}", + "chalkboard-teacher": "\u{10160}", + "charging-pile": "\u{ee26}", + "charging-pile-filled": "\u{1001b}", + "chart-arcs": "\u{ee28}", + "chart-arcs-3": "\u{ee27}", + "chart-area": "\u{ea58}", + "chart-area-filled": "\u{f66b}", + "chart-area-line": "\u{ea57}", + "chart-area-line-filled": "\u{f66c}", + "chart-arrows": "\u{ee2a}", + "chart-arrows-vertical": "\u{ee29}", + "chart-bar": "\u{ea59}", + "chart-bar-off": "\u{f3d2}", + "chart-bar-popular": "\u{fef7}", + "chart-bubble": "\u{ec75}", + "chart-bubble-filled": "\u{f66d}", + "chart-candle": "\u{ea5a}", + "chart-candle-filled": "\u{f66e}", + "chart-circles": "\u{ee2b}", + "chart-cohort": "\u{fef6}", + "chart-column": "\u{ffa5}", + "chart-covariate": "\u{ffa4}", + "chart-donut": "\u{ea5b}", + "chart-donut-2": "\u{ee2c}", + "chart-donut-3": "\u{ee2d}", + "chart-donut-4": "\u{ee2e}", + "chart-donut-filled": "\u{f66f}", + "chart-dots": "\u{ee2f}", + "chart-dots-2": "\u{f097}", + "chart-dots-2-filled": "\u{100dd}", + "chart-dots-3": "\u{f098}", + "chart-dots-3-filled": "\u{100dc}", + "chart-dots-filled": "\u{fd04}", + "chart-funnel": "\u{fef5}", + "chart-funnel-filled": "\u{100db}", + "chart-grid-dots": "\u{f4c2}", + "chart-grid-dots-filled": "\u{fd05}", + "chart-histogram": "\u{f65c}", + "chart-infographic": "\u{ee30}", + "chart-line": "\u{ea5c}", + "chart-pie": "\u{ea5d}", + "chart-pie-2": "\u{ee31}", + "chart-pie-2-filled": "\u{100da}", + "chart-pie-3": "\u{ee32}", + "chart-pie-3-filled": "\u{100d9}", + "chart-pie-4": "\u{ee33}", + "chart-pie-4-filled": "\u{100d8}", + "chart-pie-filled": "\u{f670}", + "chart-pie-off": "\u{f3d3}", + "chart-ppf": "\u{f618}", + "chart-radar": "\u{ed77}", + "chart-sankey": "\u{f619}", + "chart-scatter": "\u{fd93}", + "chart-scatter-3d": "\u{fd92}", + "chart-treemap": "\u{f381}", + "check": "\u{ea5e}", + "checkbox": "\u{eba6}", + "checklist": "\u{f074}", + "checks": "\u{ebaa}", + "checkup-list": "\u{ef5a}", + "cheese": "\u{ef26}", + "chef-hat": "\u{f21d}", + "chef-hat-filled": "\u{100d7}", + "chef-hat-off": "\u{f3d4}", + "cherry": "\u{f511}", + "cherry-filled": "\u{f728}", + "chess": "\u{f382}", + "chess-bishop": "\u{f56b}", + "chess-bishop-filled": "\u{f729}", + "chess-filled": "\u{f72a}", + "chess-king": "\u{f56c}", + "chess-king-filled": "\u{f72b}", + "chess-knight": "\u{f56d}", + "chess-knight-filled": "\u{f72c}", + "chess-queen": "\u{f56e}", + "chess-queen-filled": "\u{f72d}", + "chess-rook": "\u{f56f}", + "chess-rook-filled": "\u{f72e}", + "chevron-compact-down": "\u{faf0}", + "chevron-compact-left": "\u{faf1}", + "chevron-compact-right": "\u{faf2}", + "chevron-compact-up": "\u{faf3}", + "chevron-down": "\u{ea5f}", + "chevron-down-left": "\u{ed09}", + "chevron-down-right": "\u{ed0a}", + "chevron-left": "\u{ea60}", + "chevron-left-pipe": "\u{fae4}", + "chevron-right": "\u{ea61}", + "chevron-right-pipe": "\u{fae5}", + "chevron-up": "\u{ea62}", + "chevron-up-left": "\u{ed0b}", + "chevron-up-right": "\u{ed0c}", + "chevrons-down": "\u{ea63}", + "chevrons-down-left": "\u{ed0d}", + "chevrons-down-right": "\u{ed0e}", + "chevrons-left": "\u{ea64}", + "chevrons-right": "\u{ea65}", + "chevrons-up": "\u{ea66}", + "chevrons-up-left": "\u{ed0f}", + "chevrons-up-right": "\u{ed10}", + "chisel": "\u{f383}", + "christmas-ball": "\u{fd31}", + "christmas-tree": "\u{ed78}", + "christmas-tree-filled": "\u{1001a}", + "christmas-tree-off": "\u{f3d5}", + "circle": "\u{ea6b}", + "circle-0": "\u{ee34}", + "circle-1": "\u{ee35}", + "circle-2": "\u{ee36}", + "circle-3": "\u{ee37}", + "circle-4": "\u{ee38}", + "circle-5": "\u{ee39}", + "circle-6": "\u{ee3a}", + "circle-7": "\u{ee3b}", + "circle-8": "\u{ee3c}", + "circle-9": "\u{ee3d}", + "circle-arrow-down": "\u{f6f9}", + "circle-arrow-down-filled": "\u{f6f4}", + "circle-arrow-down-left": "\u{f6f6}", + "circle-arrow-down-left-filled": "\u{f6f5}", + "circle-arrow-down-right": "\u{f6f8}", + "circle-arrow-down-right-filled": "\u{f6f7}", + "circle-arrow-left": "\u{f6fb}", + "circle-arrow-left-filled": "\u{f6fa}", + "circle-arrow-right": "\u{f6fd}", + "circle-arrow-right-filled": "\u{f6fc}", + "circle-arrow-up": "\u{f703}", + "circle-arrow-up-filled": "\u{f6fe}", + "circle-arrow-up-left": "\u{f700}", + "circle-arrow-up-left-filled": "\u{f6ff}", + "circle-arrow-up-right": "\u{f702}", + "circle-arrow-up-right-filled": "\u{f701}", + "circle-caret-down": "\u{f4a9}", + "circle-caret-down-filled": "\u{100d6}", + "circle-caret-left": "\u{f4aa}", + "circle-caret-left-filled": "\u{100d5}", + "circle-caret-right": "\u{f4ab}", + "circle-caret-right-filled": "\u{100d4}", + "circle-caret-up": "\u{f4ac}", + "circle-caret-up-filled": "\u{100d3}", + "circle-check": "\u{ea67}", + "circle-check-filled": "\u{f704}", + "circle-chevron-down": "\u{f622}", + "circle-chevron-down-filled": "\u{100d2}", + "circle-chevron-left": "\u{f623}", + "circle-chevron-left-filled": "\u{100d1}", + "circle-chevron-right": "\u{f624}", + "circle-chevron-right-filled": "\u{100d0}", + "circle-chevron-up": "\u{f625}", + "circle-chevron-up-filled": "\u{100cf}", + "circle-chevrons-down": "\u{f642}", + "circle-chevrons-down-filled": "\u{100ef}", + "circle-chevrons-left": "\u{f643}", + "circle-chevrons-left-filled": "\u{100ee}", + "circle-chevrons-right": "\u{f644}", + "circle-chevrons-right-filled": "\u{100ed}", + "circle-chevrons-up": "\u{f645}", + "circle-chevrons-up-filled": "\u{100ec}", + "circle-dashed": "\u{ed27}", + "circle-dashed-check": "\u{feb8}", + "circle-dashed-letter-a": "\u{ff9a}", + "circle-dashed-letter-b": "\u{ff99}", + "circle-dashed-letter-c": "\u{ff98}", + "circle-dashed-letter-d": "\u{ff97}", + "circle-dashed-letter-e": "\u{ff96}", + "circle-dashed-letter-f": "\u{ff95}", + "circle-dashed-letter-g": "\u{ff94}", + "circle-dashed-letter-h": "\u{ff93}", + "circle-dashed-letter-i": "\u{ff92}", + "circle-dashed-letter-j": "\u{ff91}", + "circle-dashed-letter-k": "\u{ff90}", + "circle-dashed-letter-l": "\u{ff8f}", + "circle-dashed-letter-letter-v": "\u{ff84}", + "circle-dashed-letter-m": "\u{ff8d}", + "circle-dashed-letter-n": "\u{ff8c}", + "circle-dashed-letter-o": "\u{ff8b}", + "circle-dashed-letter-p": "\u{ff8a}", + "circle-dashed-letter-q": "\u{ff89}", + "circle-dashed-letter-r": "\u{ff88}", + "circle-dashed-letter-s": "\u{ff87}", + "circle-dashed-letter-t": "\u{ff86}", + "circle-dashed-letter-u": "\u{ff85}", + "circle-dashed-letter-v": "\u{ff84}", + "circle-dashed-letter-w": "\u{ff83}", + "circle-dashed-letter-x": "\u{ff82}", + "circle-dashed-letter-y": "\u{ff81}", + "circle-dashed-letter-z": "\u{ff80}", + "circle-dashed-minus": "\u{feb7}", + "circle-dashed-number-0": "\u{fc6b}", + "circle-dashed-number-1": "\u{fc6c}", + "circle-dashed-number-2": "\u{fc6d}", + "circle-dashed-number-3": "\u{fc6e}", + "circle-dashed-number-4": "\u{fc6f}", + "circle-dashed-number-5": "\u{fc70}", + "circle-dashed-number-6": "\u{fc71}", + "circle-dashed-number-7": "\u{fc72}", + "circle-dashed-number-8": "\u{fc73}", + "circle-dashed-number-9": "\u{fc74}", + "circle-dashed-percentage": "\u{fd7a}", + "circle-dashed-plus": "\u{feb6}", + "circle-dashed-x": "\u{fc75}", + "circle-dot": "\u{efb1}", + "circle-dot-filled": "\u{f705}", + "circle-dotted": "\u{ed28}", + "circle-dotted-letter-a": "\u{ff7f}", + "circle-dotted-letter-b": "\u{ff7e}", + "circle-dotted-letter-c": "\u{ff7d}", + "circle-dotted-letter-d": "\u{ff7c}", + "circle-dotted-letter-e": "\u{ff7b}", + "circle-dotted-letter-f": "\u{ff7a}", + "circle-dotted-letter-g": "\u{ff79}", + "circle-dotted-letter-h": "\u{ff78}", + "circle-dotted-letter-i": "\u{ff77}", + "circle-dotted-letter-j": "\u{ff76}", + "circle-dotted-letter-k": "\u{ff75}", + "circle-dotted-letter-l": "\u{ff74}", + "circle-dotted-letter-m": "\u{ff73}", + "circle-dotted-letter-n": "\u{ff72}", + "circle-dotted-letter-o": "\u{ff71}", + "circle-dotted-letter-p": "\u{ff70}", + "circle-dotted-letter-q": "\u{ff6f}", + "circle-dotted-letter-r": "\u{ff6e}", + "circle-dotted-letter-s": "\u{ff6d}", + "circle-dotted-letter-t": "\u{ff6c}", + "circle-dotted-letter-u": "\u{ff6b}", + "circle-dotted-letter-v": "\u{ff6a}", + "circle-dotted-letter-w": "\u{ff69}", + "circle-dotted-letter-x": "\u{ff68}", + "circle-dotted-letter-y": "\u{ff67}", + "circle-dotted-letter-z": "\u{ff66}", + "circle-filled": "\u{f671}", + "circle-half": "\u{ee3f}", + "circle-half-2": "\u{eff3}", + "circle-half-vertical": "\u{ee3e}", + "circle-key": "\u{f633}", + "circle-key-filled": "\u{f706}", + "circle-letter-a": "\u{f441}", + "circle-letter-a-filled": "\u{fe7f}", + "circle-letter-b": "\u{f442}", + "circle-letter-b-filled": "\u{fe7e}", + "circle-letter-c": "\u{f443}", + "circle-letter-c-filled": "\u{fe7d}", + "circle-letter-d": "\u{f444}", + "circle-letter-d-filled": "\u{fe7c}", + "circle-letter-e": "\u{f445}", + "circle-letter-e-filled": "\u{fe7b}", + "circle-letter-f": "\u{f446}", + "circle-letter-f-filled": "\u{fe7a}", + "circle-letter-g": "\u{f447}", + "circle-letter-g-filled": "\u{fe79}", + "circle-letter-h": "\u{f448}", + "circle-letter-h-filled": "\u{fe78}", + "circle-letter-i": "\u{f449}", + "circle-letter-i-filled": "\u{fe77}", + "circle-letter-j": "\u{f44a}", + "circle-letter-j-filled": "\u{fe76}", + "circle-letter-k": "\u{f44b}", + "circle-letter-k-filled": "\u{fe75}", + "circle-letter-l": "\u{f44c}", + "circle-letter-l-filled": "\u{fe74}", + "circle-letter-m": "\u{f44d}", + "circle-letter-m-filled": "\u{fe73}", + "circle-letter-n": "\u{f44e}", + "circle-letter-n-filled": "\u{fe72}", + "circle-letter-o": "\u{f44f}", + "circle-letter-o-filled": "\u{fe71}", + "circle-letter-p": "\u{f450}", + "circle-letter-p-filled": "\u{fe70}", + "circle-letter-q": "\u{f451}", + "circle-letter-q-filled": "\u{fe6f}", + "circle-letter-r": "\u{f452}", + "circle-letter-r-filled": "\u{fe6e}", + "circle-letter-s": "\u{f453}", + "circle-letter-s-filled": "\u{fe6d}", + "circle-letter-t": "\u{f454}", + "circle-letter-t-filled": "\u{fe6c}", + "circle-letter-u": "\u{f455}", + "circle-letter-u-filled": "\u{fe6b}", + "circle-letter-v": "\u{f4ad}", + "circle-letter-v-filled": "\u{fe6a}", + "circle-letter-w": "\u{f456}", + "circle-letter-w-filled": "\u{fe69}", + "circle-letter-x": "\u{f4ae}", + "circle-letter-x-filled": "\u{fe68}", + "circle-letter-y": "\u{f457}", + "circle-letter-y-filled": "\u{fe67}", + "circle-letter-z": "\u{f458}", + "circle-letter-z-filled": "\u{fe66}", + "circle-minus": "\u{ea68}", + "circle-minus-2": "\u{fc8c}", + "circle-number-0": "\u{ee34}", + "circle-number-0-filled": "\u{f72f}", + "circle-number-1": "\u{ee35}", + "circle-number-1-filled": "\u{f730}", + "circle-number-2": "\u{ee36}", + "circle-number-2-filled": "\u{f731}", + "circle-number-3": "\u{ee37}", + "circle-number-3-filled": "\u{f732}", + "circle-number-4": "\u{ee38}", + "circle-number-4-filled": "\u{f733}", + "circle-number-5": "\u{ee39}", + "circle-number-5-filled": "\u{f734}", + "circle-number-6": "\u{ee3a}", + "circle-number-6-filled": "\u{f735}", + "circle-number-7": "\u{ee3b}", + "circle-number-7-filled": "\u{f736}", + "circle-number-8": "\u{ee3c}", + "circle-number-8-filled": "\u{f737}", + "circle-number-9": "\u{ee3d}", + "circle-number-9-filled": "\u{f738}", + "circle-off": "\u{ee40}", + "circle-percentage": "\u{fd7b}", + "circle-percentage-filled": "\u{fed5}", + "circle-plus": "\u{ea69}", + "circle-plus-2": "\u{fc8d}", + "circle-plus-filled": "\u{fef9}", + "circle-rectangle": "\u{f010}", + "circle-rectangle-filled": "\u{ff63}", + "circle-rectangle-off": "\u{f0cd}", + "circle-square": "\u{ece4}", + "circle-triangle": "\u{f011}", + "circle-x": "\u{ea6a}", + "circle-x-filled": "\u{f739}", + "circles": "\u{ece5}", + "circles-filled": "\u{f672}", + "circles-relation": "\u{f4c3}", + "circuit-ammeter": "\u{f271}", + "circuit-battery": "\u{f272}", + "circuit-bulb": "\u{f273}", + "circuit-capacitor": "\u{f275}", + "circuit-capacitor-polarized": "\u{f274}", + "circuit-cell": "\u{f277}", + "circuit-cell-plus": "\u{f276}", + "circuit-changeover": "\u{f278}", + "circuit-diode": "\u{f27a}", + "circuit-diode-zener": "\u{f279}", + "circuit-ground": "\u{f27c}", + "circuit-ground-digital": "\u{f27b}", + "circuit-inductor": "\u{f27d}", + "circuit-motor": "\u{f27e}", + "circuit-pushbutton": "\u{f27f}", + "circuit-resistor": "\u{f280}", + "circuit-switch-closed": "\u{f281}", + "circuit-switch-open": "\u{f282}", + "circuit-voltmeter": "\u{f283}", + "clear-all": "\u{ee41}", + "clear-formatting": "\u{ebe5}", + "click": "\u{ebbc}", + "cliff-jumping": "\u{fefc}", + "clipboard": "\u{ea6f}", + "clipboard-check": "\u{ea6c}", + "clipboard-check-filled": "\u{100ce}", + "clipboard-copy": "\u{f299}", + "clipboard-data": "\u{f563}", + "clipboard-data-filled": "\u{100eb}", + "clipboard-filled": "\u{100cc}", + "clipboard-heart": "\u{f34e}", + "clipboard-list": "\u{ea6d}", + "clipboard-list-filled": "\u{100ea}", + "clipboard-off": "\u{f0ce}", + "clipboard-plus": "\u{efb2}", + "clipboard-plus-filled": "\u{10176}", + "clipboard-search": "\u{10098}", + "clipboard-smile": "\u{fd9a}", + "clipboard-smile-filled": "\u{10175}", + "clipboard-text": "\u{f089}", + "clipboard-text-filled": "\u{100e9}", + "clipboard-typography": "\u{f34f}", + "clipboard-typography-filled": "\u{100e8}", + "clipboard-x": "\u{ea6e}", + "clipboard-x-filled": "\u{100cd}", + "clock": "\u{ea70}", + "clock-12": "\u{fc56}", + "clock-2": "\u{f099}", + "clock-24": "\u{fc57}", + "clock-bitcoin": "\u{ff3f}", + "clock-bolt": "\u{f844}", + "clock-cancel": "\u{f546}", + "clock-check": "\u{f7c1}", + "clock-code": "\u{f845}", + "clock-cog": "\u{f7c2}", + "clock-dollar": "\u{f846}", + "clock-down": "\u{f7c3}", + "clock-edit": "\u{f547}", + "clock-exclamation": "\u{f847}", + "clock-filled": "\u{f73a}", + "clock-heart": "\u{f7c4}", + "clock-hour-1": "\u{f313}", + "clock-hour-1-filled": "\u{fe65}", + "clock-hour-10": "\u{f314}", + "clock-hour-10-filled": "\u{fe64}", + "clock-hour-11": "\u{f315}", + "clock-hour-11-filled": "\u{fe63}", + "clock-hour-12": "\u{f316}", + "clock-hour-12-filled": "\u{fe62}", + "clock-hour-2": "\u{f317}", + "clock-hour-2-filled": "\u{fe61}", + "clock-hour-3": "\u{f318}", + "clock-hour-3-filled": "\u{fe60}", + "clock-hour-4": "\u{f319}", + "clock-hour-4-filled": "\u{fe5f}", + "clock-hour-5": "\u{f31a}", + "clock-hour-5-filled": "\u{fe5e}", + "clock-hour-6": "\u{f31b}", + "clock-hour-6-filled": "\u{fe5d}", + "clock-hour-7": "\u{f31c}", + "clock-hour-7-filled": "\u{fe5c}", + "clock-hour-8": "\u{f31d}", + "clock-hour-8-filled": "\u{fe5b}", + "clock-hour-9": "\u{f31e}", + "clock-hour-9-filled": "\u{fe5a}", + "clock-minus": "\u{f848}", + "clock-off": "\u{f0cf}", + "clock-pause": "\u{f548}", + "clock-pin": "\u{f849}", + "clock-play": "\u{f549}", + "clock-plus": "\u{f7c5}", + "clock-question": "\u{f7c6}", + "clock-record": "\u{f54a}", + "clock-search": "\u{f7c7}", + "clock-share": "\u{f84a}", + "clock-shield": "\u{f7c8}", + "clock-star": "\u{f7c9}", + "clock-stop": "\u{f54b}", + "clock-up": "\u{f7ca}", + "clock-x": "\u{f7cb}", + "clothes-rack": "\u{f285}", + "clothes-rack-off": "\u{f3d6}", + "cloud": "\u{ea76}", + "cloud-bitcoin": "\u{ff3e}", + "cloud-bolt": "\u{f84b}", + "cloud-cancel": "\u{f84c}", + "cloud-check": "\u{f84d}", + "cloud-code": "\u{f84e}", + "cloud-cog": "\u{f84f}", + "cloud-computing": "\u{f1d0}", + "cloud-computing-filled": "\u{1010d}", + "cloud-data-connection": "\u{f1d1}", + "cloud-data-connection-filled": "\u{1010c}", + "cloud-dollar": "\u{f850}", + "cloud-down": "\u{f851}", + "cloud-download": "\u{ea71}", + "cloud-exclamation": "\u{f852}", + "cloud-filled": "\u{f673}", + "cloud-fog": "\u{ecd9}", + "cloud-heart": "\u{f853}", + "cloud-lock": "\u{efdb}", + "cloud-lock-open": "\u{efda}", + "cloud-minus": "\u{f854}", + "cloud-network": "\u{fc78}", + "cloud-off": "\u{ed3e}", + "cloud-pause": "\u{f855}", + "cloud-pin": "\u{f856}", + "cloud-plus": "\u{f857}", + "cloud-question": "\u{f858}", + "cloud-rain": "\u{ea72}", + "cloud-search": "\u{f859}", + "cloud-share": "\u{f85a}", + "cloud-snow": "\u{ea73}", + "cloud-star": "\u{f85b}", + "cloud-storm": "\u{ea74}", + "cloud-sun": "\u{ec6d}", + "cloud-up": "\u{f85c}", + "cloud-upload": "\u{ea75}", + "cloud-x": "\u{f85d}", + "clover": "\u{f1ea}", + "clover-2": "\u{f21e}", + "clover-filled": "\u{10013}", + "clubs": "\u{eff4}", + "clubs-filled": "\u{f674}", + "code": "\u{ea77}", + "code-asterisk": "\u{f312}", + "code-asterix": "\u{f312}", + "code-circle": "\u{f4ff}", + "code-circle-2": "\u{f4fe}", + "code-circle-2-filled": "\u{fed4}", + "code-circle-filled": "\u{fed3}", + "code-dots": "\u{f61a}", + "code-minus": "\u{ee42}", + "code-off": "\u{f0d0}", + "code-plus": "\u{ee43}", + "code-variable": "\u{100ab}", + "code-variable-minus": "\u{100ad}", + "code-variable-plus": "\u{100ac}", + "coffee": "\u{ef0e}", + "coffee-off": "\u{f106}", + "coffin": "\u{f579}", + "coin": "\u{eb82}", + "coin-bitcoin": "\u{f2be}", + "coin-bitcoin-filled": "\u{fd06}", + "coin-euro": "\u{f2bf}", + "coin-euro-filled": "\u{fd07}", + "coin-filled": "\u{fd08}", + "coin-monero": "\u{f4a0}", + "coin-monero-filled": "\u{fd09}", + "coin-off": "\u{f0d1}", + "coin-pound": "\u{f2c0}", + "coin-pound-filled": "\u{fd0a}", + "coin-rupee": "\u{f2c1}", + "coin-rupee-filled": "\u{fd0b}", + "coin-taka": "\u{fd0d}", + "coin-taka-filled": "\u{fd0c}", + "coin-yen": "\u{f2c2}", + "coin-yen-filled": "\u{fd0e}", + "coin-yuan": "\u{f2c3}", + "coin-yuan-filled": "\u{fd0f}", + "coins": "\u{f65d}", + "color-filter": "\u{f5a8}", + "color-picker": "\u{ebe6}", + "color-picker-off": "\u{f0d2}", + "color-swatch": "\u{eb61}", + "color-swatch-off": "\u{f0d3}", + "column-insert-left": "\u{ee44}", + "column-insert-right": "\u{ee45}", + "column-remove": "\u{faf4}", + "columns": "\u{eb83}", + "columns-1": "\u{f6d4}", + "columns-1-filled": "\u{10188}", + "columns-2": "\u{f6d5}", + "columns-2-filled": "\u{10187}", + "columns-3": "\u{f6d6}", + "columns-3-filled": "\u{10186}", + "columns-off": "\u{f0d4}", + "comet": "\u{ec76}", + "command": "\u{ea78}", + "command-off": "\u{f3d7}", + "compass": "\u{ea79}", + "compass-filled": "\u{fd10}", + "compass-off": "\u{f0d5}", + "components": "\u{efa5}", + "components-off": "\u{f0d6}", + "cone": "\u{efdd}", + "cone-2": "\u{efdc}", + "cone-2-filled": "\u{fe59}", + "cone-filled": "\u{fe58}", + "cone-off": "\u{f3d8}", + "cone-plus": "\u{fa94}", + "confetti": "\u{ee46}", + "confetti-filled": "\u{10185}", + "confetti-off": "\u{f3d9}", + "confucius": "\u{f58a}", + "congruent-to": "\u{ffa3}", + "container": "\u{ee47}", + "container-filled": "\u{10184}", + "container-off": "\u{f107}", + "contract": "\u{fefb}", + "contrast": "\u{ec4e}", + "contrast-2": "\u{efc7}", + "contrast-2-filled": "\u{fe57}", + "contrast-2-off": "\u{f3da}", + "contrast-filled": "\u{fe56}", + "contrast-off": "\u{f3db}", + "cooker": "\u{f57a}", + "cookie": "\u{fdb1}", + "cookie-filled": "\u{fe54}", + "cookie-man": "\u{fdb2}", + "cookie-man-filled": "\u{fe55}", + "cookie-off": "\u{f0d7}", + "copy": "\u{ea7a}", + "copy-check": "\u{fdb0}", + "copy-check-filled": "\u{fe53}", + "copy-minus": "\u{fdaf}", + "copy-minus-filled": "\u{fe52}", + "copy-off": "\u{f0d8}", + "copy-plus": "\u{fdae}", + "copy-plus-filled": "\u{fe51}", + "copy-x": "\u{fdad}", + "copy-x-filled": "\u{fe50}", + "copyleft": "\u{ec3d}", + "copyleft-filled": "\u{f73b}", + "copyleft-off": "\u{f0d9}", + "copyright": "\u{ea7b}", + "copyright-filled": "\u{f73c}", + "copyright-off": "\u{f0da}", + "corner-down-left": "\u{ea7c}", + "corner-down-left-double": "\u{ee48}", + "corner-down-right": "\u{ea7d}", + "corner-down-right-double": "\u{ee49}", + "corner-left-down": "\u{ea7e}", + "corner-left-down-double": "\u{ee4a}", + "corner-left-up": "\u{ea7f}", + "corner-left-up-double": "\u{ee4b}", + "corner-right-down": "\u{ea80}", + "corner-right-down-double": "\u{ee4c}", + "corner-right-up": "\u{ea81}", + "corner-right-up-double": "\u{ee4d}", + "corner-up-left": "\u{ea82}", + "corner-up-left-double": "\u{ee4e}", + "corner-up-right": "\u{ea83}", + "corner-up-right-double": "\u{ee4f}", + "cpu": "\u{ef8e}", + "cpu-2": "\u{f075}", + "cpu-off": "\u{f108}", + "crane": "\u{ef27}", + "crane-off": "\u{f109}", + "creative-commons": "\u{efb3}", + "creative-commons-by": "\u{f21f}", + "creative-commons-nc": "\u{f220}", + "creative-commons-nd": "\u{f221}", + "creative-commons-off": "\u{f10a}", + "creative-commons-sa": "\u{f222}", + "creative-commons-zero": "\u{f223}", + "credit-card": "\u{ea84}", + "credit-card-filled": "\u{fd11}", + "credit-card-off": "\u{ed11}", + "credit-card-pay": "\u{fd32}", + "credit-card-refund": "\u{fd33}", + "cricket": "\u{f09a}", + "crop": "\u{ea85}", + "crop-1-1": "\u{fd50}", + "crop-1-1-filled": "\u{fe4f}", + "crop-16-9": "\u{fd51}", + "crop-16-9-filled": "\u{fe4e}", + "crop-3-2": "\u{fd52}", + "crop-3-2-filled": "\u{fe4d}", + "crop-5-4": "\u{fd53}", + "crop-5-4-filled": "\u{fe4c}", + "crop-7-5": "\u{fd54}", + "crop-7-5-filled": "\u{fe4b}", + "crop-landscape": "\u{fd55}", + "crop-landscape-filled": "\u{fe4a}", + "crop-portrait": "\u{fd56}", + "crop-portrait-filled": "\u{fe49}", + "cross": "\u{ef8f}", + "cross-filled": "\u{f675}", + "cross-off": "\u{f10b}", + "crosshair": "\u{ec3e}", + "crown": "\u{ed12}", + "crown-off": "\u{ee50}", + "crutches": "\u{ef5b}", + "crutches-off": "\u{f10c}", + "crystal-ball": "\u{f57b}", + "csv": "\u{f791}", + "cube": "\u{fa97}", + "cube-3d-sphere": "\u{ecd7}", + "cube-3d-sphere-off": "\u{f3b5}", + "cube-off": "\u{fa95}", + "cube-plus": "\u{fa96}", + "cube-send": "\u{f61b}", + "cube-spark": "\u{ffbb}", + "cube-unfolded": "\u{f61c}", + "cup": "\u{ef28}", + "cup-off": "\u{f10d}", + "curling": "\u{efc8}", + "curly-loop": "\u{ecda}", + "currency": "\u{efa6}", + "currency-afghani": "\u{f65e}", + "currency-bahraini": "\u{ee51}", + "currency-baht": "\u{f08a}", + "currency-bitcoin": "\u{ebab}", + "currency-cent": "\u{ee53}", + "currency-dinar": "\u{ee54}", + "currency-dirham": "\u{ee55}", + "currency-dogecoin": "\u{ef4b}", + "currency-dollar": "\u{eb84}", + "currency-dollar-australian": "\u{ee56}", + "currency-dollar-brunei": "\u{f36c}", + "currency-dollar-canadian": "\u{ee57}", + "currency-dollar-guyanese": "\u{f36d}", + "currency-dollar-off": "\u{f3dc}", + "currency-dollar-singapore": "\u{ee58}", + "currency-dollar-zimbabwean": "\u{f36e}", + "currency-dong": "\u{f36f}", + "currency-dram": "\u{f370}", + "currency-ethereum": "\u{ee59}", + "currency-euro": "\u{eb85}", + "currency-euro-off": "\u{f3dd}", + "currency-florin": "\u{faf5}", + "currency-forint": "\u{ee5a}", + "currency-frank": "\u{ee5b}", + "currency-guarani": "\u{f371}", + "currency-hryvnia": "\u{f372}", + "currency-iranian-rial": "\u{fa58}", + "currency-kip": "\u{f373}", + "currency-krone-czech": "\u{ee5c}", + "currency-krone-danish": "\u{ee5d}", + "currency-krone-swedish": "\u{ee5e}", + "currency-lari": "\u{f374}", + "currency-leu": "\u{ee5f}", + "currency-lira": "\u{ee60}", + "currency-litecoin": "\u{ee61}", + "currency-lyd": "\u{f375}", + "currency-manat": "\u{f376}", + "currency-monero": "\u{f377}", + "currency-naira": "\u{ee62}", + "currency-nano": "\u{f7a6}", + "currency-off": "\u{f3de}", + "currency-paanga": "\u{f378}", + "currency-peso": "\u{f65f}", + "currency-pound": "\u{ebac}", + "currency-pound-off": "\u{f3df}", + "currency-quetzal": "\u{f379}", + "currency-real": "\u{ee63}", + "currency-renminbi": "\u{ee64}", + "currency-ripple": "\u{ee65}", + "currency-riyal": "\u{ee66}", + "currency-rubel": "\u{ee67}", + "currency-rufiyaa": "\u{f37a}", + "currency-rupee": "\u{ebad}", + "currency-rupee-nepalese": "\u{f37b}", + "currency-shekel": "\u{ee68}", + "currency-solana": "\u{f4a1}", + "currency-som": "\u{f37c}", + "currency-taka": "\u{ee69}", + "currency-tenge": "\u{f37d}", + "currency-tugrik": "\u{ee6a}", + "currency-won": "\u{ee6b}", + "currency-xrp": "\u{fd34}", + "currency-yen": "\u{ebae}", + "currency-yen-off": "\u{f3e0}", + "currency-yuan": "\u{f29a}", + "currency-zloty": "\u{ee6c}", + "current-location": "\u{ecef}", + "current-location-filled": "\u{10125}", + "current-location-off": "\u{f10e}", + "cursor-off": "\u{f10f}", + "cursor-text": "\u{ee6d}", + "cut": "\u{ea86}", + "cylinder": "\u{f54c}", + "cylinder-off": "\u{fa98}", + "cylinder-plus": "\u{fa99}", + "dashboard": "\u{ea87}", + "dashboard-filled": "\u{10019}", + "dashboard-off": "\u{f3e1}", + "database": "\u{ea88}", + "database-cog": "\u{fa10}", + "database-dollar": "\u{fa11}", + "database-edit": "\u{fa12}", + "database-exclamation": "\u{fa13}", + "database-export": "\u{ee6e}", + "database-heart": "\u{fa14}", + "database-import": "\u{ee6f}", + "database-leak": "\u{fa15}", + "database-minus": "\u{fa16}", + "database-off": "\u{ee70}", + "database-plus": "\u{fa17}", + "database-search": "\u{fa18}", + "database-share": "\u{fa19}", + "database-smile": "\u{fd9b}", + "database-star": "\u{fa1a}", + "database-x": "\u{fa1b}", + "decimal": "\u{fa26}", + "deer": "\u{f4c5}", + "delta": "\u{f53c}", + "dental": "\u{f025}", + "dental-broken": "\u{f286}", + "dental-off": "\u{f110}", + "deselect": "\u{f9f3}", + "desk": "\u{fd35}", + "details": "\u{ee71}", + "details-off": "\u{f3e2}", + "device-airpods": "\u{f5a9}", + "device-airpods-case": "\u{f646}", + "device-airtag": "\u{fae6}", + "device-analytics": "\u{ee72}", + "device-audio-tape": "\u{ee73}", + "device-camera-phone": "\u{f233}", + "device-cctv": "\u{ee74}", + "device-cctv-filled": "\u{1004b}", + "device-cctv-off": "\u{f3e3}", + "device-computer-camera": "\u{ee76}", + "device-computer-camera-off": "\u{ee75}", + "device-desktop": "\u{ea89}", + "device-desktop-analytics": "\u{ee77}", + "device-desktop-bolt": "\u{f85e}", + "device-desktop-cancel": "\u{f85f}", + "device-desktop-check": "\u{f860}", + "device-desktop-code": "\u{f861}", + "device-desktop-cog": "\u{f862}", + "device-desktop-dollar": "\u{f863}", + "device-desktop-down": "\u{f864}", + "device-desktop-exclamation": "\u{f865}", + "device-desktop-filled": "\u{1004a}", + "device-desktop-heart": "\u{f866}", + "device-desktop-minus": "\u{f867}", + "device-desktop-off": "\u{ee78}", + "device-desktop-pause": "\u{f868}", + "device-desktop-pin": "\u{f869}", + "device-desktop-plus": "\u{f86a}", + "device-desktop-question": "\u{f86b}", + "device-desktop-search": "\u{f86c}", + "device-desktop-share": "\u{f86d}", + "device-desktop-star": "\u{f86e}", + "device-desktop-up": "\u{f86f}", + "device-desktop-x": "\u{f870}", + "device-floppy": "\u{eb62}", + "device-gamepad": "\u{eb63}", + "device-gamepad-2": "\u{f1d2}", + "device-gamepad-3": "\u{fc58}", + "device-gamepad-3-filled": "\u{10049}", + "device-heart-monitor": "\u{f060}", + "device-heart-monitor-filled": "\u{fa38}", + "device-imac": "\u{f7a7}", + "device-imac-bolt": "\u{f871}", + "device-imac-cancel": "\u{f872}", + "device-imac-check": "\u{f873}", + "device-imac-code": "\u{f874}", + "device-imac-cog": "\u{f875}", + "device-imac-dollar": "\u{f876}", + "device-imac-down": "\u{f877}", + "device-imac-exclamation": "\u{f878}", + "device-imac-filled": "\u{10048}", + "device-imac-heart": "\u{f879}", + "device-imac-minus": "\u{f87a}", + "device-imac-off": "\u{f87b}", + "device-imac-pause": "\u{f87c}", + "device-imac-pin": "\u{f87d}", + "device-imac-plus": "\u{f87e}", + "device-imac-question": "\u{f87f}", + "device-imac-search": "\u{f880}", + "device-imac-share": "\u{f881}", + "device-imac-star": "\u{f882}", + "device-imac-up": "\u{f883}", + "device-imac-x": "\u{f884}", + "device-ipad": "\u{f648}", + "device-ipad-bolt": "\u{f885}", + "device-ipad-cancel": "\u{f886}", + "device-ipad-check": "\u{f887}", + "device-ipad-code": "\u{f888}", + "device-ipad-cog": "\u{f889}", + "device-ipad-dollar": "\u{f88a}", + "device-ipad-down": "\u{f88b}", + "device-ipad-exclamation": "\u{f88c}", + "device-ipad-filled": "\u{10047}", + "device-ipad-heart": "\u{f88d}", + "device-ipad-horizontal": "\u{f647}", + "device-ipad-horizontal-bolt": "\u{f88e}", + "device-ipad-horizontal-cancel": "\u{f88f}", + "device-ipad-horizontal-check": "\u{f890}", + "device-ipad-horizontal-code": "\u{f891}", + "device-ipad-horizontal-cog": "\u{f892}", + "device-ipad-horizontal-dollar": "\u{f893}", + "device-ipad-horizontal-down": "\u{f894}", + "device-ipad-horizontal-exclamation": "\u{f895}", + "device-ipad-horizontal-heart": "\u{f896}", + "device-ipad-horizontal-minus": "\u{f897}", + "device-ipad-horizontal-off": "\u{f898}", + "device-ipad-horizontal-pause": "\u{f899}", + "device-ipad-horizontal-pin": "\u{f89a}", + "device-ipad-horizontal-plus": "\u{f89b}", + "device-ipad-horizontal-question": "\u{f89c}", + "device-ipad-horizontal-search": "\u{f89d}", + "device-ipad-horizontal-share": "\u{f89e}", + "device-ipad-horizontal-star": "\u{f89f}", + "device-ipad-horizontal-up": "\u{f8a0}", + "device-ipad-horizontal-x": "\u{f8a1}", + "device-ipad-minus": "\u{f8a2}", + "device-ipad-off": "\u{f8a3}", + "device-ipad-pause": "\u{f8a4}", + "device-ipad-pin": "\u{f8a5}", + "device-ipad-plus": "\u{f8a6}", + "device-ipad-question": "\u{f8a7}", + "device-ipad-search": "\u{f8a8}", + "device-ipad-share": "\u{f8a9}", + "device-ipad-star": "\u{f8aa}", + "device-ipad-up": "\u{f8ab}", + "device-ipad-x": "\u{f8ac}", + "device-landline-phone": "\u{f649}", + "device-laptop": "\u{eb64}", + "device-laptop-off": "\u{f061}", + "device-mobile": "\u{ea8a}", + "device-mobile-bolt": "\u{f8ad}", + "device-mobile-cancel": "\u{f8ae}", + "device-mobile-charging": "\u{f224}", + "device-mobile-check": "\u{f8af}", + "device-mobile-code": "\u{f8b0}", + "device-mobile-cog": "\u{f8b1}", + "device-mobile-dollar": "\u{f8b2}", + "device-mobile-down": "\u{f8b3}", + "device-mobile-exclamation": "\u{f8b4}", + "device-mobile-filled": "\u{fa39}", + "device-mobile-heart": "\u{f8b5}", + "device-mobile-message": "\u{ee79}", + "device-mobile-minus": "\u{f8b6}", + "device-mobile-off": "\u{f062}", + "device-mobile-pause": "\u{f8b7}", + "device-mobile-pin": "\u{f8b8}", + "device-mobile-plus": "\u{f8b9}", + "device-mobile-question": "\u{f8ba}", + "device-mobile-rotated": "\u{ecdb}", + "device-mobile-search": "\u{f8bb}", + "device-mobile-share": "\u{f8bc}", + "device-mobile-star": "\u{f8bd}", + "device-mobile-up": "\u{f8be}", + "device-mobile-vibration": "\u{eb86}", + "device-mobile-x": "\u{f8bf}", + "device-nintendo": "\u{f026}", + "device-nintendo-off": "\u{f111}", + "device-projector": "\u{fc11}", + "device-remote": "\u{f792}", + "device-remote-filled": "\u{10046}", + "device-sd-card": "\u{f384}", + "device-sim": "\u{f4b2}", + "device-sim-1": "\u{f4af}", + "device-sim-2": "\u{f4b0}", + "device-sim-3": "\u{f4b1}", + "device-speaker": "\u{ea8b}", + "device-speaker-filled": "\u{10045}", + "device-speaker-off": "\u{f112}", + "device-tablet": "\u{ea8c}", + "device-tablet-bolt": "\u{f8c0}", + "device-tablet-cancel": "\u{f8c1}", + "device-tablet-check": "\u{f8c2}", + "device-tablet-code": "\u{f8c3}", + "device-tablet-cog": "\u{f8c4}", + "device-tablet-dollar": "\u{f8c5}", + "device-tablet-down": "\u{f8c6}", + "device-tablet-exclamation": "\u{f8c7}", + "device-tablet-filled": "\u{fa3a}", + "device-tablet-heart": "\u{f8c8}", + "device-tablet-minus": "\u{f8c9}", + "device-tablet-off": "\u{f063}", + "device-tablet-pause": "\u{f8ca}", + "device-tablet-pin": "\u{f8cb}", + "device-tablet-plus": "\u{f8cc}", + "device-tablet-question": "\u{f8cd}", + "device-tablet-search": "\u{f8ce}", + "device-tablet-share": "\u{f8cf}", + "device-tablet-star": "\u{f8d0}", + "device-tablet-up": "\u{f8d1}", + "device-tablet-x": "\u{f8d2}", + "device-tv": "\u{ea8d}", + "device-tv-filled": "\u{10043}", + "device-tv-off": "\u{f064}", + "device-tv-old": "\u{f1d3}", + "device-tv-old-filled": "\u{10044}", + "device-unknown": "\u{fef4}", + "device-unknown-filled": "\u{10018}", + "device-usb": "\u{fc59}", + "device-usb-filled": "\u{10042}", + "device-vision-pro": "\u{fae7}", + "device-vision-pro-filled": "\u{10041}", + "device-watch": "\u{ebf9}", + "device-watch-bolt": "\u{f8d3}", + "device-watch-cancel": "\u{f8d4}", + "device-watch-check": "\u{f8d5}", + "device-watch-code": "\u{f8d6}", + "device-watch-cog": "\u{f8d7}", + "device-watch-dollar": "\u{f8d8}", + "device-watch-down": "\u{f8d9}", + "device-watch-exclamation": "\u{f8da}", + "device-watch-filled": "\u{10040}", + "device-watch-heart": "\u{f8db}", + "device-watch-minus": "\u{f8dc}", + "device-watch-off": "\u{f065}", + "device-watch-pause": "\u{f8dd}", + "device-watch-pin": "\u{f8de}", + "device-watch-plus": "\u{f8df}", + "device-watch-question": "\u{f8e0}", + "device-watch-search": "\u{f8e1}", + "device-watch-share": "\u{f8e2}", + "device-watch-star": "\u{f8e3}", + "device-watch-stats": "\u{ef7d}", + "device-watch-stats-2": "\u{ef7c}", + "device-watch-up": "\u{f8e4}", + "device-watch-x": "\u{f8e5}", + "devices": "\u{eb87}", + "devices-2": "\u{ed29}", + "devices-bolt": "\u{f8e6}", + "devices-cancel": "\u{f8e7}", + "devices-check": "\u{f8e8}", + "devices-code": "\u{f8e9}", + "devices-cog": "\u{f8ea}", + "devices-dollar": "\u{f8eb}", + "devices-down": "\u{f8ec}", + "devices-exclamation": "\u{f8ed}", + "devices-heart": "\u{f8ee}", + "devices-minus": "\u{f8ef}", + "devices-off": "\u{f3e4}", + "devices-pause": "\u{f8f0}", + "devices-pc": "\u{ee7a}", + "devices-pc-off": "\u{f113}", + "devices-pin": "\u{f8f1}", + "devices-plus": "\u{f8f2}", + "devices-question": "\u{f8f3}", + "devices-search": "\u{f8f4}", + "devices-share": "\u{f8f5}", + "devices-star": "\u{f8f6}", + "devices-up": "\u{f8f7}", + "devices-x": "\u{f8f8}", + "diabolo": "\u{fa9c}", + "diabolo-off": "\u{fa9a}", + "diabolo-plus": "\u{fa9b}", + "dialpad": "\u{f067}", + "dialpad-filled": "\u{fa3b}", + "dialpad-off": "\u{f114}", + "diamond": "\u{eb65}", + "diamond-filled": "\u{f73d}", + "diamond-off": "\u{f115}", + "diamonds": "\u{eff5}", + "diamonds-filled": "\u{f676}", + "diaper": "\u{ffa2}", + "dice": "\u{eb66}", + "dice-1": "\u{f08b}", + "dice-1-filled": "\u{f73e}", + "dice-2": "\u{f08c}", + "dice-2-filled": "\u{f73f}", + "dice-3": "\u{f08d}", + "dice-3-filled": "\u{f740}", + "dice-4": "\u{f08e}", + "dice-4-filled": "\u{f741}", + "dice-5": "\u{f08f}", + "dice-5-filled": "\u{f742}", + "dice-6": "\u{f090}", + "dice-6-filled": "\u{f743}", + "dice-filled": "\u{f744}", + "dimensions": "\u{ee7b}", + "direction": "\u{ebfb}", + "direction-arrows": "\u{fd36}", + "direction-arrows-filled": "\u{100ca}", + "direction-horizontal": "\u{ebfa}", + "direction-sign": "\u{f1f7}", + "direction-sign-filled": "\u{f745}", + "direction-sign-off": "\u{f3e5}", + "directions": "\u{ea8e}", + "directions-filled": "\u{1003f}", + "directions-off": "\u{f116}", + "disabled": "\u{ea8f}", + "disabled-2": "\u{ebaf}", + "disabled-off": "\u{f117}", + "disc": "\u{ea90}", + "disc-filled": "\u{1003e}", + "disc-golf": "\u{f385}", + "disc-off": "\u{f118}", + "discount": "\u{ebbd}", + "discount-2": "\u{ee7c}", + "discount-2-off": "\u{f3e6}", + "discount-check": "\u{f1f8}", + "discount-check-filled": "\u{f746}", + "discount-filled": "\u{1003d}", + "discount-off": "\u{f3e7}", + "divide": "\u{ed5c}", + "dna": "\u{ee7d}", + "dna-2": "\u{ef5c}", + "dna-2-off": "\u{f119}", + "dna-off": "\u{f11a}", + "dog": "\u{f660}", + "dog-bowl": "\u{ef29}", + "door": "\u{ef4e}", + "door-enter": "\u{ef4c}", + "door-exit": "\u{ef4d}", + "door-off": "\u{f11b}", + "dots": "\u{ea95}", + "dots-circle-horizontal": "\u{ea91}", + "dots-diagonal": "\u{ea93}", + "dots-diagonal-2": "\u{ea92}", + "dots-vertical": "\u{ea94}", + "download": "\u{ea96}", + "download-off": "\u{f11c}", + "drag-drop": "\u{eb89}", + "drag-drop-2": "\u{eb88}", + "drone": "\u{ed79}", + "drone-off": "\u{ee7e}", + "drop-circle": "\u{efde}", + "drop-circle-filled": "\u{10137}", + "droplet": "\u{ea97}", + "droplet-bolt": "\u{f8f9}", + "droplet-cancel": "\u{f8fa}", + "droplet-check": "\u{f8fb}", + "droplet-code": "\u{f8fc}", + "droplet-cog": "\u{f8fd}", + "droplet-dollar": "\u{f8fe}", + "droplet-down": "\u{f8ff}", + "droplet-exclamation": "\u{f900}", + "droplet-filled": "\u{ee80}", + "droplet-half": "\u{ee82}", + "droplet-half-2": "\u{ee81}", + "droplet-half-2-filled": "\u{fb6c}", + "droplet-half-filled": "\u{f6c5}", + "droplet-heart": "\u{f901}", + "droplet-minus": "\u{f902}", + "droplet-off": "\u{ee83}", + "droplet-pause": "\u{f903}", + "droplet-pin": "\u{f904}", + "droplet-plus": "\u{f905}", + "droplet-question": "\u{f906}", + "droplet-search": "\u{f907}", + "droplet-share": "\u{f908}", + "droplet-star": "\u{f909}", + "droplet-up": "\u{f90a}", + "droplet-x": "\u{f90b}", + "droplets": "\u{fc12}", + "droplets-filled": "\u{100c9}", + "dual-screen": "\u{fa59}", + "dual-screen-filled": "\u{10136}", + "dumpling": "\u{feb5}", + "dumpling-filled": "\u{10135}", + "e-passport": "\u{f4df}", + "ear": "\u{ebce}", + "ear-off": "\u{ee84}", + "ear-scan": "\u{fd57}", + "ease-in": "\u{f573}", + "ease-in-control-point": "\u{f570}", + "ease-in-control-point-filled": "\u{10174}", + "ease-in-out": "\u{f572}", + "ease-in-out-control-points": "\u{f571}", + "ease-in-out-control-points-filled": "\u{10173}", + "ease-out": "\u{f575}", + "ease-out-control-point": "\u{f574}", + "ease-out-control-point-filled": "\u{10172}", + "edit": "\u{ea98}", + "edit-circle": "\u{ee85}", + "edit-circle-off": "\u{f11d}", + "edit-off": "\u{f11e}", + "egg": "\u{eb8a}", + "egg-cracked": "\u{f2d6}", + "egg-cracked-filled": "\u{10012}", + "egg-filled": "\u{f678}", + "egg-fried": "\u{f386}", + "egg-fried-filled": "\u{10134}", + "egg-off": "\u{f11f}", + "eggs": "\u{f500}", + "elevator": "\u{efdf}", + "elevator-filled": "\u{1003c}", + "elevator-off": "\u{f3e8}", + "emergency-bed": "\u{ef5d}", + "empathize": "\u{f29b}", + "empathize-off": "\u{f3e9}", + "emphasis": "\u{ebcf}", + "engine": "\u{ef7e}", + "engine-filled": "\u{100fc}", + "engine-off": "\u{f120}", + "equal": "\u{ee87}", + "equal-double": "\u{f4e1}", + "equal-not": "\u{ee86}", + "eraser": "\u{eb8b}", + "eraser-off": "\u{f121}", + "error-404": "\u{f027}", + "error-404-off": "\u{f122}", + "escalator": "\u{fb06}", + "escalator-down": "\u{fb04}", + "escalator-down-filled": "\u{10133}", + "escalator-filled": "\u{10131}", + "escalator-up": "\u{fb05}", + "escalator-up-filled": "\u{10132}", + "ethernet": "\u{eccc}", + "ethernet-off": "\u{eccd}", + "ethernet-exclamation": "\u{ecce}", + "ethernet-question": "\u{eccf}", + "exchange": "\u{ebe7}", + "exchange-filled": "\u{10130}", + "exchange-off": "\u{f123}", + "exclamation-circle": "\u{f634}", + "exclamation-circle-filled": "\u{ff62}", + "exclamation-mark": "\u{efb4}", + "exclamation-mark-off": "\u{f124}", + "explicit": "\u{f256}", + "explicit-filled": "\u{1012f}", + "explicit-off": "\u{f3ea}", + "exposure": "\u{eb8c}", + "exposure-0": "\u{f29c}", + "exposure-filled": "\u{10124}", + "exposure-minus-1": "\u{f29d}", + "exposure-minus-2": "\u{f29e}", + "exposure-off": "\u{f3eb}", + "exposure-plus-1": "\u{f29f}", + "exposure-plus-2": "\u{f2a0}", + "external-link": "\u{ea99}", + "external-link-off": "\u{f125}", + "eye": "\u{ea9a}", + "eye-bitcoin": "\u{ff3d}", + "eye-bolt": "\u{fb6d}", + "eye-cancel": "\u{fb6e}", + "eye-check": "\u{ee88}", + "eye-closed": "\u{f7ec}", + "eye-code": "\u{fb6f}", + "eye-cog": "\u{f7ed}", + "eye-discount": "\u{fb70}", + "eye-dollar": "\u{fb71}", + "eye-dotted": "\u{fead}", + "eye-down": "\u{fb72}", + "eye-edit": "\u{f7ee}", + "eye-exclamation": "\u{f7ef}", + "eye-filled": "\u{f679}", + "eye-heart": "\u{f7f0}", + "eye-minus": "\u{fb73}", + "eye-off": "\u{ecf0}", + "eye-pause": "\u{fb74}", + "eye-pin": "\u{fb75}", + "eye-plus": "\u{fb76}", + "eye-question": "\u{fb77}", + "eye-search": "\u{fb78}", + "eye-share": "\u{fb79}", + "eye-spark": "\u{ffba}", + "eye-star": "\u{fb7a}", + "eye-table": "\u{ef5e}", + "eye-table-filled": "\u{10123}", + "eye-up": "\u{fb7b}", + "eye-x": "\u{f7f1}", + "eyeglass": "\u{ee8a}", + "eyeglass-2": "\u{ee89}", + "eyeglass-2-filled": "\u{10122}", + "eyeglass-filled": "\u{100c8}", + "eyeglass-off": "\u{f126}", + "face-id": "\u{ea9b}", + "face-id-error": "\u{efa7}", + "face-mask": "\u{efb5}", + "face-mask-filled": "\u{10121}", + "face-mask-off": "\u{f127}", + "fall": "\u{ecb9}", + "favicon": "\u{fd65}", + "favicon-filled": "\u{10071}", + "feather": "\u{ee8b}", + "feather-filled": "\u{10011}", + "feather-off": "\u{f128}", + "fence": "\u{ef2a}", + "fence-filled": "\u{10120}", + "fence-off": "\u{f129}", + "ferry": "\u{10074}", + "ferry-filled": "\u{100fb}", + "fidget-spinner": "\u{f068}", + "fidget-spinner-filled": "\u{1011f}", + "file": "\u{eaa4}", + "file-3d": "\u{f032}", + "file-ai": "\u{ffa1}", + "file-alert": "\u{ede6}", + "file-analytics": "\u{ede7}", + "file-analytics-filled": "\u{10171}", + "file-arrow-left": "\u{f033}", + "file-arrow-right": "\u{f034}", + "file-barcode": "\u{f035}", + "file-bitcoin": "\u{ffa0}", + "file-broken": "\u{f501}", + "file-certificate": "\u{ed4d}", + "file-chart": "\u{f036}", + "file-check": "\u{ea9c}", + "file-check-filled": "\u{1012e}", + "file-code": "\u{ebd0}", + "file-code-2": "\u{ede8}", + "file-code-2-filled": "\u{1012d}", + "file-code-filled": "\u{10170}", + "file-cv": "\u{fa5a}", + "file-cv-filled": "\u{1012c}", + "file-database": "\u{f037}", + "file-delta": "\u{f53d}", + "file-delta-filled": "\u{1012b}", + "file-description": "\u{f028}", + "file-description-filled": "\u{1011e}", + "file-diff": "\u{ecf1}", + "file-diff-filled": "\u{1016f}", + "file-digit": "\u{efa8}", + "file-digit-filled": "\u{1011d}", + "file-dislike": "\u{ed2a}", + "file-dollar": "\u{efe0}", + "file-dots": "\u{f038}", + "file-dots-filled": "\u{1016e}", + "file-download": "\u{ea9d}", + "file-download-filled": "\u{1012a}", + "file-euro": "\u{efe1}", + "file-excel": "\u{fef3}", + "file-export": "\u{ede9}", + "file-filled": "\u{f747}", + "file-function": "\u{f53e}", + "file-function-filled": "\u{1016d}", + "file-horizontal": "\u{ebb0}", + "file-horizontal-filled": "\u{1011c}", + "file-import": "\u{edea}", + "file-infinity": "\u{f502}", + "file-info": "\u{edec}", + "file-info-filled": "\u{1016c}", + "file-invoice": "\u{eb67}", + "file-invoice-filled": "\u{1011b}", + "file-isr": "\u{feac}", + "file-lambda": "\u{f53f}", + "file-lambda-filled": "\u{10129}", + "file-like": "\u{ed2b}", + "file-minus": "\u{ea9e}", + "file-minus-filled": "\u{1011a}", + "file-music": "\u{ea9f}", + "file-neutral": "\u{fd22}", + "file-neutral-filled": "\u{10119}", + "file-off": "\u{ecf2}", + "file-orientation": "\u{f2a1}", + "file-pencil": "\u{f039}", + "file-percent": "\u{f540}", + "file-percent-filled": "\u{10128}", + "file-phone": "\u{ecdc}", + "file-phone-filled": "\u{10127}", + "file-plus": "\u{eaa0}", + "file-power": "\u{f03a}", + "file-power-filled": "\u{10118}", + "file-report": "\u{eded}", + "file-rss": "\u{f03b}", + "file-rss-filled": "\u{10126}", + "file-sad": "\u{fd23}", + "file-sad-filled": "\u{10117}", + "file-scissors": "\u{f03c}", + "file-search": "\u{ed5d}", + "file-settings": "\u{f029}", + "file-shredder": "\u{eaa1}", + "file-signal": "\u{f03d}", + "file-smile": "\u{fd24}", + "file-smile-filled": "\u{10116}", + "file-spark": "\u{ffb9}", + "file-spreadsheet": "\u{f03e}", + "file-stack": "\u{f503}", + "file-star": "\u{f03f}", + "file-star-filled": "\u{10115}", + "file-symlink": "\u{ed53}", + "file-text": "\u{eaa2}", + "file-text-ai": "\u{fa27}", + "file-text-filled": "\u{10114}", + "file-text-shield": "\u{100f2}", + "file-text-spark": "\u{ffb8}", + "file-time": "\u{f040}", + "file-type-bmp": "\u{fb07}", + "file-type-css": "\u{fb08}", + "file-type-csv": "\u{fb09}", + "file-type-doc": "\u{fb0a}", + "file-type-docx": "\u{fb0b}", + "file-type-html": "\u{fb0c}", + "file-type-jpg": "\u{fb0d}", + "file-type-js": "\u{fb0e}", + "file-type-jsx": "\u{fb0f}", + "file-type-pdf": "\u{fb10}", + "file-type-php": "\u{fb11}", + "file-type-png": "\u{fb12}", + "file-type-ppt": "\u{fb13}", + "file-type-rs": "\u{fb14}", + "file-type-sql": "\u{fb15}", + "file-type-svg": "\u{fb16}", + "file-type-ts": "\u{fb17}", + "file-type-tsx": "\u{fb18}", + "file-type-txt": "\u{fb19}", + "file-type-vue": "\u{fb1a}", + "file-type-xls": "\u{fb1b}", + "file-type-xml": "\u{fb1c}", + "file-type-zip": "\u{fb1d}", + "file-typography": "\u{f041}", + "file-typography-filled": "\u{1016b}", + "file-unknown": "\u{f042}", + "file-upload": "\u{ec91}", + "file-vector": "\u{f043}", + "file-word": "\u{fef2}", + "file-x": "\u{eaa3}", + "file-x-filled": "\u{f748}", + "file-zip": "\u{ed4e}", + "files": "\u{edef}", + "files-off": "\u{edee}", + "filter": "\u{eaa5}", + "filter-2": "\u{1014b}", + "filter-2-bolt": "\u{1015f}", + "filter-2-cancel": "\u{1015e}", + "filter-2-check": "\u{1015d}", + "filter-2-code": "\u{1015c}", + "filter-2-cog": "\u{1015b}", + "filter-2-discount": "\u{1015a}", + "filter-2-dollar": "\u{10159}", + "filter-2-down": "\u{10158}", + "filter-2-edit": "\u{10157}", + "filter-2-exclamation": "\u{10156}", + "filter-2-minus": "\u{10155}", + "filter-2-pause": "\u{10154}", + "filter-2-pin": "\u{10153}", + "filter-2-plus": "\u{10152}", + "filter-2-question": "\u{10151}", + "filter-2-search": "\u{10150}", + "filter-2-share": "\u{1014f}", + "filter-2-spark": "\u{1014e}", + "filter-2-up": "\u{1014d}", + "filter-2-x": "\u{1014c}", + "filter-bolt": "\u{fb7c}", + "filter-cancel": "\u{fb7d}", + "filter-check": "\u{fb7e}", + "filter-code": "\u{fb7f}", + "filter-cog": "\u{f9fe}", + "filter-discount": "\u{fb80}", + "filter-dollar": "\u{f9ff}", + "filter-down": "\u{fb81}", + "filter-edit": "\u{fa00}", + "filter-exclamation": "\u{fb82}", + "filter-filled": "\u{fc27}", + "filter-heart": "\u{fb83}", + "filter-minus": "\u{fa01}", + "filter-off": "\u{ed2c}", + "filter-pause": "\u{fb84}", + "filter-pin": "\u{fb85}", + "filter-plus": "\u{fa02}", + "filter-question": "\u{fb86}", + "filter-search": "\u{fb87}", + "filter-share": "\u{fb88}", + "filter-spark": "\u{1014a}", + "filter-star": "\u{fa03}", + "filter-up": "\u{fb89}", + "filter-x": "\u{fa04}", + "filters": "\u{f793}", + "filters-filled": "\u{100c7}", + "fingerprint": "\u{ebd1}", + "fingerprint-off": "\u{f12a}", + "fingerprint-scan": "\u{fcb5}", + "fire-extinguisher": "\u{faf6}", + "fire-hydrant": "\u{f3a9}", + "fire-hydrant-off": "\u{f3ec}", + "firetruck": "\u{ebe8}", + "first-aid-kit": "\u{ef5f}", + "first-aid-kit-off": "\u{f3ed}", + "fish": "\u{ef2b}", + "fish-bone": "\u{f287}", + "fish-bone-filled": "\u{1010b}", + "fish-christianity": "\u{f58b}", + "fish-hook": "\u{f1f9}", + "fish-hook-off": "\u{f3ee}", + "fish-off": "\u{f12b}", + "flag": "\u{eaa6}", + "flag-2": "\u{ee8c}", + "flag-2-filled": "\u{f707}", + "flag-2-off": "\u{f12c}", + "flag-3": "\u{ee8d}", + "flag-3-filled": "\u{f708}", + "flag-bitcoin": "\u{ff3c}", + "flag-bolt": "\u{fb8a}", + "flag-cancel": "\u{fb8b}", + "flag-check": "\u{fb8c}", + "flag-code": "\u{fb8d}", + "flag-cog": "\u{fb8e}", + "flag-discount": "\u{fb8f}", + "flag-dollar": "\u{fb90}", + "flag-down": "\u{fb91}", + "flag-exclamation": "\u{fb92}", + "flag-filled": "\u{f67a}", + "flag-heart": "\u{fb93}", + "flag-minus": "\u{fb94}", + "flag-off": "\u{f12d}", + "flag-pause": "\u{fb95}", + "flag-pin": "\u{fb96}", + "flag-plus": "\u{fb97}", + "flag-question": "\u{fb98}", + "flag-search": "\u{fb99}", + "flag-share": "\u{fb9a}", + "flag-spark": "\u{ffb7}", + "flag-star": "\u{fb9b}", + "flag-up": "\u{fb9c}", + "flag-x": "\u{fb9d}", + "flame": "\u{ec2c}", + "flame-filled": "\u{100c6}", + "flame-off": "\u{f12e}", + "flare": "\u{ee8e}", + "flare-filled": "\u{100c5}", + "flask": "\u{ebd2}", + "flask-2": "\u{ef60}", + "flask-2-filled": "\u{fd12}", + "flask-2-off": "\u{f12f}", + "flask-filled": "\u{fd13}", + "flask-off": "\u{f130}", + "flip-flops": "\u{f564}", + "flip-horizontal": "\u{eaa7}", + "flip-vertical": "\u{eaa8}", + "float-center": "\u{ebb1}", + "float-left": "\u{ebb2}", + "float-none": "\u{ed13}", + "float-right": "\u{ebb3}", + "flower": "\u{eff6}", + "flower-filled": "\u{10010}", + "flower-off": "\u{f131}", + "focus": "\u{eb8d}", + "focus-2": "\u{ebd3}", + "focus-auto": "\u{fa62}", + "focus-centered": "\u{f02a}", + "fold": "\u{ed56}", + "fold-down": "\u{ed54}", + "fold-up": "\u{ed55}", + "folder": "\u{eaad}", + "folder-bolt": "\u{f90c}", + "folder-cancel": "\u{f90d}", + "folder-check": "\u{f90e}", + "folder-code": "\u{f90f}", + "folder-cog": "\u{f910}", + "folder-dollar": "\u{f911}", + "folder-down": "\u{f912}", + "folder-exclamation": "\u{f913}", + "folder-filled": "\u{f749}", + "folder-heart": "\u{f914}", + "folder-minus": "\u{eaaa}", + "folder-off": "\u{ed14}", + "folder-open": "\u{faf7}", + "folder-pause": "\u{f915}", + "folder-pin": "\u{f916}", + "folder-plus": "\u{eaab}", + "folder-question": "\u{f917}", + "folder-root": "\u{fd43}", + "folder-search": "\u{f918}", + "folder-share": "\u{f919}", + "folder-star": "\u{f91a}", + "folder-symlink": "\u{f91b}", + "folder-up": "\u{f91c}", + "folder-x": "\u{eaac}", + "folders": "\u{eaae}", + "folders-filled": "\u{100c4}", + "folders-off": "\u{f133}", + "forbid": "\u{ebd5}", + "forbid-2": "\u{ebd4}", + "forbid-2-filled": "\u{fc28}", + "forbid-filled": "\u{fc29}", + "forklift": "\u{ebe9}", + "forms": "\u{ee8f}", + "fountain": "\u{f09b}", + "fountain-filled": "\u{fc2a}", + "fountain-off": "\u{f134}", + "frame": "\u{eaaf}", + "frame-off": "\u{f135}", + "free-rights": "\u{efb6}", + "freeze-column": "\u{fa63}", + "freeze-row": "\u{fa65}", + "freeze-row-column": "\u{fa64}", + "fridge": "\u{f1fa}", + "fridge-off": "\u{f3ef}", + "friends": "\u{eab0}", + "friends-off": "\u{f136}", + "frustum": "\u{fa9f}", + //"frustum-off": "\u{fa9d}", + "frustum-plus": "\u{fa9e}", + "function": "\u{f225}", + "function-filled": "\u{fc2b}", + "function-off": "\u{f3f0}", + "galaxy": "\u{fcb6}", + "garden-cart": "\u{f23e}", + "garden-cart-filled": "\u{100c3}", + "garden-cart-off": "\u{f3f1}", + "gas-station": "\u{ec7d}", + "gas-station-filled": "\u{100fa}", + "gas-station-off": "\u{f137}", + "gauge": "\u{eab1}", + "gauge-filled": "\u{fc2c}", + "gauge-off": "\u{f138}", + "gavel": "\u{ef90}", + "gender-agender": "\u{f0e1}", + "gender-androgyne": "\u{f0e2}", + "gender-bigender": "\u{f0e3}", + "gender-demiboy": "\u{f0e4}", + "gender-demigirl": "\u{f0e5}", + "gender-epicene": "\u{f0e6}", + "gender-female": "\u{f0e7}", + "gender-femme": "\u{f0e8}", + "gender-genderfluid": "\u{f0e9}", + "gender-genderless": "\u{f0ea}", + "gender-genderqueer": "\u{f0eb}", + "gender-hermaphrodite": "\u{f0ec}", + "gender-intergender": "\u{f0ed}", + "gender-male": "\u{f0ee}", + "gender-neutrois": "\u{f0ef}", + "gender-third": "\u{f0f0}", + "gender-transgender": "\u{f0f1}", + "gender-trasvesti": "\u{f0f2}", + "geometry": "\u{ee90}", + "ghost": "\u{eb8e}", + "ghost-2": "\u{f57c}", + "ghost-2-filled": "\u{f74a}", + "ghost-3": "\u{fc13}", + "ghost-3-filled": "\u{100a4}", + "ghost-filled": "\u{f74b}", + "ghost-off": "\u{f3f2}", + "gif": "\u{f257}", + "gift": "\u{eb68}", + "gift-card": "\u{f3aa}", + "gift-card-filled": "\u{fc2d}", + "gift-filled": "\u{fd14}", + "gift-off": "\u{f3f3}", + "git-branch": "\u{eab2}", + "git-branch-deleted": "\u{f57d}", + "git-cherry-pick": "\u{f57e}", + "git-commit": "\u{eab3}", + "git-compare": "\u{eab4}", + "git-fork": "\u{eb8f}", + "git-merge": "\u{eab5}", + "git-pull-request": "\u{eab6}", + "git-pull-request-closed": "\u{ef7f}", + "git-pull-request-draft": "\u{efb7}", + "gizmo": "\u{f02b}", + "glass": "\u{eab8}", + "glass-champagne": "\u{fd9c}", + "glass-cocktail": "\u{fd9d}", + "glass-filled": "\u{1000f}", + "glass-full": "\u{eab7}", + "glass-full-filled": "\u{fc2e}", + "glass-gin": "\u{fd9e}", + "glass-off": "\u{ee91}", + "globe": "\u{eab9}", + "globe-filled": "\u{fc2f}", + "globe-off": "\u{f139}", + "go-game": "\u{f512}", + "golf": "\u{ed8c}", + "golf-filled": "\u{100a3}", + "golf-off": "\u{f13a}", + "gps": "\u{ed7a}", + "gps-filled": "\u{fe48}", + "gradienter": "\u{f3ab}", + "grain": "\u{ee92}", + "graph": "\u{f288}", + "graph-filled": "\u{fd15}", + "graph-off": "\u{f3f4}", + "grave": "\u{f580}", + "grave-2": "\u{f57f}", + "grid-3x3": "\u{fca4}", + "grid-4x4": "\u{fca5}", + "grid-dots": "\u{eaba}", + "grid-goldenratio": "\u{fca6}", + "grid-pattern": "\u{efc9}", + "grid-pattern-filled": "\u{100c2}", + "grid-scan": "\u{fca7}", + "grill": "\u{efa9}", + "grill-fork": "\u{f35b}", + "grill-off": "\u{f3f5}", + "grill-spatula": "\u{f35c}", + "grip-horizontal": "\u{ec00}", + "grip-vertical": "\u{ec01}", + "growth": "\u{ee93}", + "guitar-pick": "\u{f4c6}", + "guitar-pick-filled": "\u{f67b}", + "gymnastics": "\u{fd44}", + "h-1": "\u{ec94}", + "h-2": "\u{ec95}", + "h-3": "\u{ec96}", + "h-4": "\u{ec97}", + "h-5": "\u{ec98}", + "h-6": "\u{ec99}", + "hammer": "\u{ef91}", + "hammer-off": "\u{f13c}", + "hand-click": "\u{ef4f}", + "hand-click-off": "\u{100f1}", + "hand-finger": "\u{ee94}", + "hand-finger-down": "\u{ff4f}", + "hand-finger-left": "\u{ff4e}", + "hand-finger-off": "\u{f13d}", + "hand-finger-right": "\u{ff4d}", + "hand-grab": "\u{f091}", + "hand-little-finger": "\u{ee95}", + "hand-love-you": "\u{ee97}", + "hand-middle-finger": "\u{ec2d}", + "hand-move": "\u{ef50}", + "hand-off": "\u{ed15}", + "hand-ring-finger": "\u{ee96}", + "hand-rock": "\u{ee97}", + "hand-sanitizer": "\u{f5f4}", + "hand-stop": "\u{ec2e}", + "hand-three-fingers": "\u{ee98}", + "hand-two-fingers": "\u{ee99}", + "hanger": "\u{ee9a}", + "hanger-2": "\u{f09c}", + "hanger-2-filled": "\u{ff61}", + "hanger-off": "\u{f13e}", + "hash": "\u{eabc}", + "haze": "\u{efaa}", + "haze-moon": "\u{faf8}", + "hdr": "\u{fa7b}", + "heading": "\u{ee9b}", + "heading-off": "\u{f13f}", + "headphones": "\u{eabd}", + "headphones-filled": "\u{fa3c}", + "headphones-off": "\u{ed1d}", + "headset": "\u{eb90}", + "headset-off": "\u{f3f6}", + "health-recognition": "\u{f1fb}", + "heart": "\u{eabe}", + "heart-bitcoin": "\u{ff3b}", + "heart-bolt": "\u{fb9e}", + "heart-broken": "\u{ecba}", + "heart-broken-filled": "\u{1016a}", + "heart-cancel": "\u{fb9f}", + "heart-check": "\u{fba0}", + "heart-code": "\u{fba1}", + "heart-cog": "\u{fba2}", + "heart-discount": "\u{fba3}", + "heart-dollar": "\u{fba4}", + "heart-down": "\u{fba5}", + "heart-exclamation": "\u{fba6}", + "heart-filled": "\u{f67c}", + "heart-handshake": "\u{f0f3}", + "heart-minus": "\u{f140}", + "heart-off": "\u{f141}", + "heart-pause": "\u{fba7}", + "heart-pin": "\u{fba8}", + "heart-plus": "\u{f142}", + "heart-question": "\u{fba9}", + "heart-rate-monitor": "\u{ef61}", + "heart-search": "\u{fbaa}", + "heart-share": "\u{fbab}", + "heart-spark": "\u{ffb6}", + "heart-star": "\u{fbac}", + "heart-up": "\u{fbad}", + "heart-x": "\u{fbae}", + "heartbeat": "\u{ef92}", + "hearts": "\u{f387}", + "hearts-off": "\u{f3f7}", + "helicopter": "\u{ed8e}", + "helicopter-filled": "\u{100f9}", + "helicopter-landing": "\u{ed8d}", + "helicopter-landing-filled": "\u{100c1}", + "helmet": "\u{efca}", + "helmet-off": "\u{f143}", + "help": "\u{eabf}", + "help-circle": "\u{f91d}", + "help-circle-filled": "\u{fa3d}", + "help-hexagon": "\u{f7a8}", + "help-hexagon-filled": "\u{fa3e}", + "help-octagon": "\u{f7a9}", + "help-octagon-filled": "\u{fa3f}", + "help-off": "\u{f3f8}", + "help-small": "\u{f91e}", + "help-square": "\u{f920}", + "help-square-filled": "\u{fa40}", + "help-square-rounded": "\u{f91f}", + "help-square-rounded-filled": "\u{fa41}", + "help-triangle": "\u{f921}", + "help-triangle-filled": "\u{fa42}", + "hemisphere": "\u{faa2}", + "hemisphere-off": "\u{faa0}", + "hemisphere-plus": "\u{faa1}", + "hexagon": "\u{ec02}", + "hexagon-0": "\u{f459}", + "hexagon-1": "\u{f45a}", + "hexagon-2": "\u{f45b}", + "hexagon-3": "\u{f45c}", + "hexagon-3d": "\u{f4c7}", + "hexagon-4": "\u{f45d}", + "hexagon-5": "\u{f45e}", + "hexagon-6": "\u{f45f}", + "hexagon-7": "\u{f460}", + "hexagon-8": "\u{f461}", + "hexagon-9": "\u{f462}", + "hexagon-filled": "\u{f67d}", + "hexagon-letter-a": "\u{f463}", + "hexagon-letter-a-filled": "\u{fe47}", + "hexagon-letter-b": "\u{f464}", + "hexagon-letter-b-filled": "\u{fe46}", + "hexagon-letter-c": "\u{f465}", + "hexagon-letter-c-filled": "\u{fe45}", + "hexagon-letter-d": "\u{f466}", + "hexagon-letter-d-filled": "\u{fe44}", + "hexagon-letter-e": "\u{f467}", + "hexagon-letter-e-filled": "\u{fe43}", + "hexagon-letter-f": "\u{f468}", + "hexagon-letter-f-filled": "\u{fe42}", + "hexagon-letter-g": "\u{f469}", + "hexagon-letter-g-filled": "\u{fe41}", + "hexagon-letter-h": "\u{f46a}", + "hexagon-letter-h-filled": "\u{fe40}", + "hexagon-letter-i": "\u{f46b}", + "hexagon-letter-i-filled": "\u{fe3f}", + "hexagon-letter-j": "\u{f46c}", + "hexagon-letter-j-filled": "\u{fe3e}", + "hexagon-letter-k": "\u{f46d}", + "hexagon-letter-k-filled": "\u{fe3d}", + "hexagon-letter-l": "\u{f46e}", + "hexagon-letter-l-filled": "\u{fe3c}", + "hexagon-letter-m": "\u{f46f}", + "hexagon-letter-m-filled": "\u{fe3b}", + "hexagon-letter-n": "\u{f470}", + "hexagon-letter-n-filled": "\u{fe3a}", + "hexagon-letter-o": "\u{f471}", + "hexagon-letter-o-filled": "\u{fe39}", + "hexagon-letter-p": "\u{f472}", + "hexagon-letter-p-filled": "\u{fe38}", + "hexagon-letter-q": "\u{f473}", + "hexagon-letter-q-filled": "\u{fe37}", + "hexagon-letter-r": "\u{f474}", + "hexagon-letter-r-filled": "\u{fe36}", + "hexagon-letter-s": "\u{f475}", + "hexagon-letter-s-filled": "\u{fe35}", + "hexagon-letter-t": "\u{f476}", + "hexagon-letter-t-filled": "\u{fe34}", + "hexagon-letter-u": "\u{f477}", + "hexagon-letter-u-filled": "\u{fe33}", + "hexagon-letter-v": "\u{f4b3}", + "hexagon-letter-v-filled": "\u{fe32}", + "hexagon-letter-w": "\u{f478}", + "hexagon-letter-w-filled": "\u{fe31}", + "hexagon-letter-x": "\u{f479}", + "hexagon-letter-x-filled": "\u{fe30}", + "hexagon-letter-y": "\u{f47a}", + //"hexagon-letter-y-filled": "\u{fe2f}", + "hexagon-letter-z": "\u{f47b}", + //"hexagon-letter-z-filled": "\u{fe2e}", + "hexagon-minus": "\u{fc8f}", + "hexagon-minus-2": "\u{fc8e}", + //"hexagon-minus-filled": "\u{fe2d}", + "hexagon-number-0": "\u{f459}", + "hexagon-number-0-filled": "\u{f74c}", + "hexagon-number-1": "\u{f45a}", + "hexagon-number-1-filled": "\u{f74d}", + "hexagon-number-2": "\u{f45b}", + "hexagon-number-2-filled": "\u{f74e}", + "hexagon-number-3": "\u{f45c}", + "hexagon-number-3-filled": "\u{f74f}", + "hexagon-number-4": "\u{f45d}", + "hexagon-number-4-filled": "\u{f750}", + "hexagon-number-5": "\u{f45e}", + "hexagon-number-5-filled": "\u{f751}", + "hexagon-number-6": "\u{f45f}", + "hexagon-number-6-filled": "\u{f752}", + "hexagon-number-7": "\u{f460}", + "hexagon-number-7-filled": "\u{f753}", + "hexagon-number-8": "\u{f461}", + "hexagon-number-8-filled": "\u{f754}", + "hexagon-number-9": "\u{f462}", + "hexagon-number-9-filled": "\u{f755}", + "hexagon-off": "\u{ee9c}", + "hexagon-plus": "\u{fc45}", + "hexagon-plus-2": "\u{fc90}", + //"hexagon-plus-filled": "\u{fe2c}", + "hexagonal-prism": "\u{faa5}", + "hexagonal-prism-off": "\u{faa3}", + "hexagonal-prism-plus": "\u{faa4}", + "hexagonal-pyramid": "\u{faa8}", + "hexagonal-pyramid-off": "\u{faa6}", + "hexagonal-pyramid-plus": "\u{faa7}", + "hexagons": "\u{f09d}", + "hexagons-off": "\u{f3f9}", + "hierarchy": "\u{ee9e}", + "hierarchy-2": "\u{ee9d}", + "hierarchy-3": "\u{f289}", + "hierarchy-off": "\u{f3fa}", + "highlight": "\u{ef3f}", + "highlight-off": "\u{f144}", + "history": "\u{ebea}", + "history-off": "\u{f3fb}", + "history-toggle": "\u{f1fc}", + "home": "\u{eac1}", + "home-2": "\u{eac0}", + "home-bitcoin": "\u{ff3a}", + "home-bolt": "\u{f336}", + "home-cancel": "\u{f350}", + "home-check": "\u{f337}", + "home-cog": "\u{f338}", + "home-dollar": "\u{f339}", + "home-dot": "\u{f33a}", + "home-down": "\u{f33b}", + "home-eco": "\u{f351}", + "home-edit": "\u{f352}", + "home-exclamation": "\u{f33c}", + //"home-filled": "\u{fe2b}", + "home-hand": "\u{f504}", + "home-heart": "\u{f353}", + "home-infinity": "\u{f505}", + "home-link": "\u{f354}", + "home-minus": "\u{f33d}", + "home-move": "\u{f33e}", + "home-off": "\u{f145}", + "home-plus": "\u{f33f}", + "home-question": "\u{f340}", + "home-ribbon": "\u{f355}", + "home-search": "\u{f341}", + "home-share": "\u{f342}", + "home-shield": "\u{f343}", + "home-signal": "\u{f356}", + "home-spark": "\u{ffb5}", + "home-star": "\u{f344}", + "home-stats": "\u{f345}", + "home-up": "\u{f346}", + "home-x": "\u{f347}", + "horse": "\u{fc46}", + "horse-toy": "\u{f28a}", + "horseshoe": "\u{fcb7}", + "hospital": "\u{fd59}", + "hospital-circle": "\u{fd58}", + "hospital-circle-filled": "\u{fed2}", + "hotel-service": "\u{ef80}", + "hourglass": "\u{ef93}", + "hourglass-empty": "\u{f146}", + "hourglass-filled": "\u{f756}", + "hourglass-high": "\u{f092}", + "hourglass-low": "\u{f093}", + "hourglass-off": "\u{f147}", + "hours-12": "\u{fc53}", + "hours-24": "\u{f5e7}", + "html": "\u{f7b1}", + "http-connect": "\u{fa28}", + "http-connect-off": "\u{100e7}", + "http-delete": "\u{fa29}", + "http-delete-off": "\u{100e6}", + "http-get": "\u{fa2a}", + "http-get-off": "\u{100e5}", + "http-head": "\u{fa2b}", + "http-head-off": "\u{100e4}", + "http-options": "\u{fa2c}", + "http-options-off": "\u{100e3}", + "http-patch": "\u{fa2d}", + "http-patch-off": "\u{100e2}", + "http-post": "\u{fa2e}", + "http-post-off": "\u{100e1}", + "http-put": "\u{fa2f}", + "http-put-off": "\u{100e0}", + "http-que": "\u{fa5b}", + "http-que-off": "\u{100df}", + "http-trace": "\u{fa30}", + "http-trace-off": "\u{100de}", + "hyprland": "\u{ec6a}", + "ice-cream": "\u{eac2}", + "ice-cream-2": "\u{ee9f}", + "ice-cream-off": "\u{f148}", + "ice-skating": "\u{efcb}", + "icons": "\u{f1d4}", + "icons-filled": "\u{10070}", + "icons-off": "\u{f3fc}", + "id": "\u{eac3}", + "id-badge": "\u{eff7}", + "id-badge-2": "\u{f076}", + "id-badge-off": "\u{f3fd}", + "id-off": "\u{f149}", + "ikosaedr": "\u{fec6}", + "image-in-picture": "\u{fd9f}", + "inbox": "\u{eac4}", + "inbox-off": "\u{f14a}", + "indent-decrease": "\u{eb91}", + "indent-increase": "\u{eb92}", + "infinity": "\u{eb69}", + "infinity-off": "\u{f3fe}", + "info-circle": "\u{eac5}", + "info-circle-filled": "\u{f6d8}", + "info-hexagon": "\u{f7aa}", + "info-hexagon-filled": "\u{fa43}", + "info-octagon": "\u{f7ab}", + "info-octagon-filled": "\u{fa44}", + "info-small": "\u{f922}", + "info-square": "\u{eac6}", + "info-square-filled": "\u{fa45}", + "info-square-rounded": "\u{f635}", + "info-square-rounded-filled": "\u{f6d9}", + "info-triangle": "\u{f923}", + "info-triangle-filled": "\u{fa46}", + "inner-shadow-bottom": "\u{f520}", + "inner-shadow-bottom-filled": "\u{f757}", + "inner-shadow-bottom-left": "\u{f51e}", + "inner-shadow-bottom-left-filled": "\u{f758}", + "inner-shadow-bottom-right": "\u{f51f}", + "inner-shadow-bottom-right-filled": "\u{f759}", + "inner-shadow-left": "\u{f521}", + "inner-shadow-left-filled": "\u{f75a}", + "inner-shadow-right": "\u{f522}", + "inner-shadow-right-filled": "\u{f75b}", + "inner-shadow-top": "\u{f525}", + "inner-shadow-top-filled": "\u{f75c}", + "inner-shadow-top-left": "\u{f523}", + "inner-shadow-top-left-filled": "\u{f75d}", + "inner-shadow-top-right": "\u{f524}", + "inner-shadow-top-right-filled": "\u{f75e}", + "input-ai": "\u{fc5a}", + "input-check": "\u{fc5b}", + "input-search": "\u{f2a2}", + "input-spark": "\u{ffb4}", + "input-x": "\u{fc5c}", + "invoice": "\u{feab}", + "ironing": "\u{fa7c}", + "ironing-1": "\u{f2f4}", + "ironing-1-filled": "\u{1006f}", + "ironing-2": "\u{f2f5}", + "ironing-2-filled": "\u{1006e}", + "ironing-3": "\u{f2f6}", + "ironing-3-filled": "\u{1006d}", + //"ironing-filled": "\u{fe2a}", + "ironing-off": "\u{f2f7}", + "ironing-steam": "\u{f2f9}", + "ironing-steam-filled": "\u{1006c}", + "ironing-steam-off": "\u{f2f8}", + "irregular-polyhedron": "\u{faab}", + "irregular-polyhedron-off": "\u{faa9}", + "irregular-polyhedron-plus": "\u{faaa}", + "italic": "\u{eb93}", + "jacket": "\u{f661}", + "jetpack": "\u{f581}", + //"jetpack-filled": "\u{fe29}", + "jewish-star": "\u{f3ff}", + "jewish-star-filled": "\u{f67e}", + "join-bevel": "\u{ff4c}", + "join-round": "\u{ff4b}", + "join-straight": "\u{ff4a}", + "joker": "\u{1005f}", + "jpg": "\u{f3ac}", + "json": "\u{f7b2}", + "jump-rope": "\u{ed8f}", + "karate": "\u{ed32}", + "kayak": "\u{f1d6}", + "kering": "\u{efb8}", + "kerning": "\u{efb8}", + "key": "\u{eac7}", + //"key-filled": "\u{fe28}", + "key-off": "\u{f14b}", + "keyboard": "\u{ebd6}", + "keyboard-filled": "\u{100a2}", + "keyboard-hide": "\u{ec7e}", + "keyboard-off": "\u{eea0}", + "keyboard-show": "\u{ec7f}", + "keyframe": "\u{f576}", + "keyframe-align-center": "\u{f582}", + "keyframe-align-center-filled": "\u{fc30}", + "keyframe-align-horizontal": "\u{f583}", + "keyframe-align-horizontal-filled": "\u{fc31}", + "keyframe-align-vertical": "\u{f584}", + "keyframe-align-vertical-filled": "\u{fc32}", + "keyframe-filled": "\u{fc33}", + "keyframes": "\u{f585}", + "keyframes-filled": "\u{fc34}", + "label": "\u{ff38}", + "label-filled": "\u{ff41}", + "label-important": "\u{ff49}", + "label-important-filled": "\u{ff60}", + "label-off": "\u{ff39}", + "ladder": "\u{efe2}", + "ladder-off": "\u{f14c}", + "ladle": "\u{fc14}", + "lambda": "\u{f541}", + "lamp": "\u{efab}", + "lamp-2": "\u{f09e}", + "lamp-off": "\u{f14d}", + "lane": "\u{faf9}", + "language": "\u{ebbe}", + "language-hiragana": "\u{ef77}", + "language-katakana": "\u{ef78}", + "language-off": "\u{f14e}", + "lasso": "\u{efac}", + "lasso-off": "\u{f14f}", + "lasso-polygon": "\u{f388}", + "lasso-polygon-filled": "\u{ff5f}", + "laurel-wreath": "\u{ff45}", + "laurel-wreath-1": "\u{ff48}", + "laurel-wreath-1-filled": "\u{10169}", + "laurel-wreath-2": "\u{ff47}", + "laurel-wreath-2-filled": "\u{10168}", + "laurel-wreath-3": "\u{ff46}", + "laurel-wreath-3-filled": "\u{10167}", + "laurel-wreath-filled": "\u{100c0}", + "layers-difference": "\u{eac8}", + "layers-intersect": "\u{eac9}", + "layers-intersect-2": "\u{eff8}", + "layers-linked": "\u{eea1}", + "layers-off": "\u{f150}", + "layers-selected": "\u{fea9}", + "layers-selected-bottom": "\u{feaa}", + "layers-subtract": "\u{eaca}", + "layers-union": "\u{eacb}", + "layout": "\u{eadb}", + "layout-2": "\u{eacc}", + //"layout-2-filled": "\u{fe27}", + //"layout-align-bottom": "\u{eacd}", + //"layout-align-bottom-filled": "\u{fe26}", + //"layout-align-center": "\u{eace}", + //"layout-align-center-filled": "\u{fe25}", + "layout-align-left": "\u{eacf}", + //"layout-align-left-filled": "\u{fe24}", + "layout-align-middle": "\u{ead0}", + //"layout-align-middle-filled": "\u{fe23}", + "layout-align-right": "\u{ead1}", + //"layout-align-right-filled": "\u{fe22}", + "layout-align-top": "\u{ead2}", + //"layout-align-top-filled": "\u{fe21}", + "layout-board": "\u{ef95}", + "layout-board-filled": "\u{10182}", + "layout-board-split": "\u{ef94}", + "layout-board-split-filled": "\u{10183}", + "layout-bottombar": "\u{ead3}", + "layout-bottombar-collapse": "\u{f28b}", + "layout-bottombar-collapse-filled": "\u{fc35}", + "layout-bottombar-expand": "\u{f28c}", + "layout-bottombar-expand-filled": "\u{fc36}", + "layout-bottombar-filled": "\u{fc37}", + "layout-bottombar-inactive": "\u{fd45}", + "layout-cards": "\u{ec13}", + //"layout-cards-filled": "\u{fe20}", + "layout-collage": "\u{f389}", + "layout-columns": "\u{ead4}", + "layout-dashboard": "\u{f02c}", + "layout-dashboard-filled": "\u{fe1f}", + "layout-distribute-horizontal": "\u{ead5}", + "layout-distribute-horizontal-filled": "\u{fe1e}", + "layout-distribute-vertical": "\u{ead6}", + "layout-distribute-vertical-filled": "\u{fe1d}", + "layout-filled": "\u{fe17}", + "layout-grid": "\u{edba}", + "layout-grid-add": "\u{edb9}", + "layout-grid-filled": "\u{fe1c}", + "layout-grid-remove": "\u{fa7d}", + "layout-kanban": "\u{ec3f}", + "layout-kanban-filled": "\u{fe1b}", + "layout-list": "\u{ec14}", + "layout-list-filled": "\u{fe1a}", + "layout-navbar": "\u{ead7}", + "layout-navbar-collapse": "\u{f28d}", + "layout-navbar-collapse-filled": "\u{fc38}", + "layout-navbar-expand": "\u{f28e}", + "layout-navbar-expand-filled": "\u{fc39}", + "layout-navbar-filled": "\u{fc3a}", + "layout-navbar-inactive": "\u{fd46}", + "layout-off": "\u{f151}", + "layout-rows": "\u{ead8}", + "layout-sidebar": "\u{eada}", + "layout-sidebar-filled": "\u{fe18}", + "layout-sidebar-inactive": "\u{fd47}", + "layout-sidebar-left-collapse": "\u{f004}", + "layout-sidebar-left-collapse-filled": "\u{fc3b}", + "layout-sidebar-left-expand": "\u{f005}", + "layout-sidebar-left-expand-filled": "\u{fc3c}", + "layout-sidebar-right": "\u{ead9}", + "layout-sidebar-right-collapse": "\u{f006}", + "layout-sidebar-right-collapse-filled": "\u{fc3d}", + "layout-sidebar-right-expand": "\u{f007}", + "layout-sidebar-right-expand-filled": "\u{fc3e}", + "layout-sidebar-right-filled": "\u{fe19}", + "layout-sidebar-right-inactive": "\u{fd48}", + "leaf": "\u{ed4f}", + "leaf-2": "\u{ff44}", + "leaf-off": "\u{f400}", + "lego": "\u{eadc}", + "lego-filled": "\u{fe16}", + "lego-off": "\u{f401}", + "lemon": "\u{ef10}", + "lemon-2": "\u{ef81}", + "lemon-2-filled": "\u{100bf}", + "letter-a": "\u{ec50}", + "letter-a-small": "\u{fcc7}", + "letter-b": "\u{ec51}", + "letter-b-small": "\u{fcc8}", + "letter-c": "\u{ec52}", + "letter-c-small": "\u{fcc9}", + "letter-case": "\u{eea5}", + "letter-case-lower": "\u{eea2}", + "letter-case-toggle": "\u{eea3}", + "letter-case-upper": "\u{eea4}", + "letter-d": "\u{ec53}", + "letter-d-small": "\u{fcca}", + "letter-e": "\u{ec54}", + "letter-e-small": "\u{fccb}", + "letter-f": "\u{ec55}", + "letter-f-small": "\u{fccc}", + "letter-g": "\u{ec56}", + "letter-g-small": "\u{fccd}", + "letter-h": "\u{ec57}", + "letter-h-small": "\u{fcce}", + "letter-i": "\u{ec58}", + "letter-i-small": "\u{fccf}", + "letter-j": "\u{ec59}", + "letter-j-small": "\u{fcd0}", + "letter-k": "\u{ec5a}", + "letter-k-small": "\u{fcd1}", + "letter-l": "\u{ec5b}", + "letter-l-small": "\u{fcd2}", + "letter-m": "\u{ec5c}", + "letter-m-small": "\u{fcd3}", + "letter-n": "\u{ec5d}", + "letter-n-small": "\u{fcd4}", + "letter-o": "\u{ec5e}", + "letter-o-small": "\u{fcd5}", + "letter-p": "\u{ec5f}", + "letter-p-small": "\u{fcd6}", + "letter-q": "\u{ec60}", + "letter-q-small": "\u{fcd7}", + "letter-r": "\u{ec61}", + "letter-r-small": "\u{fcd8}", + "letter-s": "\u{ec62}", + "letter-s-small": "\u{fcd9}", + "letter-spacing": "\u{eea6}", + "letter-t": "\u{ec63}", + "letter-t-small": "\u{fcda}", + "letter-u": "\u{ec64}", + "letter-u-small": "\u{fcdb}", + "letter-v": "\u{ec65}", + "letter-v-small": "\u{fcdc}", + "letter-w": "\u{ec66}", + "letter-w-small": "\u{fcdd}", + "letter-x": "\u{ec67}", + "letter-x-small": "\u{fcde}", + "letter-y": "\u{ec68}", + "letter-y-small": "\u{fcdf}", + "letter-z": "\u{ec69}", + "letter-z-small": "\u{fce0}", + "library": "\u{fd4c}", + "library-filled": "\u{10180}", + "library-minus": "\u{fd49}", + "library-photo": "\u{fd4a}", + "library-plus": "\u{fd4b}", + "library-plus-filled": "\u{10181}", + "license": "\u{ebc0}", + "license-off": "\u{f153}", + "lifebuoy": "\u{eadd}", + "lifebuoy-filled": "\u{100be}", + "lifebuoy-off": "\u{f154}", + "lighter": "\u{f794}", + "line": "\u{ec40}", + "line-dashed": "\u{eea7}", + "line-dotted": "\u{eea8}", + "line-height": "\u{eb94}", + "line-scan": "\u{fcb8}", + "link": "\u{eade}", + "link-minus": "\u{fd16}", + "link-off": "\u{f402}", + "link-plus": "\u{fd17}", + "list": "\u{eb6b}", + "list-check": "\u{eb6a}", + "list-details": "\u{ef40}", + "list-letters": "\u{fc47}", + "list-numbers": "\u{ef11}", + "list-search": "\u{eea9}", + "list-tree": "\u{fafa}", + "live-photo": "\u{eadf}", + "live-photo-filled": "\u{fed1}", + "live-photo-off": "\u{f403}", + "live-view": "\u{ec6b}", + "live-view-filled": "\u{100a1}", + "load-balancer": "\u{fa5c}", + "loader": "\u{eca3}", + "loader-2": "\u{f226}", + "loader-3": "\u{f513}", + "loader-quarter": "\u{eca2}", + "location": "\u{eae0}", + "location-bolt": "\u{fbaf}", + "location-broken": "\u{f2c4}", + "location-cancel": "\u{fbb0}", + "location-check": "\u{fbb1}", + "location-code": "\u{fbb2}", + "location-cog": "\u{fbb3}", + "location-discount": "\u{fbb4}", + "location-dollar": "\u{fbb5}", + "location-down": "\u{fbb6}", + "location-exclamation": "\u{fbb7}", + "location-filled": "\u{f67f}", + "location-heart": "\u{fbb8}", + "location-minus": "\u{fbb9}", + "location-off": "\u{f155}", + "location-pause": "\u{fbba}", + "location-pin": "\u{fbbb}", + "location-plus": "\u{fbbc}", + "location-question": "\u{fbbd}", + "location-search": "\u{fbbe}", + "location-share": "\u{fbbf}", + "location-star": "\u{fbc0}", + "location-up": "\u{fbc1}", + "location-x": "\u{fbc2}", + "lock": "\u{eae2}", + "lock-access": "\u{eeaa}", + "lock-access-off": "\u{f404}", + "lock-bitcoin": "\u{ff37}", + "lock-bolt": "\u{f924}", + "lock-cancel": "\u{f925}", + "lock-check": "\u{f926}", + "lock-code": "\u{f927}", + "lock-cog": "\u{f928}", + "lock-dollar": "\u{f929}", + "lock-down": "\u{f92a}", + "lock-exclamation": "\u{f92b}", + "lock-filled": "\u{fe15}", + "lock-heart": "\u{f92c}", + "lock-minus": "\u{f92d}", + "lock-off": "\u{ed1e}", + "lock-open": "\u{eae1}", + "lock-open-2": "\u{fea8}", + "lock-open-off": "\u{f156}", + "lock-password": "\u{ff9f}", + "lock-pause": "\u{f92e}", + "lock-pin": "\u{f92f}", + "lock-plus": "\u{f930}", + "lock-question": "\u{f931}", + "lock-search": "\u{f932}", + "lock-share": "\u{f933}", + "lock-square": "\u{ef51}", + "lock-square-rounded": "\u{f636}", + "lock-square-rounded-filled": "\u{f6da}", + "lock-star": "\u{f934}", + "lock-up": "\u{f935}", + "lock-x": "\u{f936}", + "logic-and": "\u{f240}", + "logic-buffer": "\u{f241}", + "logic-nand": "\u{f242}", + "logic-nor": "\u{f243}", + "logic-not": "\u{f244}", + "logic-or": "\u{f245}", + "logic-xnor": "\u{f246}", + "logic-xor": "\u{f247}", + "login": "\u{eba7}", + "login-2": "\u{fc76}", + "logout": "\u{eba8}", + "logout-2": "\u{fa7e}", + "logs": "\u{fea7}", + "lollipop": "\u{efcc}", + "lollipop-off": "\u{f157}", + "luggage": "\u{efad}", + "luggage-off": "\u{f158}", + "lungs": "\u{ef62}", + "lungs-filled": "\u{fe14}", + "lungs-off": "\u{f405}", + "macro": "\u{eeab}", + "macro-filled": "\u{fe13}", + "macro-off": "\u{f406}", + "magnet": "\u{eae3}", + "magnet-filled": "\u{fe12}", + "magnet-off": "\u{f159}", + "magnetic": "\u{fcb9}", + "mail": "\u{eae5}", + "mail-ai": "\u{fa31}", + "mail-bitcoin": "\u{ff36}", + "mail-bolt": "\u{f937}", + "mail-cancel": "\u{f938}", + "mail-check": "\u{f939}", + "mail-code": "\u{f93a}", + "mail-cog": "\u{f93b}", + "mail-dollar": "\u{f93c}", + "mail-down": "\u{f93d}", + "mail-exclamation": "\u{f93e}", + "mail-fast": "\u{f069}", + "mail-filled": "\u{fa47}", + "mail-forward": "\u{eeac}", + "mail-heart": "\u{f93f}", + "mail-minus": "\u{f940}", + "mail-off": "\u{f15a}", + "mail-opened": "\u{eae4}", + "mail-opened-filled": "\u{fa48}", + "mail-pause": "\u{f941}", + "mail-pin": "\u{f942}", + "mail-plus": "\u{f943}", + "mail-question": "\u{f944}", + "mail-search": "\u{f945}", + "mail-share": "\u{f946}", + "mail-spark": "\u{ffb3}", + "mail-star": "\u{f947}", + "mail-up": "\u{f948}", + "mail-x": "\u{f949}", + "mailbox": "\u{eead}", + "mailbox-off": "\u{f15b}", + "man": "\u{eae6}", + "man-filled": "\u{fe11}", + "manual-gearbox": "\u{ed7b}", + "manual-gearbox-filled": "\u{fe10}", + "map": "\u{eae9}", + "map-2": "\u{eae7}", + "map-bolt": "\u{fbc3}", + "map-cancel": "\u{fbc4}", + "map-check": "\u{fbc5}", + "map-code": "\u{fbc6}", + "map-cog": "\u{fbc7}", + "map-discount": "\u{fbc8}", + "map-dollar": "\u{fbc9}", + "map-down": "\u{fbca}", + "map-east": "\u{fc5d}", + "map-exclamation": "\u{fbcb}", + "map-heart": "\u{fbcc}", + "map-minus": "\u{fbcd}", + "map-north": "\u{fc5e}", + "map-off": "\u{f15c}", + "map-pause": "\u{fbce}", + "map-pin": "\u{eae8}", + "map-pin-2": "\u{fc48}", + "map-pin-bolt": "\u{f94a}", + "map-pin-cancel": "\u{f94b}", + "map-pin-check": "\u{f94c}", + "map-pin-code": "\u{f94d}", + "map-pin-cog": "\u{f94e}", + "map-pin-dollar": "\u{f94f}", + "map-pin-down": "\u{f950}", + "map-pin-exclamation": "\u{f951}", + "map-pin-filled": "\u{f680}", + "map-pin-heart": "\u{f952}", + "map-pin-minus": "\u{f953}", + "map-pin-off": "\u{ecf3}", + "map-pin-pause": "\u{f954}", + "map-pin-pin": "\u{f955}", + "map-pin-plus": "\u{f956}", + "map-pin-question": "\u{f957}", + "map-pin-search": "\u{f958}", + "map-pin-share": "\u{f795}", + "map-pin-star": "\u{f959}", + "map-pin-up": "\u{f95a}", + "map-pin-x": "\u{f95b}", + "map-pins": "\u{ed5e}", + "map-plus": "\u{fbcf}", + "map-question": "\u{fbd0}", + "map-route": "\u{fc79}", + "map-search": "\u{ef82}", + "map-share": "\u{fbd1}", + "map-south": "\u{fc5f}", + "map-star": "\u{fbd2}", + "map-up": "\u{fbd3}", + "map-west": "\u{fc60}", + "map-x": "\u{fbd4}", + "markdown": "\u{ec41}", + "markdown-off": "\u{f407}", + "marquee": "\u{ec77}", + "marquee-2": "\u{eeae}", + "marquee-off": "\u{f15d}", + "mars": "\u{ec80}", + "mask": "\u{eeb0}", + "mask-off": "\u{eeaf}", + "masks-theater": "\u{f263}", + "masks-theater-off": "\u{f408}", + "massage": "\u{eeb1}", + "matchstick": "\u{f577}", + "math": "\u{ebeb}", + "math-1-divide-2": "\u{f4e2}", + "math-1-divide-3": "\u{f4e3}", + "math-avg": "\u{f0f4}", + "math-cos": "\u{ff1f}", + "math-ctg": "\u{ff35}", + "math-equal-greater": "\u{f4e4}", + "math-equal-lower": "\u{f4e5}", + "math-function": "\u{eeb2}", + "math-function-off": "\u{f15e}", + "math-function-y": "\u{f4e6}", + "math-greater": "\u{f4e7}", + "math-integral": "\u{f4e9}", + "math-integral-x": "\u{f4e8}", + "math-integrals": "\u{f4ea}", + "math-lower": "\u{f4eb}", + "math-max": "\u{f0f5}", + "math-max-min": "\u{fda0}", + "math-min": "\u{f0f6}", + "math-not": "\u{f4ec}", + "math-off": "\u{f409}", + "math-pi": "\u{f4ee}", + "math-pi-divide-2": "\u{f4ed}", + "math-sec": "\u{ff34}", + "math-sin": "\u{ff1e}", + "math-symbols": "\u{eeb3}", + "math-tg": "\u{ff33}", + "math-x-divide-2": "\u{f4ef}", + "math-x-divide-y": "\u{f4f1}", + "math-x-divide-y-2": "\u{f4f0}", + "math-x-floor-divide-y": "\u{10073}", + "math-x-minus-x": "\u{f4f2}", + "math-x-minus-y": "\u{f4f3}", + "math-x-plus-x": "\u{f4f4}", + "math-x-plus-y": "\u{f4f5}", + "math-xy": "\u{f4f6}", + "math-y-minus-y": "\u{f4f7}", + "math-y-plus-y": "\u{f4f8}", + "matrix": "\u{100bc}", + "maximize": "\u{eaea}", + "maximize-off": "\u{f15f}", + "meat": "\u{ef12}", + "meat-off": "\u{f40a}", + "medal": "\u{ec78}", + "medal-2": "\u{efcd}", + "medical-cross": "\u{ec2f}", + "medical-cross-circle": "\u{fae8}", + "medical-cross-filled": "\u{f681}", + "medical-cross-off": "\u{f160}", + "medicine-syrup": "\u{ef63}", + "meeple": "\u{f514}", + "meeple-filled": "\u{100a0}", + "melon": "\u{fc7a}", + "melon-filled": "\u{1000e}", + "menorah": "\u{f58c}", + "menu": "\u{eaeb}", + "menu-2": "\u{ec42}", + "menu-3": "\u{ff43}", + "menu-4": "\u{ff42}", + "menu-deep": "\u{fafb}", + "menu-order": "\u{f5f5}", + "message": "\u{eaef}", + "message-2": "\u{eaec}", + "message-2-bolt": "\u{f95c}", + "message-2-cancel": "\u{f95d}", + "message-2-check": "\u{f95e}", + "message-2-code": "\u{f012}", + "message-2-cog": "\u{f95f}", + "message-2-dollar": "\u{f960}", + "message-2-down": "\u{f961}", + "message-2-exclamation": "\u{f962}", + "message-2-filled": "\u{1009f}", + "message-2-heart": "\u{f963}", + "message-2-minus": "\u{f964}", + "message-2-off": "\u{f40b}", + "message-2-pause": "\u{f965}", + "message-2-pin": "\u{f966}", + "message-2-plus": "\u{f967}", + "message-2-question": "\u{f968}", + "message-2-search": "\u{f969}", + "message-2-share": "\u{f077}", + "message-2-star": "\u{f96a}", + "message-2-up": "\u{f96b}", + "message-2-x": "\u{f96c}", + "message-bolt": "\u{f96d}", + "message-cancel": "\u{f96e}", + "message-chatbot": "\u{f38a}", + "message-chatbot-filled": "\u{fed0}", + "message-check": "\u{f96f}", + "message-circle": "\u{eaed}", + "message-circle-2": "\u{eaed}", + "message-circle-2-filled": "\u{fecf}", + "message-circle-bolt": "\u{f970}", + "message-circle-cancel": "\u{f971}", + "message-circle-check": "\u{f972}", + "message-circle-code": "\u{f973}", + "message-circle-cog": "\u{f974}", + "message-circle-dollar": "\u{f975}", + "message-circle-down": "\u{f976}", + "message-circle-exclamation": "\u{f977}", + "message-circle-filled": "\u{fecf}", + "message-circle-heart": "\u{f978}", + "message-circle-minus": "\u{f979}", + "message-circle-off": "\u{ed40}", + "message-circle-pause": "\u{f97a}", + "message-circle-pin": "\u{f97b}", + "message-circle-plus": "\u{f97c}", + "message-circle-question": "\u{f97d}", + "message-circle-search": "\u{f97e}", + "message-circle-share": "\u{f97f}", + "message-circle-star": "\u{f980}", + "message-circle-up": "\u{f981}", + "message-circle-user": "\u{fec5}", + "message-circle-x": "\u{f982}", + "message-code": "\u{f013}", + "message-cog": "\u{f983}", + "message-dollar": "\u{f984}", + "message-dots": "\u{eaee}", + "message-down": "\u{f985}", + "message-exclamation": "\u{f986}", + "message-filled": "\u{fecd}", + "message-forward": "\u{f28f}", + "message-heart": "\u{f987}", + "message-language": "\u{efae}", + "message-minus": "\u{f988}", + "message-off": "\u{ed41}", + "message-pause": "\u{f989}", + "message-pin": "\u{f98a}", + "message-plus": "\u{ec9a}", + "message-question": "\u{f98b}", + "message-reply": "\u{fd4d}", + "message-report": "\u{ec9b}", + "message-report-filled": "\u{fece}", + "message-search": "\u{f98c}", + "message-share": "\u{f078}", + "message-star": "\u{f98d}", + "message-up": "\u{f98e}", + "message-user": "\u{fec4}", + "message-x": "\u{f98f}", + "messages": "\u{eb6c}", + "messages-off": "\u{ed42}", + "meteor": "\u{f1fd}", + "meteor-filled": "\u{1000d}", + "meteor-off": "\u{f40c}", + "meter-cube": "\u{fd7c}", + "meter-square": "\u{fd7d}", + "metronome": "\u{fd25}", + "michelin-bib-gourmand": "\u{fae9}", + "michelin-star": "\u{faeb}", + "michelin-star-filled": "\u{1000c}", + "michelin-star-green": "\u{faea}", + "mickey": "\u{f2a3}", + "mickey-filled": "\u{f683}", + "microphone": "\u{eaf0}", + "microphone-2": "\u{ef2c}", + "microphone-2-off": "\u{f40d}", + //"microphone-filled": "\u{fe0f}", + "microphone-off": "\u{ed16}", + "microscope": "\u{ef64}", + "microscope-filled": "\u{10166}", + "microscope-off": "\u{f40e}", + "microwave": "\u{f248}", + //"microwave-filled": "\u{fe0e}", + "microwave-off": "\u{f264}", + "military-award": "\u{f079}", + "military-rank": "\u{efcf}", + "military-rank-filled": "\u{ff5e}", + "milk": "\u{ef13}", + "milk-filled": "\u{1000b}", + "milk-off": "\u{f40f}", + "milkshake": "\u{f4c8}", + "minimize": "\u{eaf1}", + "minus": "\u{eaf2}", + "minus-vertical": "\u{eeb4}", + "mist": "\u{ec30}", + "mist-off": "\u{f410}", + "mobiledata": "\u{f9f5}", + "mobiledata-off": "\u{f9f4}", + "moneybag": "\u{f506}", + "moneybag-edit": "\u{1013d}", + "moneybag-heart": "\u{1013c}", + "moneybag-minus": "\u{1013b}", + "moneybag-move": "\u{10139}", + "moneybag-move-back": "\u{1013a}", + "moneybag-plus": "\u{10138}", + "monkeybar": "\u{feb4}", + "mood-angry": "\u{f2de}", + "mood-angry-filled": "\u{ff0a}", + "mood-annoyed": "\u{f2e0}", + "mood-annoyed-2": "\u{f2df}", + "mood-bitcoin": "\u{ff32}", + "mood-boy": "\u{ed2d}", + "mood-check": "\u{f7b3}", + "mood-cog": "\u{f7b4}", + "mood-confuzed": "\u{eaf3}", + "mood-confuzed-filled": "\u{f7f2}", + "mood-crazy-happy": "\u{ed90}", + "mood-crazy-happy-filled": "\u{ff09}", + "mood-cry": "\u{ecbb}", + "mood-dollar": "\u{f7b5}", + "mood-edit": "\u{fa05}", + "mood-empty": "\u{eeb5}", + "mood-empty-filled": "\u{f7f3}", + "mood-happy": "\u{eaf4}", + "mood-happy-filled": "\u{f7f4}", + "mood-heart": "\u{f7b6}", + "mood-kid": "\u{ec03}", + "mood-kid-filled": "\u{f7f5}", + "mood-look-down": "\u{fd37}", + "mood-look-left": "\u{f2c5}", + "mood-look-right": "\u{f2c6}", + "mood-look-up": "\u{fd38}", + "mood-minus": "\u{f7b7}", + "mood-nerd": "\u{f2e1}", + "mood-nervous": "\u{ef96}", + "mood-neutral": "\u{eaf5}", + "mood-neutral-filled": "\u{f7f6}", + "mood-off": "\u{f161}", + "mood-pin": "\u{f7b8}", + "mood-plus": "\u{f7b9}", + "mood-puzzled": "\u{fd39}", + "mood-sad": "\u{eaf6}", + "mood-sad-2": "\u{f2e2}", + "mood-sad-dizzy": "\u{f2e3}", + "mood-sad-filled": "\u{f7f7}", + "mood-sad-squint": "\u{f2e4}", + "mood-search": "\u{f7ba}", + "mood-share": "\u{fa06}", + "mood-sick": "\u{f2e5}", + "mood-silence": "\u{f2e6}", + "mood-sing": "\u{f2c7}", + "mood-smile": "\u{eaf7}", + "mood-smile-beam": "\u{f2e7}", + "mood-smile-dizzy": "\u{f2e8}", + "mood-smile-filled": "\u{f7f8}", + "mood-spark": "\u{ffb2}", + "mood-suprised": "\u{ec04}", + "mood-surprised": "\u{ec04}", + "mood-tongue": "\u{eb95}", + "mood-tongue-wink": "\u{f2ea}", + "mood-tongue-wink-2": "\u{f2e9}", + "mood-unamused": "\u{f2eb}", + "mood-up": "\u{f7bb}", + "mood-wink": "\u{f2ed}", + "mood-wink-2": "\u{f2ec}", + "mood-wrrr": "\u{f2ee}", + "mood-wrrr-filled": "\u{ff08}", + "mood-x": "\u{f7bc}", + "mood-xd": "\u{f2ef}", + "moon": "\u{eaf8}", + "moon-2": "\u{ece6}", + "moon-filled": "\u{f684}", + "moon-off": "\u{f162}", + "moon-stars": "\u{ece7}", + "moped": "\u{ecbc}", + "motorbike": "\u{eeb6}", + "motorbike-filled": "\u{100f8}", + "mountain": "\u{ef97}", + "mountain-filled": "\u{1000a}", + "mountain-off": "\u{f411}", + "mouse": "\u{eaf9}", + "mouse-2": "\u{f1d7}", + "mouse-filled": "\u{fb2f}", + "mouse-off": "\u{f163}", + "moustache": "\u{f4c9}", + "movie": "\u{eafa}", + "movie-off": "\u{f164}", + "mug": "\u{eafb}", + "mug-filled": "\u{10009}", + "mug-off": "\u{f165}", + "multiplier-0-5x": "\u{ef41}", + "multiplier-1-5x": "\u{ef42}", + "multiplier-1x": "\u{ef43}", + "multiplier-2x": "\u{ef44}", + "mushroom": "\u{ef14}", + "mushroom-filled": "\u{f7f9}", + "mushroom-off": "\u{f412}", + "music": "\u{eafc}", + "music-bolt": "\u{fbd5}", + "music-cancel": "\u{fbd6}", + "music-check": "\u{fbd7}", + "music-code": "\u{fbd8}", + "music-cog": "\u{fbd9}", + "music-discount": "\u{fbda}", + "music-dollar": "\u{fbdb}", + "music-down": "\u{fbdc}", + "music-exclamation": "\u{fbdd}", + "music-heart": "\u{fbde}", + "music-minus": "\u{fbdf}", + "music-off": "\u{f166}", + "music-pause": "\u{fbe0}", + "music-pin": "\u{fbe1}", + "music-plus": "\u{fbe2}", + "music-question": "\u{fbe3}", + "music-search": "\u{fbe4}", + "music-share": "\u{fbe5}", + "music-star": "\u{fbe6}", + "music-up": "\u{fbe7}", + "music-x": "\u{fbe8}", + "navigation": "\u{f2c8}", + "navigation-bolt": "\u{fbe9}", + "navigation-cancel": "\u{fbea}", + "navigation-check": "\u{fbeb}", + "navigation-code": "\u{fbec}", + "navigation-cog": "\u{fbed}", + "navigation-discount": "\u{fbee}", + "navigation-dollar": "\u{fbef}", + "navigation-down": "\u{fbf0}", + "navigation-east": "\u{fcba}", + "navigation-exclamation": "\u{fbf1}", + "navigation-filled": "\u{f685}", + "navigation-heart": "\u{fbf2}", + "navigation-minus": "\u{fbf3}", + "navigation-north": "\u{fcbb}", + "navigation-off": "\u{f413}", + "navigation-pause": "\u{fbf4}", + "navigation-pin": "\u{fbf5}", + "navigation-plus": "\u{fbf6}", + "navigation-question": "\u{fbf7}", + "navigation-search": "\u{fbf8}", + "navigation-share": "\u{fbf9}", + "navigation-south": "\u{fcbc}", + "navigation-star": "\u{fbfa}", + "navigation-top": "\u{faec}", + "navigation-up": "\u{fbfb}", + "navigation-west": "\u{fcbd}", + "navigation-x": "\u{fbfc}", + "needle": "\u{f508}", + "needle-thread": "\u{f507}", + "network": "\u{f09f}", + "network-off": "\u{f414}", + "new-section": "\u{ebc1}", + "news": "\u{eafd}", + "news-off": "\u{f167}", + "nfc": "\u{eeb7}", + "nfc-off": "\u{f168}", + "niri": "\u{ec32}", + "noctalia": "\u{ec33}", + "no-copyright": "\u{efb9}", + "no-creative-commons": "\u{efba}", + "no-derivatives": "\u{efbb}", + "north-star": "\u{f014}", + "note": "\u{eb6d}", + "note-off": "\u{f169}", + "notebook": "\u{eb96}", + "notebook-off": "\u{f415}", + "notes": "\u{eb6e}", + "notes-off": "\u{f16a}", + "notification": "\u{eafe}", + "notification-off": "\u{f16b}", + "number": "\u{f1fe}", + "number-0": "\u{edf0}", + "number-0-small": "\u{fce1}", + "number-1": "\u{edf1}", + "number-1-small": "\u{fce2}", + "number-10": "\u{1005e}", + "number-10-small": "\u{fce3}", + "number-100-small": "\u{10005}", + "number-11": "\u{1005d}", + "number-11-small": "\u{fce4}", + "number-12-small": "\u{fce5}", + "number-123": "\u{f554}", + "number-13-small": "\u{fce6}", + "number-14-small": "\u{fce7}", + "number-15-small": "\u{fce8}", + "number-16-small": "\u{fce9}", + "number-17-small": "\u{fcea}", + "number-18-small": "\u{fceb}", + "number-19-small": "\u{fcec}", + "number-2": "\u{edf2}", + "number-2-small": "\u{fced}", + "number-20-small": "\u{fcee}", + "number-21-small": "\u{fcef}", + "number-22-small": "\u{fcf0}", + "number-23-small": "\u{fcf1}", + "number-24-small": "\u{fcf2}", + "number-25-small": "\u{fcf3}", + "number-26-small": "\u{fcf4}", + "number-27-small": "\u{fcf5}", + "number-28-small": "\u{fcf6}", + "number-29-small": "\u{fcf7}", + "number-3": "\u{edf3}", + "number-3-small": "\u{fcf8}", + "number-30-small": "\u{10004}", + "number-31-small": "\u{10003}", + "number-32-small": "\u{10002}", + "number-33-small": "\u{10001}", + "number-34-small": "\u{10000}", + "number-35-small": "\u{ffff}", + "number-36-small": "\u{fffe}", + "number-37-small": "\u{fffd}", + "number-38-small": "\u{fffc}", + "number-39-small": "\u{fffb}", + "number-4": "\u{edf4}", + "number-4-small": "\u{fcf9}", + "number-40-small": "\u{fffa}", + "number-41-small": "\u{fff9}", + //"number-42-small": "\u{fff8}", + //"number-43-small": "\u{fff7}", + //"number-44-small": "\u{fff6}", + //"number-45-small": "\u{fff5}", + //"number-46-small": "\u{fff4}", + //"number-47-small": "\u{fff3}", + //"number-48-small": "\u{fff2}", + //"number-49-small": "\u{fff1}", + "number-5": "\u{edf5}", + "number-5-small": "\u{fcfa}", + //"number-50-small": "\u{fff0}", + "number-51-small": "\u{ffef}", + "number-52-small": "\u{ffee}", + "number-53-small": "\u{ffed}", + "number-54-small": "\u{ffec}", + "number-55-small": "\u{ffeb}", + "number-56-small": "\u{ffea}", + "number-57-small": "\u{ffe9}", + "number-58-small": "\u{ffe8}", + "number-59-small": "\u{ffe7}", + "number-6": "\u{edf6}", + "number-6-small": "\u{fcfb}", + "number-60-small": "\u{ffe6}", + "number-61-small": "\u{ffe5}", + "number-62-small": "\u{ffe4}", + "number-63-small": "\u{ffe3}", + "number-64-small": "\u{ffe2}", + "number-65-small": "\u{ffe1}", + "number-66-small": "\u{ffe0}", + "number-67-small": "\u{ffdf}", + "number-68-small": "\u{ffde}", + "number-69-small": "\u{ffdd}", + "number-7": "\u{edf7}", + "number-7-small": "\u{fcfc}", + "number-70-small": "\u{ffdc}", + "number-71-small": "\u{ffdb}", + "number-72-small": "\u{ffda}", + "number-73-small": "\u{ffd9}", + "number-74-small": "\u{ffd8}", + "number-75-small": "\u{ffd7}", + "number-76-small": "\u{ffd6}", + "number-77-small": "\u{ffd5}", + "number-78-small": "\u{ffd4}", + "number-79-small": "\u{ffd3}", + "number-8": "\u{edf8}", + "number-8-small": "\u{fcfd}", + "number-80-small": "\u{ffd2}", + "number-81-small": "\u{ffd1}", + "number-82-small": "\u{ffd0}", + "number-83-small": "\u{ffcf}", + "number-84-small": "\u{ffce}", + "number-85-small": "\u{ffcd}", + "number-86-small": "\u{ffcc}", + "number-87-small": "\u{ffcb}", + "number-88-small": "\u{ffca}", + "number-89-small": "\u{ffc9}", + "number-9": "\u{edf9}", + "number-9-small": "\u{fcfe}", + "number-90-small": "\u{ffc8}", + "number-91-small": "\u{ffc7}", + "number-92-small": "\u{ffc6}", + "number-93-small": "\u{ffc5}", + "number-94-small": "\u{ffc4}", + "number-95-small": "\u{ffc3}", + "number-96-small": "\u{ffc2}", + "number-97-small": "\u{ffc1}", + "number-98-small": "\u{ffc0}", + "number-99-small": "\u{ffbf}", + "numbers": "\u{f015}", + "nurse": "\u{ef65}", + "nurse-filled": "\u{1009e}", + "nut": "\u{fc61}", + "object-scan": "\u{fef1}", + "octagon": "\u{ecbd}", + "octagon-filled": "\u{f686}", + "octagon-minus": "\u{fc92}", + "octagon-minus-2": "\u{fc91}", + "octagon-minus-filled": "\u{1017f}", + "octagon-off": "\u{eeb8}", + "octagon-plus": "\u{fc94}", + "octagon-plus-2": "\u{fc93}", + "octagon-plus-filled": "\u{1017e}", + "octahedron": "\u{faae}", + "octahedron-off": "\u{faac}", + "octahedron-plus": "\u{faad}", + "old": "\u{eeb9}", + "olympics": "\u{eeba}", + "olympics-off": "\u{f416}", + "om": "\u{f58d}", + "omega": "\u{eb97}", + "outbound": "\u{f249}", + "outlet": "\u{ebd7}", + "oval": "\u{f02e}", + "oval-filled": "\u{f687}", + "oval-vertical": "\u{f02d}", + "oval-vertical-filled": "\u{f688}", + "overline": "\u{eebb}", + "package": "\u{eaff}", + "package-export": "\u{f07a}", + "package-import": "\u{f07b}", + "package-off": "\u{f16c}", + "packages": "\u{f2c9}", + "pacman": "\u{eebc}", + "page-break": "\u{ec81}", + "paint": "\u{eb00}", + "paint-filled": "\u{f75f}", + "paint-off": "\u{f16d}", + "palette": "\u{eb01}", + "palette-filled": "\u{1009d}", + "palette-off": "\u{f16e}", + "panorama-horizontal": "\u{ed33}", + "panorama-horizontal-filled": "\u{fecc}", + "panorama-horizontal-off": "\u{f417}", + "panorama-vertical": "\u{ed34}", + "panorama-vertical-filled": "\u{fecb}", + "panorama-vertical-off": "\u{f418}", + "paper-bag": "\u{f02f}", + "paper-bag-off": "\u{f16f}", + "paperclip": "\u{eb02}", + "parachute": "\u{ed7c}", + "parachute-off": "\u{f170}", + "parentheses": "\u{ebd8}", + "parentheses-off": "\u{f171}", + "parking": "\u{eb03}", + "parking-circle": "\u{fd5a}", + "parking-circle-filled": "\u{feca}", + "parking-off": "\u{f172}", + "password": "\u{f4ca}", + "password-fingerprint": "\u{fc7b}", + "password-mobile-phone": "\u{fc7c}", + "password-user": "\u{fc7d}", + "paw": "\u{eff9}", + "paw-filled": "\u{f689}", + "paw-off": "\u{f419}", + "paywall": "\u{fd7e}", + "pdf": "\u{f7ac}", + "peace": "\u{ecbe}", + "pencil": "\u{eb04}", + "pencil-bolt": "\u{fbfd}", + "pencil-cancel": "\u{fbfe}", + "pencil-check": "\u{fbff}", + "pencil-code": "\u{fc00}", + "pencil-cog": "\u{fc01}", + "pencil-discount": "\u{fc02}", + "pencil-dollar": "\u{fc03}", + "pencil-down": "\u{fc04}", + "pencil-exclamation": "\u{fc05}", + "pencil-heart": "\u{fc06}", + "pencil-minus": "\u{f1eb}", + "pencil-off": "\u{f173}", + "pencil-pause": "\u{fc07}", + "pencil-pin": "\u{fc08}", + "pencil-plus": "\u{f1ec}", + "pencil-question": "\u{fc09}", + "pencil-search": "\u{fc0a}", + "pencil-share": "\u{fc0b}", + "pencil-star": "\u{fc0c}", + "pencil-up": "\u{fc0d}", + "pencil-x": "\u{fc0e}", + "pennant": "\u{ed7d}", + "pennant-2": "\u{f06a}", + "pennant-2-filled": "\u{f68a}", + "pennant-filled": "\u{f68b}", + "pennant-off": "\u{f174}", + "pentagon": "\u{efe3}", + "pentagon-filled": "\u{f68c}", + "pentagon-minus": "\u{feb3}", + "pentagon-number-0": "\u{fc7e}", + "pentagon-number-1": "\u{fc7f}", + "pentagon-number-2": "\u{fc80}", + "pentagon-number-3": "\u{fc81}", + "pentagon-number-4": "\u{fc82}", + "pentagon-number-5": "\u{fc83}", + "pentagon-number-6": "\u{fc84}", + "pentagon-number-7": "\u{fc85}", + "pentagon-number-8": "\u{fc86}", + "pentagon-number-9": "\u{fc87}", + "pentagon-off": "\u{f41a}", + "pentagon-plus": "\u{fc49}", + "pentagon-x": "\u{fc88}", + "pentagram": "\u{f586}", + "pepper": "\u{ef15}", + "pepper-off": "\u{f175}", + "percentage": "\u{ecf4}", + "percentage-0": "\u{fee5}", + "percentage-10": "\u{fee4}", + "percentage-100": "\u{fee3}", + "percentage-20": "\u{fee2}", + "percentage-25": "\u{fee1}", + "percentage-30": "\u{fee0}", + "percentage-33": "\u{fedf}", + "percentage-40": "\u{fede}", + "percentage-50": "\u{fedd}", + "percentage-60": "\u{fedc}", + "percentage-66": "\u{fedb}", + "percentage-70": "\u{feda}", + "percentage-75": "\u{fed9}", + "percentage-80": "\u{fed8}", + "percentage-90": "\u{fed7}", + "perfume": "\u{f509}", + "perspective": "\u{eebd}", + "perspective-off": "\u{f176}", + "phone": "\u{eb09}", + "phone-call": "\u{eb05}", + "phone-calling": "\u{ec43}", + "phone-check": "\u{ec05}", + "phone-done": "\u{ff9e}", + "phone-end": "\u{ff9d}", + "phone-filled": "\u{fa49}", + "phone-incoming": "\u{eb06}", + "phone-off": "\u{ecf5}", + "phone-outgoing": "\u{eb07}", + "phone-pause": "\u{eb08}", + "phone-plus": "\u{ec06}", + "phone-ringing": "\u{ff9c}", + "phone-spark": "\u{ffb1}", + "phone-x": "\u{ec07}", + "photo": "\u{eb0a}", + "photo-ai": "\u{fa32}", + "photo-bitcoin": "\u{ff31}", + "photo-bolt": "\u{f990}", + "photo-cancel": "\u{f35d}", + "photo-check": "\u{f35e}", + "photo-circle": "\u{fc4a}", + "photo-circle-minus": "\u{fc62}", + "photo-circle-plus": "\u{fc63}", + "photo-code": "\u{f991}", + "photo-cog": "\u{f992}", + "photo-dollar": "\u{f993}", + "photo-down": "\u{f35f}", + "photo-edit": "\u{f360}", + "photo-exclamation": "\u{f994}", + "photo-filled": "\u{fa4a}", + "photo-heart": "\u{f361}", + "photo-hexagon": "\u{fc4b}", + "photo-minus": "\u{f362}", + "photo-off": "\u{ecf6}", + "photo-pause": "\u{f995}", + "photo-pentagon": "\u{fc4c}", + "photo-pin": "\u{f996}", + "photo-plus": "\u{f363}", + "photo-question": "\u{f997}", + "photo-scan": "\u{fca8}", + "photo-search": "\u{f364}", + "photo-sensor": "\u{f798}", + "photo-sensor-2": "\u{f796}", + "photo-sensor-3": "\u{f797}", + "photo-share": "\u{f998}", + "photo-shield": "\u{f365}", + "photo-spark": "\u{ffb0}", + "photo-square-rounded": "\u{fc4d}", + "photo-star": "\u{f366}", + "photo-up": "\u{f38b}", + "photo-video": "\u{fc95}", + "photo-x": "\u{f367}", + "physotherapist": "\u{eebe}", + "piano": "\u{fad3}", + "pick": "\u{fafc}", + "picnic-table": "\u{fed6}", + "picture-in-picture": "\u{ed35}", + "picture-in-picture-filled": "\u{fec1}", + "picture-in-picture-off": "\u{ed43}", + "picture-in-picture-on": "\u{ed44}", + "picture-in-picture-top": "\u{efe4}", + "picture-in-picture-top-filled": "\u{fec2}", + "pig": "\u{ef52}", + "pig-filled": "\u{1010a}", + "pig-money": "\u{f38c}", + "pig-off": "\u{f177}", + "pilcrow": "\u{f5f6}", + "pilcrow-left": "\u{fd7f}", + "pilcrow-right": "\u{fd80}", + "pill": "\u{ec44}", + "pill-filled": "\u{ff07}", + "pill-off": "\u{f178}", + "pills": "\u{ef66}", + "pin": "\u{ec9c}", + "pin-end": "\u{fd5b}", + "pin-filled": "\u{f68d}", + "pin-invoke": "\u{fd5c}", + "ping-pong": "\u{f38d}", + "pinned": "\u{ed60}", + "pinned-filled": "\u{f68e}", + "pinned-off": "\u{ed5f}", + "pizza": "\u{edbb}", + "pizza-filled": "\u{10008}", + "pizza-off": "\u{f179}", + "placeholder": "\u{f626}", + "plane": "\u{eb6f}", + "plane-arrival": "\u{eb99}", + "plane-departure": "\u{eb9a}", + "plane-inflight": "\u{ef98}", + "plane-off": "\u{f17a}", + "plane-tilt": "\u{f1ed}", + "planet": "\u{ec08}", + "planet-off": "\u{f17b}", + "plant": "\u{ed50}", + "plant-2": "\u{ed7e}", + "plant-2-off": "\u{f17c}", + "plant-off": "\u{f17d}", + "play-basketball": "\u{fa66}", + "play-card": "\u{eebf}", + "play-card-1": "\u{1005c}", + "play-card-1-filled": "\u{10083}", + "play-card-10": "\u{1005b}", + "play-card-10-filled": "\u{10082}", + "play-card-2": "\u{1005a}", + "play-card-2-filled": "\u{10081}", + "play-card-3": "\u{10059}", + "play-card-3-filled": "\u{10080}", + "play-card-4": "\u{10058}", + "play-card-4-filled": "\u{1007f}", + "play-card-5": "\u{10057}", + "play-card-5-filled": "\u{1007e}", + "play-card-6": "\u{10056}", + "play-card-6-filled": "\u{1007d}", + "play-card-7": "\u{10055}", + "play-card-7-filled": "\u{1007c}", + "play-card-8": "\u{10054}", + "play-card-8-filled": "\u{1007b}", + "play-card-9": "\u{10053}", + "play-card-9-filled": "\u{1007a}", + "play-card-a": "\u{10052}", + "play-card-a-filled": "\u{10079}", + "play-card-j": "\u{10051}", + "play-card-j-filled": "\u{10078}", + "play-card-k": "\u{10050}", + "play-card-k-filled": "\u{10077}", + "play-card-off": "\u{f17e}", + "play-card-q": "\u{1004f}", + "play-card-q-filled": "\u{10076}", + "play-card-star": "\u{1004e}", + "play-card-star-filled": "\u{10075}", + "play-football": "\u{fa67}", + "play-handball": "\u{fa68}", + "play-volleyball": "\u{fa69}", + "player-eject": "\u{efbc}", + "player-eject-filled": "\u{f68f}", + "player-pause": "\u{ed45}", + "player-pause-filled": "\u{f690}", + "player-play": "\u{ed46}", + "player-play-filled": "\u{f691}", + "player-record": "\u{ed47}", + "player-record-filled": "\u{f692}", + "player-skip-back": "\u{ed48}", + "player-skip-back-filled": "\u{f693}", + "player-skip-forward": "\u{ed49}", + "player-skip-forward-filled": "\u{f694}", + "player-stop": "\u{ed4a}", + "player-stop-filled": "\u{f695}", + "player-track-next": "\u{ed4b}", + "player-track-next-filled": "\u{f696}", + "player-track-prev": "\u{ed4c}", + "player-track-prev-filled": "\u{f697}", + "playlist": "\u{eec0}", + "playlist-add": "\u{f008}", + "playlist-off": "\u{f17f}", + "playlist-x": "\u{f009}", + "playstation-circle": "\u{f2ad}", + "playstation-square": "\u{f2ae}", + "playstation-triangle": "\u{f2af}", + "playstation-x": "\u{f2b0}", + "plug": "\u{ebd9}", + "plug-filled": "\u{f6b3}", + "plug-connected": "\u{f00a}", + "plug-connected-x": "\u{f0a0}", + "plug-off": "\u{f180}", + "plug-x": "\u{f0a1}", + "plus": "\u{eb0b}", + "plus-equal": "\u{f7ad}", + "plus-minus": "\u{f7ae}", + "png": "\u{f3ad}", + "podium": "\u{f1d8}", + "podium-off": "\u{f41b}", + "point": "\u{eb0c}", + "point-filled": "\u{f698}", + "point-off": "\u{f181}", + "pointer": "\u{f265}", + "pointer-bolt": "\u{f999}", + "pointer-cancel": "\u{f99a}", + "pointer-check": "\u{f99b}", + "pointer-code": "\u{f99c}", + "pointer-cog": "\u{f99d}", + "pointer-dollar": "\u{f99e}", + "pointer-down": "\u{f99f}", + "pointer-exclamation": "\u{f9a0}", + "pointer-filled": "\u{fb30}", + "pointer-heart": "\u{f9a1}", + "pointer-minus": "\u{f9a2}", + "pointer-off": "\u{f9a3}", + "pointer-pause": "\u{f9a4}", + "pointer-pin": "\u{f9a5}", + "pointer-plus": "\u{f9a6}", + "pointer-question": "\u{f9a7}", + "pointer-search": "\u{f9a8}", + "pointer-share": "\u{f9a9}", + "pointer-star": "\u{f9aa}", + "pointer-up": "\u{f9ab}", + "pointer-x": "\u{f9ac}", + "pokeball": "\u{eec1}", + "pokeball-off": "\u{f41c}", + "poker-chip": "\u{f515}", + "polaroid": "\u{eec2}", + "polaroid-filled": "\u{fa4b}", + "polygon": "\u{efd0}", + "polygon-off": "\u{f182}", + "poo": "\u{f258}", + "poo-filled": "\u{fec9}", + "pool": "\u{ed91}", + "pool-off": "\u{f41d}", + "power": "\u{eb0d}", + "pray": "\u{ecbf}", + "premium-rights": "\u{efbd}", + "prescription": "\u{ef99}", + "presentation": "\u{eb70}", + "presentation-analytics": "\u{eec3}", + "presentation-analytics-filled": "\u{ff5d}", + "presentation-filled": "\u{ff5c}", + "presentation-off": "\u{f183}", + "printer": "\u{eb0e}", + "printer-off": "\u{f184}", + "prism": "\u{fab1}", + "prism-light": "\u{fea6}", + "prism-off": "\u{faaf}", + "prism-plus": "\u{fab0}", + "prison": "\u{ef79}", + "progress": "\u{fa0d}", + "progress-alert": "\u{fa07}", + "progress-bolt": "\u{fa08}", + "progress-check": "\u{fa09}", + "progress-down": "\u{fa0a}", + "progress-help": "\u{fa0b}", + "progress-x": "\u{fa0c}", + "prompt": "\u{eb0f}", + "prong": "\u{fda1}", + "propeller": "\u{eec4}", + "propeller-off": "\u{f185}", + "protocol": "\u{fd81}", + "pumpkin-scary": "\u{f587}", + "puzzle": "\u{eb10}", + "puzzle-2": "\u{ef83}", + "puzzle-filled": "\u{f699}", + "puzzle-off": "\u{f186}", + "pyramid": "\u{eec5}", + "pyramid-off": "\u{f187}", + "pyramid-plus": "\u{fab2}", + "qrcode": "\u{eb11}", + "qrcode-off": "\u{f41e}", + "question-mark": "\u{ec9d}", + "quote": "\u{efbe}", + "quote-filled": "\u{1009c}", + "quote-off": "\u{f188}", + //"quotes": "\u{fb1e}", + "radar": "\u{f017}", + "radar-2": "\u{f016}", + //"radar-filled": "\u{fe0d}", + "radar-off": "\u{f41f}", + "radio": "\u{ef2d}", + "radio-off": "\u{f420}", + "radioactive": "\u{ecc0}", + "radioactive-filled": "\u{f760}", + "radioactive-off": "\u{f189}", + "radius-bottom-left": "\u{eec6}", + "radius-bottom-right": "\u{eec7}", + "radius-top-left": "\u{eec8}", + "radius-top-right": "\u{eec9}", + "rainbow": "\u{edbc}", + "rainbow-off": "\u{f18a}", + "rating-12-plus": "\u{f266}", + "rating-14-plus": "\u{f267}", + "rating-16-plus": "\u{f268}", + "rating-18-plus": "\u{f269}", + "rating-21-plus": "\u{f26a}", + "razor": "\u{f4b5}", + "razor-electric": "\u{f4b4}", + "receipt": "\u{edfd}", + "receipt-2": "\u{edfa}", + "receipt-bitcoin": "\u{fd66}", + "receipt-dollar": "\u{fd67}", + "receipt-dollar-filled": "\u{1017d}", + "receipt-euro": "\u{fd68}", + "receipt-euro-filled": "\u{1017c}", + "receipt-filled": "\u{ff06}", + "receipt-off": "\u{edfb}", + "receipt-pound": "\u{fd69}", + "receipt-pound-filled": "\u{1017b}", + "receipt-refund": "\u{edfc}", + "receipt-rupee": "\u{fd82}", + "receipt-rupee-filled": "\u{1017a}", + "receipt-tax": "\u{edbd}", + "receipt-yen": "\u{fd6a}", + "receipt-yen-filled": "\u{10179}", + "receipt-yuan": "\u{fd6b}", + "receipt-yuan-filled": "\u{10178}", + "recharging": "\u{eeca}", + "record-mail": "\u{eb12}", + "record-mail-off": "\u{f18b}", + "rectangle": "\u{ed37}", + "rectangle-filled": "\u{f69a}", + "rectangle-rounded-bottom": "\u{faed}", + "rectangle-rounded-top": "\u{faee}", + "rectangle-vertical": "\u{ed36}", + "rectangle-vertical-filled": "\u{f69b}", + "rectangular-prism": "\u{fab5}", + "rectangular-prism-off": "\u{fab3}", + "rectangular-prism-plus": "\u{fab4}", + "recycle": "\u{eb9b}", + "recycle-off": "\u{f18c}", + "refresh": "\u{eb13}", + "refresh-alert": "\u{ed57}", + "refresh-dot": "\u{efbf}", + "refresh-off": "\u{f18d}", + "regex": "\u{f31f}", + "regex-off": "\u{f421}", + "registered": "\u{eb14}", + "relation-many-to-many": "\u{ed7f}", + //"relation-many-to-many-filled": "\u{fe0c}", + "relation-one-to-many": "\u{ed80}", + //"relation-one-to-many-filled": "\u{fe0b}", + "relation-one-to-one": "\u{ed81}", + //"relation-one-to-one-filled": "\u{fe0a}", + "reload": "\u{f3ae}", + "reorder": "\u{fc15}", + "repeat": "\u{eb72}", + "repeat-off": "\u{f18e}", + "repeat-once": "\u{eb71}", + "replace": "\u{ebc7}", + "replace-filled": "\u{f69c}", + "replace-off": "\u{f422}", + "replace-user": "\u{100f0}", + "report": "\u{eece}", + "report-analytics": "\u{eecb}", + "report-medical": "\u{eecc}", + "report-money": "\u{eecd}", + "report-off": "\u{f18f}", + "report-search": "\u{ef84}", + "reserved-line": "\u{f9f6}", + "resize": "\u{eecf}", + "restore": "\u{fafd}", + "rewind-backward-10": "\u{faba}", + "rewind-backward-15": "\u{fabb}", + "rewind-backward-20": "\u{fabc}", + "rewind-backward-30": "\u{fabd}", + "rewind-backward-40": "\u{fabe}", + "rewind-backward-5": "\u{fabf}", + "rewind-backward-50": "\u{fac0}", + "rewind-backward-60": "\u{fac1}", + "rewind-forward-10": "\u{fac2}", + "rewind-forward-15": "\u{fac3}", + "rewind-forward-20": "\u{fac4}", + "rewind-forward-30": "\u{fac5}", + "rewind-forward-40": "\u{fac6}", + "rewind-forward-5": "\u{fac7}", + "rewind-forward-50": "\u{fac8}", + "rewind-forward-60": "\u{fac9}", + "ribbon-health": "\u{f58e}", + "rings": "\u{fa6a}", + "ripple": "\u{ed82}", + "ripple-off": "\u{f190}", + "road": "\u{f018}", + "road-off": "\u{f191}", + "road-sign": "\u{ecdd}", + "robot": "\u{f00b}", + "robot-face": "\u{fcbe}", + "robot-off": "\u{f192}", + "rocket": "\u{ec45}", + "rocket-off": "\u{f193}", + "roller-skating": "\u{efd1}", + "rollercoaster": "\u{f0a2}", + "rollercoaster-filled": "\u{100f7}", + "rollercoaster-off": "\u{f423}", + "rosette": "\u{f599}", + "rosette-discount": "\u{ee7c}", + "rosette-discount-check": "\u{f1f8}", + "rosette-discount-check-filled": "\u{f746}", + "rosette-discount-check-off": "\u{ff10}", + "rosette-discount-filled": "\u{ff05}", + "rosette-discount-off": "\u{f3e6}", + "rosette-filled": "\u{f69d}", + "rosette-number-0": "\u{f58f}", + "rosette-number-1": "\u{f590}", + "rosette-number-2": "\u{f591}", + "rosette-number-3": "\u{f592}", + "rosette-number-4": "\u{f593}", + "rosette-number-5": "\u{f594}", + "rosette-number-6": "\u{f595}", + "rosette-number-7": "\u{f596}", + "rosette-number-8": "\u{f597}", + "rosette-number-9": "\u{f598}", + "rotate": "\u{eb16}", + "rotate-2": "\u{ebb4}", + "rotate-360": "\u{ef85}", + "rotate-3d": "\u{f020}", + "rotate-clockwise": "\u{eb15}", + "rotate-clockwise-2": "\u{ebb5}", + "rotate-dot": "\u{efe5}", + "rotate-rectangle": "\u{ec15}", + "route": "\u{eb17}", + "route-2": "\u{f4b6}", + "route-alt-left": "\u{fca9}", + "route-alt-right": "\u{fcaa}", + "route-off": "\u{f194}", + "route-scan": "\u{fcbf}", + "route-square": "\u{fcac}", + "route-square-2": "\u{fcab}", + "route-x": "\u{fcae}", + "route-x-2": "\u{fcad}", + "router": "\u{eb18}", + "router-off": "\u{f424}", + "row-insert-bottom": "\u{eed0}", + "row-insert-top": "\u{eed1}", + "row-remove": "\u{fafe}", + "rss": "\u{eb19}", + "rubber-stamp": "\u{f5ab}", + "rubber-stamp-off": "\u{f5aa}", + "ruler": "\u{eb1a}", + "ruler-2": "\u{eed2}", + "ruler-2-off": "\u{f195}", + "ruler-3": "\u{f290}", + "ruler-measure": "\u{f291}", + "ruler-measure-2": "\u{ff0f}", + "ruler-off": "\u{f196}", + "run": "\u{ec82}", + "rv-truck": "\u{fcc0}", + "s-turn-down": "\u{f516}", + "s-turn-left": "\u{f517}", + "s-turn-right": "\u{f518}", + "s-turn-up": "\u{f519}", + "sailboat": "\u{ec83}", + "sailboat-2": "\u{f5f7}", + "sailboat-off": "\u{f425}", + "salad": "\u{f50a}", + "salad-filled": "\u{10007}", + "salt": "\u{ef16}", + "sandbox": "\u{fd6c}", + "satellite": "\u{eed3}", + "satellite-off": "\u{f197}", + "sausage": "\u{ef17}", + "scale": "\u{ebc2}", + "scale-off": "\u{f198}", + "scale-outline": "\u{ef53}", + "scale-outline-off": "\u{f199}", + "scan": "\u{ebc8}", + "scan-eye": "\u{f1ff}", + "scan-position": "\u{fdac}", + "schema": "\u{f200}", + "schema-off": "\u{f426}", + "school": "\u{ecf7}", + "school-bell": "\u{f64a}", + "school-off": "\u{f19a}", + "scissors": "\u{eb1b}", + "scissors-off": "\u{f19b}", + "scooter": "\u{ec6c}", + "scooter-electric": "\u{ecc1}", + "scoreboard": "\u{fa6b}", + "screen-share": "\u{ed18}", + "screen-share-off": "\u{ed17}", + "screenshot": "\u{f201}", + "scribble": "\u{f0a3}", + "scribble-off": "\u{f427}", + "script": "\u{f2da}", + "script-minus": "\u{f2d7}", + "script-plus": "\u{f2d8}", + "script-x": "\u{f2d9}", + "scuba-diving": "\u{fd4e}", + "scuba-diving-tank": "\u{fefa}", + "scuba-diving-tank-filled": "\u{ff04}", + "scuba-mask": "\u{eed4}", + "scuba-mask-off": "\u{f428}", + "sdk": "\u{f3af}", + "search": "\u{eb1c}", + "search-off": "\u{f19c}", + "section": "\u{eed5}", + //"section-filled": "\u{fe09}", + "section-sign": "\u{f019}", + "seeding": "\u{ed51}", + "seeding-filled": "\u{10006}", + "seeding-off": "\u{f19d}", + "seedling": "\u{ed51}", + "seedling-filled": "\u{10006}", + "seedling-off": "\u{f19d}", + "select": "\u{ec9e}", + "select-all": "\u{f9f7}", + "selector": "\u{eb1d}", + "send": "\u{eb1e}", + "send-2": "\u{fd5d}", + "send-off": "\u{f429}", + "seo": "\u{f26b}", + "separator": "\u{ebda}", + "separator-horizontal": "\u{ec79}", + "separator-vertical": "\u{ec7a}", + "server": "\u{eb1f}", + "server-2": "\u{f07c}", + "server-bolt": "\u{f320}", + "server-cog": "\u{f321}", + "server-off": "\u{f19e}", + "server-spark": "\u{ffaf}", + "servicemark": "\u{ec09}", + "settings": "\u{eb20}", + "settings-2": "\u{f5ac}", + "settings-automation": "\u{eed6}", + "settings-bolt": "\u{f9ad}", + "settings-cancel": "\u{f9ae}", + "settings-check": "\u{f9af}", + "settings-code": "\u{f9b0}", + "settings-cog": "\u{f9b1}", + "settings-dollar": "\u{f9b2}", + "settings-down": "\u{f9b3}", + "settings-exclamation": "\u{f9b4}", + "settings-filled": "\u{f69e}", + "settings-heart": "\u{f9b5}", + "settings-minus": "\u{f9b6}", + "settings-off": "\u{f19f}", + "settings-pause": "\u{f9b7}", + "settings-pin": "\u{f9b8}", + "settings-plus": "\u{f9b9}", + "settings-question": "\u{f9ba}", + "settings-search": "\u{f9bb}", + "settings-share": "\u{f9bc}", + "settings-spark": "\u{ffae}", + "settings-star": "\u{f9bd}", + "settings-up": "\u{f9be}", + "settings-x": "\u{f9bf}", + "shadow": "\u{eed8}", + "shadow-off": "\u{eed7}", + "shape": "\u{eb9c}", + "shape-2": "\u{eed9}", + "shape-3": "\u{eeda}", + "shape-off": "\u{f1a0}", + "share": "\u{eb21}", + "share-2": "\u{f799}", + "share-3": "\u{f7bd}", + "share-off": "\u{f1a1}", + "shareplay": "\u{fea5}", + "shi-jumping": "\u{fa6c}", + "shield": "\u{eb24}", + "shield-bolt": "\u{f9c0}", + "shield-cancel": "\u{f9c1}", + "shield-check": "\u{eb22}", + "shield-check-filled": "\u{f761}", + "shield-checkered": "\u{ef9a}", + "shield-checkered-filled": "\u{f762}", + "shield-chevron": "\u{ef9b}", + "shield-code": "\u{f9c2}", + "shield-cog": "\u{f9c3}", + "shield-dollar": "\u{f9c4}", + "shield-down": "\u{f9c5}", + "shield-exclamation": "\u{f9c6}", + "shield-filled": "\u{f69f}", + "shield-half": "\u{f358}", + "shield-half-filled": "\u{f357}", + "shield-heart": "\u{f9c7}", + "shield-lock": "\u{ed58}", + "shield-lock-filled": "\u{f763}", + "shield-minus": "\u{f9c8}", + "shield-off": "\u{ecf8}", + "shield-pause": "\u{f9c9}", + "shield-pin": "\u{f9ca}", + "shield-plus": "\u{f9cb}", + "shield-question": "\u{f9cc}", + "shield-search": "\u{f9cd}", + "shield-share": "\u{f9ce}", + "shield-star": "\u{f9cf}", + "shield-up": "\u{f9d0}", + "shield-x": "\u{eb23}", + "ship": "\u{ec84}", + "ship-off": "\u{f42a}", + "shirt": "\u{ec0a}", + "shirt-filled": "\u{f6a0}", + "shirt-off": "\u{f1a2}", + "shirt-sport": "\u{f26c}", + "shoe": "\u{efd2}", + "shoe-off": "\u{f1a4}", + "shopping-bag": "\u{f5f8}", + "shopping-bag-check": "\u{fc16}", + "shopping-bag-discount": "\u{fc17}", + "shopping-bag-edit": "\u{fc18}", + "shopping-bag-exclamation": "\u{fc19}", + "shopping-bag-heart": "\u{fda2}", + "shopping-bag-minus": "\u{fc1a}", + "shopping-bag-plus": "\u{fc1b}", + "shopping-bag-search": "\u{fc1c}", + "shopping-bag-x": "\u{fc1d}", + "shopping-cart": "\u{eb25}", + "shopping-cart-bolt": "\u{fb57}", + "shopping-cart-cancel": "\u{fb58}", + "shopping-cart-check": "\u{fb59}", + "shopping-cart-code": "\u{fb5a}", + "shopping-cart-cog": "\u{fb5b}", + "shopping-cart-copy": "\u{fb5c}", + "shopping-cart-discount": "\u{fb5d}", + "shopping-cart-dollar": "\u{fb5e}", + "shopping-cart-down": "\u{fb5f}", + "shopping-cart-exclamation": "\u{fb60}", + "shopping-cart-filled": "\u{fc3f}", + "shopping-cart-heart": "\u{fb61}", + "shopping-cart-minus": "\u{fb62}", + "shopping-cart-off": "\u{eedc}", + "shopping-cart-pause": "\u{fb63}", + "shopping-cart-pin": "\u{fb64}", + "shopping-cart-plus": "\u{fb65}", + "shopping-cart-question": "\u{fb66}", + "shopping-cart-search": "\u{fb67}", + "shopping-cart-share": "\u{fb68}", + "shopping-cart-star": "\u{fb69}", + "shopping-cart-up": "\u{fb6a}", + "shopping-cart-x": "\u{fb6b}", + "shovel": "\u{f1d9}", + "shovel-pitchforks": "\u{fd3a}", + "shredder": "\u{eedf}", + "sign-left": "\u{f06b}", + "sign-left-filled": "\u{f6a1}", + "sign-right": "\u{f06c}", + "sign-right-filled": "\u{f6a2}", + "signal-2g": "\u{f79a}", + "signal-3g": "\u{f1ee}", + "signal-4g": "\u{f1ef}", + "signal-4g-plus": "\u{f259}", + "signal-5g": "\u{f1f0}", + "signal-6g": "\u{f9f8}", + "signal-e": "\u{f9f9}", + "signal-g": "\u{f9fa}", + "signal-h": "\u{f9fc}", + "signal-h-plus": "\u{f9fb}", + "signal-lte": "\u{f9fd}", + "signature": "\u{eee0}", + "signature-off": "\u{f1a5}", + "sitemap": "\u{eb9d}", + "sitemap-filled": "\u{1006b}", + "sitemap-off": "\u{f1a6}", + "skateboard": "\u{ecc2}", + "skateboard-off": "\u{f42b}", + "skateboarding": "\u{faca}", + "skew-x": "\u{fd3b}", + "skew-y": "\u{fd3c}", + "ski-jumping": "\u{fa6c}", + "skull": "\u{f292}", + "slash": "\u{f4f9}", + "slashes": "\u{f588}", + "sleigh": "\u{ef9c}", + "slice": "\u{ebdb}", + "slideshow": "\u{ebc9}", + "smart-home": "\u{ecde}", + "smart-home-off": "\u{f1a7}", + "smoking": "\u{ecc4}", + "smoking-no": "\u{ecc3}", + "snowboarding": "\u{fd4f}", + "snowflake": "\u{ec0b}", + "snowflake-off": "\u{f1a8}", + "snowman": "\u{f26d}", + "soccer-field": "\u{ed92}", + "social": "\u{ebec}", + "social-off": "\u{f1a9}", + "sock": "\u{eee1}", + "sofa": "\u{efaf}", + "sofa-off": "\u{f42c}", + "solar-electricity": "\u{fcc1}", + "solar-panel": "\u{f7bf}", + "solar-panel-2": "\u{f7be}", + "sort-0-9": "\u{f54d}", + "sort-9-0": "\u{f54e}", + "sort-a-z": "\u{f54f}", + "sort-ascending": "\u{eb26}", + "sort-ascending-2": "\u{eee2}", + "sort-ascending-2-filled": "\u{ff5b}", + "sort-ascending-letters": "\u{ef18}", + "sort-ascending-numbers": "\u{ef19}", + "sort-ascending-shapes": "\u{fd94}", + "sort-ascending-shapes-filled": "\u{ff5a}", + "sort-ascending-small-big": "\u{fd95}", + "sort-deacending-small-big": "\u{fd96}", + "sort-descending": "\u{eb27}", + "sort-descending-2": "\u{eee3}", + "sort-descending-2-filled": "\u{ff59}", + "sort-descending-letters": "\u{ef1a}", + "sort-descending-numbers": "\u{ef1b}", + "sort-descending-shapes": "\u{fd97}", + "sort-descending-shapes-filled": "\u{ff58}", + "sort-descending-small-big": "\u{fd96}", + "sort-z-a": "\u{f550}", + "sos": "\u{f24a}", + "soup": "\u{ef2e}", + //"soup-filled": "\u{fe08}", + "soup-off": "\u{f42d}", + "source-code": "\u{f4a2}", + "space": "\u{ec0c}", + "space-off": "\u{f1aa}", + "spaces": "\u{fea4}", + "spacing-horizontal": "\u{ef54}", + "spacing-vertical": "\u{ef55}", + "spade": "\u{effa}", + "spade-filled": "\u{f6a3}", + "sparkles": "\u{f6d7}", + "speakerphone": "\u{ed61}", + "speedboat": "\u{ed93}", + "speedboat-filled": "\u{100f6}", + "sphere": "\u{fab8}", + "sphere-off": "\u{fab6}", + "sphere-plus": "\u{fab7}", + "spider": "\u{f293}", + "spider-filled": "\u{10109}", + "spiral": "\u{f294}", + "spiral-off": "\u{f42e}", + "sport-billard": "\u{eee4}", + "spray": "\u{f50b}", + "spy": "\u{f227}", + "spy-off": "\u{f42f}", + "sql": "\u{f7c0}", + "square": "\u{eb2c}", + "square-0": "\u{eee5}", + "square-1": "\u{eee6}", + "square-2": "\u{eee7}", + "square-3": "\u{eee8}", + "square-4": "\u{eee9}", + "square-5": "\u{eeea}", + "square-6": "\u{eeeb}", + "square-7": "\u{eeec}", + "square-8": "\u{eeed}", + "square-9": "\u{eeee}", + "square-arrow-down": "\u{f4b7}", + "square-arrow-down-filled": "\u{fb31}", + "square-arrow-left": "\u{f4b8}", + "square-arrow-left-filled": "\u{fb32}", + "square-arrow-right": "\u{f4b9}", + "square-arrow-right-filled": "\u{fb33}", + "square-arrow-up": "\u{f4ba}", + "square-arrow-up-filled": "\u{fb34}", + "square-asterisk": "\u{f01a}", + "square-asterisk-filled": "\u{fb35}", + "square-check": "\u{eb28}", + "square-check-filled": "\u{f76d}", + "square-chevron-down": "\u{f627}", + "square-chevron-down-filled": "\u{fb36}", + "square-chevron-left": "\u{f628}", + "square-chevron-left-filled": "\u{fb37}", + "square-chevron-right": "\u{f629}", + "square-chevron-right-filled": "\u{fb38}", + "square-chevron-up": "\u{f62a}", + "square-chevron-up-filled": "\u{fb39}", + "square-chevrons-down": "\u{f64b}", + "square-chevrons-down-filled": "\u{fb3a}", + "square-chevrons-left": "\u{f64c}", + "square-chevrons-left-filled": "\u{fb3b}", + "square-chevrons-right": "\u{f64d}", + "square-chevrons-right-filled": "\u{fb3c}", + "square-chevrons-up": "\u{f64e}", + "square-chevrons-up-filled": "\u{fb3d}", + "square-dashed": "\u{100bb}", + "square-dot": "\u{ed59}", + "square-dot-filled": "\u{fb3e}", + "square-f0": "\u{f526}", + "square-f0-filled": "\u{f76e}", + "square-f1": "\u{f527}", + "square-f1-filled": "\u{f76f}", + "square-f2": "\u{f528}", + "square-f2-filled": "\u{f770}", + "square-f3": "\u{f529}", + "square-f3-filled": "\u{f771}", + "square-f4": "\u{f52a}", + "square-f4-filled": "\u{f772}", + "square-f5": "\u{f52b}", + "square-f5-filled": "\u{f773}", + "square-f6": "\u{f52c}", + "square-f6-filled": "\u{f774}", + "square-f7": "\u{f52d}", + "square-f7-filled": "\u{f775}", + "square-f8": "\u{f52e}", + "square-f8-filled": "\u{f776}", + "square-f9": "\u{f52f}", + "square-f9-filled": "\u{f777}", + "square-filled": "\u{fc40}", + "square-forbid": "\u{ed5b}", + "square-forbid-2": "\u{ed5a}", + "square-half": "\u{effb}", + "square-key": "\u{f638}", + "square-letter-a": "\u{f47c}", + //"square-letter-a-filled": "\u{fe07}", + "square-letter-b": "\u{f47d}", + //"square-letter-b-filled": "\u{fe06}", + "square-letter-c": "\u{f47e}", + //"square-letter-c-filled": "\u{fe05}", + "square-letter-d": "\u{f47f}", + //"square-letter-d-filled": "\u{fe04}", + "square-letter-e": "\u{f480}", + //"square-letter-e-filled": "\u{fe03}", + "square-letter-f": "\u{f481}", + //"square-letter-f-filled": "\u{fe02}", + "square-letter-g": "\u{f482}", + //"square-letter-g-filled": "\u{fe01}", + "square-letter-h": "\u{f483}", + //"square-letter-h-filled": "\u{fe00}", + "square-letter-i": "\u{f484}", + "square-letter-i-filled": "\u{fdff}", + "square-letter-j": "\u{f485}", + "square-letter-j-filled": "\u{fdfe}", + "square-letter-k": "\u{f486}", + "square-letter-k-filled": "\u{fdfd}", + "square-letter-l": "\u{f487}", + "square-letter-l-filled": "\u{fdfc}", + "square-letter-m": "\u{f488}", + "square-letter-m-filled": "\u{fdfb}", + "square-letter-n": "\u{f489}", + "square-letter-n-filled": "\u{fdfa}", + "square-letter-o": "\u{f48a}", + "square-letter-o-filled": "\u{fdf9}", + "square-letter-p": "\u{f48b}", + "square-letter-p-filled": "\u{fdf8}", + "square-letter-q": "\u{f48c}", + "square-letter-q-filled": "\u{fdf7}", + "square-letter-r": "\u{f48d}", + "square-letter-r-filled": "\u{fdf6}", + "square-letter-s": "\u{f48e}", + "square-letter-s-filled": "\u{fdf5}", + "square-letter-t": "\u{f48f}", + "square-letter-t-filled": "\u{fdf4}", + "square-letter-u": "\u{f490}", + "square-letter-u-filled": "\u{fdf3}", + "square-letter-v": "\u{f4bb}", + "square-letter-v-filled": "\u{fdf2}", + "square-letter-w": "\u{f491}", + "square-letter-w-filled": "\u{fdf1}", + "square-letter-x": "\u{f4bc}", + "square-letter-x-filled": "\u{fdf0}", + "square-letter-y": "\u{f492}", + "square-letter-y-filled": "\u{fdef}", + "square-letter-z": "\u{f493}", + "square-letter-z-filled": "\u{fdee}", + "square-minus": "\u{eb29}", + "square-minus-filled": "\u{fb3f}", + "square-number-0": "\u{eee5}", + "square-number-0-filled": "\u{f764}", + "square-number-1": "\u{eee6}", + "square-number-1-filled": "\u{f765}", + "square-number-2": "\u{eee7}", + "square-number-2-filled": "\u{f7fa}", + "square-number-3": "\u{eee8}", + "square-number-3-filled": "\u{f766}", + "square-number-4": "\u{eee9}", + "square-number-4-filled": "\u{f767}", + "square-number-5": "\u{eeea}", + "square-number-5-filled": "\u{f768}", + "square-number-6": "\u{eeeb}", + "square-number-6-filled": "\u{f769}", + "square-number-7": "\u{eeec}", + "square-number-7-filled": "\u{f76a}", + "square-number-8": "\u{eeed}", + "square-number-8-filled": "\u{f76b}", + "square-number-9": "\u{eeee}", + "square-number-9-filled": "\u{f76c}", + "square-off": "\u{eeef}", + "square-percentage": "\u{fd83}", + "square-plus": "\u{eb2a}", + "square-plus-2": "\u{fc96}", + "square-root": "\u{eef1}", + "square-root-2": "\u{eef0}", + "square-rotated": "\u{ecdf}", + "square-rotated-filled": "\u{f6a4}", + "square-rotated-forbid": "\u{f01c}", + "square-rotated-forbid-2": "\u{f01b}", + "square-rotated-off": "\u{eef2}", + "square-rounded": "\u{f59a}", + "square-rounded-arrow-down": "\u{f639}", + "square-rounded-arrow-down-filled": "\u{f6db}", + "square-rounded-arrow-left": "\u{f63a}", + "square-rounded-arrow-left-filled": "\u{f6dc}", + "square-rounded-arrow-right": "\u{f63b}", + "square-rounded-arrow-right-filled": "\u{f6dd}", + "square-rounded-arrow-up": "\u{f63c}", + "square-rounded-arrow-up-filled": "\u{f6de}", + "square-rounded-check": "\u{f63d}", + "square-rounded-check-filled": "\u{f6df}", + "square-rounded-chevron-down": "\u{f62b}", + "square-rounded-chevron-down-filled": "\u{f6e0}", + "square-rounded-chevron-left": "\u{f62c}", + "square-rounded-chevron-left-filled": "\u{f6e1}", + "square-rounded-chevron-right": "\u{f62d}", + "square-rounded-chevron-right-filled": "\u{f6e2}", + "square-rounded-chevron-up": "\u{f62e}", + "square-rounded-chevron-up-filled": "\u{f6e3}", + "square-rounded-chevrons-down": "\u{f64f}", + "square-rounded-chevrons-down-filled": "\u{f6e4}", + "square-rounded-chevrons-left": "\u{f650}", + "square-rounded-chevrons-left-filled": "\u{f6e5}", + "square-rounded-chevrons-right": "\u{f651}", + "square-rounded-chevrons-right-filled": "\u{f6e6}", + "square-rounded-chevrons-up": "\u{f652}", + "square-rounded-chevrons-up-filled": "\u{f6e7}", + "square-rounded-filled": "\u{f6a5}", + "square-rounded-letter-a": "\u{f5ae}", + "square-rounded-letter-a-filled": "\u{fded}", + "square-rounded-letter-b": "\u{f5af}", + "square-rounded-letter-b-filled": "\u{fdec}", + "square-rounded-letter-c": "\u{f5b0}", + "square-rounded-letter-c-filled": "\u{fdeb}", + "square-rounded-letter-d": "\u{f5b1}", + "square-rounded-letter-d-filled": "\u{fdea}", + "square-rounded-letter-e": "\u{f5b2}", + "square-rounded-letter-e-filled": "\u{fde9}", + "square-rounded-letter-f": "\u{f5b3}", + "square-rounded-letter-f-filled": "\u{fde8}", + "square-rounded-letter-g": "\u{f5b4}", + "square-rounded-letter-g-filled": "\u{fde7}", + "square-rounded-letter-h": "\u{f5b5}", + "square-rounded-letter-h-filled": "\u{fde6}", + "square-rounded-letter-i": "\u{f5b6}", + "square-rounded-letter-i-filled": "\u{fde5}", + "square-rounded-letter-j": "\u{f5b7}", + "square-rounded-letter-j-filled": "\u{fde4}", + "square-rounded-letter-k": "\u{f5b8}", + "square-rounded-letter-k-filled": "\u{fde3}", + "square-rounded-letter-l": "\u{f5b9}", + "square-rounded-letter-l-filled": "\u{fde2}", + "square-rounded-letter-m": "\u{f5ba}", + "square-rounded-letter-m-filled": "\u{fde1}", + "square-rounded-letter-n": "\u{f5bb}", + "square-rounded-letter-n-filled": "\u{fde0}", + "square-rounded-letter-o": "\u{f5bc}", + "square-rounded-letter-o-filled": "\u{fddf}", + "square-rounded-letter-p": "\u{f5bd}", + "square-rounded-letter-p-filled": "\u{fdde}", + "square-rounded-letter-q": "\u{f5be}", + "square-rounded-letter-q-filled": "\u{fddd}", + "square-rounded-letter-r": "\u{f5bf}", + "square-rounded-letter-r-filled": "\u{fddc}", + "square-rounded-letter-s": "\u{f5c0}", + "square-rounded-letter-s-filled": "\u{fddb}", + "square-rounded-letter-t": "\u{f5c1}", + "square-rounded-letter-t-filled": "\u{fdda}", + "square-rounded-letter-u": "\u{f5c2}", + "square-rounded-letter-u-filled": "\u{fdd9}", + "square-rounded-letter-v": "\u{f5c3}", + "square-rounded-letter-v-filled": "\u{fdd8}", + "square-rounded-letter-w": "\u{f5c4}", + "square-rounded-letter-w-filled": "\u{fdd7}", + "square-rounded-letter-x": "\u{f5c5}", + "square-rounded-letter-x-filled": "\u{fdd6}", + "square-rounded-letter-y": "\u{f5c6}", + "square-rounded-letter-y-filled": "\u{fdd5}", + "square-rounded-letter-z": "\u{f5c7}", + "square-rounded-letter-z-filled": "\u{fdd4}", + "square-rounded-minus": "\u{f63e}", + "square-rounded-minus-2": "\u{fc97}", + "square-rounded-minus-filled": "\u{fb40}", + "square-rounded-number-0": "\u{f5c8}", + "square-rounded-number-0-filled": "\u{f778}", + "square-rounded-number-1": "\u{f5c9}", + "square-rounded-number-1-filled": "\u{f779}", + "square-rounded-number-2": "\u{f5ca}", + "square-rounded-number-2-filled": "\u{f77a}", + "square-rounded-number-3": "\u{f5cb}", + "square-rounded-number-3-filled": "\u{f77b}", + "square-rounded-number-4": "\u{f5cc}", + "square-rounded-number-4-filled": "\u{f77c}", + "square-rounded-number-5": "\u{f5cd}", + "square-rounded-number-5-filled": "\u{f77d}", + "square-rounded-number-6": "\u{f5ce}", + "square-rounded-number-6-filled": "\u{f77e}", + "square-rounded-number-7": "\u{f5cf}", + "square-rounded-number-7-filled": "\u{f77f}", + "square-rounded-number-8": "\u{f5d0}", + "square-rounded-number-8-filled": "\u{f780}", + "square-rounded-number-9": "\u{f5d1}", + "square-rounded-number-9-filled": "\u{f781}", + "square-rounded-percentage": "\u{fd84}", + "square-rounded-plus": "\u{f63f}", + "square-rounded-plus-2": "\u{fc98}", + "square-rounded-plus-filled": "\u{f6e8}", + "square-rounded-x": "\u{f640}", + "square-rounded-x-filled": "\u{f6e9}", + "square-toggle": "\u{eef4}", + "square-toggle-horizontal": "\u{eef3}", + "square-x": "\u{eb2b}", + "square-x-filled": "\u{fb41}", + "squares": "\u{eef6}", + "squares-diagonal": "\u{eef5}", + "squares-filled": "\u{fe9f}", + "squares-selected": "\u{fea3}", + "stack": "\u{eb2d}", + "stack-2": "\u{eef7}", + "stack-2-filled": "\u{fdd3}", + "stack-3": "\u{ef9d}", + "stack-3-filled": "\u{fdd2}", + "stack-back": "\u{fd26}", + "stack-backward": "\u{fd27}", + "stack-filled": "\u{fdd1}", + "stack-forward": "\u{fd28}", + "stack-front": "\u{fd29}", + "stack-middle": "\u{fd2a}", + "stack-pop": "\u{f234}", + "stack-push": "\u{f235}", + "stairs": "\u{eca6}", + "stairs-down": "\u{eca4}", + "stairs-up": "\u{eca5}", + "star": "\u{eb2e}", + "star-filled": "\u{f6a6}", + "star-half": "\u{ed19}", + "star-half-filled": "\u{f6a7}", + "star-off": "\u{ed62}", + "stars": "\u{ed38}", + "stars-filled": "\u{f6a8}", + "stars-off": "\u{f430}", + "status-change": "\u{f3b0}", + "steam": "\u{f24b}", + "steering-wheel": "\u{ec7b}", + "steering-wheel-filled": "\u{ff03}", + "steering-wheel-off": "\u{f431}", + "step-into": "\u{ece0}", + "step-out": "\u{ece1}", + "stereo-glasses": "\u{f4cb}", + "stethoscope": "\u{edbe}", + "stethoscope-off": "\u{f432}", + "sticker": "\u{eb2f}", + "sticker-2": "\u{fd3d}", + "stopwatch": "\u{ff9b}", + "storm": "\u{f24c}", + "storm-off": "\u{f433}", + "stretching": "\u{f2db}", + "stretching-2": "\u{fa6d}", + "strikethrough": "\u{eb9e}", + "submarine": "\u{ed94}", + "subscript": "\u{eb9f}", + "subtask": "\u{ec9f}", + "sum": "\u{eb73}", + "sum-off": "\u{f1ab}", + "sun": "\u{eb30}", + "sun-electricity": "\u{fcc2}", + "sun-filled": "\u{f6a9}", + "sun-high": "\u{f236}", + "sun-high-filled": "\u{10108}", + "sun-low": "\u{f237}", + "sun-low-filled": "\u{10107}", + "sun-moon": "\u{f4a3}", + "sun-off": "\u{ed63}", + "sun-wind": "\u{f238}", + "sunglasses": "\u{f239}", + "sunglasses-filled": "\u{fec8}", + "sunrise": "\u{ef1c}", + "sunrise-filled": "\u{10106}", + "sunset": "\u{ec31}", + "sunset-2": "\u{f23a}", + "sunset-2-filled": "\u{10105}", + "sunset-filled": "\u{10104}", + "superscript": "\u{eba0}", + "svg": "\u{f25a}", + "swimming": "\u{ec92}", + "swipe": "\u{f551}", + "swipe-down": "\u{fd5e}", + "swipe-down-filled": "\u{ff57}", + "swipe-left": "\u{fd5f}", + "swipe-left-filled": "\u{ff56}", + "swipe-right": "\u{fd60}", + "swipe-right-filled": "\u{ff55}", + "swipe-up": "\u{fd61}", + "swipe-up-filled": "\u{ff54}", + "switch": "\u{eb33}", + "switch-2": "\u{edbf}", + "switch-3": "\u{edc0}", + "switch-horizontal": "\u{eb31}", + "switch-vertical": "\u{eb32}", + "sword": "\u{f030}", + "sword-off": "\u{f434}", + "swords": "\u{f132}", + "table": "\u{eba1}", + "table-alias": "\u{f25b}", + "table-column": "\u{faff}", + "table-dashed": "\u{100ba}", + "table-down": "\u{fa1c}", + "table-export": "\u{eef8}", + "table-filled": "\u{f782}", + "table-heart": "\u{fa1d}", + "table-import": "\u{eef9}", + "table-minus": "\u{fa1e}", + "table-off": "\u{eefa}", + "table-options": "\u{f25c}", + "table-plus": "\u{fa1f}", + "table-row": "\u{fb00}", + "table-share": "\u{fa20}", + "table-shortcut": "\u{f25d}", + "table-spark": "\u{ffad}", + "tag": "\u{10096}", + "tag-filled": "\u{ff02}", + "tag-minus": "\u{eb34}", + "tag-off": "\u{efc0}", + "tag-plus": "\u{10097}", + "tag-starred": "\u{fc99}", + "tags": "\u{ef86}", + "tags-filled": "\u{ff01}", + "tags-off": "\u{efc1}", + "tallymark-1": "\u{ec46}", + "tallymark-2": "\u{ec47}", + "tallymark-3": "\u{ec48}", + "tallymark-4": "\u{ec49}", + "tallymarks": "\u{ec4a}", + "tank": "\u{ed95}", + "target": "\u{eb35}", + "target-arrow": "\u{f51a}", + "target-off": "\u{f1ad}", + "tax": "\u{feee}", + "tax-euro": "\u{fef0}", + "tax-pound": "\u{feef}", + "teapot": "\u{f552}", + "telescope": "\u{f07d}", + "telescope-off": "\u{f1ae}", + "temperature": "\u{eb38}", + "temperature-celsius": "\u{eb36}", + "temperature-fahrenheit": "\u{eb37}", + "temperature-minus": "\u{ebed}", + "temperature-minus-filled": "\u{10103}", + "temperature-off": "\u{f1af}", + "temperature-plus": "\u{ebee}", + "temperature-plus-filled": "\u{10102}", + "temperature-snow": "\u{fda3}", + "temperature-sun": "\u{fda4}", + "template": "\u{eb39}", + "template-filled": "\u{10177}", + "template-off": "\u{f1b0}", + "tent": "\u{eefb}", + "tent-off": "\u{f435}", + "terminal": "\u{ebdc}", + "terminal-2": "\u{ebef}", + "test-pipe": "\u{eb3a}", + "test-pipe-2": "\u{f0a4}", + "test-pipe-2-filled": "\u{ff53}", + "test-pipe-off": "\u{f1b1}", + "tex": "\u{f4e0}", + "text-caption": "\u{f4a4}", + "text-color": "\u{f2dc}", + "text-decrease": "\u{f202}", + "text-direction-ltr": "\u{eefc}", + "text-direction-rtl": "\u{eefd}", + "text-grammar": "\u{fd6d}", + "text-increase": "\u{f203}", + "text-orientation": "\u{f2a4}", + "text-plus": "\u{f2a5}", + "text-recognition": "\u{f204}", + "text-resize": "\u{ef87}", + "text-scan-2": "\u{fcc3}", + "text-size": "\u{f2b1}", + "text-spellcheck": "\u{f2a6}", + "text-wrap": "\u{ebdd}", + "text-wrap-column": "\u{feb2}", + "text-wrap-disabled": "\u{eca7}", + "texture": "\u{f51b}", + "theater": "\u{f79b}", + "thermometer": "\u{ef67}", + "thumb-down": "\u{eb3b}", + "thumb-down-filled": "\u{f6aa}", + "thumb-down-off": "\u{f436}", + "thumb-up": "\u{eb3c}", + "thumb-up-filled": "\u{f6ab}", + "thumb-up-off": "\u{f437}", + "tic-tac": "\u{f51c}", + "ticket": "\u{eb3d}", + "ticket-off": "\u{f1b2}", + "tie": "\u{f07e}", + "tilde": "\u{f4a5}", + "tilt-shift": "\u{eefe}", + "tilt-shift-filled": "\u{fec7}", + "tilt-shift-off": "\u{f1b3}", + "time-duration-0": "\u{fad4}", + "time-duration-10": "\u{fad5}", + "time-duration-15": "\u{fad6}", + "time-duration-30": "\u{fad7}", + "time-duration-45": "\u{fad8}", + "time-duration-5": "\u{fad9}", + "time-duration-60": "\u{fada}", + "time-duration-90": "\u{fadb}", + "time-duration-off": "\u{fadc}", + "timeline": "\u{f031}", + "timeline-event": "\u{f553}", + "timeline-event-exclamation": "\u{f662}", + "timeline-event-filled": "\u{fd18}", + "timeline-event-minus": "\u{f663}", + "timeline-event-plus": "\u{f664}", + "timeline-event-text": "\u{f665}", + "timeline-event-x": "\u{f666}", + "timezone": "\u{feed}", + "tip-jar": "\u{feea}", + "tip-jar-euro": "\u{feec}", + "tip-jar-pound": "\u{feeb}", + "tir": "\u{ebf0}", + "toggle-left": "\u{eb3e}", + "toggle-left-filled": "\u{fec0}", + "toggle-right": "\u{eb3f}", + "toggle-right-filled": "\u{febf}", + "toilet-paper": "\u{efd3}", + "toilet-paper-off": "\u{f1b4}", + "toml": "\u{fa5d}", + "tool": "\u{eb40}", + "tools": "\u{ebca}", + "tools-kitchen": "\u{ed64}", + "tools-kitchen-2": "\u{eeff}", + "tools-kitchen-2-off": "\u{f1b5}", + "tools-kitchen-3": "\u{fd2b}", + "tools-kitchen-off": "\u{f1b6}", + "tools-off": "\u{f1b7}", + "tooltip": "\u{f2dd}", + "topology-bus": "\u{f5d9}", + "topology-complex": "\u{f5da}", + "topology-full": "\u{f5dc}", + "topology-full-hierarchy": "\u{f5db}", + "topology-ring": "\u{f5df}", + "topology-ring-2": "\u{f5dd}", + "topology-ring-3": "\u{f5de}", + "topology-star": "\u{f5e5}", + "topology-star-2": "\u{f5e0}", + "topology-star-3": "\u{f5e1}", + "topology-star-ring": "\u{f5e4}", + "topology-star-ring-2": "\u{f5e2}", + "topology-star-ring-3": "\u{f5e3}", + "torii": "\u{f59b}", + "tornado": "\u{ece2}", + "tournament": "\u{ecd0}", + "tower": "\u{f2cb}", + "tower-off": "\u{f2ca}", + "track": "\u{ef00}", + "tractor": "\u{ec0d}", + "trademark": "\u{ec0e}", + "traffic-cone": "\u{ec0f}", + "traffic-cone-off": "\u{f1b8}", + "traffic-lights": "\u{ed39}", + "traffic-lights-off": "\u{f1b9}", + "train": "\u{ed96}", + "train-filled": "\u{100f5}", + "transaction-bitcoin": "\u{fd6e}", + "transaction-dollar": "\u{fd6f}", + "transaction-euro": "\u{fd70}", + "transaction-pound": "\u{fd71}", + "transaction-rupee": "\u{fd85}", + "transaction-yen": "\u{fd72}", + "transaction-yuan": "\u{fd73}", + "transfer": "\u{fc1f}", + "transfer-in": "\u{ef2f}", + "transfer-out": "\u{ef30}", + "transfer-vertical": "\u{fc1e}", + "transform": "\u{f38e}", + "transform-filled": "\u{f6ac}", + "transform-point": "\u{fda9}", + "transform-point-bottom-left": "\u{fda5}", + "transform-point-bottom-right": "\u{fda6}", + "transform-point-top-left": "\u{fda7}", + "transform-point-top-right": "\u{fda8}", + "transition-bottom": "\u{f2b2}", + "transition-bottom-filled": "\u{fdd0}", + "transition-left": "\u{f2b3}", + "transition-left-filled": "\u{fdcf}", + "transition-right": "\u{f2b4}", + "transition-right-filled": "\u{fdce}", + "transition-top": "\u{f2b5}", + "transition-top-filled": "\u{fdcd}", + "trash": "\u{eb41}", + "trash-filled": "\u{f783}", + "trash-off": "\u{ed65}", + "trash-x": "\u{ef88}", + "trash-x-filled": "\u{f784}", + "treadmill": "\u{fa6e}", + "tree": "\u{ef01}", + "trees": "\u{ec10}", + "trekking": "\u{f5ad}", + "trending-down": "\u{eb42}", + "trending-down-2": "\u{edc1}", + "trending-down-3": "\u{edc2}", + "trending-up": "\u{eb43}", + "trending-up-2": "\u{edc3}", + "trending-up-3": "\u{edc4}", + "triangle": "\u{eb44}", + "triangle-filled": "\u{f6ad}", + "triangle-inverted": "\u{f01d}", + "triangle-inverted-filled": "\u{f6ae}", + "triangle-minus": "\u{fc9b}", + "triangle-minus-2": "\u{fc9a}", + "triangle-off": "\u{ef02}", + "triangle-plus": "\u{fc9d}", + "triangle-plus-2": "\u{fc9c}", + "triangle-square-circle": "\u{ece8}", + "triangle-square-circle-filled": "\u{fb42}", + "triangles": "\u{f0a5}", + "trident": "\u{ecc5}", + "trolley": "\u{f4cc}", + "trolley-filled": "\u{100f4}", + "trophy": "\u{eb45}", + "trophy-filled": "\u{f6af}", + "trophy-off": "\u{f438}", + "trowel": "\u{f368}", + "truck": "\u{ebc4}", + "truck-delivery": "\u{ec4b}", + "truck-filled": "\u{100f3}", + "truck-loading": "\u{f1da}", + "truck-off": "\u{ef03}", + "truck-return": "\u{ec4c}", + "txt": "\u{f3b1}", + "typeface": "\u{fdab}", + "typography": "\u{ebc5}", + "typography-off": "\u{f1ba}", + "u-turn-left": "\u{fea2}", + "u-turn-right": "\u{fea1}", + "ufo": "\u{f26f}", + "ufo-filled": "\u{10165}", + "ufo-off": "\u{f26e}", + "uhd": "\u{100aa}", + "umbrella": "\u{ebf1}", + "umbrella-2": "\u{ff0e}", + "umbrella-closed": "\u{ff0c}", + "umbrella-closed-2": "\u{ff0d}", + "umbrella-filled": "\u{f6b0}", + "umbrella-off": "\u{f1bb}", + "underline": "\u{eba2}", + "universe": "\u{fcc4}", + "unlink": "\u{eb46}", + "upload": "\u{eb47}", + "urgent": "\u{eb48}", + "usb": "\u{f00c}", + "user": "\u{eb4d}", + "user-bitcoin": "\u{ff30}", + "user-bolt": "\u{f9d1}", + "user-cancel": "\u{f9d2}", + "user-check": "\u{eb49}", + "user-circle": "\u{ef68}", + "user-code": "\u{f9d3}", + "user-cog": "\u{f9d4}", + "user-dollar": "\u{f9d5}", + "user-down": "\u{f9d6}", + "user-edit": "\u{f7cc}", + "user-exclamation": "\u{ec12}", + "user-filled": "\u{fd19}", + "user-heart": "\u{f7cd}", + "user-hexagon": "\u{fc4e}", + "user-minus": "\u{eb4a}", + "user-off": "\u{ecf9}", + "user-pause": "\u{f9d7}", + "user-pentagon": "\u{fc4f}", + "user-pin": "\u{f7ce}", + "user-plus": "\u{eb4b}", + "user-question": "\u{f7cf}", + "user-scan": "\u{fcaf}", + "user-screen": "\u{fea0}", + "user-search": "\u{ef89}", + "user-share": "\u{f9d8}", + "user-shield": "\u{f7d0}", + "user-square": "\u{fc51}", + "user-square-rounded": "\u{fc50}", + "user-star": "\u{f7d1}", + "user-up": "\u{f7d2}", + "user-x": "\u{eb4c}", + "users": "\u{ebf2}", + "users-group": "\u{fa21}", + "users-minus": "\u{fa0e}", + "users-plus": "\u{fa0f}", + "uv-index": "\u{f3b2}", + "ux-circle": "\u{f369}", + "vaccine": "\u{ef04}", + "vaccine-bottle": "\u{ef69}", + "vaccine-bottle-off": "\u{f439}", + "vaccine-off": "\u{f1bc}", + "vacuum-cleaner": "\u{f5e6}", + "variable": "\u{ef05}", + "variable-minus": "\u{f36a}", + "variable-off": "\u{f1bd}", + "variable-plus": "\u{f36b}", + "vector": "\u{eca9}", + "vector-bezier": "\u{ef1d}", + "vector-bezier-2": "\u{f1a3}", + "vector-bezier-arc": "\u{f4cd}", + "vector-bezier-circle": "\u{f4ce}", + "vector-off": "\u{f1be}", + "vector-spline": "\u{f565}", + "vector-triangle": "\u{eca8}", + "vector-triangle-off": "\u{f1bf}", + "venus": "\u{ec86}", + "versions": "\u{ed52}", + "versions-filled": "\u{f6b1}", + "versions-off": "\u{f1c0}", + "video": "\u{ed22}", + "video-filled": "\u{1009b}", + "video-minus": "\u{ed1f}", + "video-off": "\u{ed20}", + "video-plus": "\u{ed21}", + "view-360": "\u{ed84}", + "view-360-arrow": "\u{f62f}", + "view-360-number": "\u{f566}", + "view-360-off": "\u{f1c1}", + "viewfinder": "\u{eb4e}", + "viewfinder-off": "\u{f1c2}", + "viewport-narrow": "\u{ebf3}", + "viewport-short": "\u{fee9}", + "viewport-tall": "\u{fee8}", + "viewport-wide": "\u{ebf4}", + "vinyl": "\u{f00d}", + "vip": "\u{f3b3}", + "vip-off": "\u{f43a}", + "virus": "\u{eb74}", + "virus-off": "\u{ed66}", + "virus-search": "\u{ed67}", + "vocabulary": "\u{ef1e}", + "vocabulary-off": "\u{f43b}", + "volcano": "\u{f79c}", + "volume": "\u{eb51}", + "volume-2": "\u{eb4f}", + "volume-3": "\u{eb50}", + "volume-off": "\u{f1c3}", + "vs": "\u{fc52}", + "walk": "\u{ec87}", + "wall": "\u{ef7a}", + "wall-off": "\u{f43c}", + "wallet": "\u{eb75}", + "wallet-off": "\u{f1c4}", + "wallpaper": "\u{ef56}", + "wallpaper-off": "\u{f1c5}", + "wand": "\u{ebcb}", + "wand-off": "\u{f1c6}", + "wash": "\u{f311}", + "wash-dry": "\u{f304}", + "wash-dry-1": "\u{f2fa}", + "wash-dry-2": "\u{f2fb}", + "wash-dry-3": "\u{f2fc}", + "wash-dry-a": "\u{f2fd}", + "wash-dry-dip": "\u{f2fe}", + "wash-dry-f": "\u{f2ff}", + "wash-dry-flat": "\u{fa7f}", + "wash-dry-hang": "\u{f300}", + "wash-dry-off": "\u{f301}", + "wash-dry-p": "\u{f302}", + "wash-dry-shade": "\u{f303}", + "wash-dry-w": "\u{f322}", + "wash-dryclean": "\u{f305}", + "wash-dryclean-off": "\u{f323}", + "wash-eco": "\u{fa80}", + "wash-gentle": "\u{f306}", + "wash-hand": "\u{fa81}", + "wash-machine": "\u{f25e}", + "wash-off": "\u{f307}", + "wash-press": "\u{f308}", + "wash-temperature-1": "\u{f309}", + "wash-temperature-2": "\u{f30a}", + "wash-temperature-3": "\u{f30b}", + "wash-temperature-4": "\u{f30c}", + "wash-temperature-5": "\u{f30d}", + "wash-temperature-6": "\u{f30e}", + "wash-tumble-dry": "\u{f30f}", + "wash-tumble-off": "\u{f310}", + "waterpolo": "\u{fa6f}", + "wave-saw-tool": "\u{ecd3}", + "wave-sine": "\u{ecd4}", + "wave-square": "\u{ecd5}", + "waves-electricity": "\u{fcc5}", + "webhook": "\u{f01e}", + "webhook-off": "\u{f43d}", + "weight": "\u{f589}", + "wheat": "\u{100a8}", + "wheat-off": "\u{100a9}", + "wheel": "\u{fc64}", + "wheelchair": "\u{f1db}", + "wheelchair-off": "\u{f43e}", + "whirl": "\u{f51d}", + "wifi": "\u{eb52}", + "wifi-0": "\u{eba3}", + "wifi-1": "\u{eba4}", + "wifi-2": "\u{eba5}", + "wifi-3": "\u{ebfc}", + "wifi-off": "\u{ecfa}", + "wifi-exclamation": "\u{ebfd}", + "wifi-question": "\u{ebfe}", + "wind": "\u{ec34}", + "wind-electricity": "\u{fcc6}", + "wind-off": "\u{f1c7}", + "windmill": "\u{ed85}", + "windmill-filled": "\u{f6b2}", + "windmill-off": "\u{f1c8}", + "window": "\u{ef06}", + "window-maximize": "\u{f1f1}", + "window-minimize": "\u{f1f2}", + "window-off": "\u{f1c9}", + "windsock": "\u{f06d}", + "windsock-filled": "\u{1009a}", + "wiper": "\u{ecab}", + "wiper-wash": "\u{ecaa}", + "woman": "\u{eb53}", + "woman-filled": "\u{fdcc}", + "wood": "\u{f359}", + "world": "\u{eb54}", + "world-bolt": "\u{f9d9}", + "world-cancel": "\u{f9da}", + "world-check": "\u{f9db}", + "world-code": "\u{f9dc}", + "world-cog": "\u{f9dd}", + "world-dollar": "\u{f9de}", + "world-down": "\u{f9df}", + "world-download": "\u{ef8a}", + "world-exclamation": "\u{f9e0}", + "world-heart": "\u{f9e1}", + "world-latitude": "\u{ed2e}", + "world-longitude": "\u{ed2f}", + "world-minus": "\u{f9e2}", + "world-off": "\u{f1ca}", + "world-pause": "\u{f9e3}", + "world-pin": "\u{f9e4}", + "world-plus": "\u{f9e5}", + "world-question": "\u{f9e6}", + "world-search": "\u{f9e7}", + "world-share": "\u{f9e8}", + "world-star": "\u{f9e9}", + "world-up": "\u{f9ea}", + "world-upload": "\u{ef8b}", + "world-www": "\u{f38f}", + "world-x": "\u{f9eb}", + "wrecking-ball": "\u{ed97}", + "writing": "\u{ef08}", + "writing-off": "\u{f1cb}", + "writing-sign": "\u{ef07}", + "writing-sign-off": "\u{f1cc}", + "x": "\u{eb55}", + "x-power-y": "\u{10072}", + "xbox-a": "\u{f2b6}", + "xbox-a-filled": "\u{fdcb}", + "xbox-b": "\u{f2b7}", + "xbox-b-filled": "\u{fdca}", + "xbox-x": "\u{f2b8}", + "xbox-x-filled": "\u{fdc9}", + "xbox-y": "\u{f2b9}", + "xbox-y-filled": "\u{fdc8}", + "xd": "\u{fa33}", + "xxx": "\u{fc20}", + "yin-yang": "\u{ec35}", + "yin-yang-filled": "\u{f785}", + "yoga": "\u{f01f}", + "zeppelin": "\u{f270}", + "zeppelin-filled": "\u{fdc7}", + "zeppelin-off": "\u{f43f}", + "zip": "\u{f3b4}", + "zodiac-aquarius": "\u{ecac}", + "zodiac-aries": "\u{ecad}", + "zodiac-cancer": "\u{ecae}", + "zodiac-capricorn": "\u{ecaf}", + "zodiac-gemini": "\u{ecb0}", + "zodiac-leo": "\u{ecb1}", + "zodiac-libra": "\u{ecb2}", + "zodiac-pisces": "\u{ecb3}", + "zodiac-sagittarius": "\u{ecb4}", + "zodiac-scorpio": "\u{ecb5}", + "zodiac-taurus": "\u{ecb6}", + "zodiac-virgo": "\u{ecb7}", + "zoom": "\u{fdaa}", + "zoom-cancel": "\u{ec4d}", + "zoom-cancel-filled": "\u{fdc6}", + "zoom-check": "\u{ef09}", + "zoom-check-filled": "\u{f786}", + "zoom-code": "\u{f07f}", + "zoom-code-filled": "\u{fdc5}", + "zoom-exclamation": "\u{f080}", + "zoom-exclamation-filled": "\u{fdc4}", + "zoom-filled": "\u{f787}", + "zoom-in": "\u{eb56}", + "zoom-in-area": "\u{f1dc}", + "zoom-in-area-filled": "\u{f788}", + "zoom-in-filled": "\u{f789}", + "zoom-money": "\u{ef0a}", + "zoom-money-filled": "\u{fdc3}", + "zoom-out": "\u{eb57}", + "zoom-out-area": "\u{f1dd}", + "zoom-out-area-filled": "\u{fdc2}", + "zoom-out-filled": "\u{f78a}", + "zoom-pan": "\u{f1de}", + "zoom-pan-filled": "\u{fdc1}", + "zoom-question": "\u{edeb}", + "zoom-question-filled": "\u{fdc0}", + "zoom-replace": "\u{f2a7}", + "zoom-reset": "\u{f295}", + "zoom-scan": "\u{fcb0}", + "zoom-scan-filled": "\u{fdbf}", + "zzz": "\u{f228}", + "zzz-off": "\u{f440}" + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Keybinds.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Keybinds.qml new file mode 100644 index 0000000..1bebe3e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Keybinds.qml @@ -0,0 +1,184 @@ +pragma Singleton + +import QtQuick + +QtObject { + function getKeybindString(event) { + let keyStr = ""; + if (event.modifiers & Qt.ControlModifier) + keyStr += "Ctrl+"; + if (event.modifiers & Qt.AltModifier) + keyStr += "Alt+"; + if (event.modifiers & Qt.ShiftModifier) + keyStr += "Shift+"; + + let keyName = ""; + let rawText = event.text; + + if (event.key >= Qt.Key_A && event.key <= Qt.Key_Z || event.key >= Qt.Key_0 && event.key <= Qt.Key_9) { + keyName = String.fromCharCode(event.key); + } else if (event.key >= Qt.Key_F1 && event.key <= Qt.Key_F12) { + keyName = "F" + (event.key - Qt.Key_F1 + 1); + } else if (rawText && rawText.length > 0 && rawText.charCodeAt(0) > 31 && rawText.charCodeAt(0) !== 127) { + keyName = rawText.toUpperCase(); + + if (event.modifiers & Qt.ShiftModifier) { + const shiftMap = { + "!": "1", + "\"": "2", + "§": "3", + "$": "4", + "%": "5", + "&": "6", + "/": "7", + "(": "8", + ")": "9", + "=": "0", + "@": "2", + "#": "3", + "^": "6", + "*": "8" + }; + if (shiftMap[keyName]) { + keyName = shiftMap[keyName]; + } + } + } else { + switch (event.key) { + case Qt.Key_Escape: + keyName = "Esc"; + break; + case Qt.Key_Space: + keyName = "Space"; + break; + case Qt.Key_Return: + keyName = "Return"; + break; + case Qt.Key_Enter: + keyName = "Enter"; + break; + case Qt.Key_Tab: + keyName = "Tab"; + break; + case Qt.Key_Backspace: + keyName = "Backspace"; + break; + case Qt.Key_Delete: + keyName = "Del"; + break; + case Qt.Key_Insert: + keyName = "Ins"; + break; + case Qt.Key_Home: + keyName = "Home"; + break; + case Qt.Key_End: + keyName = "End"; + break; + case Qt.Key_PageUp: + keyName = "PgUp"; + break; + case Qt.Key_PageDown: + keyName = "PgDn"; + break; + case Qt.Key_Left: + keyName = "Left"; + break; + case Qt.Key_Right: + keyName = "Right"; + break; + case Qt.Key_Up: + keyName = "Up"; + break; + case Qt.Key_Down: + keyName = "Down"; + break; + } + } + + if (!keyName) + return ""; + return keyStr + keyName; + } + + function checkKey(event, settingName, settings) { + // Accept both simplified names ("remove") and full property names ("keyRemove") + // so callers are less error-prone. + var normalized = String(settingName || ""); + if (!normalized) + return false; + + var propName = normalized.startsWith("key") ? normalized : ("key" + normalized.charAt(0).toUpperCase() + normalized.slice(1)); + var boundKeys = settings.data.general.keybinds[propName]; + if (!boundKeys || boundKeys.length === 0) + return false; + var eventString = getKeybindString(event); + for (var i = 0; i < boundKeys.length; i++) { + if (boundKeys[i] === eventString) + return true; + } + return false; + } + + /** + * Check if a keybind string conflicts with any other existing keybinds. + * @param {string} keyStr - The keybind string to check (e.g., "Ctrl+A"). + * @param {string} currentPath - The settings path of the keybind being edited (to skip checking itself). + * @param {object} data - The settings data object (from Settings.data). + * @returns {string|null} - The name of the conflicting action, or null if no conflict. + */ + function getKeybindConflict(keyStr, currentPath, data) { + if (!keyStr || !data) + return null; + + const searchKey = String(keyStr).trim().toLowerCase(); + + // 1. Check navigation keybinds + const navKeybinds = data.general ? data.general.keybinds : null; + const navMap = { + "keyUp": "Navigation: Up", + "keyDown": "Navigation: Down", + "keyLeft": "Navigation: Left", + "keyRight": "Navigation: Right", + "keyEnter": "Navigation: Enter", + "keyEscape": "Navigation: Escape" + }; + + if (navKeybinds) { + for (const prop in navMap) { + const fullPath = "general.keybinds." + prop; + if (fullPath === currentPath) + continue; + + const boundKeys = navKeybinds[prop]; + if (boundKeys && boundKeys.length !== undefined) { + for (let i = 0; i < boundKeys.length; i++) { + if (String(boundKeys[i]).trim().toLowerCase() === searchKey) { + return navMap[prop]; + } + } + } + } + } + + // 2. Check session menu power options + const sessionMenu = data.sessionMenu; + if (sessionMenu && sessionMenu.powerOptions) { + const powerOptions = sessionMenu.powerOptions; + for (let i = 0; i < powerOptions.length; i++) { + const entry = powerOptions[i]; + const fullPath = "sessionMenu.powerOptions[" + i + "].keybind"; + if (fullPath === currentPath) + continue; + + if (entry.keybind && String(entry.keybind).trim().toLowerCase() === searchKey) { + // Capitalize action name + const actionName = entry.action ? entry.action.charAt(0).toUpperCase() + entry.action.slice(1) : "Unknown"; + return "Session Menu: " + actionName; + } + } + } + + return null; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Logger.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Logger.qml new file mode 100644 index 0000000..4fb4b32 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Logger.qml @@ -0,0 +1,69 @@ +pragma Singleton + +import Quickshell +import qs.Commons + +Singleton { + id: root + + function _formatMessage(...args) { + var t = Time.getFormattedTimestamp(); + if (args.length > 1) { + const maxLength = 14; + var module = args.shift().substring(0, maxLength).padStart(maxLength, " "); + return `\x1b[36m[${t}]\x1b[0m \x1b[35m${module}\x1b[0m ` + args.join(" "); + } else { + return `[\x1b[36m[${t}]\x1b[0m ` + args.join(" "); + } + } + + function _getStackTrace() { + try { + throw new Error("Stack trace"); + } catch (e) { + return e.stack; + } + } + + // Debug log (only when Settings.isDebug is true) + function d(...args) { + if (Settings?.isDebug) { + var msg = _formatMessage(...args); + console.debug(msg); + } + } + + // Info log (always visible) + function i(...args) { + var msg = _formatMessage(...args); + console.info(msg); + } + + // Warning log (always visible) + function w(...args) { + var msg = _formatMessage(...args); + console.warn(msg); + } + + // Error log (always visible) + function e(...args) { + var msg = _formatMessage(...args); + console.error(msg); + } + + function callStack() { + var stack = _getStackTrace(); + Logger.i("Debug", "--------------------------"); + Logger.i("Debug", "Current call stack"); + // Split the stack into lines and log each one + var stackLines = stack.split('\n'); + for (var i = 0; i < stackLines.length; i++) { + var line = stackLines[i].trim(); // Remove leading/trailing whitespace + if (line.length > 0) { + // Only log non-empty lines + Logger.i("Debug", `- ${line}`); + } + } + Logger.i("Debug", "--------------------------"); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration27.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration27.qml new file mode 100644 index 0000000..644218d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration27.qml @@ -0,0 +1,23 @@ +import QtQuick + +QtObject { + id: root + + // Migrate from version < 27 to version 27 + // Converts settingsPanelAttachToBar boolean to settingsPanelMode string + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v27"); + + // Check rawJson for old property (adapter doesn't expose removed properties) + if (rawJson?.ui?.settingsPanelAttachToBar !== undefined) { + if (rawJson.ui.settingsPanelAttachToBar === true) { + adapter.ui.settingsPanelMode = "attached"; + } else { + adapter.ui.settingsPanelMode = "centered"; + } + logger.i("Settings", "Migrated settingsPanelAttachToBar to settingsPanelMode: " + adapter.ui.settingsPanelMode); + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration28.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration28.qml new file mode 100644 index 0000000..d3ef7b7 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration28.qml @@ -0,0 +1,37 @@ +import QtQuick + +QtObject { + id: root + + // Migrate TaskbarGrouped widgets to Workspace with showApplications: true + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v28"); + + // Check bar widgets in all sections + const sections = ["left", "center", "right"]; + + for (const section of sections) { + if (!rawJson?.bar?.widgets?.[section]) + continue; + + const widgets = rawJson.bar.widgets[section]; + for (let i = 0; i < widgets.length; i++) { + if (widgets[i].id === "TaskbarGrouped") { + // Convert TaskbarGrouped to Workspace with showApplications + const oldWidget = widgets[i]; + adapter.bar.widgets[section][i] = { + id: "Workspace", + showApplications: true, + labelMode: oldWidget.labelMode || "index", + hideUnoccupied: oldWidget.hideUnoccupied || false, + showLabelsOnlyWhenOccupied: oldWidget.showLabelsOnlyWhenOccupied ?? true, + colorizeIcons: oldWidget.colorizeIcons || false + }; + logger.i("Settings", "Migrated TaskbarGrouped to Workspace in " + section + " section"); + } + } + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration32.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration32.qml new file mode 100644 index 0000000..34ca759 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration32.qml @@ -0,0 +1,18 @@ +import QtQuick + +QtObject { + id: root + + // Migrate systemMonitor.enableNvidiaGpu to systemMonitor.enableDgpuMonitoring + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v32"); + + // Check rawJson for old property (adapter doesn't expose removed properties) + if (rawJson?.systemMonitor?.enableNvidiaGpu !== undefined) { + adapter.systemMonitor.enableDgpuMonitoring = rawJson.systemMonitor.enableNvidiaGpu; + logger.i("Settings", "Migrated systemMonitor.enableNvidiaGpu to enableDgpuMonitoring: " + adapter.systemMonitor.enableDgpuMonitoring); + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration33.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration33.qml new file mode 100644 index 0000000..f012fa1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration33.qml @@ -0,0 +1,26 @@ +import QtQuick + +QtObject { + id: root + + // Migrate wallpaper.randomEnabled to wallpaper.wallpaperChangeMode + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v33"); + + if (rawJson?.wallpaper?.randomEnabled !== undefined) { + // We'll keep randomEnabled for backward compatibility but set wallpaperChangeMode + if (rawJson.wallpaper.randomEnabled === true) { + adapter.wallpaper.wallpaperChangeMode = "random"; + logger.i("Settings", "Migrated wallpaper.randomEnabled=true to wallpaperChangeMode=random"); + } else { + adapter.wallpaper.wallpaperChangeMode = "random"; + logger.i("Settings", "Set wallpaperChangeMode=random (randomEnabled was false)"); + } + } else { + adapter.wallpaper.wallpaperChangeMode = "random"; + logger.i("Settings", "Set default wallpaperChangeMode=random"); + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration35.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration35.qml new file mode 100644 index 0000000..3721782 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration35.qml @@ -0,0 +1,20 @@ +import QtQuick + +QtObject { + id: root + + // Migrate bar.transparent to bar.backgroundOpacity + bar.useSeparateOpacity + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v35"); + + if (rawJson?.bar?.transparent !== undefined) { + if (rawJson.bar.transparent === true) { + adapter.bar.backgroundOpacity = 0; + adapter.bar.useSeparateOpacity = true; + logger.i("Settings", "Migrated bar.transparent=true to backgroundOpacity=0, useSeparateOpacity=true"); + } + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration36.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration36.qml new file mode 100644 index 0000000..563b2c7 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration36.qml @@ -0,0 +1,29 @@ +import QtQuick +import Quickshell +import qs.Commons + +QtObject { + id: root + + // Clear legacy emoji usage cache to adopt new format + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v36"); + + const usagePath = Settings.cacheDir + "emoji_usage.json"; + + if (!usagePath.endsWith("emoji_usage.json")) { + logger.w("Settings", "Skipping emoji usage cleanup due to unexpected path: " + usagePath); + return true; + } + + try { + // Ensure dir exists then remove the file + Quickshell.execDetached(["sh", "-c", `mkdir -p "${Settings.cacheDir}" && rm -f -- "${usagePath}"`]); + logger.i("Settings", "Cleared legacy emoji usage file at: " + usagePath); + } catch (e) { + logger.w("Settings", "Failed to clear emoji usage cache: " + e); + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration37.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration37.qml new file mode 100644 index 0000000..26d64ce --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration37.qml @@ -0,0 +1,79 @@ +import QtQuick + +QtObject { + id: root + + // Rename WiFi widgets/shortcuts to Network + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v37 (WiFi → Network)"); + + let changed = false; + + function migrateArray(contextName, rawArr, adapterArr, setItem) { + if (!adapterArr) + return; + const len = adapterArr.length; + for (let i = 0; i < len; i++) { + // Prefer raw item if present, otherwise use adapter item + const item = rawArr && rawArr[i] !== undefined ? rawArr[i] : adapterArr[i]; + + if (item === undefined || item === null) + continue; + + // Case 1: simple string entries + if (typeof item === "string") { + if (item === "WiFi") { + setItem(i, "Network"); + changed = true; + logger.i("Settings", `Migrated ${contextName}[${i}] from WiFi to Network (string entry)`); + } + continue; + } + + // Case 2: object entries with id + let id = item.id !== undefined ? item.id : (item.widgetId !== undefined ? item.widgetId : undefined); + if (id === "WiFi") { + var newObj = {}; + for (var key in item) + newObj[key] = item[key]; + if (newObj.id !== undefined) + newObj.id = "Network"; + if (newObj.widgetId !== undefined && newObj.id === undefined) + newObj.widgetId = "Network"; // fallback if older schema used widgetId + setItem(i, newObj); + changed = true; + logger.i("Settings", `Migrated ${contextName}[${i}] from WiFi to Network (object entry)`); + } + } + } + + // --- Bar widgets: left/center/right + const sections = ["left", "center", "right"]; + for (const section of sections) { + const rawArr = rawJson?.bar?.widgets?.[section]; + const adapterArr = adapter?.bar?.widgets?.[section]; + migrateArray(`bar.widgets.${section}`, rawArr, adapterArr, function (i, v) { + adapter.bar.widgets[section][i] = v; + }); + } + + // --- Control Center shortcuts: left/right + const rawLeft = rawJson?.controlCenter?.shortcuts?.left; + const adLeft = adapter?.controlCenter?.shortcuts?.left; + migrateArray("controlCenter.shortcuts.left", rawLeft, adLeft, function (i, v) { + adapter.controlCenter.shortcuts.left[i] = v; + }); + + const rawRight = rawJson?.controlCenter?.shortcuts?.right; + const adRight = adapter?.controlCenter?.shortcuts?.right; + migrateArray("controlCenter.shortcuts.right", rawRight, adRight, function (i, v) { + adapter.controlCenter.shortcuts.right[i] = v; + }); + + if (!changed) { + logger.i("Settings", "No WiFi widget IDs found to migrate; leaving settings unchanged"); + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration38.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration38.qml new file mode 100644 index 0000000..22dce4b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration38.qml @@ -0,0 +1,27 @@ +import QtQuick +import qs.Commons + +QtObject { + function migrate(adapter, logger, rawJson) { + logger.i("Migration38", "Migrating bar margins from percentages to integers"); + + // Use rawJson to read original values, adapter to write new values (following migration patterns) + const rawVertical = rawJson?.bar?.marginVertical; + const rawHorizontal = rawJson?.bar?.marginHorizontal; + + // Only migrate if values exist and are percentages (<= 1.0) + if (rawVertical !== undefined && typeof rawVertical === 'number' && rawVertical <= 1.0) { + const marginXL = 18; // Standard value of Style.marginXL + adapter.bar.marginVertical = Math.round(rawVertical * marginXL); + logger.d("Migration38", "Converted marginVertical from " + rawVertical + " to " + adapter.bar.marginVertical + "px"); + } + + if (rawHorizontal !== undefined && typeof rawHorizontal === 'number' && rawHorizontal <= 1.0) { + const marginXL = 18; // Standard value of Style.marginXL + adapter.bar.marginHorizontal = Math.round(rawHorizontal * marginXL); + logger.d("Migration38", "Converted marginHorizontal from " + rawHorizontal + " to " + adapter.bar.marginHorizontal + "px"); + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration40.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration40.qml new file mode 100644 index 0000000..7cb2399 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration40.qml @@ -0,0 +1,43 @@ +import QtQuick + +QtObject { + id: root + + // List of all template IDs that existed as booleans in the old format + readonly property var templateIds: ["gtk", "qt", "kcolorscheme", "alacritty", "kitty", "ghostty", "foot", "wezterm", "fuzzel", "discord", "pywalfox", "vicinae", "walker", "code", "spicetify", "telegram", "cava", "yazi", "emacs", "niri", "hyprland", "mango", "zed", "helix", "zenBrowser"] + + function migrate(adapter, logger, rawJson) { + logger.i("Migration39", "Migrating templates from boolean format to activeTemplates array"); + + // Check if old format exists (any boolean template property) + const oldTemplates = rawJson?.templates; + if (!oldTemplates) { + logger.d("Migration39", "No templates section found, skipping migration"); + return true; + } + + // Check if already migrated (has activeTemplates array) + if (Array.isArray(oldTemplates.activeTemplates)) { + logger.d("Migration39", "Already has activeTemplates array, skipping migration"); + return true; + } + + // Build the new activeTemplates array from old boolean values + const activeTemplates = []; + for (const templateId of templateIds) { + if (oldTemplates[templateId] === true) { + activeTemplates.push({ + "id": templateId, + "enabled": true + }); + logger.d("Migration40", "Migrated enabled template: " + templateId); + } + } + + // Write the new format + adapter.templates.activeTemplates = activeTemplates; + logger.i("Migration40", "Migrated " + activeTemplates.length + " templates to new array format"); + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration42.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration42.qml new file mode 100644 index 0000000..64358c8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration42.qml @@ -0,0 +1,28 @@ +import QtQuick + +QtObject { + id: root + + function migrate(adapter, logger, rawJson) { + logger.i("Migration42", "Migrating randomEnabled to automationEnabled"); + + const wallpaper = rawJson?.wallpaper; + if (!wallpaper) { + logger.d("Migration42", "No wallpaper section found, skipping migration"); + return true; + } + + // Check if already migrated (has automationEnabled) + if (wallpaper.automationEnabled !== undefined) { + logger.d("Migration42", "Already has automationEnabled, skipping migration"); + return true; + } + + // Migrate randomEnabled to automationEnabled + const oldValue = wallpaper.randomEnabled ?? false; + adapter.wallpaper.automationEnabled = oldValue; + logger.i("Migration42", "Migrated randomEnabled=" + oldValue + " to automationEnabled=" + oldValue); + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration43.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration43.qml new file mode 100644 index 0000000..93daf31 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration43.qml @@ -0,0 +1,29 @@ +import QtQuick + +QtObject { + id: root + + function migrate(adapter, logger, rawJson) { + logger.i("Migration43", "Migrating recursiveSearch to viewMode"); + + const wallpaper = rawJson?.wallpaper; + if (!wallpaper) { + logger.d("Migration43", "No wallpaper section found, skipping migration"); + return true; + } + + // Check if already migrated (has viewMode) + if (wallpaper.viewMode !== undefined) { + logger.d("Migration43", "Already has viewMode, skipping migration"); + return true; + } + + // Migrate recursiveSearch to viewMode + const oldValue = wallpaper.recursiveSearch ?? false; + const newValue = oldValue ? "recursive" : "single"; + adapter.wallpaper.viewMode = newValue; + logger.i("Migration43", "Migrated recursiveSearch=" + oldValue + " to viewMode=" + newValue); + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration44.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration44.qml new file mode 100644 index 0000000..a8623b7 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration44.qml @@ -0,0 +1,35 @@ +import QtQuick +import Quickshell +import qs.Commons + +QtObject { + id: root + + function migrate(adapter, logger, rawJson) { + logger.i("Migration44", "Updating PAM pam/password.conf"); + + const configDir = Settings.configDir; + const pamConfigDir = configDir + "pam"; + const pamConfigFile = pamConfigDir + "/password.conf"; + const pamConfigDirEsc = pamConfigDir.replace(/'/g, "'\\''"); + const pamConfigFileEsc = pamConfigFile.replace(/'/g, "'\\''"); + + // Ensure directory exists + Quickshell.execDetached(["mkdir", "-p", pamConfigDir]); + + // Generate the PAM config file content (updated version) + var configContent = "auth sufficient pam_fprintd.so timeout=-1\n"; + configContent += "auth sufficient /run/current-system/sw/lib/security/pam_fprintd.so timeout=-1 # for NixOS\n"; + configContent += "auth required pam_unix.so\n"; + + // Write the config file using heredoc + var script = `cat > '${pamConfigFileEsc}' << 'EOF'\n`; + script += configContent; + script += "EOF\n"; + Quickshell.execDetached(["sh", "-c", script]); + + logger.d("Migration44", "PAM config file updated at:", pamConfigFile); + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration45.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration45.qml new file mode 100644 index 0000000..25c0f8d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration45.qml @@ -0,0 +1,18 @@ +import QtQuick + +QtObject { + /** + * Migration 45: Migrate 'floating' bar setting to 'barType' + */ + function migrate(adapter, logger, rawJson) { + logger.i("Migration45", "Migrating bar settings..."); + + if (rawJson?.bar?.floating) { + adapter.bar.barType = "floating"; + } else { + adapter.bar.barType = "simple"; + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration46.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration46.qml new file mode 100644 index 0000000..06a5aac --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration46.qml @@ -0,0 +1,21 @@ +import QtQuick +import Quickshell +import qs.Commons + +QtObject { + id: root + + function migrate(adapter, logger, rawJson) { + logger.i("Migration46", "Removing legacy PAM configuration file"); + + const configDir = Settings.configDir; + const pamConfigDir = configDir + "pam"; + // Remove the entire pam directory if it exists + const script = `rm -rf '${pamConfigDir}'`; + Quickshell.execDetached(["sh", "-c", script]); + + logger.d("Migration46", "Cleaned up legacy PAM config"); + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration47.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration47.qml new file mode 100644 index 0000000..9eef2c0 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration47.qml @@ -0,0 +1,18 @@ +import QtQuick +import Quickshell +import qs.Commons + +QtObject { + id: root + + function migrate(adapter, logger, rawJson) { + logger.i("Migration47", "Removing network_stats.json cache"); + + const networkStatsFile = Settings.cacheDir + "network_stats.json"; + Quickshell.execDetached(["rm", "-f", networkStatsFile]); + + logger.d("Migration47", "Removed network_stats.json"); + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration48.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration48.qml new file mode 100644 index 0000000..a775e95 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration48.qml @@ -0,0 +1,22 @@ +import QtQuick + +QtObject { + id: root + + // Migrate battery thresholds from notifications to systemMonitor + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v48"); + + if (rawJson?.notifications?.batteryWarningThreshold !== undefined) { + adapter.systemMonitor.batteryWarningThreshold = rawJson.notifications.batteryWarningThreshold; + logger.i("Settings", "Migrated notifications.batteryWarningThreshold to systemMonitor: " + adapter.systemMonitor.batteryWarningThreshold); + } + + if (rawJson?.notifications?.batteryCriticalThreshold !== undefined) { + adapter.systemMonitor.batteryCriticalThreshold = rawJson.notifications.batteryCriticalThreshold; + logger.i("Settings", "Migrated notifications.batteryCriticalThreshold to systemMonitor: " + adapter.systemMonitor.batteryCriticalThreshold); + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration49.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration49.qml new file mode 100644 index 0000000..d65ed19 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration49.qml @@ -0,0 +1,17 @@ +import QtQuick +import Quickshell +import qs.Commons + +QtObject { + id: root + + // Remove old launcher_app_usage.json (usage tracking moved to ShellState) + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v49"); + + Quickshell.execDetached(["rm", "-f", Settings.cacheDir + "launcher_app_usage.json"]); + logger.i("Settings", "Removed old launcher_app_usage.json"); + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration50.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration50.qml new file mode 100644 index 0000000..398b266 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration50.qml @@ -0,0 +1,24 @@ +import QtQuick + +QtObject { + id: root + + // Migrate keybinds from single strings to arrays of strings + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v50"); + + const keybinds = rawJson?.general?.keybinds; + if (!keybinds) + return true; + + const keys = ["keyUp", "keyDown", "keyLeft", "keyRight", "keyEnter", "keyEscape"]; + for (const key of keys) { + if (keybinds[key] !== undefined && typeof keybinds[key] === "string") { + adapter.general.keybinds[key] = [keybinds[key]]; + logger.i("Settings", "Migrated keybinds." + key + " from string to array: [" + keybinds[key] + "]"); + } + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration53.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration53.qml new file mode 100644 index 0000000..cf2382d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration53.qml @@ -0,0 +1,39 @@ +import QtQuick + +QtObject { + id: root + + // Add default keybinds (1-6) to session menu power options if none are defined + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v53"); + + const powerOptions = rawJson?.sessionMenu?.powerOptions; + if (!powerOptions || !Array.isArray(powerOptions)) + return true; + + // Check if any power option has a keybind defined + const hasAnyKeybind = powerOptions.some(opt => opt.keybind && opt.keybind !== ""); + if (hasAnyKeybind) + return true; + + // No keybinds defined — apply defaults matching the action order + const defaultKeybinds = { + "lock": "1", + "suspend": "2", + "hibernate": "3", + "reboot": "4", + "logout": "5", + "shutdown": "6" + }; + + for (let i = 0; i < powerOptions.length; i++) { + const action = powerOptions[i].action; + if (defaultKeybinds[action]) { + adapter.sessionMenu.powerOptions[i].keybind = defaultKeybinds[action]; + logger.i("Settings", "Set keybind '" + defaultKeybinds[action] + "' for session menu action: " + action); + } + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration54.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration54.qml new file mode 100644 index 0000000..367ab12 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration54.qml @@ -0,0 +1,28 @@ +import QtQuick + +QtObject { + id: root + + // Add numpad Enter as a second default keybind for keyEnter + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v54"); + + const keybinds = rawJson?.general?.keybinds; + if (!keybinds) + return true; + + const keyEnter = keybinds.keyEnter; + if (!keyEnter || !Array.isArray(keyEnter)) + return true; + + // Only add "Enter" if the first entry is "Return" and "Enter" isn't already present + if (keyEnter[0] === "Return" && !keyEnter.includes("Enter")) { + var updated = Array.from(keyEnter); + updated.splice(1, 0, "Enter"); + adapter.general.keybinds.keyEnter = updated; + logger.i("Settings", "Added 'Enter' (numpad) as second default keybind for keyEnter"); + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration55.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration55.qml new file mode 100644 index 0000000..0e08276 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration55.qml @@ -0,0 +1,22 @@ +import QtQuick + +QtObject { + id: root + + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v55"); + + // Check if the old setting exists + if (rawJson.controlCenter && rawJson.controlCenter.openAtMouseOnBarRightClick !== undefined) { + if (!rawJson.bar) + rawJson.bar = {}; + + rawJson.bar.rightClickFollowMouse = rawJson.controlCenter.openAtMouseOnBarRightClick; + delete rawJson.controlCenter.openAtMouseOnBarRightClick; + + logger.i("Settings", "Successfully moved openAtMouseOnBarRightClick to bar.rightClickFollowMouse"); + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration56.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration56.qml new file mode 100644 index 0000000..95dec53 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration56.qml @@ -0,0 +1,21 @@ +import QtQuick +import Quickshell +import qs.Commons + +QtObject { + id: root + + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v56 (Color Scheme Migration)"); + + const scriptPath = Quickshell.shellDir + "/Scripts/python/src/theming/migrate-colorschemes.py"; + const configDir = Settings.configDir; + + logger.i("Settings", `Running color scheme migration script: ${scriptPath} with configDir: ${configDir}`); + + // Run the migration script detached + Quickshell.execDetached(["python3", scriptPath, configDir]); + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration57.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration57.qml new file mode 100644 index 0000000..a8e3444 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration57.qml @@ -0,0 +1,16 @@ +import QtQuick + +QtObject { + id: root + + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v57 (cavaFrameRate -> spectrumFrameRate)"); + + if (rawJson && rawJson.audio && rawJson.audio.cavaFrameRate !== undefined) { + adapter.audio.spectrumFrameRate = rawJson.audio.cavaFrameRate; + logger.i("Settings", "Migrated cavaFrameRate:", rawJson.audio.cavaFrameRate, "-> spectrumFrameRate"); + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration58.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration58.qml new file mode 100644 index 0000000..32dd42f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration58.qml @@ -0,0 +1,14 @@ +import QtQuick + +QtObject { + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v58 (dock.dockType: static -> attached)"); + + if (rawJson && rawJson.dock && rawJson.dock.dockType === "static") { + adapter.dock.dockType = "attached"; + logger.i("Settings", "Migrated dock.dockType: static -> attached"); + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration59.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration59.qml new file mode 100644 index 0000000..89d8bba --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/Migration59.qml @@ -0,0 +1,25 @@ +import QtQuick + +QtObject { + function migrate(adapter, logger, rawJson) { + logger.i("Settings", "Migrating settings to v59 (wallpaper.transitionType: string -> array)"); + + if (rawJson && rawJson.wallpaper && typeof rawJson.wallpaper.transitionType === "string") { + var oldValue = rawJson.wallpaper.transitionType; + var newValue; + + if (oldValue === "random") { + newValue = ["fade", "disc", "stripes", "wipe", "pixelate", "honeycomb"]; + } else if (oldValue === "none") { + newValue = []; + } else { + newValue = [oldValue]; + } + + adapter.wallpaper.transitionType = newValue; + logger.i("Settings", "Migrated wallpaper.transitionType:", oldValue, "->", JSON.stringify(newValue)); + } + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/MigrationRegistry.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/MigrationRegistry.qml new file mode 100644 index 0000000..80a9571 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Migrations/MigrationRegistry.qml @@ -0,0 +1,63 @@ +pragma Singleton + +import QtQuick + +QtObject { + id: root + + // Map of version number to migration component + readonly property var migrations: ({ + 27: migration27Component, + 28: migration28Component, + 32: migration32Component, + 33: migration33Component, + 35: migration35Component, + 36: migration36Component, + 37: migration37Component, + 38: migration38Component, + 40: migration40Component, + 42: migration42Component, + 43: migration43Component, + 44: migration44Component, + 45: migration45Component, + 46: migration46Component, + 47: migration47Component, + 48: migration48Component, + 49: migration49Component, + 50: migration50Component, + 53: migration53Component, + 54: migration54Component, + 55: migration55Component, + 56: migration56Component, + 57: migration57Component, + 58: migration58Component, + 59: migration59Component + }) + + // Migration components + property Component migration27Component: Migration27 {} + property Component migration28Component: Migration28 {} + property Component migration32Component: Migration32 {} + property Component migration33Component: Migration33 {} + property Component migration35Component: Migration35 {} + property Component migration36Component: Migration36 {} + property Component migration37Component: Migration37 {} + property Component migration38Component: Migration38 {} + property Component migration40Component: Migration40 {} + property Component migration42Component: Migration42 {} + property Component migration43Component: Migration43 {} + property Component migration44Component: Migration44 {} + property Component migration45Component: Migration45 {} + property Component migration46Component: Migration46 {} + property Component migration47Component: Migration47 {} + property Component migration48Component: Migration48 {} + property Component migration49Component: Migration49 {} + property Component migration50Component: Migration50 {} + property Component migration53Component: Migration53 {} + property Component migration54Component: Migration54 {} + property Component migration55Component: Migration55 {} + property Component migration56Component: Migration56 {} + property Component migration57Component: Migration57 {} + property Component migration58Component: Migration58 {} + property Component migration59Component: Migration59 {} +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Settings.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Settings.qml new file mode 100644 index 0000000..d4deed9 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Settings.qml @@ -0,0 +1,1267 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import "../Helpers/QtObj2JS.js" as QtObj2JS +import qs.Commons +import qs.Commons.Migrations +import qs.Modules.OSD +import qs.Services.Noctalia +import qs.Services.UI + +Singleton { + id: root + + property bool isLoaded: false + property bool reloadSettings: false + property bool directoriesCreated: false + property bool shouldOpenSetupWizard: false + property bool isFreshInstall: false + + /* + Shell directories. + - Default config directory: ~/.config/noctalia + - Default cache directory: ~/.cache/noctalia + */ + readonly property alias data: adapter // Used to access via Settings.data.xxx.yyy + readonly property int settingsVersion: 59 + property bool isDebug: Quickshell.env("NOCTALIA_DEBUG") === "1" + readonly property string shellName: "noctalia" + readonly property string configDir: ensureTrailingSlash(Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/") + readonly property string cacheDir: ensureTrailingSlash(Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env("HOME") + "/.cache") + "/" + shellName + "/") + + readonly property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json") + readonly property string defaultAvatar: Quickshell.env("HOME") + "/.face" + readonly property string defaultVideosDirectory: Quickshell.env("HOME") + "/Videos" + readonly property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers" + + signal settingsLoaded + signal settingsSaved + signal settingsReloaded + + // ----------------------------------------------------- + // ----------------------------------------------------- + // Ensure directories exist before FileView tries to read files + Component.onCompleted: { + // ensure settings dir exists + Quickshell.execDetached(["mkdir", "-p", configDir]); + Quickshell.execDetached(["mkdir", "-p", cacheDir]); + + // Mark directories as created and trigger file loading + directoriesCreated = true; + + // This should only be activated once when the settings structure has changed + // Then it should be commented out again, regular users don't need to generate + // default settings on every start + if (isDebug) { + generateDefaultSettings(); + generateWidgetDefaultSettings(); + } + + // Patch-in the local default, resolved to user's home + adapter.general.avatarImage = defaultAvatar; + adapter.wallpaper.directory = defaultWallpapersDirectory; + adapter.ui.fontDefault = Qt.application.font.family; + adapter.ui.fontFixed = "monospace"; + + // Set the adapter to the settingsFileView to trigger the real settings load + settingsFileView.adapter = adapter; + } + + // Don't write settings to disk immediately + // This avoid excessive IO when a variable changes rapidly (ex: sliders) + Timer { + id: saveTimer + running: false + interval: 500 + onTriggered: { + root.saveImmediate(); + } + } + + FileView { + id: settingsFileView + path: directoriesCreated ? settingsFile : undefined + printErrors: false + watchChanges: true + onAdapterUpdated: saveTimer.start() + + onFileChanged: { + reloadSettings = true; + reload(); + } + + // Trigger initial load when path changes from empty to actual path + onPathChanged: { + if (path !== undefined) { + reload(); + } + } + onLoaded: function () { + if (!isLoaded) { + Logger.i("Settings", "Settings loaded"); + + // Load raw JSON for migrations (adapter doesn't expose removed properties) + var rawJson = null; + try { + rawJson = JSON.parse(settingsFileView.text()); + } catch (e) { + Logger.w("Settings", "Could not parse raw JSON for migrations"); + } + + // Run versioned migrations immediately, don't move it in upgradeSettings + runVersionedMigrations(rawJson); + + // Finally, update our local settings version + adapter.settingsVersion = settingsVersion; + + // Emit the signal + root.isLoaded = true; + root.settingsLoaded(); + + upgradeSettings(); + } else { + Logger.d("Settings", "Settings reloaded from external file change"); + root.settingsReloaded(); + } + } + onLoadFailed: function (error) { + if (reloadSettings) { + reloadSettings = false; + return; + } + if (error.toString().includes("No such file") || error === 2) { + // File doesn't exist, create it with default values + root.isFreshInstall = true; + writeAdapter(); + + // We started without settings, we should open the setupWizard + root.shouldOpenSetupWizard = true; + } + } + } + + // FileView to load default settings for comparison + FileView { + id: defaultSettingsFileView + path: Quickshell.shellDir + "/Assets/settings-default.json" + printErrors: false + watchChanges: false + } + + // Cached default settings object + property var _defaultSettings: null + + // Load default settings when file is loaded + Connections { + target: defaultSettingsFileView + function onLoaded() { + try { + root._defaultSettings = JSON.parse(defaultSettingsFileView.text()); + } catch (e) { + Logger.w("Settings", "Failed to parse default settings file: " + e); + root._defaultSettings = null; + } + } + } + + JsonAdapter { + id: adapter + + property int settingsVersion: 0 + + // bar + property JsonObject bar: JsonObject { + property string barType: "simple" // "simple", "floating", "framed" + property string position: "top" // "top", "bottom", "left", or "right" + property list monitors: [] // holds bar visibility per monitor + property string density: "default" // "compact", "default", "comfortable" + property bool showOutline: false + property bool showCapsule: true + property real capsuleOpacity: 1.0 + property string capsuleColorKey: "none" + property int widgetSpacing: 6 + property int contentPadding: 2 + property real fontScale: 1.0 + property bool enableExclusionZoneInset: true + + // Bar background opacity settings + property real backgroundOpacity: 0.93 + property bool useSeparateOpacity: false + + // Floating bar settings + property int marginVertical: 4 + property int marginHorizontal: 4 + + // Framed bar settings + property int frameThickness: 8 + property int frameRadius: 12 + + // Bar outer corners (inverted/concave corners at bar edges when not floating) + property bool outerCorners: true + + // Hide bar/panels when compositor overview is active + property bool hideOnOverview: false + + // Auto-hide settings + property string displayMode: "always_visible" + property int autoHideDelay: 500 // ms before hiding after mouse leaves + property int autoShowDelay: 150 // ms before showing when mouse enters + property bool showOnWorkspaceSwitch: true // show bar briefly on workspace switch + + // Widget configuration for modular bar system + property JsonObject widgets + widgets: JsonObject { + property list left: [ + { + "id": "Launcher" + }, + { + "id": "Clock" + }, + { + "id": "SystemMonitor" + }, + { + "id": "ActiveWindow" + }, + { + "id": "MediaMini" + } + ] + property list center: [ + { + "id": "Workspace" + } + ] + property list right: [ + { + "id": "Tray" + }, + { + "id": "NotificationHistory" + }, + { + "id": "Battery" + }, + { + "id": "Volume" + }, + { + "id": "Brightness" + }, + { + "id": "ControlCenter" + } + ] + } + property string mouseWheelAction: "none" + property bool reverseScroll: false + property bool mouseWheelWrap: true + property string middleClickAction: "none" + property bool middleClickFollowMouse: false + property string middleClickCommand: "" + property string rightClickAction: "controlCenter" + property bool rightClickFollowMouse: true + property string rightClickCommand: "" + // Per-screen overrides for position and widgets + // Format: [{ "name": "HDMI-1", "position": "left" }, { "name": "DP-1", "position": "bottom", "widgets": {...} }] + property list screenOverrides: [] + } + + // general + property JsonObject general: JsonObject { + property string avatarImage: "" + property real dimmerOpacity: 0.2 + property bool showScreenCorners: false + property bool forceBlackScreenCorners: false + property real scaleRatio: 1.0 + property real radiusRatio: 1.0 + property real iRadiusRatio: 1.0 + property real boxRadiusRatio: 1.0 + property real screenRadiusRatio: 1.0 + property real animationSpeed: 1.0 + property bool animationDisabled: false + property bool compactLockScreen: false + property bool lockScreenAnimations: false + property bool lockOnSuspend: true + property bool showSessionButtonsOnLockScreen: true + property bool showHibernateOnLockScreen: false + property bool enableLockScreenMediaControls: false + property bool enableShadows: true + property bool enableBlurBehind: true + property string shadowDirection: "bottom_right" + property int shadowOffsetX: 2 + property int shadowOffsetY: 3 + property string language: "" + property bool allowPanelsOnScreenWithoutBar: true + property bool showChangelogOnStartup: true + property bool telemetryEnabled: false + property bool enableLockScreenCountdown: true + property int lockScreenCountdownDuration: 10000 + property bool autoStartAuth: false + property bool allowPasswordWithFprintd: false + property string clockStyle: "custom" + property string clockFormat: "hh\\nmm" + property bool passwordChars: false + property list lockScreenMonitors: [] // holds lock screen visibility per monitor + property real lockScreenBlur: 0.0 + property real lockScreenTint: 0.0 + property JsonObject keybinds: JsonObject { + property list keyUp: ["Up"] + property list keyDown: ["Down"] + property list keyLeft: ["Left"] + property list keyRight: ["Right"] + property list keyEnter: ["Return", "Enter"] + property list keyEscape: ["Esc"] + property list keyRemove: ["Del"] + } + property bool reverseScroll: false + property bool smoothScrollEnabled: true + } + + // ui + property JsonObject ui: JsonObject { + property string fontDefault: "" + property string fontFixed: "" + property real fontDefaultScale: 1.0 + property real fontFixedScale: 1.0 + property bool tooltipsEnabled: true + property bool scrollbarAlwaysVisible: true + property bool boxBorderEnabled: false + property real panelBackgroundOpacity: 0.93 + property bool translucentWidgets: false + property bool panelsAttachedToBar: true + property string settingsPanelMode: "attached" // "centered", "attached", "window" + property bool settingsPanelSideBarCardStyle: false + } + + // location + property JsonObject location: JsonObject { + property string name: "" + property bool weatherEnabled: true + property bool weatherShowEffects: true + property bool weatherTaliaMascotAlways: false + property bool useFahrenheit: false + property bool use12hourFormat: false + property bool showWeekNumberInCalendar: false + property bool showCalendarEvents: true + property bool showCalendarWeather: true + property bool analogClockInCalendar: false + property int firstDayOfWeek: -1 // -1 = auto (use locale), 0 = Sunday, 1 = Monday, 6 = Saturday + property bool hideWeatherTimezone: false + property bool hideWeatherCityName: false + property bool autoLocate: false + } + + // calendar + property JsonObject calendar: JsonObject { + property list cards: [ + { + "id": "calendar-header-card", + "enabled": true + }, + { + "id": "calendar-month-card", + "enabled": true + }, + { + "id": "weather-card", + "enabled": true + } + ] + } + + // wallpaper + property JsonObject wallpaper: JsonObject { + property bool enabled: true + property bool overviewEnabled: false + property string directory: "" + property list monitorDirectories: [] + property bool enableMultiMonitorDirectories: false + property bool showHiddenFiles: false + property string viewMode: "single" // "single" | "recursive" | "browse" + property bool setWallpaperOnAllMonitors: true + property bool linkLightAndDarkWallpapers: true + property string fillMode: "crop" + property color fillColor: "#000000" + property bool useSolidColor: false + property color solidColor: "#1a1a2e" + property bool automationEnabled: false + property string wallpaperChangeMode: "random" // "random" or "alphabetical" + property int randomIntervalSec: 300 // 5 min + property int transitionDuration: 1500 // 1500 ms + property list transitionType: ["fade", "disc", "stripes", "wipe", "pixelate", "honeycomb"] + property bool skipStartupTransition: false + property real transitionEdgeSmoothness: 0.05 + property string panelPosition: "follow_bar" + property bool hideWallpaperFilenames: false + property bool useOriginalImages: false + property real overviewBlur: 0.4 + property real overviewTint: 0.6 + // Wallhaven settings + property bool useWallhaven: false + property string wallhavenQuery: "" + property string wallhavenSorting: "relevance" + property string wallhavenOrder: "desc" + property string wallhavenCategories: "111" // general,anime,people + property string wallhavenPurity: "100" // sfw only + property string wallhavenRatios: "" + property string wallhavenApiKey: "" + property string wallhavenResolutionMode: "atleast" // "atleast" or "exact" + property string wallhavenResolutionWidth: "" + + property string wallhavenResolutionHeight: "" + property string sortOrder: "name" // "name", "name_desc", "date", "date_desc", "random" + property list favorites: [] + // Format: [{ "path": "...", "appearance": "light"|"dark", "colorScheme": "...", "darkMode": bool, "useWallpaperColors": bool, "generationMethod": "...", "paletteColors": [...] }] + // Legacy entries omit "appearance" and use darkMode to infer light vs dark slot. + } + + // applauncher + property JsonObject appLauncher: JsonObject { + property bool enableClipboardHistory: false + property bool autoPasteClipboard: false + property bool enableClipPreview: true + property bool clipboardWrapText: true + property bool enableClipboardSmartIcons: true + property bool enableClipboardChips: true + property string clipboardWatchTextCommand: "wl-paste --type text --watch cliphist store" + property string clipboardWatchImageCommand: "wl-paste --type image --watch cliphist store" + property string position: "center" // Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center + property list pinnedApps: [] + property bool sortByMostUsed: true + property string terminalCommand: "alacritty -e" + property bool customLaunchPrefixEnabled: false + property string customLaunchPrefix: "" + // View mode: "list" or "grid" + property string viewMode: "list" + property bool showCategories: true + // Icon mode: "tabler" or "native" + property string iconMode: "tabler" + property bool showIconBackground: false + property bool enableSettingsSearch: true + property bool enableWindowsSearch: true + property bool enableSessionSearch: true + property bool ignoreMouseInput: false + property string screenshotAnnotationTool: "" + property bool overviewLayer: false + property string density: "default" // "compact", "default", "comfortable" + } + + // control center + property JsonObject controlCenter: JsonObject { + // Position: close_to_bar_button, center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center + property string position: "close_to_bar_button" + property string diskPath: "/" + property JsonObject shortcuts + shortcuts: JsonObject { + property list left: [ + { + "id": "Network" + }, + { + "id": "Bluetooth" + }, + { + "id": "WallpaperSelector" + }, + { + "id": "NoctaliaPerformance" + } + ] + property list right: [ + { + "id": "Notifications" + }, + { + "id": "PowerProfile" + }, + { + "id": "KeepAwake" + }, + { + "id": "NightLight" + } + ] + } + property list cards: [ + { + "id": "profile-card", + "enabled": true + }, + { + "id": "shortcuts-card", + "enabled": true + }, + { + "id": "audio-card", + "enabled": true + }, + { + "id": "brightness-card", + "enabled": false + }, + { + "id": "weather-card", + "enabled": true + }, + { + "id": "media-sysmon-card", + "enabled": true + } + ] + } + + // system monitor + property JsonObject systemMonitor: JsonObject { + property int cpuWarningThreshold: 80 + property int cpuCriticalThreshold: 90 + property int tempWarningThreshold: 80 + property int tempCriticalThreshold: 90 + property int gpuWarningThreshold: 80 + property int gpuCriticalThreshold: 90 + property int memWarningThreshold: 80 + property int memCriticalThreshold: 90 + property int swapWarningThreshold: 80 + property int swapCriticalThreshold: 90 + property int diskWarningThreshold: 80 + property int diskCriticalThreshold: 90 + property int diskAvailWarningThreshold: 20 + property int diskAvailCriticalThreshold: 10 + property int batteryWarningThreshold: 20 + property int batteryCriticalThreshold: 5 + property bool enableDgpuMonitoring: false // Opt-in: reading dGPU sysfs/nvidia-smi wakes it from D3cold, draining battery + property bool useCustomColors: false + property string warningColor: "" + property string criticalColor: "" + property string externalMonitor: "resources || missioncenter || jdsystemmonitor || corestats || system-monitoring-center || gnome-system-monitor || plasma-systemmonitor || mate-system-monitor || ukui-system-monitor || deepin-system-monitor || pantheon-system-monitor" + } + + // performance + property JsonObject noctaliaPerformance: JsonObject { + property bool disableWallpaper: true + property bool disableDesktopWidgets: true + } + + // dock + property JsonObject dock: JsonObject { + property bool enabled: true + property string position: "bottom" // "top", "bottom", "left", "right" + property string displayMode: "auto_hide" // "always_visible", "auto_hide", "exclusive" + property string dockType: "floating" // "floating", "attached" + property real backgroundOpacity: 1.0 + property real floatingRatio: 1.0 + property real size: 1 + property bool onlySameOutput: true + property list monitors: [] // holds dock visibility per monitor + property list pinnedApps: [] // Desktop entry IDs pinned to the dock (e.g., "org.kde.konsole", "firefox.desktop") + property bool colorizeIcons: false + property bool showLauncherIcon: false + property string launcherPosition: "end" // "start", "end" + property bool launcherUseDistroLogo: false + property string launcherIcon: "" + property string launcherIconColor: "none" + property bool pinnedStatic: false + property bool inactiveIndicators: false + property bool groupApps: false + property string groupContextMenuMode: "extended" // "list", "extended" + property string groupClickAction: "cycle" // "cycle", "list" + property string groupIndicatorStyle: "dots" // "number", "dots" + property double deadOpacity: 0.6 + property real animationSpeed: 1.0 // Speed multiplier for hide/show animations (0.1 = slowest, 2.0 = fastest) + property bool sitOnFrame: false + property bool showDockIndicator: false + property int indicatorThickness: 3 + property string indicatorColor: "primary" + property real indicatorOpacity: 0.6 + } + + // network + property JsonObject network: JsonObject { + property bool bluetoothRssiPollingEnabled: false // Opt-in Bluetooth RSSI polling (uses bluetoothctl) + property int bluetoothRssiPollIntervalMs: 60000 // Polling interval in milliseconds for RSSI queries + property string networkPanelView: "wifi" + property string wifiDetailsViewMode: "grid" // "grid" or "list" + property string bluetoothDetailsViewMode: "grid" // "grid" or "list" + property bool bluetoothHideUnnamedDevices: false + property bool disableDiscoverability: false + property bool bluetoothAutoConnect: true + } + + // session menu + property JsonObject sessionMenu: JsonObject { + property bool enableCountdown: true + property int countdownDuration: 10000 + property string position: "center" + property bool showHeader: true + property bool showKeybinds: true + property bool largeButtonsStyle: true + property string largeButtonsLayout: "single-row" + property list powerOptions: [ + { + "action": "lock", + "enabled": true, + "keybind": "1" + }, + { + "action": "suspend", + "enabled": true, + "keybind": "2" + }, + { + "action": "hibernate", + "enabled": true, + "keybind": "3" + }, + { + "action": "reboot", + "enabled": true, + "keybind": "4" + }, + { + "action": "logout", + "enabled": true, + "keybind": "5" + }, + { + "action": "shutdown", + "enabled": true, + "keybind": "6" + }, + { + "action": "rebootToUefi", + "enabled": true, + "keybind": "7" + } + ] + } + + // notifications + property JsonObject notifications: JsonObject { + property bool enabled: true + property bool enableMarkdown: false + property string density: "default" // "default", "compact" + property list monitors: [] // holds notifications visibility per monitor + property string location: "top_right" + property bool overlayLayer: true + property real backgroundOpacity: 1.0 + property bool respectExpireTimeout: false + property int lowUrgencyDuration: 3 + property int normalUrgencyDuration: 8 + property int criticalUrgencyDuration: 15 + property bool clearDismissed: true + property JsonObject saveToHistory: JsonObject { + property bool low: true + property bool normal: true + property bool critical: true + } + property JsonObject sounds: JsonObject { + property bool enabled: false + property real volume: 0.5 + property bool separateSounds: false + property string criticalSoundFile: "" + property string normalSoundFile: "" + property string lowSoundFile: "" + property string excludedApps: "discord,firefox,chrome,chromium,edge" + } + property bool enableMediaToast: false + property bool enableKeyboardLayoutToast: true + property bool enableBatteryToast: true + } + + // on-screen display + property JsonObject osd: JsonObject { + property bool enabled: true + property string location: "top_right" + property int autoHideMs: 2000 + property bool overlayLayer: true + property real backgroundOpacity: 1.0 + property list enabledTypes: [OSD.Type.Volume, OSD.Type.InputVolume, OSD.Type.Brightness] + property list monitors: [] // holds osd visibility per monitor + } + + // audio + property JsonObject audio: JsonObject { + property int volumeStep: 5 + property bool volumeOverdrive: false + property int spectrumFrameRate: 30 + property string visualizerType: "linear" + property bool spectrumMirrored: true + property list mprisBlacklist: [] + property string preferredPlayer: "" + property bool volumeFeedback: false + property string volumeFeedbackSoundFile: "" + } + + // brightness + property JsonObject brightness: JsonObject { + property int brightnessStep: 5 + property bool enforceMinimum: true + property bool enableDdcSupport: false + property list backlightDeviceMappings: [] + // Format: [{ "output": "eDP-1", "device": "/sys/class/backlight/intel_backlight" }] + } + + property JsonObject colorSchemes: JsonObject { + property bool useWallpaperColors: false + property string predefinedScheme: "Noctalia (default)" + property bool darkMode: true + property string schedulingMode: "off" + property string manualSunrise: "06:30" + property string manualSunset: "18:30" + property string generationMethod: "tonal-spot" + property string monitorForColors: "" + property bool syncGsettings: true + } + + // templates toggles + property JsonObject templates: JsonObject { + property list activeTemplates: [] + // Format: [{ "id": "gtk", "enabled": true }, { "id": "qt", "enabled": true }, ...] + property bool enableUserTheming: false + } + + // night light + property JsonObject nightLight: JsonObject { + property bool enabled: false + property bool forced: false + property bool autoSchedule: true + property string nightTemp: "4000" + property string dayTemp: "6500" + property string manualSunrise: "06:30" + property string manualSunset: "18:30" + } + + // hooks + property JsonObject hooks: JsonObject { + property bool enabled: false + property string wallpaperChange: "" + property string darkModeChange: "" + property string screenLock: "" + property string screenUnlock: "" + property string performanceModeEnabled: "" + property string performanceModeDisabled: "" + property string startup: "" + property string session: "" + property string colorGeneration: "" + } + + // plugins + property JsonObject plugins: JsonObject { + property bool autoUpdate: false + property bool notifyUpdates: true + } + + // idle management + property JsonObject idle: JsonObject { + property bool enabled: false + property int screenOffTimeout: 600 // seconds, 0 = disabled + property int lockTimeout: 660 // seconds, 0 = disabled + property int suspendTimeout: 1800 // seconds, 0 = disabled + property int fadeDuration: 5 // seconds of fade-to-black before action fires + property string screenOffCommand: "" + property string lockCommand: "" + property string suspendCommand: "" + property string resumeScreenOffCommand: "" + property string resumeLockCommand: "" + property string resumeSuspendCommand: "" + property string customCommands: "[]" // JSON array of {timeout, command, resumeCommand} + } + + // desktop widgets + property JsonObject desktopWidgets: JsonObject { + property bool enabled: false + property bool overviewEnabled: true + property bool gridSnap: false + property bool gridSnapScale: false + property list monitorWidgets: [] + // Format: [{ "name": "DP-1", "widgets": [...] }, { "name": "HDMI-1", "widgets": [...] }] + } + } + + // ----------------------------------------------------- + // Preprocess paths by adding trailing "/" + function ensureTrailingSlash(path) { + return path.endsWith("/") ? path : path + "/"; + } + + // ----------------------------------------------------- + // Preprocess paths by expanding "~" to user's home directory + function preprocessPath(path) { + if (typeof path !== "string" || path === "") { + return path; + } + + // Expand "~" to user's home directory + if (path.startsWith("~/")) { + return Quickshell.env("HOME") + path.substring(1); + } else if (path === "~") { + return Quickshell.env("HOME"); + } + + return path; + } + + // ----------------------------------------------------- + // Get default value for a setting path (e.g., "general.scaleRatio" or "bar.position") + // Returns undefined if not found + function getDefaultValue(path) { + if (!root._defaultSettings) { + return undefined; + } + + var parts = path.split("."); + var current = root._defaultSettings; + + for (var i = 0; i < parts.length; i++) { + if (current === undefined || current === null) { + return undefined; + } + current = current[parts[i]]; + } + + return current; + } + + // ----------------------------------------------------- + // Compare current value with default value + // Returns true if values differ, false if they match or default is not found + function isValueChanged(path, currentValue) { + var defaultValue = getDefaultValue(path); + if (defaultValue === undefined) { + return false; // Can't compare if default not found + } + + // Deep comparison for objects and arrays + if (typeof currentValue === "object" && typeof defaultValue === "object") { + return JSON.stringify(currentValue) !== JSON.stringify(defaultValue); + } + + // Simple comparison for primitives + return currentValue !== defaultValue; + } + + // ----------------------------------------------------- + // Format default value for tooltip display + // Returns a human-readable string representation of the default value + function formatDefaultValueForTooltip(path) { + var defaultValue = getDefaultValue(path); + if (defaultValue === undefined) { + return ""; + } + + // Format based on type + if (typeof defaultValue === "boolean") { + return defaultValue ? "true" : "false"; + } else if (typeof defaultValue === "number") { + return defaultValue.toString(); + } else if (typeof defaultValue === "string") { + return defaultValue === "" ? "(empty)" : defaultValue; + } else if (Array.isArray(defaultValue)) { + return defaultValue.length === 0 ? "(empty)" : "[" + defaultValue.length + " items]"; + } else if (typeof defaultValue === "object") { + return "(object)"; + } + + return String(defaultValue); + } + + // ----------------------------------------------------- + // Helper to find a screen override entry by name in the array + // Format: [{ "name": "HDMI-A-1", "position": "left" }, ...] + // Note: QML's list is not a true JS array, so we check for .length instead of Array.isArray() + function _findScreenOverride(screenName) { + var overrides = data.bar.screenOverrides; + if (!screenName || !overrides || overrides.length === undefined) { + return null; + } + for (var i = 0; i < overrides.length; i++) { + if (overrides[i] && overrides[i].name === screenName) { + return overrides[i]; + } + } + return null; + } + + // Helper to find index of a screen override entry + function _findScreenOverrideIndex(screenName) { + var overrides = data.bar.screenOverrides; + if (!screenName || !overrides || overrides.length === undefined) { + return -1; + } + for (var i = 0; i < overrides.length; i++) { + if (overrides[i] && overrides[i].name === screenName) { + return i; + } + } + return -1; + } + + // ----------------------------------------------------- + // Check if a screen's overrides are enabled + // Returns true if enabled flag is true or undefined (backward compat) + // Returns false only if enabled is explicitly false + function isScreenOverrideEnabled(screenName) { + var override = _findScreenOverride(screenName); + if (!override) { + return false; + } + return override.enabled !== false; + } + + // ----------------------------------------------------- + // Get effective bar position for a screen (with inheritance) + // If the screen has a position override and overrides are enabled, use it; otherwise use global default + function getBarPositionForScreen(screenName) { + var override = _findScreenOverride(screenName); + if (override && override.enabled !== false && override.position !== undefined) { + return override.position; + } + return data.bar.position || "top"; + } + + // ----------------------------------------------------- + // Get effective bar widgets for a screen (with inheritance) + // If the screen has widget overrides and overrides are enabled, use them; otherwise use global defaults + function getBarWidgetsForScreen(screenName) { + var override = _findScreenOverride(screenName); + if (override && override.enabled !== false && override.widgets !== undefined) { + return override.widgets; + } + return data.bar.widgets; + } + + // ----------------------------------------------------- + // Get effective bar density for a screen (with inheritance) + // If the screen has a density override and overrides are enabled, use it; otherwise use global default + function getBarDensityForScreen(screenName) { + var override = _findScreenOverride(screenName); + if (override && override.enabled !== false && override.density !== undefined) { + return override.density; + } + return data.bar.density || "default"; + } + + // ----------------------------------------------------- + // Get effective bar display mode for a screen (with inheritance) + // If the screen has a displayMode override and overrides are enabled, use it; otherwise use global default + function getBarDisplayModeForScreen(screenName) { + var override = _findScreenOverride(screenName); + if (override && override.enabled !== false && override.displayMode !== undefined) { + return override.displayMode; + } + return data.bar.displayMode || "always_visible"; + } + + // ----------------------------------------------------- + // Check if a screen has any overrides, optionally for a specific property + function hasScreenOverride(screenName, property) { + var override = _findScreenOverride(screenName); + if (!override) { + return false; + } + if (property) { + return override[property] !== undefined; + } + // Check if screen has any override property (besides "name") + var keys = Object.keys(override); + return keys.length > 1 || (keys.length === 1 && keys[0] !== "name"); + } + + // ----------------------------------------------------- + // Get the screen override entry directly (for in-place modifications) + // Returns the actual entry object from the array, not a copy + function getScreenOverrideEntry(screenName) { + return _findScreenOverride(screenName); + } + + // ----------------------------------------------------- + // Set a per-screen override + function setScreenOverride(screenName, property, value) { + if (!screenName) + return; + + var overrides = JSON.parse(JSON.stringify(data.bar.screenOverrides || [])); + if (overrides.length === undefined) { + overrides = []; + } + + var index = -1; + for (var i = 0; i < overrides.length; i++) { + if (overrides[i] && overrides[i].name === screenName) { + index = i; + break; + } + } + + if (index === -1) { + // Create new entry + var newEntry = { + "name": screenName + }; + newEntry[property] = value; + overrides.push(newEntry); + } else { + // Update existing entry + overrides[index][property] = value; + } + data.bar.screenOverrides = overrides; + } + + // ----------------------------------------------------- + // Clear a per-screen override (revert to global default) + // If property is null, clears all overrides for that screen + function clearScreenOverride(screenName, property) { + if (!screenName) + return; + + var overrides = data.bar.screenOverrides; + if (!overrides || overrides.length === undefined) { + return; + } + + overrides = JSON.parse(JSON.stringify(overrides)); + + var index = -1; + for (var i = 0; i < overrides.length; i++) { + if (overrides[i] && overrides[i].name === screenName) { + index = i; + break; + } + } + + if (index === -1) { + return; + } + + if (property) { + delete overrides[index][property]; + // Remove screen entry if only "name" remains + var keys = Object.keys(overrides[index]); + if (keys.length <= 1 && (keys.length === 0 || keys[0] === "name")) { + overrides.splice(index, 1); + } + } else { + overrides.splice(index, 1); + } + data.bar.screenOverrides = overrides; + } + + // ----------------------------------------------------- + // Public function to trigger immediate settings saving + function saveImmediate() { + settingsFileView.writeAdapter(); + root.settingsSaved(); // Emit signal after saving + } + + // ----------------------------------------------------- + // Generate default settings: for reference only, not used by the shell + function generateDefaultSettings() { + try { + Logger.d("Settings", "Generating settings-default.json"); + + // Prepare a clean JSON + var plainAdapter = QtObj2JS.qtObjectToPlainObject(adapter); + var jsonData = JSON.stringify(plainAdapter, null, 2); + + var defaultPath = Quickshell.shellDir + "/Assets/settings-default.json"; + + Quickshell.execDetached(["sh", "-c", `cat > "${defaultPath}" << 'NOCTALIA_EOF'\n${jsonData}\nNOCTALIA_EOF`]); + } catch (error) { + Logger.e("Settings", "Failed to generate default settings file: " + error); + } + } + + // ----------------------------------------------------- + // Generate default widget settings: for reference only, not used by the shell + function generateWidgetDefaultSettings() { + try { + Logger.d("Settings", "Generating settings-widgets-default.json"); + + var output = { + "bar": QtObj2JS.qtObjectToPlainObject(BarWidgetRegistry.widgetMetadata), + "controlCenter": QtObj2JS.qtObjectToPlainObject(ControlCenterWidgetRegistry.widgetMetadata), + "desktop": QtObj2JS.qtObjectToPlainObject(DesktopWidgetRegistry.widgetMetadata) + }; + var jsonData = JSON.stringify(output, null, 2); + + var defaultPath = Quickshell.shellDir + "/Assets/settings-widgets-default.json"; + + Quickshell.execDetached(["sh", "-c", `cat > "${defaultPath}" << 'NOCTALIA_EOF'\n${jsonData}\nNOCTALIA_EOF`]); + } catch (error) { + Logger.e("Settings", "Failed to generate widget default settings file: " + error); + } + } + + // ----------------------------------------------------- + // Run versioned migrations using MigrationRegistry + // rawJson is the parsed JSON file content (before adapter filtering) + function runVersionedMigrations(rawJson) { + // Skip migrations on fresh installs (no prior settings file) + if (!rawJson || root.isFreshInstall) { + Logger.i("Settings", "Fresh install detected, skipping migrations"); + return; + } + + const currentVersion = adapter.settingsVersion; + const migrations = MigrationRegistry.migrations; + + Logger.i("Settings", "adapter.settingsVersion:", adapter.settingsVersion); + + // Get all migration versions and sort them + const versions = Object.keys(migrations).map(v => parseInt(v)).sort((a, b) => a - b); + + // Run migrations in order for versions newer than current + for (var i = 0; i < versions.length; i++) { + const version = versions[i]; + + if (currentVersion < version) { + // Create migration instance and run it + const migrationComponent = migrations[version]; + const migration = migrationComponent.createObject(root); + + if (migration && typeof migration.migrate === "function") { + const success = migration.migrate(adapter, Logger, rawJson); + if (!success) { + Logger.e("Settings", "Migration to v" + version + " failed"); + } + } else { + Logger.e("Settings", "Invalid migration for v" + version); + } + + // Clean up migration instance + if (migration) { + migration.destroy(); + } + } + } + } + + // ----------------------------------------------------- + // If the settings structure has changed, ensure + // backward compatibility by upgrading the settings + function upgradeSettings() { + // Wait for PluginService to finish loading plugins first + // This prevents deleting plugin widgets during reload before plugins are registered + if (!PluginService.initialized || !PluginService.pluginsFullyLoaded) { + Logger.d("Settings", "Plugins not fully loaded yet, deferring upgrade"); + Qt.callLater(upgradeSettings); + return; + } + + // Wait for BarWidgetRegistry to be ready + if (!BarWidgetRegistry.widgets || Object.keys(BarWidgetRegistry.widgets).length === 0) { + Logger.d("Settings", "BarWidgetRegistry not ready, deferring upgrade"); + Qt.callLater(upgradeSettings); + return; + } + + // ----------------- + const sections = ["left", "center", "right"]; + + // 1. remove any non existing bar widget type + var removedWidget = false; + for (var s = 0; s < sections.length; s++) { + const sectionName = sections[s]; + const widgets = adapter.bar.widgets[sectionName]; + // Iterate backward through the widgets array, so it does not break when removing a widget + for (var i = widgets.length - 1; i >= 0; i--) { + var widget = widgets[i]; + if (!BarWidgetRegistry.hasWidget(widget.id)) { + Logger.w(`Settings`, `!!! Deleted invalid bar widget ${widget.id} !!!`); + widgets.splice(i, 1); + removedWidget = true; + } + } + } + + // ----------------- + // 2. remove any non existing control center widget type + const ccSections = ["left", "right"]; + for (var s = 0; s < ccSections.length; s++) { + const sectionName = ccSections[s]; + const shortcuts = adapter.controlCenter.shortcuts[sectionName]; + for (var i = shortcuts.length - 1; i >= 0; i--) { + var shortcut = shortcuts[i]; + if (!ControlCenterWidgetRegistry.hasWidget(shortcut.id)) { + Logger.w(`Settings`, `!!! Deleted invalid control center widget ${shortcut.id} !!!`); + shortcuts.splice(i, 1); + removedWidget = true; + } + } + } + + // ----------------- + // 3. remove any non existing desktop widget type + const monitorWidgets = adapter.desktopWidgets.monitorWidgets; + for (var m = 0; m < monitorWidgets.length; m++) { + const monitor = monitorWidgets[m]; + if (!monitor.widgets) + continue; + for (var i = monitor.widgets.length - 1; i >= 0; i--) { + var desktopWidget = monitor.widgets[i]; + if (!DesktopWidgetRegistry.hasWidget(desktopWidget.id)) { + Logger.w(`Settings`, `!!! Deleted invalid desktop widget ${desktopWidget.id} !!!`); + monitor.widgets.splice(i, 1); + removedWidget = true; + } + } + } + + // ----------------- + // 4. upgrade user widget settings + for (var s = 0; s < sections.length; s++) { + const sectionName = sections[s]; + for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) { + var widget = adapter.bar.widgets[sectionName][i]; + + // Check if widget registry supports user settings, if it does not, then there is nothing to do + if (BarWidgetRegistry.widgetMetadata[widget.id] === undefined) { + continue; + } + + if (upgradeWidget(widget)) { + Logger.d("Settings", `Upgraded ${widget.id} widget:`, JSON.stringify(widget)); + } + } + } + } + + // ----------------------------------------------------- + // Function to clean up deprecated user/custom bar widgets settings + function upgradeWidget(widget) { + // Backup the widget definition before altering + const widgetBefore = JSON.stringify(widget); + + // Get all existing custom settings keys + const keys = Object.keys(BarWidgetRegistry.widgetMetadata[widget.id]); + + // Delete deprecated user settings from the wiget + for (const k of Object.keys(widget)) { + if (k === "id") { + continue; + } + if (!keys.includes(k)) { + delete widget[k]; + } + } + + // Inject missing default setting (metaData) from BarWidgetRegistry + for (var i = 0; i < keys.length; i++) { + const k = keys[i]; + if (k === "id") { + continue; + } + + if (widget[k] === undefined) { + widget[k] = BarWidgetRegistry.widgetMetadata[widget.id][k]; + } + } + + // Compare settings, to detect if something has been upgraded + const widgetAfter = JSON.stringify(widget); + return (widgetAfter !== widgetBefore); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/ShellState.qml b/fedora/.config/quickshell/noctalia-shell/Commons/ShellState.qml new file mode 100644 index 0000000..187443b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/ShellState.qml @@ -0,0 +1,287 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import "../Helpers/QtObj2JS.js" as QtObj2JS +import qs.Services.Power +import qs.Services.System +import qs.Services.UI + +// Centralized shell state management for small cache files +Singleton { + id: root + + property string stateFile: "" + property bool isLoaded: false + + // State properties for different services + readonly property alias data: adapter + + // Signals for state changes + signal displayStateChanged + signal notificationsStateChanged + signal changelogStateChanged + signal colorSchemesListChanged + + Component.onCompleted: { + // Setup state file path (needs Settings to be available) + Qt.callLater(() => { + if (typeof Settings !== 'undefined' && Settings.cacheDir) { + stateFile = Settings.cacheDir + "shell-state.json"; + stateFileView.path = stateFile; + } + }); + } + + // FileView for shell state + FileView { + id: stateFileView + printErrors: false + watchChanges: false + + adapter: JsonAdapter { + id: adapter + + // CompositorService: display scales + property var display: ({}) + + // NotificationService: notification state + property var notificationsState: ({ + lastSeenTs: 0 + }) + + // UpdateService: changelog state + property var changelogState: ({ + lastSeenVersion: "" + }) + + // SchemeDownloader: color schemes list + property var colorSchemesList: ({ + schemes: [], + timestamp: 0 + }) + + // UI state: settings panel, etc. + property var ui: ({ + settingsSidebarExpanded: true + }) + + // Telemetry state + property var telemetry: ({ + instanceId: "" + }) + + // Launcher app usage counts + property var launcherUsage: ({}) + } + + onLoaded: { + root.isLoaded = true; + Logger.d("ShellState", "Loaded state file"); + } + + onLoadFailed: error => { + if (error === 2) { + // File doesn't exist, will be created on first write + root.isLoaded = true; + Logger.d("ShellState", "State file doesn't exist, will create on first write"); + } else { + Logger.e("ShellState", "Failed to load state file:", error); + root.isLoaded = true; + } + } + } + + // Launcher usage + function getLauncherUsageCount(key) { + const m = adapter.launcherUsage; + if (!m) + return 0; + const v = m[key]; + return typeof v === 'number' && isFinite(v) ? v : 0; + } + + function recordLauncherUsage(key) { + let counts = Object.assign({}, adapter.launcherUsage || {}); + counts[key] = getLauncherUsageCount(key) + 1; + adapter.launcherUsage = counts; + save(); + } + + // Migrate usage from one key to another, merging counts in a single save + function migrateLauncherUsage(fromKey, toKey) { + let counts = Object.assign({}, adapter.launcherUsage || {}); + const fromCount = typeof counts[fromKey] === 'number' && isFinite(counts[fromKey]) ? counts[fromKey] : 0; + const toCount = typeof counts[toKey] === 'number' && isFinite(counts[toKey]) ? counts[toKey] : 0; + counts[toKey] = toCount + fromCount; + delete counts[fromKey]; + adapter.launcherUsage = counts; + save(); + } + + // Debounced save timer + Timer { + id: saveTimer + interval: 500 + onTriggered: performSave() + } + + property bool saveQueued: false + + function save() { + saveQueued = true; + saveTimer.restart(); + } + + function performSave() { + if (!saveQueued || !stateFile) { + return; + } + + saveQueued = false; + + try { + // Ensure cache directory exists + Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]); + + Qt.callLater(() => { + try { + stateFileView.writeAdapter(); + Logger.d("ShellState", "Saved state file"); + } catch (writeError) { + Logger.e("ShellState", "Failed to write state file:", writeError); + } + }); + } catch (error) { + Logger.e("ShellState", "Failed to save state:", error); + } + } + + // Convenience functions for each service + + // Display state (CompositorService) + function setDisplay(displayData) { + adapter.display = displayData; + save(); + displayStateChanged(); + } + + function getDisplay() { + return adapter.display || {}; + } + + // Notifications state (NotificationService) + function setNotificationsState(stateData) { + adapter.notificationsState = stateData; + save(); + notificationsStateChanged(); + } + + function getNotificationsState() { + return adapter.notificationsState || { + lastSeenTs: 0 + }; + } + + // Changelog state (UpdateService) + function setChangelogState(stateData) { + adapter.changelogState = stateData; + save(); + changelogStateChanged(); + } + + function getChangelogState() { + return adapter.changelogState || { + lastSeenVersion: "" + }; + } + + // Color schemes list (SchemeDownloader) + function setColorSchemesList(listData) { + adapter.colorSchemesList = listData; + save(); + colorSchemesListChanged(); + } + + function getColorSchemesList() { + return adapter.colorSchemesList || { + schemes: [], + timestamp: 0 + }; + } + + // UI state + function setUiState(stateData) { + adapter.ui = stateData; + save(); + } + + function getUiState() { + return adapter.ui || { + settingsSidebarExpanded: true + }; + } + + function setSettingsSidebarExpanded(expanded) { + let uiState = getUiState(); + uiState.settingsSidebarExpanded = expanded; + setUiState(uiState); + } + + function getSettingsSidebarExpanded() { + return getUiState().settingsSidebarExpanded !== false; // default to true + } + + // Telemetry state + function setTelemetryState(stateData) { + adapter.telemetry = stateData; + save(); + } + + function getTelemetryState() { + return adapter.telemetry || { + instanceId: "" + }; + } + + function getTelemetryInstanceId() { + return getTelemetryState().instanceId || ""; + } + + function setTelemetryInstanceId(instanceId) { + let state = getTelemetryState(); + state.instanceId = instanceId; + setTelemetryState(state); + } + + // ----------------------------------------------------- + function buildStateSnapshot() { + try { + const settingsData = QtObj2JS.qtObjectToPlainObject(Settings.data); + const shellStateData = ShellState?.data ? QtObj2JS.qtObjectToPlainObject(ShellState.data) || {} : {}; + + return { + settings: settingsData, + state: { + doNotDisturb: NotificationService.doNotDisturb, + noctaliaPerformanceMode: PowerProfileService.noctaliaPerformanceMode, + barVisible: BarService.isVisible, + openedPanel: PanelService.openedPanel?.objectName || "", + lockScreenActive: PanelService.lockScreen?.active || false, + wallpapers: WallpaperService.getWallpapersEffectiveMap(), + desktopWidgetsEditMode: DesktopWidgetRegistry.editMode || false, + // ------------- + display: shellStateData.display || {}, + notificationsState: shellStateData.notificationsState || {}, + changelogState: shellStateData.changelogState || {}, + colorSchemesList: shellStateData.colorSchemesList || {}, + ui: shellStateData.ui || {} + } + }; + } catch (error) { + Logger.e("Settings", "Failed to build state snapshot:", error); + return null; + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Style.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Style.qml new file mode 100644 index 0000000..1910401 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Style.qml @@ -0,0 +1,247 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Services.Power + +Singleton { + id: root + + // Font size + readonly property real fontSizeXXS: 8 + readonly property real fontSizeXS: 9 + readonly property real fontSizeS: 10 + readonly property real fontSizeM: 11 + readonly property real fontSizeL: 13 + readonly property real fontSizeXL: 16 + readonly property real fontSizeXXL: 18 + readonly property real fontSizeXXXL: 24 + + // Font weight + readonly property int fontWeightRegular: 400 + readonly property int fontWeightMedium: 500 + readonly property int fontWeightSemiBold: 600 + readonly property int fontWeightBold: 700 + + // Container Radii: major layout sections (sidebars, cards, content panels) + readonly property int radiusXXXS: Math.round(3 * Settings.data.general.radiusRatio) + readonly property int radiusXXS: Math.round(4 * Settings.data.general.radiusRatio) + readonly property int radiusXS: Math.round(8 * Settings.data.general.radiusRatio) + readonly property int radiusS: Math.round(12 * Settings.data.general.radiusRatio) + readonly property int radiusM: Math.round(16 * Settings.data.general.radiusRatio) + readonly property int radiusL: Math.round(20 * Settings.data.general.radiusRatio) + + // Input radii: interactive elements (buttons, toggles, text fields) + readonly property int iRadiusXXXS: Math.round(3 * Settings.data.general.iRadiusRatio) + readonly property int iRadiusXXS: Math.round(4 * Settings.data.general.iRadiusRatio) + readonly property int iRadiusXS: Math.round(8 * Settings.data.general.iRadiusRatio) + readonly property int iRadiusS: Math.round(12 * Settings.data.general.iRadiusRatio) + readonly property int iRadiusM: Math.round(16 * Settings.data.general.iRadiusRatio) + readonly property int iRadiusL: Math.round(20 * Settings.data.general.iRadiusRatio) + + readonly property int screenRadius: Math.round(20 * Settings.data.general.screenRadiusRatio) + + // Border + readonly property int borderS: Math.max(1, Math.round(1 * uiScaleRatio)) + readonly property int borderM: Math.max(1, Math.round(2 * uiScaleRatio)) + readonly property int borderL: Math.max(1, Math.round(3 * uiScaleRatio)) + + // Margins (for margins and spacing) + readonly property int marginXXXS: Math.round(1 * uiScaleRatio) + readonly property int marginXXS: Math.round(2 * uiScaleRatio) + readonly property int marginXS: Math.round(4 * uiScaleRatio) + readonly property int marginS: Math.round(6 * uiScaleRatio) + readonly property int marginM: Math.round(9 * uiScaleRatio) + readonly property int marginL: Math.round(13 * uiScaleRatio) + readonly property int marginXL: Math.round(18 * uiScaleRatio) + + // Double margins, for proper container sizing only (e.g. height: id.implicitHeight + Style.margin2M) + readonly property int margin2XXXS: marginXXXS * 2 + readonly property int margin2XXS: marginXXS * 2 + readonly property int margin2XS: marginXS * 2 + readonly property int margin2S: marginS * 2 + readonly property int margin2M: marginM * 2 + readonly property int margin2L: marginL * 2 + readonly property int margin2XL: marginXL * 2 + + // Opacity + readonly property real opacityNone: 0.0 + readonly property real opacityLight: 0.25 + readonly property real opacityMedium: 0.5 + readonly property real opacityHeavy: 0.75 + readonly property real opacityAlmost: 0.95 + readonly property real opacityFull: 1.0 + + // Shadows + readonly property real shadowOpacity: 0.85 + readonly property real shadowBlur: 1.0 + readonly property int shadowBlurMax: 22 + readonly property real shadowHorizontalOffset: Settings.data.general.shadowOffsetX + readonly property real shadowVerticalOffset: Settings.data.general.shadowOffsetY + + // Animation duration (ms) + readonly property int animationFaster: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(75 / Settings.data.general.animationSpeed) + readonly property int animationFast: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(150 / Settings.data.general.animationSpeed) + readonly property int animationNormal: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(300 / Settings.data.general.animationSpeed) + readonly property int animationSlow: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(450 / Settings.data.general.animationSpeed) + readonly property int animationSlowest: (Settings.data.general.animationDisabled || PowerProfileService.noctaliaPerformanceMode) ? 0 : Math.round(750 / Settings.data.general.animationSpeed) + + // Delays + readonly property int tooltipDelay: 300 + readonly property int tooltipDelayLong: 1200 + readonly property int pillDelay: 500 + + // Widgets base size + readonly property real baseWidgetSize: 33 + readonly property real sliderWidth: 200 + + readonly property real uiScaleRatio: Settings.data.general.scaleRatio + + // Bar Height + readonly property real barHeight: { + let h; + switch (Settings.data.bar.density) { + case "mini": + h = (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 23 : 21; + break; + case "compact": + h = (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 27 : 25; + break; + case "comfortable": + h = (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 39 : 37; + break; + case "spacious": + h = (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 49 : 47; + break; + default: + case "default": + h = (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 33 : 31; + } + return toOdd(h); + } + + // Capsule Height + // Note: capsule must always be smaller than barHeight to account for border rendering + // Qt Quick Rectangle borders are drawn centered on edges (half inside, half outside) + readonly property real capsuleHeight: { + let h; + switch (Settings.data.bar.density) { + case "mini": + h = Math.round(barHeight * 0.90); + break; + case "compact": + h = Math.round(barHeight * 0.85); + break; + case "comfortable": + h = Math.round(barHeight * 0.75); + break; + case "spacious": + h = Math.round(barHeight * 0.65); + break; + default: + h = Math.round(barHeight * 0.82); + break; + } + return toOdd(h); + } + + // The base/default font size for all texts in the bar + readonly property real _barBaseFontSize: Math.max(1, (Style.barHeight / Style.capsuleHeight) * Style.fontSizeXXS) + readonly property real barFontSize: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? _barBaseFontSize * 0.9 * Settings.data.bar.fontScale : _barBaseFontSize * Settings.data.bar.fontScale + + readonly property color capsuleColor: Settings.data.bar.showCapsule ? Qt.alpha(Settings.data.bar.capsuleColorKey !== "none" ? Color.resolveColorKey(Settings.data.bar.capsuleColorKey) : Color.mSurfaceVariant, Settings.data.bar.capsuleOpacity) : "transparent" + + readonly property color capsuleBorderColor: Settings.data.bar.showOutline ? Color.mPrimary : "transparent" + readonly property int capsuleBorderWidth: Settings.data.bar.showOutline ? Style.borderS : 0 + + readonly property color boxBorderColor: Settings.data.ui.boxBorderEnabled ? Color.mOutline : "transparent" + + // Pixel-perfect utility for centering content without subpixel positioning + function pixelAlignCenter(containerSize, contentSize) { + return Math.round((containerSize - contentSize) / 2); + } + + // Ensures a number is always odd (rounds down to nearest odd) + function toOdd(n) { + return Math.floor(n / 2) * 2 + 1; + } + + // Ensures a number is always even (rounds down to nearest even) + function toEven(n) { + return Math.floor(n / 2) * 2; + } + + // Get bar height for a specific density and orientation + function getBarHeightForDensity(density, isVertical) { + let h; + switch (density) { + case "mini": + h = isVertical ? 23 : 21; + break; + case "compact": + h = isVertical ? 27 : 25; + break; + case "comfortable": + h = isVertical ? 39 : 37; + break; + case "spacious": + h = isVertical ? 49 : 47; + break; + default: + case "default": + h = isVertical ? 33 : 31; + } + return toOdd(h); + } + + // Get capsule height for a specific density and bar height + function getCapsuleHeightForDensity(density, barHeight) { + let h; + switch (density) { + case "mini": + h = Math.round(barHeight * 0.90); + break; + case "compact": + h = Math.round(barHeight * 0.85); + break; + case "comfortable": + h = Math.round(barHeight * 0.75); + break; + case "spacious": + h = Math.round(barHeight * 0.65); + break; + default: + h = Math.round(barHeight * 0.82); + break; + } + return toOdd(h); + } + + // Get bar font size for a specific bar height, capsule height, and orientation + function getBarFontSizeForDensity(barHeight, capsuleHeight, isVertical) { + const baseFontSize = Math.max(1, (barHeight / capsuleHeight) * Style.fontSizeXXS); + return isVertical ? baseFontSize * 0.9 * Settings.data.bar.fontScale : baseFontSize * Settings.data.bar.fontScale; + } + + // Convenience functions for per-screen bar sizing + function getBarHeightForScreen(screenName) { + var density = Settings.getBarDensityForScreen(screenName); + var position = Settings.getBarPositionForScreen(screenName); + var isVertical = position === "left" || position === "right"; + return getBarHeightForDensity(density, isVertical); + } + + function getCapsuleHeightForScreen(screenName) { + var barHeight = getBarHeightForScreen(screenName); + var density = Settings.getBarDensityForScreen(screenName); + return getCapsuleHeightForDensity(density, barHeight); + } + + function getBarFontSizeForScreen(screenName) { + var barHeight = getBarHeightForScreen(screenName); + var capsuleHeight = getCapsuleHeightForScreen(screenName); + var position = Settings.getBarPositionForScreen(screenName); + var isVertical = position === "left" || position === "right"; + return getBarFontSizeForDensity(barHeight, capsuleHeight, isVertical); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/ThemeIcons.qml b/fedora/.config/quickshell/noctalia-shell/Commons/ThemeIcons.qml new file mode 100644 index 0000000..f4dc67d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/ThemeIcons.qml @@ -0,0 +1,289 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons + +Singleton { + id: root + + property real scoreThreshold: 0.2 + + // Manual overrides for tricky apps + property var substitutions: ({ + "code-url-handler": "visual-studio-code", + "Code": "visual-studio-code", + "gnome-tweaks": "org.gnome.tweaks", + "pavucontrol-qt": "pavucontrol", + "wps": "wps-office2019-kprometheus", + "wpsoffice": "wps-office2019-kprometheus", + "footclient": "foot" + }) + + // Dynamic fixups + property var regexSubstitutions: [ + { + "regex": /^steam_app_(\d+)$/, + "replace": "steam_icon_$1" + }, + { + "regex": /Minecraft.*/, + "replace": "minecraft-launcher" + }, + { + "regex": /.*polkit.*/, + "replace": "system-lock-screen" + }, + { + "regex": /gcr.prompter/, + "replace": "system-lock-screen" + } + ] + + property list entryList: [] + property var preppedNames: [] + property var preppedIcons: [] + property var preppedIds: [] + + Component.onCompleted: refreshEntries() + + Connections { + target: DesktopEntries.applications + function onValuesChanged() { + refreshEntries(); + } + } + + function refreshEntries() { + if (typeof DesktopEntries === 'undefined') + return; + + const values = Array.from(DesktopEntries.applications.values); + if (values) { + entryList = values.sort((a, b) => a.name.localeCompare(b.name)); + updatePreppedData(); + } + } + + function updatePreppedData() { + if (typeof FuzzySort === 'undefined') + return; + + const list = Array.from(entryList); + preppedNames = list.map(a => ({ + name: FuzzySort.prepare(`${a.name} `), + entry: a + })); + preppedIcons = list.map(a => ({ + name: FuzzySort.prepare(`${a.icon} `), + entry: a + })); + preppedIds = list.map(a => ({ + name: FuzzySort.prepare(`${a.id} `), + entry: a + })); + } + + function iconForAppId(appId, fallbackName) { + const fallback = fallbackName || "application-x-executable"; + if (!appId) + return iconFromName(fallback, fallback); + + const entry = findAppEntry(appId); + if (entry) { + return iconFromName(entry.icon, fallback); + } + + return iconFromName(appId, fallback); + } + + // Robust lookup strategy + function findAppEntry(str) { + if (!str || str.length === 0) + return null; + + let result = null; + + if (result = checkHeuristic(str)) + return result; + if (result = checkSubstitutions(str)) + return result; + if (result = checkRegex(str)) + return result; + if (result = checkSimpleTransforms(str)) + return result; + if (result = checkFuzzySearch(str)) + return result; + if (result = checkCleanMatch(str)) + return result; + + return null; + } + + function iconFromName(iconName, fallbackName) { + const fallback = fallbackName || "application-x-executable"; + try { + if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) { + const p = Quickshell.iconPath(iconName, fallback); + if (p && p !== "") + return p; + } + } catch (e) {} + + try { + return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : ""; + } catch (e2) { + return ""; + } + } + + function distroLogoPath() { + try { + return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : ""; + } catch (e) { + return ""; + } + } + + // --- Lookup Helpers --- + + function checkHeuristic(str) { + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) { + const entry = DesktopEntries.heuristicLookup(str); + if (entry) + return entry; + } + return null; + } + + function checkSubstitutions(str) { + let effectiveStr = substitutions[str]; + if (!effectiveStr) + effectiveStr = substitutions[str.toLowerCase()]; + + if (effectiveStr && effectiveStr !== str) { + return findAppEntry(effectiveStr); + } + return null; + } + + function checkRegex(str) { + for (let i = 0; i < regexSubstitutions.length; i++) { + const sub = regexSubstitutions[i]; + const replaced = str.replace(sub.regex, sub.replace); + if (replaced !== str) { + return findAppEntry(replaced); + } + } + return null; + } + + function checkSimpleTransforms(str) { + if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId) + return null; + + const lower = str.toLowerCase(); + + const variants = [str, lower, getFromReverseDomain(str), getFromReverseDomain(str)?.toLowerCase(), normalizeWithHyphens(str), str.replace(/_/g, '-').toLowerCase(), str.replace(/-/g, '_').toLowerCase()]; + + for (let i = 0; i < variants.length; i++) { + const variant = variants[i]; + if (variant) { + const entry = DesktopEntries.byId(variant); + if (entry) + return entry; + } + } + return null; + } + + function checkFuzzySearch(str) { + if (typeof FuzzySort === 'undefined') + return null; + + // Check filenames (IDs) first + if (preppedIds.length > 0) { + let results = fuzzyQuery(str, preppedIds); + if (results.length === 0) { + const underscored = str.replace(/-/g, '_').toLowerCase(); + if (underscored !== str) + results = fuzzyQuery(underscored, preppedIds); + } + if (results.length > 0) + return results[0]; + } + + // Then icons + if (preppedIcons.length > 0) { + const results = fuzzyQuery(str, preppedIcons); + if (results.length > 0) + return results[0]; + } + + // Then names + if (preppedNames.length > 0) { + const results = fuzzyQuery(str, preppedNames); + if (results.length > 0) + return results[0]; + } + + return null; + } + + function checkCleanMatch(str) { + if (!str || str.length <= 3) + return null; + if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId) + return null; + + // Aggressive fallback: strip all separators + const cleanStr = str.toLowerCase().replace(/[\.\-_]/g, ''); + const list = Array.from(entryList); + + for (let i = 0; i < list.length; i++) { + const entry = list[i]; + const cleanId = (entry.id || "").toLowerCase().replace(/[\.\-_]/g, ''); + if (cleanId.includes(cleanStr) || cleanStr.includes(cleanId)) { + return entry; + } + } + return null; + } + + function fuzzyQuery(search, preppedData) { + if (!search || !preppedData || preppedData.length === 0) + return []; + return FuzzySort.go(search, preppedData, { + all: true, + key: "name" + }).map(r => r.obj.entry); + } + + function iconExists(iconName) { + if (!iconName || iconName.length === 0) + return false; + if (iconName.startsWith("/")) + return true; + + const path = Quickshell.iconPath(iconName, true); + return path && path.length > 0 && !path.includes("image-missing"); + } + + function getFromReverseDomain(str) { + if (!str) + return ""; + return str.split('.').slice(-1)[0]; + } + + function normalizeWithHyphens(str) { + if (!str) + return ""; + return str.toLowerCase().replace(/\s+/g, "-"); + } + + // Deprecated shim + function guessIcon(str) { + const entry = findAppEntry(str); + return entry ? entry.icon : "image-missing"; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Commons/Time.qml b/fedora/.config/quickshell/noctalia-shell/Commons/Time.qml new file mode 100644 index 0000000..69a2469 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Commons/Time.qml @@ -0,0 +1,232 @@ +pragma Singleton +import QtQuick + +import Quickshell +import qs.Commons +import qs.Services.System + +Singleton { + id: root + + // Current date + property var now: new Date() + + // Unix timestamp of the last update + property real _lastUpdateTs: Date.now() + + // Signal emitted when a significant time jump is detected (e.g. system resume) + signal resumed + + // Returns a Unix Timestamp (in seconds) + readonly property int timestamp: { + return Math.floor(root.now / 1000); + } + + // Timer state (for countdown/stopwatch) + property bool timerRunning: false + property bool timerStopwatchMode: false + property int timerRemainingSeconds: 0 + property int timerTotalSeconds: 0 + property int timerElapsedSeconds: 0 + property bool timerSoundPlaying: false + property int timerStartTimestamp: 0 // Unix timestamp when timer was started + property int timerPausedAt: 0 // Value when paused (for resuming) + + Timer { + id: updateTimer + interval: 1000 + repeat: true + running: true + triggeredOnStart: false + onTriggered: { + var newTime = new Date(); + var currentTs = newTime.getTime(); + + // Detect time jump (e.g. system resume) - threshold: 5 seconds + if (currentTs - root._lastUpdateTs > 5000) { + Logger.i("Time", "Time jump detected (" + Math.round((currentTs - root._lastUpdateTs) / 1000) + "s) - likely system resume"); + root.resumed(); + } + root._lastUpdateTs = currentTs; + + root.now = newTime; + + // Update timer if running + if (root.timerRunning && root.timerStartTimestamp > 0) { + const elapsedSinceStart = root.timestamp - root.timerStartTimestamp; + + if (root.timerStopwatchMode) { + root.timerElapsedSeconds = root.timerPausedAt + elapsedSinceStart; + } else { + root.timerRemainingSeconds = root.timerTotalSeconds - elapsedSinceStart; + if (root.timerRemainingSeconds <= 0) { + root.timerOnFinished(); + } + } + } + + // Adjust next interval to sync with the start of the next second + var msIntoSecond = newTime.getMilliseconds(); + if (msIntoSecond > 100) { + // If we're more than 100ms into the second, adjust for next time + updateTimer.interval = 1000 - msIntoSecond + 10; // +10ms buffer + updateTimer.restart(); + } else { + updateTimer.interval = 1000; + } + } + } + + Component.onCompleted: { + // Start by syncing to the next second boundary + var now = new Date(); + var msUntilNextSecond = 1000 - now.getMilliseconds(); + updateTimer.interval = msUntilNextSecond + 10; // +10ms buffer + updateTimer.restart(); + } + + // Formats a Date object into a YYYYMMDD-HHMMSS string. + function getFormattedTimestamp(date) { + if (!date) { + date = new Date(); + } + const year = date.getFullYear(); + + // getMonth() is zero-based, so we add 1 + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + + return `${year}${month}${day}-${hours}${minutes}${seconds}`; + } + + // Format an easy to read approximate duration ex: 4h 32m + // Used to display the time remaining on the Battery widget, computer uptime, etc.. + function formatVagueHumanReadableDuration(totalSeconds) { + if (typeof totalSeconds !== 'number' || totalSeconds < 0) { + return '0s'; + } + + // Floor the input to handle decimal seconds + totalSeconds = Math.floor(totalSeconds); + + const days = Math.floor(totalSeconds / 86400); + const hours = Math.floor((totalSeconds % 86400) / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + const parts = []; + if (days) + parts.push(`${days}d`); + if (hours) + parts.push(`${hours}h`); + if (minutes) + parts.push(`${minutes}m`); + + // Only show seconds if no hours and no minutes + if (!hours && !minutes) { + parts.push(`${seconds}s`); + } + + return parts.join(' '); + } + + // Format a date into + function formatRelativeTime(date) { + if (!date) + return ""; + const diff = Date.now() - date.getTime(); + if (diff < 60000) + return I18n.tr("notifications.time.now"); + if (diff < 120000) + return I18n.tr("notifications.time.diff-m"); + if (diff < 3600000) + return I18n.tr("notifications.time.diff-mm", { + "diff": Math.floor(diff / 60000) + }); + if (diff < 7200000) + return I18n.tr("notifications.time.diff-h"); + if (diff < 86400000) + return I18n.tr("notifications.time.diff-hh", { + "diff": Math.floor(diff / 3600000) + }); + if (diff < 172800000) + return I18n.tr("notifications.time.diff-d"); + return I18n.tr("notifications.time.diff-dd", { + "diff": Math.floor(diff / 86400000) + }); + } + + // Timer functions + function timerStart() { + if (root.timerStopwatchMode) { + root.timerRunning = true; + root.timerStartTimestamp = root.timestamp; + root.timerPausedAt = root.timerElapsedSeconds; + } else { + if (root.timerRemainingSeconds <= 0) { + return; + } + root.timerRunning = true; + root.timerTotalSeconds = root.timerRemainingSeconds; + root.timerStartTimestamp = root.timestamp; + root.timerPausedAt = 0; + } + } + + function timerPause() { + if (root.timerRunning) { + // Calculate and set remainingSeconds BEFORE changing timerRunning + // This ensures the UI sees the correct value when it reacts to timerRunning changing + if (root.timerStopwatchMode) { + root.timerPausedAt = root.timerElapsedSeconds; + } else { + // Calculate remaining seconds at the exact moment of pause + // Use current time directly to avoid stale timestamp issues + const currentTimestamp = Math.floor(Date.now() / 1000); + const elapsedSinceStart = currentTimestamp - root.timerStartTimestamp; + const currentRemaining = root.timerTotalSeconds - elapsedSinceStart; + root.timerPausedAt = Math.max(0, currentRemaining); + + // CRITICAL: Update timerRemainingSeconds to the paused value BEFORE changing timerRunning + // This ensures UI sees correct value when it reacts to timerRunning change + root.timerRemainingSeconds = root.timerPausedAt; + } + } + root.timerRunning = false; + root.timerStartTimestamp = 0; + // Stop any repeating notification sound when pausing + SoundService.stopSound("alarm-beep.wav"); + root.timerSoundPlaying = false; + } + + function timerReset() { + root.timerRunning = false; + root.timerStartTimestamp = 0; + if (root.timerStopwatchMode) { + root.timerElapsedSeconds = 0; + root.timerPausedAt = 0; + } else { + root.timerRemainingSeconds = 0; + root.timerTotalSeconds = 0; + root.timerPausedAt = 0; + } + // Stop any repeating notification sound + SoundService.stopSound("alarm-beep.wav"); + root.timerSoundPlaying = false; + } + + function timerOnFinished() { + root.timerRunning = false; + root.timerRemainingSeconds = 0; + // Play notification sound with repeat at lower volume + root.timerSoundPlaying = true; + SoundService.playSound("alarm-beep.wav", { + repeat: true, + volume: 0.3 + }); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Helpers/AdvancedMath.js b/fedora/.config/quickshell/noctalia-shell/Helpers/AdvancedMath.js new file mode 100644 index 0000000..d7bc367 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Helpers/AdvancedMath.js @@ -0,0 +1,161 @@ +// AdvancedMath.js - Lightweight math library for Noctalia Calculator +// Provides advanced mathematical functions beyond basic arithmetic + +// Helper function to convert degrees to radians +function toRadians(degrees) { + return degrees * (Math.PI / 180); +} + +// Helper function to convert radians to degrees +function toDegrees(radians) { + return radians * (180 / Math.PI); +} + +// Constants +var constants = { + PI: Math.PI, + E: Math.E, + LN2: Math.LN2, + LN10: Math.LN10, + LOG2E: Math.LOG2E, + LOG10E: Math.LOG10E, + SQRT1_2: Math.SQRT1_2, + SQRT2: Math.SQRT2 +}; + +// Safe evaluation function that handles advanced math +function evaluate(expression) { + try { + // Fixes decimal arithmetic + var cleanExpr = expression.replace(/\s+/g, '').toLowerCase(); + + // Allows numbers (including decimals), basic operators, and explicitly permitted math terms only + var safeRegex = /^(\d*\.?\d+|[+\-*/()^%,]|sin|cos|tan|asin|acos|atan|atan2|sinh|cosh|tanh|asinh|acosh|atanh|log|ln|exp|pow|sqrt|cbrt|abs|floor|ceil|round|trunc|min|max|random|pi|e|sind|cosd|tand)+$/; + + if (!safeRegex.test(cleanExpr)) { + throw new Error("Invalid characters or unauthorized functions in expression"); + } + + // Replace mathematical constants (Original Structure) + var processed = cleanExpr + .replace(/\bpi\b/gi, Math.PI) + .replace(/\be\b/gi, Math.E); + + // Replace function calls with Math object equivalents + processed = processed + // Trigonometric functions + .replace(/\bsin\s*\(/g, 'Math.sin(') + .replace(/\bcos\s*\(/g, 'Math.cos(') + .replace(/\btan\s*\(/g, 'Math.tan(') + .replace(/\basin\s*\(/g, 'Math.asin(') + .replace(/\bacos\s*\(/g, 'Math.acos(') + .replace(/\batan\s*\(/g, 'Math.atan(') + .replace(/\batan2\s*\(/g, 'Math.atan2(') + + // Hyperbolic functions + .replace(/\bsinh\s*\(/g, 'Math.sinh(') + .replace(/\bcosh\s*\(/g, 'Math.cosh(') + .replace(/\btanh\s*\(/g, 'Math.tanh(') + .replace(/\basinh\s*\(/g, 'Math.asinh(') + .replace(/\bacosh\s*\(/g, 'Math.acosh(') + .replace(/\batanh\s*\(/g, 'Math.atanh(') + + // Logarithmic and exponential functions + .replace(/\blog\s*\(/g, 'Math.log10(') + .replace(/\bln\s*\(/g, 'Math.log(') + .replace(/\bexp\s*\(/g, 'Math.exp(') + .replace(/\bpow\s*\(/g, 'Math.pow(') + + // Root functions + .replace(/\bsqrt\s*\(/g, 'Math.sqrt(') + .replace(/\bcbrt\s*\(/g, 'Math.cbrt(') + + // Rounding and absolute + .replace(/\babs\s*\(/g, 'Math.abs(') + .replace(/\bfloor\s*\(/g, 'Math.floor(') + .replace(/\bceil\s*\(/g, 'Math.ceil(') + .replace(/\bround\s*\(/g, 'Math.round(') + .replace(/\btrunc\s*\(/g, 'Math.trunc(') + + // Min/Max + .replace(/\bmin\s*\(/g, 'Math.min(') + .replace(/\bmax\s*\(/g, 'Math.max(') + + // Random + .replace(/\brandom\s*\(\s*\)/g, 'Math.random()'); + + // Handle degree versions of trig functions + processed = processed + .replace(/\bsind\s*\(/g, '(function(x) { return Math.sin(' + (Math.PI / 180) + ' * x); })(') + .replace(/\bcosd\s*\(/g, '(function(x) { return Math.cos(' + (Math.PI / 180) + ' * x); })(') + .replace(/\btand\s*\(/g, '(function(x) { return Math.tan(' + (Math.PI / 180) + ' * x); })('); + + // Handle ^ for exponentiation: convert 2^3 to Math.pow(2,3) + processed = processed.replace(/([\d.]+|\))\^([\d.]+|\([^)]*\))/g, 'Math.pow($1,$2)'); + + // Replacing eval() with a scoped function constructor + // This is safe because the strict whitelist guarantees only math reaches this point + var result = new Function('return ' + processed)(); + + if (!isFinite(result) || isNaN(result)) { + throw new Error("Invalid result"); + } + + return result; + } catch (error) { + throw new Error("Evaluation failed: " + error.message); + } +} + +// Format result for display +function formatResult(result) { + if (Number.isInteger(result)) { + return result.toString(); + } + + // Handle very large or very small numbers + if (Math.abs(result) >= 1e15 || (Math.abs(result) < 1e-6 && result !== 0)) { + return result.toExponential(6); + } + + // Normal decimal formatting + return parseFloat(result.toFixed(10)).toString(); +} + +// Get list of available functions for help +function getAvailableFunctions() { + return [ + // Basic arithmetic: +, -, *, /, %, ^, () + + // Trigonometric functions + "sin(x), cos(x), tan(x) - trigonometric functions (radians)", + "sind(x), cosd(x), tand(x) - trigonometric functions (degrees)", + "asin(x), acos(x), atan(x) - inverse trigonometric", + "atan2(y, x) - two-argument arctangent", + + // Hyperbolic functions + "sinh(x), cosh(x), tanh(x) - hyperbolic functions", + "asinh(x), acosh(x), atanh(x) - inverse hyperbolic", + + // Logarithmic and exponential + "log(x) - base 10 logarithm", + "ln(x) - natural logarithm", + "exp(x) - e^x", + "pow(x, y) - x^y", + + // Root functions + "sqrt(x) - square root", + "cbrt(x) - cube root", + + // Rounding and absolute + "abs(x) - absolute value", + "floor(x), ceil(x), round(x), trunc(x)", + + // Min/Max/Random + "min(a, b, ...), max(a, b, ...)", + "random() - random number 0-1", + + // Constants + "pi, e - mathematical constants" + ]; +} diff --git a/fedora/.config/quickshell/noctalia-shell/Helpers/BluetoothUtils.js b/fedora/.config/quickshell/noctalia-shell/Helpers/BluetoothUtils.js new file mode 100644 index 0000000..879b275 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Helpers/BluetoothUtils.js @@ -0,0 +1,127 @@ +.pragma library + +// Address helpers +var macFromDevice = (dev) => { + if (!dev) return ""; + if (dev.address && dev.address.length > 0) return dev.address; + if (dev.nativePath && dev.nativePath.indexOf("/dev_") !== -1) + return dev.nativePath.split("dev_")[1].split("_").join(":"); + return ""; +}; + +var deviceKey = (dev) => { + if (!dev) return ""; + if (dev.address && dev.address.length > 0) return dev.address.toUpperCase(); + if (dev.nativePath && dev.nativePath.length > 0) return dev.nativePath; + if (dev.devicePath && dev.devicePath.length > 0) return dev.devicePath; + return (dev.name || dev.deviceName || "") + "|" + (dev.icon || ""); +}; + +var dedupeDevices = (list) => { + if (!list || list.length === 0) return []; + var seen = ({}); + var out = []; + for (var i = 0; i < list.length; ++i) { + var d = list[i]; + if (!d) continue; + var k = deviceKey(d); + if (k && !seen[k]) { seen[k] = true; out.push(d); } + } + return out; +}; + +// RSSI parsing +var parseRssiOutput = (text) => { + try { + text = text || ""; + var mParen = text.match(/\(\s*(-?\d+)\s*(?:d?b?m?)?\s*\)/i); + if (mParen && mParen.length > 1) return Number(mParen[1]); + var mDec = text.match(/RSSI:\s*(-?\d+)/i); + if (mDec && mDec.length > 1) return Number(mDec[1]); + var mHex = text.match(/RSSI:\s*0x([0-9a-fA-F]+)/i); + if (mHex && mHex.length > 1) { + var v = parseInt(mHex[1], 16); + if (v >= 0x80000000) v = v - 0x100000000; // 32-bit two's complement + else if (v >= 0x8000) v = v - 0x10000; // 16-bit + else if (v >= 0x80) v = v - 0x100; // 8-bit + return v; + } + } catch (e) {} + return null; +}; + +var dbmToPercent = (dbm) => { + if (dbm === null || dbm === undefined || isNaN(dbm)) return null; + // Clamp simple linear map roughly from -100..0 dBm to 0..100% + var pct = Math.round((Number(dbm) + 100) * 2); + if (isNaN(pct)) return null; + return Math.max(0, Math.min(100, pct)); +}; + +// Signal helpers +var signalPercent = (device, cache, _version) => { + if (!device) return null; + try { + var addr = macFromDevice(device); + if (addr && cache && cache[addr] !== undefined) { + var cached = Number(cache[addr]) | 0; + return Math.max(0, Math.min(100, cached)); + } + } catch (e) {} + var s = device && device.signalStrength; + if (s === undefined || s <= 0) return null; + var p = Number(s) | 0; + return Math.max(0, Math.min(100, p)); +}; + +var signalIcon = (p) => { + if (p === null) return "antenna-bars-off"; + if (p >= 80) return "antenna-bars-5"; + if (p >= 60) return "antenna-bars-4"; + if (p >= 40) return "antenna-bars-3"; + if (p >= 20) return "antenna-bars-2"; + return "antenna-bars-1"; +}; + +// Icon mapping +var deviceIcon = (name, icon) => { + var s1 = String(name || "").toLowerCase(); + var s2 = String(icon || "").toLowerCase(); + + // Prefer icon-based hints for display devices first to avoid "audio" catching TVs + var displayHints = ["display", "tv", "monitor", "projector", "screen", "chromecast", "cast"]; + for (var dh = 0; dh < displayHints.length; dh++) { + if (s2.indexOf(displayHints[dh]) !== -1) return "bt-device-tv"; + } + + var tests = [ + [["controller", "gamepad"], "bt-device-gamepad"], + [["microphone"], "bt-device-microphone"], + [["pod", "bud", "minor"], "bt-device-earbuds"], + [["headset", "arctis", "major"], "bt-device-headset"], + [["headphone"], "bt-device-headphones"], + [["mouse"], "bt-device-mouse"], + [["keyboard"], "bt-device-keyboard"], + [["watch"], "bt-device-watch"], + [["display", "tv", "monitor", "projector", "screen", "chromecast", "cast"], "bt-device-tv"], + [["speaker", "audio", "sound"], "bt-device-speaker"], + [["phone", "iphone", "android", "samsung"], "bt-device-phone"] + ]; + for (var i = 0; i < tests.length; i++) { + var keys = tests[i][0]; + var out = tests[i][1]; + for (var j = 0; j < keys.length; j++) { + var k = keys[j]; + if (s1.indexOf(k) !== -1 || s2.indexOf(k) !== -1) return out; + } + } + return "bt-device-generic"; +}; + +// Battery percent helper +var batteryPercent = (device) => { + if (!device || !device.batteryAvailable || device.battery === undefined) return null; + var val = Math.round(Number(device.battery) * 100); + if (isNaN(val)) return null; + return Math.max(0, Math.min(100, val)); +}; diff --git a/fedora/.config/quickshell/noctalia-shell/Helpers/ColorList.js b/fedora/.config/quickshell/noctalia-shell/Helpers/ColorList.js new file mode 100644 index 0000000..3893214 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Helpers/ColorList.js @@ -0,0 +1,187 @@ +var colors = [ + // --- REDS --- + { name: "MistyRose", color: "mistyrose" }, + { name: "LightPink", color: "lightpink" }, + { name: "Pink", color: "pink" }, + { name: "PaleVioletRed", color: "palevioletred" }, + { name: "Pink 500", color: "#E91E63" }, // Material + { name: "HotPink", color: "hotpink" }, + { name: "DeepPink", color: "deeppink" }, + { name: "MediumVioletRed", color: "mediumvioletred" }, + { name: "LightSalmon", color: "lightsalmon" }, + { name: "Salmon", color: "salmon" }, + { name: "DarkSalmon", color: "darksalmon" }, + { name: "LightCoral", color: "lightcoral" }, + { name: "IndianRed", color: "indianred" }, + { name: "Alizarin", color: "#E74C3C" }, // Flat UI + { name: "Red 500", color: "#F44336" }, // Material + { name: "Crimson", color: "crimson" }, + { name: "Red", color: "red" }, + { name: "FireBrick", color: "firebrick" }, + { name: "DarkRed", color: "darkred" }, + { name: "Maroon", color: "maroon" }, + { name: "Brown", color: "brown" }, + + // --- ORANGES & BROWNS --- + { name: "Coral", color: "coral" }, + { name: "Tomato", color: "tomato" }, + { name: "OrangeRed", color: "orangered" }, + { name: "Deep Orange 500", color: "#FF5722" }, // Material + { name: "DarkOrange", color: "darkorange" }, + { name: "Carrot", color: "#E67E22" }, // Flat UI + { name: "Orange 500", color: "#FF9800" }, // Material + { name: "Orange", color: "orange" }, + { name: "SandyBrown", color: "sandybrown" }, + { name: "Peru", color: "peru" }, + { name: "Chocolate", color: "chocolate" }, + { name: "SaddleBrown", color: "saddlebrown" }, + { name: "Sienna", color: "sienna" }, + { name: "Brown 500", color: "#795548" }, // Material + + // --- YELLOWS, BEIGES & GOLDS --- + { name: "LightYellow", color: "lightyellow" }, + { name: "LemonChiffon", color: "lemonchiffon" }, + { name: "LightGoldenrodYellow", color: "lightgoldenrodyellow" }, + { name: "PapayaWhip", color: "papayawhip" }, + { name: "Moccasin", color: "moccasin" }, + { name: "PeachPuff", color: "peachpuff" }, + { name: "NavajoWhite", color: "navajowhite" }, + { name: "Wheat", color: "wheat" }, + { name: "BurlyWood", color: "burlywood" }, + { name: "Tan", color: "tan" }, + { name: "Bisque", color: "bisque" }, + { name: "BlanchedAlmond", color: "blanchedalmond" }, + { name: "Cornsilk", color: "cornsilk" }, + { name: "PaleGoldenrod", color: "palegoldenrod" }, + { name: "Khaki", color: "khaki" }, + { name: "DarkKhaki", color: "darkkhaki" }, + { name: "Goldenrod", color: "goldenrod" }, + { name: "DarkGoldenrod", color: "darkgoldenrod" }, + { name: "Sun Flower", color: "#F1C40F" }, // Flat UI + { name: "Yellow 500", color: "#FFEB3B" }, // Material + { name: "Yellow", color: "yellow" }, + { name: "Gold", color: "gold" }, + { name: "Amber 500", color: "#FFC107" }, // Material + + // --- GREENS --- + { name: "GreenYellow", color: "greenyellow" }, + { name: "Chartreuse", color: "chartreuse" }, + { name: "LawnGreen", color: "lawngreen" }, + { name: "Lime 500", color: "#CDDC39" }, // Material + { name: "Lime", color: "lime" }, + { name: "LimeGreen", color: "limegreen" }, + { name: "PaleGreen", color: "palegreen" }, + { name: "LightGreen", color: "lightgreen" }, + { name: "Light Green 500", color: "#8BC34A" }, // Material + { name: "MediumSpringGreen", color: "mediumspringgreen" }, + { name: "SpringGreen", color: "springgreen" }, + { name: "Emerald", color: "#2ECC71" }, // Flat UI + { name: "Green 500", color: "#4CAF50" }, // Material + { name: "MediumSeaGreen", color: "mediumseagreen" }, + { name: "SeaGreen", color: "seagreen" }, + { name: "ForestGreen", color: "forestgreen" }, + { name: "Green", color: "green" }, + { name: "DarkGreen", color: "darkgreen" }, + { name: "YellowGreen", color: "yellowgreen" }, + { name: "OliveDrab", color: "olivedrab" }, + { name: "Olive", color: "olive" }, + { name: "DarkOliveGreen", color: "darkolivegreen" }, + + // --- TEALS & CYANS --- + { name: "MediumAquamarine", color: "mediumaquamarine" }, + { name: "DarkSeaGreen", color: "darkseagreen" }, + { name: "LightSeaGreen", color: "lightseagreen" }, + { name: "DarkCyan", color: "darkcyan" }, + { name: "Teal", color: "teal" }, + { name: "Turquoise", color: "#1ABC9C" }, // Flat UI + { name: "LightCyan", color: "lightcyan" }, + { name: "PaleTurquoise", color: "paleturquoise" }, + { name: "Aquamarine", color: "aquamarine" }, + { name: "Turquoise", color: "turquoise" }, + { name: "MediumTurquoise", color: "mediumturquoise" }, + { name: "DarkTurquoise", color: "darkturquoise" }, + { name: "Aqua", color: "aqua" }, + { name: "Cyan", color: "cyan" }, + { name: "Cyan 500", color: "#00BCD4" }, // Material + { name: "CadetBlue", color: "cadetblue" }, + { name: "Teal 500", color: "#009688" }, // Material + { name: "DarkSlateGray", color: "darkslategray" }, + + // --- BLUES --- + { name: "PowderBlue", color: "powderblue" }, + { name: "LightBlue", color: "lightblue" }, + { name: "SkyBlue", color: "skyblue" }, + { name: "LightSkyBlue", color: "lightskyblue" }, + { name: "Light Blue 500", color: "#03A9F4" }, // Material + { name: "DeepSkyBlue", color: "deepskyblue" }, + { name: "DodgerBlue", color: "dodgerblue" }, + { name: "CornflowerBlue", color: "cornflowerblue" }, + { name: "Peter River", color: "#3498DB" }, // Flat UI + { name: "Blue 500", color: "#2196F3" }, // Material + { name: "SteelBlue", color: "steelblue" }, + { name: "LightSteelBlue", color: "lightsteelblue" }, + { name: "RoyalBlue", color: "royalblue" }, + { name: "Blue", color: "blue" }, + { name: "MediumBlue", color: "mediumblue" }, + { name: "Belize Hole", color: "#2980B9" }, // Flat UI + { name: "DarkBlue", color: "darkblue" }, + { name: "Navy", color: "navy" }, + { name: "MidnightBlue", color: "midnightblue" }, + { name: "Midnight Blue", color: "#2C3E50" }, // Flat UI (Same name, different color) + { name: "Indigo 500", color: "#3F51B5" }, // Material + { name: "DarkSlateBlue", color: "darkslateblue" }, + { name: "MediumSlateBlue", color: "mediumslateblue" }, + { name: "SlateBlue", color: "slateblue" }, + + // --- PURPLES & MAGENTAS --- + { name: "Lavender", color: "lavender" }, + { name: "Thistle", color: "thistle" }, + { name: "Plum", color: "plum" }, + { name: "Violet", color: "violet" }, + { name: "Orchid", color: "orchid" }, + { name: "Fuchsia", color: "fuchsia" }, + { name: "Magenta", color: "magenta" }, + { name: "MediumOrchid", color: "mediumorchid" }, + { name: "MediumPurple", color: "mediumpurple" }, + { name: "Amethyst", color: "#9B59B6" }, // Flat UI + { name: "Purple 500", color: "#9C27B0" }, // Material + { name: "BlueViolet", color: "blueviolet" }, + { name: "DarkViolet", color: "darkviolet" }, + { name: "DarkOrchid", color: "darkorchid" }, + { name: "DarkMagenta", color: "darkmagenta" }, + { name: "Purple", color: "purple" }, + { name: "Deep Purple 500", color: "#673AB7" }, // Material + { name: "Indigo", color: "indigo" }, + + // --- NEUTRALS --- + { name: "White", color: "white" }, + { name: "Snow", color: "snow" }, + { name: "HoneyDew", color: "honeydew" }, + { name: "MintCream", color: "mintcream" }, + { name: "Azure", color: "azure" }, + { name: "AliceBlue", color: "aliceblue" }, + { name: "GhostWhite", color: "ghostwhite" }, + { name: "WhiteSmoke", color: "whitesmoke" }, + { name: "Seashell", color: "seashell" }, + { name: "Beige", color: "beige" }, + { name: "OldLace", color: "oldlace" }, + { name: "FloralWhite", color: "floralwhite" }, + { name: "Ivory", color: "ivory" }, + { name: "AntiqueWhite", color: "antiquewhite" }, + { name: "Linen", color: "linen" }, + { name: "LavenderBlush", color: "lavenderblush" }, + { name: "Gainsboro", color: "gainsboro" }, + { name: "LightGray", color: "lightgray" }, + { name: "Silver", color: "silver" }, + { name: "DarkGray", color: "darkgray" }, + { name: "Gray", color: "gray" }, + { name: "Grey 500", color: "#9E9E9E" }, // Material + { name: "Concrete", color: "#95A5A6" }, // Flat UI + { name: "DimGray", color: "dimgray" }, + { name: "LightSlateGray", color: "lightslategray" }, + { name: "SlateGray", color: "slategray" }, + { name: "Asbestos", color: "#7F8C8D" }, // Flat UI + { name: "Blue Grey 500", color: "#607D8B" }, // Material + { name: "Wet Asphalt", color: "#34495E" }, // Flat UI + { name: "Black", color: "black" } + ] diff --git a/fedora/.config/quickshell/noctalia-shell/Helpers/ColorsConvert.js b/fedora/.config/quickshell/noctalia-shell/Helpers/ColorsConvert.js new file mode 100644 index 0000000..0e5eb7a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Helpers/ColorsConvert.js @@ -0,0 +1,270 @@ +// Convert hex color to HSL +function hexToHSL(hex) { + const rgb = hexToRgb(hex); + if (!rgb) return null; + return rgbToHsl(rgb.r, rgb.g, rgb.b); +} + +// Convert HSL to hex color +function hslToHex(h, s, l) { + const rgb = hslToRgb(h, s, l); + return rgbToHex(rgb.r, rgb.g, rgb.b); +} + +// Convert hex color to RGB +function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : { r: 0, g: 0, b: 0 }; +} + +// Convert RGB to hex color +function rgbToHex(r, g, b) { + return "#" + [r, g, b].map(x => { + const hex = Math.round(Math.max(0, Math.min(255, x))).toString(16); + return hex.length === 1 ? "0" + hex : hex; + }).join(""); +} + +// Convert RGB to HSL +function rgbToHsl(r, g, b) { + r /= 255; + g /= 255; + b /= 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h, s, l = (max + min) / 2; + + if (max === min) { + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + switch (max) { + case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break; + case g: h = ((b - r) / d + 2) / 6; break; + case b: h = ((r - g) / d + 4) / 6; break; + } + } + + return { h: h * 360, s: s * 100, l: l * 100 }; +} + +// Convert HSL to RGB +function hslToRgb(h, s, l) { + h /= 360; + s /= 100; + l /= 100; + + let r, g, b; + + if (s === 0) { + r = g = b = l; + } else { + const hue2rgb = (p, q, t) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1/6) return p + (q - p) * 6 * t; + if (t < 1/2) return q; + if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; + return p; + }; + + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + + r = hue2rgb(p, q, h + 1/3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1/3); + } + + return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }; +} + +// Convert RGB to HSV +function rgbToHsv(r, g, b) { + r /= 255; + g /= 255; + b /= 255; + var max = Math.max(r, g, b), min = Math.min(r, g, b); + var h, s, v = max; + var d = max - min; + s = max === 0 ? 0 : d / max; + if (max === min) { + h = 0; + } else { + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + return { h: h * 360, s: s * 100, v: v * 100 }; +} + +// Convert HSV to RGB +function hsvToRgb(h, s, v) { + h /= 360; + s /= 100; + v /= 100; + + var r, g, b; + var i = Math.floor(h * 6); + var f = h * 6 - i; + var p = v * (1 - s); + var q = v * (1 - f * s); + var t = v * (1 - (1 - f) * s); + + switch (i % 6) { + case 0: + r = v; + g = t; + b = p; + break; + case 1: + r = q; + g = v; + b = p; + break; + case 2: + r = p; + g = v; + b = t; + break; + case 3: + r = p; + g = q; + b = v; + break; + case 4: + r = t; + g = p; + b = v; + break; + case 5: + r = v; + g = p; + b = q; + break; + } + + return { r: Math.round(r * 255), g: Math.round(g * 255), b: Math.round(b * 255) }; +} + +// Calculate relative luminance (WCAG standard) +function getLuminance(hex) { + const rgb = hexToRgb(hex); + const [r, g, b] = [rgb.r, rgb.g, rgb.b].map(val => { + val /= 255; + return val <= 0.03928 ? val / 12.92 : Math.pow((val + 0.055) / 1.055, 2.4); + }); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; +} + +// Calculate contrast ratio between two colors +function getContrastRatio(hex1, hex2) { + const lum1 = getLuminance(hex1); + const lum2 = getLuminance(hex2); + const brightest = Math.max(lum1, lum2); + const darkest = Math.min(lum1, lum2); + return (brightest + 0.05) / (darkest + 0.05); +} + +// Check if a color is considered "light" +function isLightColor(hex) { + return getLuminance(hex) > 0.5; +} + +// Adjust color lightness +function adjustLightness(hex, amount) { + const hsl = hexToHSL(hex); + hsl.l = Math.max(0, Math.min(100, hsl.l + amount)); + return hslToHex(hsl.h, hsl.s, hsl.l); +} + +// Adjust color saturation +function adjustSaturation(hex, amount) { + const hsl = hexToHSL(hex); + hsl.s = Math.max(0, Math.min(100, hsl.s + amount)); + return hslToHex(hsl.h, hsl.s, hsl.l); +} + +// Adjust both lightness and saturation +function adjustLightnessAndSaturation(hex, lightnessAmount, saturationAmount) { + const hsl = hexToHSL(hex); + hsl.l = Math.max(0, Math.min(100, hsl.l + lightnessAmount)); + hsl.s = Math.max(0, Math.min(100, hsl.s + saturationAmount)); + return hslToHex(hsl.h, hsl.s, hsl.l); +} + +// Generate "on" color with proper contrast (for text/icons) +function generateOnColor(baseColor, isDarkMode) { + const isBaseLight = isLightColor(baseColor); + + // If base is light, we need dark text; if base is dark, we need light text + if (isBaseLight) { + // Try darker variants + let testColor = "#000000"; + if (getContrastRatio(baseColor, testColor) >= 4.5) { + return testColor; + } + // Fallback to dark gray + return "#1c1b1f"; + } else { + // Try lighter variants + let testColor = "#ffffff"; + if (getContrastRatio(baseColor, testColor) >= 4.5) { + return testColor; + } + // Fallback to light gray + return "#e6e1e5"; + } +} + +// Generate container color (lighter in light mode, darker in dark mode) +function generateContainerColor(baseColor, isDarkMode) { + const rgb = hexToRgb(baseColor); + const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b); + + if (isDarkMode) { + // In dark mode, containers are darker and more saturated + hsl.l = Math.max(10, Math.min(30, hsl.l - 20)); + hsl.s = Math.min(100, hsl.s + 10); + } else { + // In light mode, containers are lighter and less saturated + hsl.l = Math.min(90, Math.max(75, hsl.l + 30)); + hsl.s = Math.max(0, hsl.s - 10); + } + + const newRgb = hslToRgb(hsl.h, hsl.s, hsl.l); + return rgbToHex(newRgb.r, newRgb.g, newRgb.b); +} + +// Generate surface variant colors +function generateSurfaceVariant(backgroundColor, step, isDarkMode) { + const rgb = hexToRgb(backgroundColor); + const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b); + + if (isDarkMode) { + // In dark mode, variants get progressively lighter + hsl.l = Math.min(100, hsl.l + (step * 3)); + } else { + // In light mode, variants get progressively darker + hsl.l = Math.max(0, hsl.l - (step * 2)); + } + + const newRgb = hslToRgb(hsl.h, hsl.s, hsl.l); + return rgbToHex(newRgb.r, newRgb.g, newRgb.b); +} diff --git a/fedora/.config/quickshell/noctalia-shell/Helpers/Debug.js b/fedora/.config/quickshell/noctalia-shell/Helpers/Debug.js new file mode 100644 index 0000000..90dc27e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Helpers/Debug.js @@ -0,0 +1,25 @@ +// Helper function to stringify even if there are circular refs. +function stringify(obj, replacer, spaces, cycleReplacer) { + return JSON.stringify(obj, serializer(replacer, cycleReplacer), spaces) +} + +function serializer(replacer, cycleReplacer) { + var stack = [], keys = [] + + if (cycleReplacer == null) cycleReplacer = function(key, value) { + if (stack[0] === value) return "[Circular ~]" + return "[Circular ~." + keys.slice(0, stack.indexOf(value)).join(".") + "]" + } + + return function(key, value) { + if (stack.length > 0) { + var thisPos = stack.indexOf(this) + ~thisPos ? stack.splice(thisPos + 1) : stack.push(this) + ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key) + if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value) + } + else stack.push(value) + + return replacer == null ? value : replacer.call(this, key, value) + } +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Helpers/QtObj2JS.js b/fedora/.config/quickshell/noctalia-shell/Helpers/QtObj2JS.js new file mode 100644 index 0000000..75a248f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Helpers/QtObj2JS.js @@ -0,0 +1,114 @@ +// ----------------------------------------------------- +// Helper function to convert Qt objects to plain JavaScript objects +// Only used when generating settings-default.json +function qtObjectToPlainObject(obj) { + if (obj === null || obj === undefined) { + return obj; + } + + // Handle primitive types + if (typeof obj !== "object") { + return obj; + } + + // Handle native JavaScript arrays + if (Array.isArray(obj)) { + return obj.map((item) => qtObjectToPlainObject(item)); + } + + // Detect QML arrays FIRST (before color detection) + // QML arrays have a numeric length property and indexed properties + if (typeof obj.length === "number" && obj.length >= 0) { + // Check if it has indexed properties - be more flexible about detection + var hasIndexedProps = true; + var hasNumericKeys = false; + + // Check if we have at least some numeric properties + for (var i = 0; i < obj.length; i++) { + if (obj.hasOwnProperty(i) || obj[i] !== undefined) { + hasNumericKeys = true; + break; + } + } + + // If we have length > 0 and some numeric keys, treat as array + if (obj.length > 0 && hasNumericKeys) { + var arr = []; + for (var i = 0; i < obj.length; i++) { + // Use direct property access, handle undefined gracefully + var item = obj[i]; + if (item !== undefined) { + arr.push(qtObjectToPlainObject(item)); + } + } + return arr; // Return here to avoid processing as object + } + + // Handle empty arrays (length = 0) + if (obj.length === 0) { + return []; + } + } + + // Detect and convert QML color objects to hex strings + if ( + typeof obj.r === "number" && + typeof obj.g === "number" && + typeof obj.b === "number" && + typeof obj.a === "number" && + typeof obj.valid === "boolean" + ) { + // This looks like a QML color object + try { + // Try to get the string representation (should be hex like "#000000") + if (typeof obj.toString === "function") { + return obj.toString(); + } else { + // Fallback: convert RGBA to hex manually + var r = Math.round(obj.r * 255); + var g = Math.round(obj.g * 255); + var b = Math.round(obj.b * 255); + var hex = + "#" + + r.toString(16).padStart(2, "0") + + g.toString(16).padStart(2, "0") + + b.toString(16).padStart(2, "0"); + return hex; + } + } catch (e) { + // If conversion fails, fall through to regular object handling + } + } + + // Handle regular objects + var plainObj = {}; + + // Get all property names, but filter out Qt-specific ones + var propertyNames = Object.getOwnPropertyNames(obj); + + for (var i = 0; i < propertyNames.length; i++) { + var propName = propertyNames[i]; + + // Skip Qt-specific properties, functions, and array-like properties + if ( + propName === "objectName" || + propName === "objectNameChanged" || + propName === "length" || // Skip length property + /^\d+$/.test(propName) || // Skip numeric keys (0, 1, 2, etc.) + propName.endsWith("Changed") || + typeof obj[propName] === "function" + ) { + continue; + } + + try { + var value = obj[propName]; + plainObj[propName] = qtObjectToPlainObject(value); + } catch (e) { + // Skip properties that can't be accessed + continue; + } + } + + return plainObj; +} diff --git a/fedora/.config/quickshell/noctalia-shell/Helpers/sha256.js b/fedora/.config/quickshell/noctalia-shell/Helpers/sha256.js new file mode 100644 index 0000000..39f255d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Helpers/sha256.js @@ -0,0 +1,192 @@ +function sha256(message) { + // SHA-256 constants + const K = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + ]; + + // Initial hash values + let h0 = 0x6a09e667; + let h1 = 0xbb67ae85; + let h2 = 0x3c6ef372; + let h3 = 0xa54ff53a; + let h4 = 0x510e527f; + let h5 = 0x9b05688c; + let h6 = 0x1f83d9ab; + let h7 = 0x5be0cd19; + + // Convert string to UTF-8 bytes manually + const msgBytes = stringToUtf8Bytes(message); + const msgLength = msgBytes.length; + const bitLength = msgLength * 8; + + // Calculate padding + // Message + 1 bit (0x80) + padding zeros + 8 bytes for length = multiple of 64 bytes + const totalBitsNeeded = bitLength + 1 + 64; // message bits + padding bit + 64-bit length + const totalBytesNeeded = Math.ceil(totalBitsNeeded / 8); + const paddedLength = Math.ceil(totalBytesNeeded / 64) * 64; // Round up to multiple of 64 + + const paddedMsg = new Array(paddedLength).fill(0); + + // Copy original message + for (let i = 0; i < msgLength; i++) { + paddedMsg[i] = msgBytes[i]; + } + + // Add padding bit (0x80 = 10000000 in binary) + paddedMsg[msgLength] = 0x80; + + // Add length as 64-bit big-endian integer at the end + // JavaScript numbers are not precise enough for 64-bit integers, so we handle high/low separately + const highBits = Math.floor(bitLength / 0x100000000); + const lowBits = bitLength % 0x100000000; + + // Write 64-bit length in big-endian format + paddedMsg[paddedLength - 8] = (highBits >>> 24) & 0xFF; + paddedMsg[paddedLength - 7] = (highBits >>> 16) & 0xFF; + paddedMsg[paddedLength - 6] = (highBits >>> 8) & 0xFF; + paddedMsg[paddedLength - 5] = highBits & 0xFF; + paddedMsg[paddedLength - 4] = (lowBits >>> 24) & 0xFF; + paddedMsg[paddedLength - 3] = (lowBits >>> 16) & 0xFF; + paddedMsg[paddedLength - 2] = (lowBits >>> 8) & 0xFF; + paddedMsg[paddedLength - 1] = lowBits & 0xFF; + + // Process message in 512-bit (64-byte) chunks + for (let chunk = 0; chunk < paddedLength; chunk += 64) { + const w = new Array(64); + + // Break chunk into sixteen 32-bit big-endian words + for (let i = 0; i < 16; i++) { + const offset = chunk + i * 4; + w[i] = (paddedMsg[offset] << 24) | + (paddedMsg[offset + 1] << 16) | + (paddedMsg[offset + 2] << 8) | + paddedMsg[offset + 3]; + // Ensure unsigned 32-bit + w[i] = w[i] >>> 0; + } + + // Extend the sixteen 32-bit words into sixty-four 32-bit words + for (let i = 16; i < 64; i++) { + const s0 = rightRotate(w[i - 15], 7) ^ rightRotate(w[i - 15], 18) ^ (w[i - 15] >>> 3); + const s1 = rightRotate(w[i - 2], 17) ^ rightRotate(w[i - 2], 19) ^ (w[i - 2] >>> 10); + w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0; + } + + // Initialize working variables for this chunk + let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7; + + // Main loop + for (let i = 0; i < 64; i++) { + const S1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25); + const ch = (e & f) ^ (~e & g); + const temp1 = (h + S1 + ch + K[i] + w[i]) >>> 0; + const S0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22); + const maj = (a & b) ^ (a & c) ^ (b & c); + const temp2 = (S0 + maj) >>> 0; + + h = g; + g = f; + f = e; + e = (d + temp1) >>> 0; + d = c; + c = b; + b = a; + a = (temp1 + temp2) >>> 0; + } + + // Add this chunk's hash to result so far + h0 = (h0 + a) >>> 0; + h1 = (h1 + b) >>> 0; + h2 = (h2 + c) >>> 0; + h3 = (h3 + d) >>> 0; + h4 = (h4 + e) >>> 0; + h5 = (h5 + f) >>> 0; + h6 = (h6 + g) >>> 0; + h7 = (h7 + h) >>> 0; + } + + // Produce the final hash value as a hex string + return [h0, h1, h2, h3, h4, h5, h6, h7] + .map(h => h.toString(16).padStart(8, '0')) + .join(''); +} + +function stringToUtf8Bytes(str) { + const bytes = []; + for (let i = 0; i < str.length; i++) { + let code = str.charCodeAt(i); + + if (code < 0x80) { + // 1-byte character (ASCII) + bytes.push(code); + } else if (code < 0x800) { + // 2-byte character + bytes.push(0xC0 | (code >> 6)); + bytes.push(0x80 | (code & 0x3F)); + } else if (code < 0xD800 || code > 0xDFFF) { + // 3-byte character (not surrogate) + bytes.push(0xE0 | (code >> 12)); + bytes.push(0x80 | ((code >> 6) & 0x3F)); + bytes.push(0x80 | (code & 0x3F)); + } else { + // 4-byte character (surrogate pair) + i++; // Move to next character + const code2 = str.charCodeAt(i); + const codePoint = 0x10000 + (((code & 0x3FF) << 10) | (code2 & 0x3FF)); + bytes.push(0xF0 | (codePoint >> 18)); + bytes.push(0x80 | ((codePoint >> 12) & 0x3F)); + bytes.push(0x80 | ((codePoint >> 6) & 0x3F)); + bytes.push(0x80 | (codePoint & 0x3F)); + } + } + return bytes; +} + +function rightRotate(value, amount) { + return ((value >>> amount) | (value << (32 - amount))) >>> 0; +} + +// Test function to verify implementation +// function testSHA256() { +// const tests = [ +// { +// input: "", +// expected: +// "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", +// }, +// { +// input: "Hello World", +// expected: +// "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e", +// }, +// { +// input: "abc", +// expected: +// "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", +// }, +// { +// input: "The quick brown fox jumps over the lazy dog", +// expected: +// "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", +// }, +// ]; + +// console.log("Running SHA-256 tests:"); +// tests.forEach((test, i) => { +// const result = Crypto.sha256(test.input); +// const passed = result === test.expected; +// console.log(`Test ${i + 1}: ${passed ? "PASS" : "FAIL"}`); +// if (!passed) { +// console.log(` Input: "${test.input}"`); +// console.log(` Expected: ${test.expected}`); +// console.log(` Got: ${result}`); +// } +// }); +// } diff --git a/fedora/.config/quickshell/noctalia-shell/LICENSE b/fedora/.config/quickshell/noctalia-shell/LICENSE new file mode 100644 index 0000000..2bdaed9 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 noctalia-dev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Background/Background.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Background/Background.qml new file mode 100644 index 0000000..517de91 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Background/Background.qml @@ -0,0 +1,801 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services.Compositor +import qs.Services.Power +import qs.Services.UI + +Variants { + id: backgroundVariants + model: Quickshell.screens + + delegate: Loader { + + required property ShellScreen modelData + + active: modelData && Settings.data.wallpaper.enabled && (!PowerProfileService.noctaliaPerformanceMode || !Settings.data.noctaliaPerformance.disableWallpaper) + + sourceComponent: PanelWindow { + id: root + + // Internal state management + property string transitionType: "fade" + property real transitionProgress: 0 + property bool isStartupTransition: true + property bool wallpaperReady: false + + visible: wallpaperReady + + readonly property real edgeSmoothness: Settings.data.wallpaper.transitionEdgeSmoothness + readonly property var allTransitions: WallpaperService.allTransitions + readonly property bool transitioning: transitionAnimation.running + + // Wipe direction: 0=left, 1=right, 2=up, 3=down + property real wipeDirection: 0 + + // Disc + property real discCenterX: 0.5 + property real discCenterY: 0.5 + + // Stripe + property real stripesCount: 16 + property real stripesAngle: 0 + + // Pixelate + property real pixelateMaxBlockSize: 64.0 + + // Honeycomb + property real honeycombCellSize: 0.04 + property real honeycombCenterX: 0.5 + property real honeycombCenterY: 0.5 + + // Used to debounce wallpaper changes + property string futureWallpaper: "" + // Track the original wallpaper path being transitioned to (before caching) + property string transitioningToOriginalPath: "" + + // Fillmode default is "crop" + property real fillMode: WallpaperService.getFillModeUniform() + property vector4d fillColor: Qt.vector4d(Settings.data.wallpaper.fillColor.r, Settings.data.wallpaper.fillColor.g, Settings.data.wallpaper.fillColor.b, 1.0) + + // Solid color mode - track whether current/next are solid colors + property bool isSolid1: false + property bool isSolid2: false + property color _solidColor1: Settings.data.wallpaper.solidColor + property color _solidColor2: Settings.data.wallpaper.solidColor + property vector4d solidColor1: Qt.vector4d(_solidColor1.r, _solidColor1.g, _solidColor1.b, 1.0) + property vector4d solidColor2: Qt.vector4d(_solidColor2.r, _solidColor2.g, _solidColor2.b, 1.0) + + Component.onCompleted: setWallpaperInitial() + + Component.onDestruction: { + transitionAnimation.stop(); + startupTransitionTimer.stop(); + debounceTimer.stop(); + shaderLoader.active = false; + currentWallpaper.source = ""; + nextWallpaper.source = ""; + } + + Connections { + target: Settings.data.wallpaper + function onFillModeChanged() { + fillMode = WallpaperService.getFillModeUniform(); + } + } + + // External state management + Connections { + target: WallpaperService + function onWallpaperChanged(screenName, path) { + if (screenName === modelData.name) { + requestPreprocessedWallpaper(path); + } + } + } + + Connections { + target: CompositorService + function onDisplayScalesChanged() { + if (!WallpaperService.isInitialized) { + return; + } + + const currentPath = WallpaperService.getWallpaper(modelData.name); + if (!currentPath || WallpaperService.isSolidColorPath(currentPath)) { + return; + } + + if (isStartupTransition) { + // During startup, just ensure the correct cache exists without visual changes + const compositorScale = CompositorService.getDisplayScale(modelData.name); + const targetWidth = Math.round(modelData.width * compositorScale); + const targetHeight = Math.round(modelData.height * compositorScale); + ImageCacheService.getLarge(currentPath, targetWidth, targetHeight, function (cachedPath, success) { + WallpaperService.wallpaperProcessingComplete(modelData.name, currentPath, success ? cachedPath : ""); + }); + return; + } + + requestPreprocessedWallpaper(currentPath); + } + } + + color: "transparent" + screen: modelData + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "noctalia-wallpaper-" + (screen?.name || "unknown") + + anchors { + bottom: true + top: true + right: true + left: true + } + + Timer { + id: debounceTimer + interval: 333 + running: false + repeat: false + onTriggered: changeWallpaper() + } + + // Delay startup transition to ensure the compositor has mapped the window + Timer { + id: startupTransitionTimer + interval: 100 + running: false + repeat: false + onTriggered: _executeStartupTransition() + } + + Image { + id: currentWallpaper + + source: "" + smooth: true + mipmap: false + visible: false + cache: true // Cached so Overview can share the same texture + asynchronous: true + onStatusChanged: { + if (status === Image.Error) { + Logger.w("Current wallpaper failed to load:", source); + } else if (status === Image.Ready && !wallpaperReady) { + wallpaperReady = true; + } + } + } + + Image { + id: nextWallpaper + + property bool pendingTransition: false + + source: "" + smooth: true + mipmap: false + visible: false + cache: false // Not cached - temporary during transitions + asynchronous: true + onStatusChanged: { + if (status === Image.Error) { + Logger.w("Next wallpaper failed to load:", source); + pendingTransition = false; + } else if (status === Image.Ready) { + if (!wallpaperReady) { + wallpaperReady = true; + } + if (pendingTransition) { + pendingTransition = false; + currentWallpaper.asynchronous = false; + transitionAnimation.start(); + } + } + } + } + + // Dynamic shader loader - only loads the active transition shader + Loader { + id: shaderLoader + anchors.fill: parent + active: true + + sourceComponent: { + switch (transitionType) { + case "wipe": + return wipeShaderComponent; + case "disc": + return discShaderComponent; + case "stripes": + return stripesShaderComponent; + case "pixelate": + return pixelateShaderComponent; + case "honeycomb": + return honeycombShaderComponent; + case "fade": + case "none": + default: + return fadeShaderComponent; + } + } + } + + // Fade or None transition shader component + Component { + id: fadeShaderComponent + ShaderEffect { + anchors.fill: parent + + property variant source1: currentWallpaper + property variant source2: nextWallpaper.status === Image.Ready ? nextWallpaper : currentWallpaper.status === Image.Ready ? nextWallpaper : currentWallpaper + property real progress: root.transitionProgress + + // Fill mode properties + property real fillMode: root.fillMode + property vector4d fillColor: root.fillColor + property real imageWidth1: source1.sourceSize.width + property real imageHeight1: source1.sourceSize.height + property real imageWidth2: source2.sourceSize.width + property real imageHeight2: source2.sourceSize.height + property real screenWidth: width + property real screenHeight: height + + // Solid color mode + property real isSolid1: root.isSolid1 ? 1.0 : 0.0 + property real isSolid2: root.isSolid2 ? 1.0 : 0.0 + property vector4d solidColor1: root.solidColor1 + property vector4d solidColor2: root.solidColor2 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_fade.frag.qsb") + } + } + + // Wipe transition shader component + Component { + id: wipeShaderComponent + ShaderEffect { + anchors.fill: parent + + property variant source1: currentWallpaper + property variant source2: nextWallpaper.status === Image.Ready ? nextWallpaper : currentWallpaper + property real progress: root.transitionProgress + property real smoothness: root.edgeSmoothness + property real direction: root.wipeDirection + + // Fill mode properties + property real fillMode: root.fillMode + property vector4d fillColor: root.fillColor + property real imageWidth1: source1.sourceSize.width + property real imageHeight1: source1.sourceSize.height + property real imageWidth2: source2.sourceSize.width + property real imageHeight2: source2.sourceSize.height + property real screenWidth: width + property real screenHeight: height + + // Solid color mode + property real isSolid1: root.isSolid1 ? 1.0 : 0.0 + property real isSolid2: root.isSolid2 ? 1.0 : 0.0 + property vector4d solidColor1: root.solidColor1 + property vector4d solidColor2: root.solidColor2 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_wipe.frag.qsb") + } + } + + // Disc reveal transition shader component + Component { + id: discShaderComponent + ShaderEffect { + anchors.fill: parent + + property variant source1: currentWallpaper + property variant source2: nextWallpaper.status === Image.Ready ? nextWallpaper : currentWallpaper + property real progress: root.transitionProgress + property real smoothness: root.edgeSmoothness + property real aspectRatio: root.width / root.height + property real centerX: root.discCenterX + property real centerY: root.discCenterY + + // Fill mode properties + property real fillMode: root.fillMode + property vector4d fillColor: root.fillColor + property real imageWidth1: source1.sourceSize.width + property real imageHeight1: source1.sourceSize.height + property real imageWidth2: source2.sourceSize.width + property real imageHeight2: source2.sourceSize.height + property real screenWidth: width + property real screenHeight: height + + // Solid color mode + property real isSolid1: root.isSolid1 ? 1.0 : 0.0 + property real isSolid2: root.isSolid2 ? 1.0 : 0.0 + property vector4d solidColor1: root.solidColor1 + property vector4d solidColor2: root.solidColor2 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_disc.frag.qsb") + } + } + + // Diagonal stripes transition shader component + Component { + id: stripesShaderComponent + ShaderEffect { + anchors.fill: parent + + property variant source1: currentWallpaper + property variant source2: nextWallpaper.status === Image.Ready ? nextWallpaper : currentWallpaper + property real progress: root.transitionProgress + property real smoothness: root.edgeSmoothness + property real aspectRatio: root.width / root.height + property real stripeCount: root.stripesCount + property real angle: root.stripesAngle + + // Fill mode properties + property real fillMode: root.fillMode + property vector4d fillColor: root.fillColor + property real imageWidth1: source1.sourceSize.width + property real imageHeight1: source1.sourceSize.height + property real imageWidth2: source2.sourceSize.width + property real imageHeight2: source2.sourceSize.height + property real screenWidth: width + property real screenHeight: height + + // Solid color mode + property real isSolid1: root.isSolid1 ? 1.0 : 0.0 + property real isSolid2: root.isSolid2 ? 1.0 : 0.0 + property vector4d solidColor1: root.solidColor1 + property vector4d solidColor2: root.solidColor2 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_stripes.frag.qsb") + } + } + + // Pixelate transition shader component + Component { + id: pixelateShaderComponent + ShaderEffect { + anchors.fill: parent + + property variant source1: currentWallpaper + property variant source2: nextWallpaper.status === Image.Ready ? nextWallpaper : currentWallpaper + property real progress: root.transitionProgress + property real maxBlockSize: root.pixelateMaxBlockSize + + // Fill mode properties + property real fillMode: root.fillMode + property vector4d fillColor: root.fillColor + property real imageWidth1: source1.sourceSize.width + property real imageHeight1: source1.sourceSize.height + property real imageWidth2: source2.sourceSize.width + property real imageHeight2: source2.sourceSize.height + property real screenWidth: width + property real screenHeight: height + + // Solid color mode + property real isSolid1: root.isSolid1 ? 1.0 : 0.0 + property real isSolid2: root.isSolid2 ? 1.0 : 0.0 + property vector4d solidColor1: root.solidColor1 + property vector4d solidColor2: root.solidColor2 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_pixelate.frag.qsb") + } + } + + // Honeycomb transition shader component + Component { + id: honeycombShaderComponent + ShaderEffect { + anchors.fill: parent + + property variant source1: currentWallpaper + property variant source2: nextWallpaper.status === Image.Ready ? nextWallpaper : currentWallpaper + property real progress: root.transitionProgress + property real cellSize: root.honeycombCellSize + property real centerX: root.honeycombCenterX + property real centerY: root.honeycombCenterY + property real aspectRatio: root.width / root.height + + // Fill mode properties + property real fillMode: root.fillMode + property vector4d fillColor: root.fillColor + property real imageWidth1: source1.sourceSize.width + property real imageHeight1: source1.sourceSize.height + property real imageWidth2: source2.sourceSize.width + property real imageHeight2: source2.sourceSize.height + property real screenWidth: width + property real screenHeight: height + + // Solid color mode + property real isSolid1: root.isSolid1 ? 1.0 : 0.0 + property real isSolid2: root.isSolid2 ? 1.0 : 0.0 + property vector4d solidColor1: root.solidColor1 + property vector4d solidColor2: root.solidColor2 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_honeycomb.frag.qsb") + } + } + + // Animation for the transition progress + NumberAnimation { + id: transitionAnimation + target: root + property: "transitionProgress" + from: 0.0 + to: 1.0 + duration: Settings.data.wallpaper.transitionDuration + easing.type: Easing.InOutCubic + onFinished: { + // Mark startup complete now that the animation has finished, + // so displayScalesChanged doesn't trigger a duplicate transition. + if (isStartupTransition) { + isStartupTransition = false; + } + + // Clear the tracking of what we're transitioning to + transitioningToOriginalPath = ""; + + // Transfer solid color state from next to current + isSolid1 = isSolid2; + _solidColor1 = _solidColor2; + + // Assign new image to current BEFORE clearing to prevent flicker + const tempSource = nextWallpaper.source; + currentWallpaper.source = tempSource; + transitionProgress = 0.0; + + // Now clear nextWallpaper after currentWallpaper has the new source + // Force complete cleanup to free texture memory + Qt.callLater(() => { + nextWallpaper.source = ""; + isSolid2 = false; + Qt.callLater(() => { + currentWallpaper.asynchronous = true; + }); + }); + } + } + + // Normalize a path (string or QUrl) to a plain string for comparison. + // QML Image.source is a url type; comparing url === string can return + // false even for identical paths. This converts both sides to strings. + function _pathStr(p) { + var s = p.toString(); + // QUrl.toString() may add a file:// prefix for local paths + if (s.startsWith("file://")) { + return s.substring(7); + } + return s; + } + + // ------------------------------------------------------ + function setWallpaperInitial() { + // On startup, defer assigning wallpaper until the services are ready + if (!WallpaperService || !WallpaperService.isInitialized) { + Qt.callLater(setWallpaperInitial); + return; + } + if (!ImageCacheService || !ImageCacheService.initialized) { + Qt.callLater(setWallpaperInitial); + return; + } + + // Check if we're in solid color mode + if (Settings.data.wallpaper.useSolidColor) { + var solidPath = WallpaperService.createSolidColorPath(Settings.data.wallpaper.solidColor.toString()); + futureWallpaper = solidPath; + performStartupTransition(); + WallpaperService.wallpaperProcessingComplete(modelData.name, solidPath, ""); + return; + } + + const wallpaperPath = WallpaperService.getWallpaper(modelData.name); + + // Check if the path is a solid color + if (WallpaperService.isSolidColorPath(wallpaperPath)) { + futureWallpaper = wallpaperPath; + performStartupTransition(); + WallpaperService.wallpaperProcessingComplete(modelData.name, wallpaperPath, ""); + return; + } + + const compositorScale = CompositorService.getDisplayScale(modelData.name); + const targetWidth = Math.round(modelData.width * compositorScale); + const targetHeight = Math.round(modelData.height * compositorScale); + + ImageCacheService.getLarge(wallpaperPath, targetWidth, targetHeight, function (cachedPath, success) { + if (success) { + futureWallpaper = cachedPath; + } else { + // Fallback to original + futureWallpaper = wallpaperPath; + } + performStartupTransition(); + // Pass cached path for blur optimization (already resized) + WallpaperService.wallpaperProcessingComplete(modelData.name, wallpaperPath, success ? cachedPath : ""); + }); + } + + // ------------------------------------------------------ + function requestPreprocessedWallpaper(originalPath) { + // If we're already transitioning to this exact wallpaper, skip the request + if (transitioning && originalPath === transitioningToOriginalPath) { + return; + } + + // Store the original path we're working towards + transitioningToOriginalPath = originalPath; + + // Handle solid color paths - no preprocessing needed + if (WallpaperService.isSolidColorPath(originalPath)) { + futureWallpaper = originalPath; + debounceTimer.restart(); + WallpaperService.wallpaperProcessingComplete(modelData.name, originalPath, ""); + return; + } + + const compositorScale = CompositorService.getDisplayScale(modelData.name); + const targetWidth = Math.round(modelData.width * compositorScale); + const targetHeight = Math.round(modelData.height * compositorScale); + + ImageCacheService.getLarge(originalPath, targetWidth, targetHeight, function (cachedPath, success) { + // Ignore stale callback if we've moved on to a different wallpaper + if (originalPath !== transitioningToOriginalPath) { + return; + } + if (success) { + futureWallpaper = cachedPath; + } else { + futureWallpaper = originalPath; + } + + // Skip transition if the resolved path matches what's already displayed + if (_pathStr(futureWallpaper) === _pathStr(currentWallpaper.source)) { + transitioningToOriginalPath = ""; + WallpaperService.wallpaperProcessingComplete(modelData.name, originalPath, success ? cachedPath : ""); + return; + } + + debounceTimer.restart(); + // Pass cached path for blur optimization (already resized) + WallpaperService.wallpaperProcessingComplete(modelData.name, originalPath, success ? cachedPath : ""); + }); + } + + // ------------------------------------------------------ + function setWallpaperImmediate(source) { + transitionAnimation.stop(); + transitionProgress = 0.0; + + // Check if this is a solid color + var isSolidSource = WallpaperService.isSolidColorPath(source); + isSolid1 = isSolidSource; + isSolid2 = false; + + if (isSolidSource) { + var colorStr = WallpaperService.getSolidColor(source); + _solidColor1 = colorStr; + // Clear image sources for memory efficiency + currentWallpaper.source = ""; + nextWallpaper.source = ""; + if (!wallpaperReady) { + wallpaperReady = true; + } + return; + } + + // Clear nextWallpaper completely to free texture memory + nextWallpaper.source = ""; + nextWallpaper.sourceSize = undefined; + + currentWallpaper.source = ""; + + Qt.callLater(() => { + currentWallpaper.source = source; + }); + } + + // ------------------------------------------------------ + function setWallpaperWithTransition(source) { + // Check if this is a solid color transition + var isSolidSource = WallpaperService.isSolidColorPath(source); + + // For solid colors, check if we're already showing the same color + if (isSolidSource && isSolid1) { + var newColor = WallpaperService.getSolidColor(source); + if (newColor === _solidColor1.toString()) { + return; + } + } + + // For images, check if source matches (use _pathStr to normalize url vs string) + if (!isSolidSource && _pathStr(source) === _pathStr(currentWallpaper.source)) { + return; + } + + // If we're already transitioning to this same wallpaper, skip + if (transitioning && source === nextWallpaper.source) { + return; + } + + if (transitioning) { + // We are interrupting a transition - handle cleanup properly + transitionAnimation.stop(); + transitionProgress = 0; + + // Transfer next state to current + isSolid1 = isSolid2; + _solidColor1 = _solidColor2; + const newCurrentSource = nextWallpaper.source; + currentWallpaper.source = newCurrentSource; + + // Now clear nextWallpaper after current has the new source + Qt.callLater(() => { + nextWallpaper.source = ""; + isSolid2 = false; + + // Now set the next wallpaper after a brief delay + Qt.callLater(() => { + _startTransitionTo(source, isSolidSource); + }); + }); + return; + } + + _startTransitionTo(source, isSolidSource); + } + + // Helper to start transition to a new source + function _startTransitionTo(source, isSolidSource) { + isSolid2 = isSolidSource; + + if (isSolidSource) { + var colorStr = WallpaperService.getSolidColor(source); + _solidColor2 = colorStr; + // No image to load, start transition immediately + nextWallpaper.source = ""; + if (!wallpaperReady) { + wallpaperReady = true; + } + currentWallpaper.asynchronous = false; + transitionAnimation.start(); + } else { + nextWallpaper.source = source; + if (nextWallpaper.status === Image.Ready) { + if (!wallpaperReady) { + wallpaperReady = true; + } + currentWallpaper.asynchronous = false; + transitionAnimation.start(); + } else { + nextWallpaper.pendingTransition = true; + } + } + } + + // ------------------------------------------------------ + // Main method that actually trigger the wallpaper change + function changeWallpaper() { + // Pick a transition from the user's selected list + var selected = Settings.data.wallpaper.transitionType; + if (!selected || selected.length === 0) { + transitionType = "none"; + } else if (selected.length === 1) { + transitionType = selected[0]; + } else { + var index = Math.floor(Math.random() * selected.length); + transitionType = selected[index]; + } + + // Ensure the transition type really exists + if (transitionType !== "none" && !allTransitions.includes(transitionType)) { + transitionType = "fade"; + } + + //Logger.i("Background", "New wallpaper: ", futureWallpaper, "On:", modelData.name, "Transition:", transitionType) + switch (transitionType) { + case "none": + setWallpaperImmediate(futureWallpaper); + break; + case "wipe": + wipeDirection = Math.random() * 4; + setWallpaperWithTransition(futureWallpaper); + break; + case "disc": + discCenterX = Math.random(); + discCenterY = Math.random(); + setWallpaperWithTransition(futureWallpaper); + break; + case "stripes": + stripesCount = Math.round(Math.random() * 20 + 4); + stripesAngle = Math.random() * 360; + setWallpaperWithTransition(futureWallpaper); + break; + case "pixelate": + pixelateMaxBlockSize = Math.round(Math.random() * 80 + 32); + setWallpaperWithTransition(futureWallpaper); + break; + case "honeycomb": + honeycombCellSize = Math.random() * 0.04 + 0.02; + honeycombCenterX = Math.random(); + honeycombCenterY = Math.random(); + setWallpaperWithTransition(futureWallpaper); + break; + default: + setWallpaperWithTransition(futureWallpaper); + break; + } + } + + // ------------------------------------------------------ + // Dedicated function for startup animation + // Sets up transition params, then defers the actual animation + // to allow the compositor time to map the window. + function performStartupTransition() { + if (Settings.data.wallpaper.skipStartupTransition) { + setWallpaperImmediate(futureWallpaper); + isStartupTransition = false; + return; + } + + // Pick a transition from the user's selected list + var selected = Settings.data.wallpaper.transitionType; + if (!selected || selected.length === 0) { + transitionType = "none"; + } else if (selected.length === 1) { + transitionType = selected[0]; + } else { + var index = Math.floor(Math.random() * selected.length); + transitionType = selected[index]; + } + + // Ensure the transition type really exists + if (transitionType !== "none" && !allTransitions.includes(transitionType)) { + transitionType = "fade"; + } + + // Pre-compute per-type params so the shader is ready + switch (transitionType) { + case "wipe": + wipeDirection = Math.random() * 4; + break; + case "disc": + // Force center origin for elegant startup animation + discCenterX = 0.5; + discCenterY = 0.5; + break; + case "stripes": + stripesCount = Math.round(Math.random() * 20 + 4); + stripesAngle = Math.random() * 360; + break; + case "pixelate": + pixelateMaxBlockSize = 64.0; + break; + case "honeycomb": + honeycombCellSize = 0.04; + honeycombCenterX = 0.5; + honeycombCenterY = 0.5; + break; + } + + // Defer the actual transition start so the compositor can map the window + startupTransitionTimer.start(); + } + + // Actually kick off the startup transition after the delay + function _executeStartupTransition() { + if (transitionType === "none") { + setWallpaperImmediate(futureWallpaper); + isStartupTransition = false; + } else { + // isStartupTransition stays true until transitionAnimation.onFinished + // to prevent displayScalesChanged from triggering a duplicate transition. + setWallpaperWithTransition(futureWallpaper); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Background/FadeOverlay.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Background/FadeOverlay.qml new file mode 100644 index 0000000..2274716 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Background/FadeOverlay.qml @@ -0,0 +1,52 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services.Power + +/** +* IdleFadeOverlay — full-screen fade-to-black shown before each idle action. +* +* A single Loader wraps a Variants so per-screen windows only exist while +* a fade is in progress, keeping VRAM usage at zero at rest. +* +* Any mouse movement cancels the fade and unloads the windows immediately. +*/ +Item { + id: root + + Loader { + active: IdleService.fadePending !== "" + asynchronous: false + + sourceComponent: Variants { + model: Quickshell.screens + delegate: PanelWindow { + id: overlay + required property ShellScreen modelData + screen: modelData + + color: Qt.rgba(0, 0, 0, 0) + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.namespace: "noctalia-fade-overlay" + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.anchors { + top: true + bottom: true + left: true + right: true + } + + ColorAnimation on color { + running: true + from: Qt.rgba(0, 0, 0, 0) + to: Qt.rgba(0, 0, 0, 1) + duration: IdleService.fadeDuration * 1000 + easing.type: Easing.InQuad + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Background/Overview.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Background/Overview.qml new file mode 100644 index 0000000..579fe0d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Background/Overview.qml @@ -0,0 +1,121 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services.Compositor +import qs.Services.Power +import qs.Services.UI + +Loader { + active: CompositorService.isNiri && Settings.data.wallpaper.enabled && Settings.data.wallpaper.overviewEnabled && (!PowerProfileService.noctaliaPerformanceMode || !Settings.data.noctaliaPerformance.disableWallpaper) + + sourceComponent: Variants { + model: Quickshell.screens + + delegate: PanelWindow { + id: panelWindow + + required property ShellScreen modelData + property string wallpaper: "" + property string preprocessedWallpaper: "" // Pre-resized wallpaper from Background.qml + property bool isSolidColor: Settings.data.wallpaper.useSolidColor + property color solidColor: Settings.data.wallpaper.solidColor + property color tintColor: Settings.data.colorSchemes.darkMode ? Color.mSurface : Color.mOnSurface + + visible: wallpaper !== "" || isSolidColor + + Component.onCompleted: { + if (modelData) { + Logger.d("Overview", "Loading overview for Niri on", modelData.name); + } + } + + Component.onDestruction: { + bgImage.source = ""; + } + + // External state management - wait for wallpaper processing to complete + // Reuses the same cached image as Background.qml for memory efficiency + GPU blur + Connections { + target: WallpaperService + function onWallpaperProcessingComplete(screenName, path, cachedPath) { + if (screenName === modelData.name) { + preprocessedWallpaper = cachedPath || ""; + wallpaper = path; + } + } + } + + // Handle wallpaper changes (solid color detection) + onWallpaperChanged: { + if (!wallpaper) + return; + + // Check if this is a solid color path + if (WallpaperService.isSolidColorPath(wallpaper)) { + isSolidColor = true; + var colorStr = WallpaperService.getSolidColor(wallpaper); + solidColor = colorStr; + return; + } + + isSolidColor = false; + } + + color: "transparent" + screen: modelData + WlrLayershell.layer: WlrLayer.Background + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "noctalia-overview-" + (screen?.name || "unknown") + + anchors { + top: true + bottom: true + right: true + left: true + } + + // Solid color background + Rectangle { + anchors.fill: parent + visible: isSolidColor + color: solidColor + + Rectangle { + anchors.fill: parent + color: tintColor + opacity: Settings.data.wallpaper.overviewTint + } + } + + // Image background with GPU-based blur + Image { + id: bgImage + anchors.fill: parent + visible: !isSolidColor + fillMode: Image.PreserveAspectCrop + source: preprocessedWallpaper || wallpaper + smooth: true + mipmap: false + cache: true // Shares texture with Background's currentWallpaper + asynchronous: true + + layer.enabled: Settings.data.wallpaper.overviewBlur > 0 && !PowerProfileService.noctaliaPerformanceMode + layer.smooth: false + layer.effect: MultiEffect { + blurEnabled: true + blur: Settings.data.wallpaper.overviewBlur + blurMax: 48 + } + + // Tint overlay + Rectangle { + anchors.fill: parent + color: tintColor + opacity: Settings.data.wallpaper.overviewTint + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Bar.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Bar.qml new file mode 100644 index 0000000..51eac97 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Bar.qml @@ -0,0 +1,727 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.UPower +import Quickshell.Wayland +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Modules.Notification +import qs.Modules.Panels.Settings +import qs.Services.Compositor +import qs.Services.Media +import qs.Services.UI +import qs.Widgets + +// Bar Component +Item { + id: root + + // This property will be set by MainScreen + property ShellScreen screen: null + + // Filter widgets to only include those that exist in the registry + // This prevents errors when plugins are missing or widgets are being cleaned up + function filterValidWidgets(widgets: list): list { + if (!widgets) + return []; + return widgets.filter(function (w) { + return w && w.id && BarWidgetRegistry.hasWidget(w.id); + }); + } + + // Hot corner: trigger click on first widget in a section + function triggerFirstWidgetInSection(sectionName: string) { + var widgets = BarService.getWidgetsBySection(sectionName, screen?.name); + for (var i = 0; i < widgets.length; i++) { + var widget = widgets[i]; + if (widget && widget.visible && widget.widgetId !== "Spacer") { + if (typeof widget.clicked === "function") { + widget.clicked(); + } + return; + } + } + } + + // Hot corner: trigger click on last widget in a section + function triggerLastWidgetInSection(sectionName: string) { + var widgets = BarService.getWidgetsBySection(sectionName, screen?.name); + for (var i = widgets.length - 1; i >= 0; i--) { + var widget = widgets[i]; + if (widget && widget.visible && widget.widgetId !== "Spacer") { + if (typeof widget.clicked === "function") { + widget.clicked(); + } + return; + } + } + } + + // Expose bar region for click-through mask + readonly property var barRegion: barContentLoader.item?.children[0] || null + + // Expose the actual bar Item for unified background system + readonly property var barItem: barRegion + + // Bar positioning properties (per-screen) + readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name) + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property bool barFloating: Settings.data.bar.barType === "floating" + + // Bar density (per-screen) + readonly property string barDensity: Settings.getBarDensityForScreen(screen?.name) + + // Bar sizing based on per-screen density + readonly property real barHeight: Style.getBarHeightForDensity(barDensity, barIsVertical) + readonly property real capsuleHeight: Style.getCapsuleHeightForDensity(barDensity, barHeight) + readonly property real barFontSize: Style.getBarFontSizeForDensity(barHeight, capsuleHeight, barIsVertical) + + // Bar widgets (per-screen) - initial configuration + // Note: Updates are handled via Connections to BarService.widgetsRevisionChanged + readonly property var barWidgets: Settings.getBarWidgetsForScreen(screen?.name) + + // Stable ListModels for each section - prevents Repeater recreation on settings changes + property ListModel leftWidgetsModel: ListModel {} + property ListModel centerWidgetsModel: ListModel {} + property ListModel rightWidgetsModel: ListModel {} + + // Guard: set when Bar is destroyed; prevents Qt.callLater callbacks from running + // during/after teardown (avoids SIGSEGV in QV4::Object::insertMember when rapid + // workspace switch causes load/unload overlap with async widget incubation) + property bool _destroyed: false + Component.onDestruction: root._destroyed = true + + // Sync a ListModel with widget data, preserving delegates when only settings change + function syncWidgetModel(model, newWidgets) { + var validWidgets = filterValidWidgets(newWidgets); + + // Build list of current IDs in model + var currentIds = []; + for (var i = 0; i < model.count; i++) { + currentIds.push(model.get(i).id); + } + + // Build list of new IDs + var newIds = validWidgets.map(w => w.id); + + // Check if structure changed (different IDs or order) + var structureChanged = currentIds.length !== newIds.length; + if (!structureChanged) { + for (var i = 0; i < currentIds.length; i++) { + if (currentIds[i] !== newIds[i]) { + structureChanged = true; + break; + } + } + } + + Logger.d("Bar", "syncWidgetModel:", currentIds.join("|"), "→", newIds.join("|"), "changed:", structureChanged); + + if (structureChanged) { + // Rebuild model - IDs changed + model.clear(); + for (var i = 0; i < validWidgets.length; i++) { + model.append(validWidgets[i]); + } + } + // If structure didn't change, delegates are preserved and will read fresh settings + } + + // Sync models when widget revision changes + // Note: We use Connections instead of onBarWidgetsChanged because getBarWidgetsForScreen + // returns the same object reference (Settings.data.bar.widgets) even when content changes, + // so QML won't detect the change via property binding. + Connections { + target: BarService + function onWidgetsRevisionChanged() { + Logger.d("Bar", "onWidgetsRevisionChanged, revision:", BarService.widgetsRevision, "screen:", root.screen?.name); + Qt.callLater(root._syncFromRevision); + } + } + + function _syncFromRevision() { + if (root._destroyed) + return; + var widgets = Settings.getBarWidgetsForScreen(screen?.name); + if (widgets) { + syncWidgetModel(leftWidgetsModel, widgets.left); + syncWidgetModel(centerWidgetsModel, widgets.center); + syncWidgetModel(rightWidgetsModel, widgets.right); + } + } + + // Initialize models — deferred to next event-loop tick via Qt.callLater to avoid + // re-entrant incubation: Component.onCompleted fires during QQmlObjectCreator::finalize, + // and ListModel.append synchronously creates Repeater delegates whose own finalization + // can corrupt the V4 heap (SIGSEGV in QV4::Object::insertMember). + Component.onCompleted: { + Logger.d("Bar", "Bar Component.onCompleted for screen:", screen?.name); + Qt.callLater(root._initModels); + } + + function _initModels() { + if (root._destroyed) + return; + var widgets = Settings.getBarWidgetsForScreen(screen?.name); + if (widgets) { + syncWidgetModel(leftWidgetsModel, widgets.left); + syncWidgetModel(centerWidgetsModel, widgets.center); + syncWidgetModel(rightWidgetsModel, widgets.right); + } + } + + // Fill the parent (the Loader) + anchors.fill: parent + + // Register bar when screen becomes available + onScreenChanged: { + if (screen && screen.name) { + Logger.d("Bar", "Bar screen set to:", screen.name); + Logger.d("Bar", " Position:", barPosition, "Floating:", barFloating); + BarService.registerBar(screen.name); + } + } + + // Wait for screen to be set before loading bar content + Loader { + id: barContentLoader + anchors.fill: parent + active: { + if (root.screen === null || root.screen === undefined) { + return false; + } + + var monitors = Settings.data.bar.monitors || []; + var result = monitors.length === 0 || monitors.includes(root.screen.name); + return result; + } + + sourceComponent: Item { + anchors.fill: parent + + // Bar container - Content + Item { + id: bar + + // Wheel scroll handling (empty bar area) + property int barWheelAccumulatedDelta: 0 + property bool barWheelCooldown: false + readonly property string barWheelAction: { + return Settings.data.bar.mouseWheelAction || "none"; + } + readonly property string barRightClickAction: Settings.data.bar.rightClickAction || "controlCenter" + + // Position and size the bar content based on orientation + x: (root.barPosition === "right") ? (parent.width - root.barHeight) : 0 + y: (root.barPosition === "bottom") ? (parent.height - root.barHeight) : 0 + width: root.barIsVertical ? root.barHeight : parent.width + height: root.barIsVertical ? parent.height : root.barHeight + + // Corner states for new unified background system + // State -1: No radius (flat/square corner) + // State 0: Normal (inner curve) + // State 1: Horizontal inversion (outer curve on X-axis) + // State 2: Vertical inversion (outer curve on Y-axis) + readonly property int topLeftCornerState: { + // Floating bar: always simple rounded corners + if (barFloating) + return 0; + // Top bar: top corners against screen edge = no radius + if (barPosition === "top") + return -1; + // Left bar: top-left against screen edge = no radius + if (barPosition === "left") + return -1; + // Bottom/Right bar with outerCorners: inverted corner + if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right")) { + return barIsVertical ? 1 : 2; // horizontal invert for vertical bars, vertical invert for horizontal + } + // No outerCorners = square + return -1; + } + + readonly property int topRightCornerState: { + // Floating bar: always simple rounded corners + if (barFloating) + return 0; + // Top bar: top corners against screen edge = no radius + if (barPosition === "top") + return -1; + // Right bar: top-right against screen edge = no radius + if (barPosition === "right") + return -1; + // Bottom/Left bar with outerCorners: inverted corner + if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left")) { + return barIsVertical ? 1 : 2; + } + // No outerCorners = square + return -1; + } + + readonly property int bottomLeftCornerState: { + // Floating bar: always simple rounded corners + if (barFloating) + return 0; + // Bottom bar: bottom corners against screen edge = no radius + if (barPosition === "bottom") + return -1; + // Left bar: bottom-left against screen edge = no radius + if (barPosition === "left") + return -1; + // Top/Right bar with outerCorners: inverted corner + if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right")) { + return barIsVertical ? 1 : 2; + } + // No outerCorners = square + return -1; + } + + readonly property int bottomRightCornerState: { + // Floating bar: always simple rounded corners + if (barFloating) + return 0; + // Bottom bar: bottom corners against screen edge = no radius + if (barPosition === "bottom") + return -1; + // Right bar: bottom-right against screen edge = no radius + if (barPosition === "right") + return -1; + // Top/Left bar with outerCorners: inverted corner + if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left")) { + return barIsVertical ? 1 : 2; + } + // No outerCorners = square + return -1; + } + + function isPointOverWidget(xPos, yPos) { + var widgets = BarService.getAllWidgetInstances(null, screen.name); + for (var i = 0; i < widgets.length; i++) { + var widget = widgets[i]; + if (!widget || !widget.visible || widget.widgetId === "Spacer") { + continue; + } + var localPos = mapToItem(widget, xPos, yPos); + + if (root.barIsVertical) { + if (localPos.y >= -Style.marginS && localPos.y <= widget.height + Style.marginS) { + return true; + } + } else { + if (localPos.x >= -Style.marginS && localPos.x <= widget.width + Style.marginS) { + return true; + } + } + } + return false; + } + + function switchWorkspaceByOffset(offset) { + if (!root.screen || CompositorService.workspaces.count === 0) + return; + + var screenName = root.screen.name.toLowerCase(); + var candidates = []; + for (var i = 0; i < CompositorService.workspaces.count; i++) { + var ws = CompositorService.workspaces.get(i); + var matchesScreen = CompositorService.globalWorkspaces || (ws.output && ws.output.toLowerCase() === screenName); + if (matchesScreen) + candidates.push(ws); + } + + if (candidates.length <= 1) + return; + + var current = -1; + for (var j = 0; j < candidates.length; j++) { + if (candidates[j].isFocused) { + current = j; + break; + } + } + if (current < 0) + current = 0; + + var next = current + offset; + if (Settings.data.bar.mouseWheelWrap) { + next = next % candidates.length; + if (next < 0) + next = candidates.length - 1; + } else { + if (next < 0 || next >= candidates.length) + return; + } + + if (next === current) + return; + CompositorService.switchToWorkspace(candidates[next]); + } + + function handleEmptyBarClick(action, followMouse, command, mouse) { + if (action === "none") + return; + if (action === "controlCenter") { + var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen); + controlCenterPanel?.toggle(null, followMouse ? mapToItem(null, mouse.x, mouse.y) : "ControlCenter"); + mouse.accepted = true; + } else if (action === "settings") { + var settingsPanel = PanelService.getPanel("settingsPanel", screen); + settingsPanel?.toggle(null, followMouse ? mapToItem(null, mouse.x, mouse.y) : null); + mouse.accepted = true; + } else if (action === "launcherPanel") { + var launcherPanel = PanelService.getPanel("launcherPanel", screen); + launcherPanel?.toggle(null, followMouse ? mapToItem(null, mouse.x, mouse.y) : null); + mouse.accepted = true; + } else if (action === "command") { + runCustomCommand(command); + mouse.accepted = true; + } + } + + function runCustomCommand(command) { + if (!command || command.trim() === "") + return; + + const processString = "import QtQuick; import Quickshell.Io; Process { command: [\"sh\", \"-lc\", \"\"] }"; + + try { + const processObj = Qt.createQmlObject(processString, root, "BarCommandProcess_" + Date.now()); + processObj.command = ["sh", "-lc", command]; + + processObj.exited.connect(function (exitCode) { + if (exitCode !== 0) { + ToastService.showError(I18n.tr("toast.custom-command-failed.title"), I18n.tr("toast.custom-command-failed.description", { + command: command, + code: exitCode + })); + } + processObj.destroy(); + }); + + processObj.running = true; + } catch (e) { + Logger.e("Bar", "Failed to start custom command:", e); + ToastService.showError(I18n.tr("toast.custom-command-failed.title"), I18n.tr("toast.custom-command-failed.description", { + command: command, + code: "start_error" + })); + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton | Qt.MiddleButton + enabled: bar.barRightClickAction !== "none" || Settings.data.bar.middleClickAction !== "none" + hoverEnabled: false + preventStealing: true + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + if (bar.isPointOverWidget(mouse.x, mouse.y)) + return; + bar.handleEmptyBarClick(bar.barRightClickAction, Settings.data.bar.rightClickFollowMouse, Settings.data.bar.rightClickCommand, mouse); + return; + } + if (mouse.button === Qt.MiddleButton) { + if (bar.isPointOverWidget(mouse.x, mouse.y)) + return; + bar.handleEmptyBarClick(Settings.data.bar.middleClickAction || "none", Settings.data.bar.middleClickFollowMouse, Settings.data.bar.middleClickCommand, mouse); + return; + } + } + } + + // Debounce timer for wheel interactions + Timer { + id: barWheelDebounce + interval: 150 + repeat: false + onTriggered: { + bar.barWheelCooldown = false; + bar.barWheelAccumulatedDelta = 0; + } + } + + // Scroll on empty bar area action + WheelHandler { + id: barWheelHandler + target: bar + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + enabled: bar.barWheelAction !== "none" + + onWheel: function (event) { + if (bar.isPointOverWidget(event.x, event.y)) + return; + + var dy = event.angleDelta.y; + var dx = event.angleDelta.x; + var useDy = Math.abs(dy) >= Math.abs(dx); + var delta = useDy ? dy : dx; + var step = 120; + + if (bar.barWheelAction === "volume") { + if (Settings.data.bar.reverseScroll) + delta *= -1; + + bar.barWheelAccumulatedDelta += delta; + if (bar.barWheelAccumulatedDelta >= step) { + AudioService.increaseVolume(); + bar.barWheelAccumulatedDelta = 0; + event.accepted = true; + } else if (bar.barWheelAccumulatedDelta <= -step) { + AudioService.decreaseVolume(); + bar.barWheelAccumulatedDelta = 0; + event.accepted = true; + } + return; + } + + if (bar.barWheelCooldown) + return; + + bar.barWheelAccumulatedDelta += delta; + if (Math.abs(bar.barWheelAccumulatedDelta) >= step) { + var direction = bar.barWheelAccumulatedDelta > 0 ? -1 : 1; + if (Settings.data.bar.reverseScroll) + direction *= -1; + if (bar.barWheelAction === "workspace") { + bar.switchWorkspaceByOffset(direction); + } else if (bar.barWheelAction === "content") { + CompositorService.scrollWorkspaceContent(direction); + } + bar.barWheelCooldown = true; + barWheelDebounce.restart(); + bar.barWheelAccumulatedDelta = 0; + event.accepted = true; + } + } + } + + Loader { + anchors.fill: parent + sourceComponent: root.barIsVertical ? verticalBarComponent : horizontalBarComponent + } + } + } + } + + // For vertical bars + Component { + id: verticalBarComponent + Item { + anchors.fill: parent + clip: true + + // Top edge hot corner - triggers first widget in left (top) section + MouseArea { + width: parent.width + height: Style.marginS + x: 0 + y: 0 + onClicked: root.triggerFirstWidgetInSection("left") + } + + // Bottom edge hot corner - triggers last widget in right (bottom) section + MouseArea { + width: parent.width + height: Style.marginS + x: 0 + anchors.bottom: parent.bottom + onClicked: root.triggerLastWidgetInSection("right") + } + + // Calculate margin to center widgets vertically within the bar height + readonly property real verticalBarMargin: Math.round((root.barHeight - root.capsuleHeight) / 2) + + // Top section (left widgets) + ColumnLayout { + x: Style.pixelAlignCenter(parent.width, width) + anchors.top: parent.top + anchors.topMargin: verticalBarMargin + Settings.data.bar.contentPadding + spacing: Settings.data.bar.widgetSpacing + + Repeater { + model: root.leftWidgetsModel + delegate: BarWidgetLoader { + required property var model + required property int index + + widgetId: model.id || "" + widgetScreen: root.screen + widgetProps: ({ + "widgetId": model.id, + "section": "left", + "sectionWidgetIndex": index, + "sectionWidgetsCount": root.leftWidgetsModel.count + }) + Layout.alignment: Qt.AlignHCenter + } + } + } + + // Center section (center widgets) + ColumnLayout { + x: Style.pixelAlignCenter(parent.width, width) + anchors.verticalCenter: parent.verticalCenter + spacing: Settings.data.bar.widgetSpacing + + Repeater { + model: root.centerWidgetsModel + delegate: BarWidgetLoader { + required property var model + required property int index + + widgetId: model.id || "" + widgetScreen: root.screen + widgetProps: ({ + "widgetId": model.id, + "section": "center", + "sectionWidgetIndex": index, + "sectionWidgetsCount": root.centerWidgetsModel.count + }) + Layout.alignment: Qt.AlignHCenter + } + } + } + + // Bottom section (right widgets) + ColumnLayout { + x: Style.pixelAlignCenter(parent.width, width) + anchors.bottom: parent.bottom + anchors.bottomMargin: verticalBarMargin + Settings.data.bar.contentPadding + spacing: Settings.data.bar.widgetSpacing + + Repeater { + model: root.rightWidgetsModel + delegate: BarWidgetLoader { + required property var model + required property int index + + widgetId: model.id || "" + widgetScreen: root.screen + widgetProps: ({ + "widgetId": model.id, + "section": "right", + "sectionWidgetIndex": index, + "sectionWidgetsCount": root.rightWidgetsModel.count + }) + Layout.alignment: Qt.AlignHCenter + } + } + } + } + } + + // For horizontal bars + Component { + id: horizontalBarComponent + Item { + anchors.fill: parent + clip: true + + // Left edge hot corner - triggers first widget in left section + MouseArea { + width: Style.marginS + height: parent.height + x: 0 + y: 0 + onClicked: root.triggerFirstWidgetInSection("left") + } + + // Right edge hot corner - triggers last widget in right section + MouseArea { + width: Style.marginS + height: parent.height + anchors.right: parent.right + y: 0 + onClicked: root.triggerLastWidgetInSection("right") + } + + // Calculate margin to center widgets horizontally within the bar height + readonly property real horizontalBarMargin: Math.round((root.barHeight - root.capsuleHeight) / 2) + + // Left Section + RowLayout { + id: leftSection + objectName: "leftSection" + anchors.left: parent.left + anchors.leftMargin: horizontalBarMargin + Settings.data.bar.contentPadding + y: Style.pixelAlignCenter(parent.height, height) + spacing: Settings.data.bar.widgetSpacing + + Repeater { + model: root.leftWidgetsModel + delegate: BarWidgetLoader { + required property var model + required property int index + + widgetId: model.id || "" + widgetScreen: root.screen + widgetProps: ({ + "widgetId": model.id, + "section": "left", + "sectionWidgetIndex": index, + "sectionWidgetsCount": root.leftWidgetsModel.count + }) + Layout.alignment: Qt.AlignVCenter + } + } + } + + // Center Section + RowLayout { + id: centerSection + objectName: "centerSection" + anchors.horizontalCenter: parent.horizontalCenter + y: Style.pixelAlignCenter(parent.height, height) + spacing: Settings.data.bar.widgetSpacing + + Repeater { + model: root.centerWidgetsModel + delegate: BarWidgetLoader { + required property var model + required property int index + + widgetId: model.id || "" + widgetScreen: root.screen + widgetProps: ({ + "widgetId": model.id, + "section": "center", + "sectionWidgetIndex": index, + "sectionWidgetsCount": root.centerWidgetsModel.count + }) + Layout.alignment: Qt.AlignVCenter + } + } + } + + // Right Section + RowLayout { + id: rightSection + objectName: "rightSection" + anchors.right: parent.right + anchors.rightMargin: horizontalBarMargin + Settings.data.bar.contentPadding + y: Style.pixelAlignCenter(parent.height, height) + spacing: Settings.data.bar.widgetSpacing + + Repeater { + model: root.rightWidgetsModel + delegate: BarWidgetLoader { + required property var model + required property int index + + widgetId: model.id || "" + widgetScreen: root.screen + widgetProps: ({ + "widgetId": model.id, + "section": "right", + "sectionWidgetIndex": index, + "sectionWidgetsCount": root.rightWidgetsModel.count + }) + Layout.alignment: Qt.AlignVCenter + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/BarExclusionZone.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/BarExclusionZone.qml new file mode 100644 index 0000000..c2e7b18 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/BarExclusionZone.qml @@ -0,0 +1,72 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons + +/** +* BarExclusionZone - Invisible PanelWindow that reserves exclusive space for the bar +* +* This is a minimal window that works with the compositor to reserve space, +* while the actual bar UI is rendered in NFullScreenWindow. +*/ +PanelWindow { + id: root + + readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name) + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property bool barFloating: Settings.data.bar.barType === "floating" + readonly property real barMarginH: barFloating ? Settings.data.bar.marginHorizontal : 0 + readonly property real barMarginV: barFloating ? Settings.data.bar.marginVertical : 0 + readonly property real barHeight: Style.getBarHeightForScreen(screen?.name) + + // Invisible - just reserves space + color: "transparent" + + mask: Region {} + + // Wayland layer shell configuration + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.namespace: "noctalia-bar-exclusion-" + (screen?.name || "unknown") + WlrLayershell.exclusionMode: ExclusionMode.Auto + + // Anchor based on bar position + anchors { + top: barPosition === "top" + bottom: barPosition === "bottom" + left: barPosition === "left" || barPosition === "top" || barPosition === "bottom" + right: barPosition === "right" || barPosition === "top" || barPosition === "bottom" + } + + // Size based on bar orientation + // When floating, only reserve space for the bar + margin on the anchored edge + implicitWidth: { + if (barIsVertical) { + // Vertical bar: reserve bar height + margin on the anchored edge only + if (barFloating) { + // For left bar, reserve left margin; for right bar, reserve right margin + return barHeight + barMarginH; + } + return barHeight; + } + return 0; // Auto-width when left/right anchors are true + } + + implicitHeight: { + if (!barIsVertical) { + // Horizontal bar: reserve bar height + margin on the anchored edge only + if (barFloating) { + // For top bar, reserve top margin; for bottom bar, reserve bottom margin + return barHeight + barMarginV; + } + return barHeight; + } + return 0; // Auto-height when top/bottom anchors are true + } + + Component.onCompleted: { + Logger.d("BarExclusionZone", "Created for screen:", screen?.name); + Logger.d("BarExclusionZone", " Position:", barPosition, "Floating:", barFloating); + Logger.d("BarExclusionZone", " Anchors - top:", anchors.top, "bottom:", anchors.bottom, "left:", anchors.left, "right:", anchors.right); + Logger.d("BarExclusionZone", " Size:", width, "x", height, "implicitWidth:", implicitWidth, "implicitHeight:", implicitHeight); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/BarPill.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/BarPill.qml new file mode 100644 index 0000000..c988487 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/BarPill.qml @@ -0,0 +1,128 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Commons +import qs.Widgets + +Item { + id: root + + required property ShellScreen screen + + property string icon: "" + property string text: "" + property string suffix: "" + property var tooltipText + property bool autoHide: false + property bool forceOpen: false + property bool forceClose: false + property bool oppositeDirection: false + property bool hovered: false + property bool rotateText: false + property color customBackgroundColor: "transparent" + property color customTextIconColor: "transparent" + property color customIconColor: "transparent" + property color customTextColor: "transparent" + + readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name) + readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" + + signal shown + signal hidden + signal entered + signal exited + signal clicked + signal rightClicked + signal middleClicked + signal wheel(int delta) + + // Size based on content for the content dimension, fill parent for the extended dimension + // Horizontal bars: width = content, height = fill parent (for extended click area) + // Vertical bars: width = fill parent, height = content + width: isVerticalBar ? parent.width : (pillLoader.item ? pillLoader.item.implicitWidth : 0) + height: isVerticalBar ? (pillLoader.item ? pillLoader.item.implicitHeight : 0) : parent.height + implicitWidth: pillLoader.item ? pillLoader.item.implicitWidth : 0 + implicitHeight: pillLoader.item ? pillLoader.item.implicitHeight : 0 + + // Loader fills BarPill so child components can extend to full bar dimension + Loader { + id: pillLoader + anchors.fill: parent + sourceComponent: isVerticalBar ? verticalPillComponent : horizontalPillComponent + + Component { + id: verticalPillComponent + BarPillVertical { + screen: root.screen + icon: root.icon + text: root.text + suffix: root.suffix + tooltipText: root.tooltipText + autoHide: root.autoHide + forceOpen: root.forceOpen + forceClose: root.forceClose + oppositeDirection: root.oppositeDirection + hovered: root.hovered + rotateText: root.rotateText + customBackgroundColor: root.customBackgroundColor + customTextIconColor: root.customTextIconColor + customIconColor: root.customIconColor + customTextColor: root.customTextColor + onShown: root.shown() + onHidden: root.hidden() + onEntered: root.entered() + onExited: root.exited() + onClicked: root.clicked() + onRightClicked: root.rightClicked() + onMiddleClicked: root.middleClicked() + onWheel: delta => root.wheel(delta) + } + } + + Component { + id: horizontalPillComponent + BarPillHorizontal { + screen: root.screen + icon: root.icon + text: root.text + suffix: root.suffix + tooltipText: root.tooltipText + autoHide: root.autoHide + forceOpen: root.forceOpen + forceClose: root.forceClose + oppositeDirection: root.oppositeDirection + hovered: root.hovered + customBackgroundColor: root.customBackgroundColor + customTextIconColor: root.customTextIconColor + customIconColor: root.customIconColor + customTextColor: root.customTextColor + onShown: root.shown() + onHidden: root.hidden() + onEntered: root.entered() + onExited: root.exited() + onClicked: root.clicked() + onRightClicked: root.rightClicked() + onMiddleClicked: root.middleClicked() + onWheel: delta => root.wheel(delta) + } + } + } + + function show() { + if (pillLoader.item && pillLoader.item.show) { + pillLoader.item.show(); + } + } + + function hide() { + if (pillLoader.item && pillLoader.item.hide) { + pillLoader.item.hide(); + } + } + + function showDelayed() { + if (pillLoader.item && pillLoader.item.showDelayed) { + pillLoader.item.showDelayed(); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/BarPillHorizontal.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/BarPillHorizontal.qml new file mode 100644 index 0000000..2076618 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/BarPillHorizontal.qml @@ -0,0 +1,349 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + required property ShellScreen screen + + property string icon: "" + property string text: "" + property string suffix: "" + property var tooltipText + property bool autoHide: false + property bool forceOpen: false + property bool forceClose: false + property bool oppositeDirection: false + property bool hovered: false + property color customBackgroundColor: "transparent" + property color customTextIconColor: "transparent" + property color customIconColor: "transparent" + property color customTextColor: "transparent" + + readonly property bool collapseToIcon: forceClose && !forceOpen + + // Effective shown state (true if hovered/animated open or forced) + readonly property bool revealed: !forceClose && (forceOpen || showPill) + readonly property bool hasIcon: root.icon !== "" + + signal shown + signal hidden + signal entered + signal exited + signal clicked + signal rightClicked + signal middleClicked + signal wheel(int delta) + + // Internal state + property bool showPill: false + property bool shouldAnimateHide: false + + readonly property int pillHeight: Style.getCapsuleHeightForScreen(screen?.name) + readonly property real barFontSize: Style.getBarFontSizeForScreen(screen?.name) + readonly property int pillPaddingHorizontal: Math.round(pillHeight * 0.2) + readonly property int pillOverlap: Math.round(pillHeight * 0.5) + readonly property int pillMaxWidth: Math.max(1, Math.round(textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap)) + + // Always prioritize hover color, then the custom one and finally the fallback color + readonly property color bgColor: hovered ? Color.mHover : (customBackgroundColor.a > 0) ? customBackgroundColor : Style.capsuleColor + readonly property color fgColor: hovered ? Color.mOnHover : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface + readonly property color iconFgColor: hovered ? Color.mOnHover : (customIconColor.a > 0) ? customIconColor : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface + readonly property color textFgColor: hovered ? Color.mOnHover : (customTextColor.a > 0) ? customTextColor : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface + + readonly property real iconSize: Style.toOdd(pillHeight * 0.48) + + // Content width calculation (for implicit sizing) + readonly property real contentWidth: { + if (collapseToIcon) { + return hasIcon ? pillHeight : 0; + } + var overlap = hasIcon ? pillOverlap : 0; + var baseWidth = hasIcon ? pillHeight : 0; + return baseWidth + Math.max(0, pill.width - overlap); + } + + // Fill parent to extend click area to full bar height + // Visual content is centered vertically within + anchors.fill: parent + implicitWidth: contentWidth + implicitHeight: pillHeight + + Connections { + target: root + function onTooltipTextChanged() { + if (hovered) { + TooltipService.updateText(root.tooltipText); + } + } + } + + // Unified background for the entire pill area to avoid overlapping opacity + Rectangle { + id: pillBackground + width: collapseToIcon ? pillHeight : root.width + height: pillHeight + radius: Style.radiusM + color: root.bgColor + anchors.verticalCenter: parent.verticalCenter + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + } + + Rectangle { + id: pill + + width: revealed ? pillMaxWidth : 1 + height: pillHeight + + x: { + if (!hasIcon) + return 0; + return oppositeDirection ? (iconCircle.x + iconCircle.width / 2) : (iconCircle.x + iconCircle.width / 2) - width; + } + + opacity: revealed ? Style.opacityFull : Style.opacityNone + color: "transparent" // Make pill background transparent to avoid double opacity + + topLeftRadius: oppositeDirection ? 0 : Style.radiusM + bottomLeftRadius: oppositeDirection ? 0 : Style.radiusM + topRightRadius: oppositeDirection ? Style.radiusM : 0 + bottomRightRadius: oppositeDirection ? Style.radiusM : 0 + anchors.verticalCenter: parent.verticalCenter + + NText { + id: textItem + anchors.verticalCenter: parent.verticalCenter + x: { + if (!hasIcon) + return (parent.width - width) / 2; + + // Better text horizontal centering + var centerX = (parent.width - width) / 2; + var offset = oppositeDirection ? Style.marginXS : -Style.marginXS; + if (forceOpen) { + // If its force open, the icon disc background is the same color as the bg pill move text slightly + offset += oppositeDirection ? -Style.marginXXS : Style.marginXXS; + } + return centerX + offset; + } + text: root.text + root.suffix + family: Settings.data.ui.fontFixed + pointSize: root.barFontSize + applyUiScale: false + color: root.textFgColor + visible: revealed + } + + Behavior on width { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on opacity { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + Rectangle { + id: iconCircle + width: hasIcon ? pillHeight : 0 + height: pillHeight + radius: Math.min(Style.radiusL, width / 2) + color: "transparent" // Make icon background transparent to avoid double opacity + anchors.verticalCenter: parent.verticalCenter + + x: oppositeDirection ? 0 : (parent.width - width) + + NIcon { + icon: root.icon + pointSize: iconSize + applyUiScale: false + color: root.iconFgColor + // Center horizontally + x: (iconCircle.width - width) / 2 + // Center vertically accounting for font metrics + y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2 + } + } + + ParallelAnimation { + id: showAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: 1 + to: pillMaxWidth + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 0 + to: 1 + duration: Style.animationFast + easing.type: Easing.OutCubic + } + onStarted: { + showPill = true; + } + onStopped: { + delayedHideAnim.start(); + root.shown(); + } + } + + SequentialAnimation { + id: delayedHideAnim + running: false + PauseAnimation { + duration: 2500 + } + ScriptAction { + script: if (shouldAnimateHide) { + hideAnim.start(); + } + } + } + + ParallelAnimation { + id: hideAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: pillMaxWidth + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 1 + to: 0 + duration: Style.animationFast + easing.type: Easing.InCubic + } + onStopped: { + showPill = false; + shouldAnimateHide = false; + root.hidden(); + } + } + + Timer { + id: showTimer + interval: Style.pillDelay + onTriggered: { + if (!showPill) { + showAnim.start(); + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + cursorShape: root.clicked ? Qt.PointingHandCursor : Qt.ArrowCursor + onEntered: { + hovered = true; + root.entered(); + TooltipService.show(root, root.tooltipText, BarService.getTooltipDirection(root.screen?.name), (forceOpen || forceClose) ? Style.tooltipDelay : Style.tooltipDelayLong); + if (forceClose) { + return; + } + if (!forceOpen) { + showDelayed(); + } + } + onExited: { + hovered = false; + root.exited(); + if (!forceOpen && !forceClose) { + hide(); + } + TooltipService.hide(); + } + onClicked: mouse => { + TooltipService.hide(); + if (mouse.button === Qt.LeftButton) { + root.clicked(); + } else if (mouse.button === Qt.RightButton) { + root.rightClicked(); + } else if (mouse.button === Qt.MiddleButton) { + root.middleClicked(); + } + } + onWheel: wheel => root.wheel(wheel.angleDelta.y) + } + + function show() { + if (collapseToIcon || root.text.trim().length === 0) + return; + if (!showPill) { + shouldAnimateHide = autoHide; + showAnim.start(); + } else { + hideAnim.stop(); + delayedHideAnim.restart(); + } + } + + function hide() { + if (collapseToIcon) + return; + if (forceOpen) { + return; + } + if (showPill) { + hideAnim.start(); + } + showTimer.stop(); + } + + function showDelayed() { + if (collapseToIcon || root.text.trim().length === 0) + return; + if (!showPill) { + shouldAnimateHide = autoHide; + showTimer.start(); + } else { + hideAnim.stop(); + delayedHideAnim.restart(); + } + } + + onForceOpenChanged: { + if (forceOpen) { + // Immediately lock open without animations + showAnim.stop(); + hideAnim.stop(); + delayedHideAnim.stop(); + showPill = true; + } else { + hide(); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/BarPillVertical.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/BarPillVertical.qml new file mode 100644 index 0000000..caf30af --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/BarPillVertical.qml @@ -0,0 +1,386 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + required property ShellScreen screen + + property string icon: "" + property string text: "" + property string suffix: "" + property var tooltipText + property bool autoHide: false + property bool forceOpen: false + property bool forceClose: false + property bool oppositeDirection: false + property bool hovered: false + property bool rotateText: false + property color customBackgroundColor: "transparent" + property color customTextIconColor: "transparent" + property color customIconColor: "transparent" + property color customTextColor: "transparent" + + readonly property bool collapseToIcon: forceClose && !forceOpen + + signal shown + signal hidden + signal entered + signal exited + signal clicked + signal rightClicked + signal middleClicked + signal wheel(int delta) + + // Internal state + property bool showPill: false + property bool shouldAnimateHide: false + + // Sizing logic for vertical bars + readonly property int buttonSize: Style.getCapsuleHeightForScreen(screen?.name) + readonly property real barFontSize: Style.getBarFontSizeForScreen(screen?.name) + readonly property int pillHeight: buttonSize + readonly property int pillOverlap: Math.round(buttonSize * 0.5) + readonly property int maxPillWidth: rotateText ? Math.max(buttonSize, Math.round(textItem.implicitHeight + Style.margin2M)) : buttonSize + readonly property int maxPillHeight: rotateText ? Math.max(1, Math.round(textItem.implicitWidth + Style.margin2M + Math.round(iconCircle.height / 4))) : Math.max(1, Math.round(textItem.implicitHeight + Style.margin2M)) + + // Determine pill direction based on section position + readonly property bool openDownward: oppositeDirection + readonly property bool openUpward: !oppositeDirection + + // Effective shown state (true if animated open or forced, but not if force closed) + readonly property bool revealed: !forceClose && (forceOpen || showPill) + readonly property bool hasIcon: root.icon !== "" + + // Always prioritize hover color, then the custom one and finally the fallback color + readonly property color bgColor: hovered ? Color.mHover : (customBackgroundColor.a > 0) ? customBackgroundColor : Style.capsuleColor + readonly property color fgColor: hovered ? Color.mOnHover : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface + readonly property color iconFgColor: hovered ? Color.mOnHover : (customIconColor.a > 0) ? customIconColor : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface + readonly property color textFgColor: hovered ? Color.mOnHover : (customTextColor.a > 0) ? customTextColor : (customTextIconColor.a > 0) ? customTextIconColor : Color.mOnSurface + + readonly property real iconSize: Style.toOdd(pillHeight * 0.48) + + // Content height calculation (for implicit sizing) + readonly property real contentHeight: { + if (collapseToIcon) { + return hasIcon ? buttonSize : 0; + } + var overlap = hasIcon ? pillOverlap : 0; + var baseHeight = hasIcon ? buttonSize : 0; + return baseHeight + Math.max(0, pill.height - overlap); + } + + // Fill parent width to extend horizontal click area + // Keep content-based height for visual layout + anchors.left: parent ? parent.left : undefined + anchors.right: parent ? parent.right : undefined + anchors.verticalCenter: parent ? parent.verticalCenter : undefined + height: contentHeight + implicitWidth: buttonSize + implicitHeight: contentHeight + + Connections { + target: root + function onTooltipTextChanged() { + if (hovered) { + TooltipService.updateText(root.tooltipText); + } + } + } + + // Unified background for the entire pill area to avoid overlapping opacity + Rectangle { + id: pillBackground + width: buttonSize + height: root.contentHeight + radius: Style.radiusM + color: root.bgColor + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + } + + Rectangle { + id: pill + + width: revealed ? maxPillWidth : 1 + height: revealed ? maxPillHeight : 1 + + // Position based on direction - center the pill relative to the icon + x: 0 + y: { + if (!hasIcon) + return 0; + return openUpward ? (iconCircle.y + iconCircle.height / 2 - height) : (iconCircle.y + iconCircle.height / 2); + } + + opacity: revealed ? Style.opacityFull : Style.opacityNone + color: "transparent" // Make pill background transparent to avoid double opacity + + // Radius logic for vertical expansion - rounded on the side that connects to icon + topLeftRadius: openUpward ? Style.radiusM : 0 + bottomLeftRadius: openDownward ? Style.radiusM : 0 + topRightRadius: openUpward ? Style.radiusM : 0 + bottomRightRadius: openDownward ? Style.radiusM : 0 + + anchors.horizontalCenter: parent.horizontalCenter + + NText { + id: textItem + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: hasIcon ? (openDownward ? Style.marginXXS : -Style.marginXXS) : 0 + rotation: rotateText ? -90 : 0 + text: root.text + root.suffix + family: Settings.data.ui.fontFixed + pointSize: root.barFontSize + applyUiScale: false + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: root.textFgColor + visible: revealed + + function getVerticalCenterOffset() { + // A small, symmetrical offset to push the text slightly away from the icon's edge. + return openDownward ? Style.marginXS : -Style.marginXS; + } + } + Behavior on width { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on height { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + Behavior on opacity { + enabled: showAnim.running || hideAnim.running + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + Rectangle { + id: iconCircle + width: buttonSize + height: buttonSize + radius: Math.min(Style.radiusL, width / 2) + color: "transparent" // Make icon background transparent to avoid double opacity + + // Icon positioning based on direction + x: 0 + y: openUpward ? (root.contentHeight - height) : 0 + anchors.horizontalCenter: parent.horizontalCenter + + NIcon { + icon: root.icon + pointSize: iconSize + applyUiScale: false + color: root.iconFgColor + // Center horizontally + x: (iconCircle.width - width) / 2 + // Center vertically accounting for font metrics + y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2 + } + } + + ParallelAnimation { + id: showAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: 1 + to: maxPillWidth + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "height" + from: 1 + to: maxPillHeight + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 0 + to: 1 + duration: Style.animationFast + easing.type: Easing.OutCubic + } + onStarted: { + showPill = true; + } + onStopped: { + delayedHideAnim.start(); + root.shown(); + } + } + + SequentialAnimation { + id: delayedHideAnim + running: false + PauseAnimation { + duration: 2500 + } + ScriptAction { + script: if (shouldAnimateHide) { + hideAnim.start(); + } + } + } + + ParallelAnimation { + id: hideAnim + running: false + NumberAnimation { + target: pill + property: "width" + from: maxPillWidth + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + NumberAnimation { + target: pill + property: "height" + from: maxPillHeight + to: 1 + duration: Style.animationNormal + easing.type: Easing.InCubic + } + NumberAnimation { + target: pill + property: "opacity" + from: 1 + to: 0 + duration: Style.animationFast + easing.type: Easing.InCubic + } + onStopped: { + showPill = false; + shouldAnimateHide = false; + root.hidden(); + } + } + + Timer { + id: showTimer + interval: Style.pillDelay + onTriggered: { + if (!showPill) { + showAnim.start(); + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + cursorShape: root.clicked ? Qt.PointingHandCursor : Qt.ArrowCursor + onEntered: { + hovered = true; + root.entered(); + TooltipService.show(root, root.tooltipText, BarService.getTooltipDirection(root.screen?.name), (forceOpen || forceClose) ? Style.tooltipDelay : Style.tooltipDelayLong); + if (forceClose) { + return; + } + if (!forceOpen) { + showDelayed(); + } + } + onExited: { + hovered = false; + root.exited(); + if (!forceOpen && !forceClose) { + hide(); + } + TooltipService.hide(); + } + onClicked: mouse => { + TooltipService.hide(); + if (mouse.button === Qt.LeftButton) { + root.clicked(); + } else if (mouse.button === Qt.RightButton) { + root.rightClicked(); + } else if (mouse.button === Qt.MiddleButton) { + root.middleClicked(); + } + } + onWheel: wheel => root.wheel(wheel.angleDelta.y) + } + + function show() { + if (collapseToIcon || root.text.trim().length === 0) + return; + if (!showPill) { + shouldAnimateHide = autoHide; + showAnim.start(); + } else { + hideAnim.stop(); + delayedHideAnim.restart(); + } + } + + function hide() { + if (collapseToIcon) + return; + if (forceOpen) { + return; + } + if (showPill) { + hideAnim.start(); + } + showTimer.stop(); + } + + function showDelayed() { + if (collapseToIcon || root.text.trim().length === 0) + return; + if (!showPill) { + shouldAnimateHide = autoHide; + showTimer.start(); + } else { + hideAnim.stop(); + delayedHideAnim.restart(); + } + } + + onForceOpenChanged: { + if (forceOpen) { + // Immediately lock open without animations + showAnim.stop(); + hideAnim.stop(); + delayedHideAnim.stop(); + showPill = true; + } else { + hide(); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/BarWidgetLoader.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/BarWidgetLoader.qml new file mode 100644 index 0000000..b369467 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/BarWidgetLoader.qml @@ -0,0 +1,191 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.Noctalia +import qs.Services.UI + +Item { + id: root + + required property string widgetId + required property var widgetScreen + required property var widgetProps + + // Extract section info from widgetProps + readonly property string section: widgetProps ? (widgetProps.section || "") : "" + readonly property int sectionIndex: widgetProps ? (widgetProps.sectionWidgetIndex || 0) : 0 + + // Store registration key at registration time so unregistration always uses the correct key, + // even if binding properties (section, sectionIndex) have changed by destruction time + property string _regScreen: "" + property string _regSection: "" + property string _regWidgetId: "" + property int _regIndex: -1 + + function _unregister() { + if (_regScreen !== "") { + BarService.unregisterWidget(_regScreen, _regSection, _regWidgetId, _regIndex); + _regScreen = ""; + } + } + + // Bar orientation and height for extended click areas + readonly property string barPosition: Settings.getBarPositionForScreen(widgetScreen?.name) + readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" + readonly property real barHeight: Style.getBarHeightForScreen(widgetScreen?.name) + + // Request full bar dimension from layout to extend click areas above/below widgets + // For horizontal bars: full bar height, widget's content width + // For vertical bars: full bar width, widget's content height + implicitWidth: isVerticalBar ? barHeight : getImplicitSize(loader.item, "implicitWidth") + implicitHeight: isVerticalBar ? getImplicitSize(loader.item, "implicitHeight") : barHeight + + // Remove layout space left by hidden widgets + visible: loader.item ? ((loader.item.opacity > 0.0) || (loader.item.hasOwnProperty("hideMode") && loader.item.hideMode === "transparent")) : false + + function getImplicitSize(item, prop) { + return (item && item.visible) ? Math.round(item[prop]) : 0; + } + + // Only load if widget exists in registry + function checkWidgetExists(): bool { + return root.widgetId !== "" && BarWidgetRegistry.hasWidget(root.widgetId); + } + + // Force reload counter - incremented when plugin widget registry changes + property int reloadCounter: 0 + + // Listen for plugin widget registry changes to force reload + Connections { + target: BarWidgetRegistry + enabled: BarWidgetRegistry.isPluginWidget(root.widgetId) + + function onPluginWidgetRegistryUpdated() { + if (BarWidgetRegistry.hasWidget(root.widgetId)) { + root.reloadCounter++; + // Plugin widgets use setSource, so also trigger reload directly + if (root._isPlugin && loader.active) + root._loadWidget(); + Logger.d("BarWidgetLoader", "Plugin widget registry updated, reloading:", root.widgetId); + } + } + } + + readonly property bool _isPlugin: BarWidgetRegistry.isPluginWidget(widgetId) + + // Build initial properties that must be available during Component.onCompleted. + // This prevents registration-key mismatches in widgets that build IDs from + // screen.name, section, or sectionWidgetIndex. + // All standard bar widget props are passed for both core and plugin widgets. + // Plugins that don't define some of these properties will get harmless warnings + // but still load correctly — and plugins that DO define them (e.g. for unique + // SpectrumService keys with multiple instances) get correct values from the start. + function _initialProps() { + return { + "screen": widgetScreen, + "widgetId": widgetProps.widgetId || "", + "section": widgetProps.section || "", + "sectionWidgetIndex": widgetProps.sectionWidgetIndex || 0, + "sectionWidgetsCount": widgetProps.sectionWidgetsCount || 0 + }; + } + + // Core widget URLs: file names match widget IDs exactly + readonly property string _barWidgetsDir: Quickshell.shellDir + "/Modules/Bar/Widgets/" + + function _loadWidget() { + if (!BarWidgetRegistry.hasWidget(root.widgetId)) + return; + + var props = _initialProps(); + + if (_isPlugin) { + var comp = BarWidgetRegistry.getWidget(root.widgetId); + if (!comp) + return; + var pluginId = root.widgetId.replace("plugin:", ""); + var api = PluginService.getPluginAPI(pluginId); + if (api) + props.pluginApi = api; + loader.setSource(comp.url, props); + } else { + loader.setSource(_barWidgetsDir + root.widgetId + ".qml", props); + } + } + + Loader { + id: loader + anchors.fill: parent + asynchronous: true + active: root.checkWidgetExists() && (root.reloadCounter >= 0) + + // All widgets use setSource() so that screen and widget properties + // are set as initial properties, available during Component.onCompleted. + Component.onCompleted: root._loadWidget() + + onActiveChanged: { + if (active) + root._loadWidget(); + } + + // Unregister when the loaded item is destroyed (Loader deactivated or sourceComponent changed) + onItemChanged: { + if (!item) { + root._unregister(); + } + } + + onLoaded: { + if (!item) + return; + + Logger.d("BarWidgetLoader", "Loading widget", widgetId, "on screen:", widgetScreen.name); + + // Extend widget to fill full bar dimension for extended click areas + // For horizontal bars: widget fills bar height (content width preserved) + // For vertical bars: widget fills bar width (content height preserved) + if (root.isVerticalBar) { + item.width = Qt.binding(function () { + return root.barHeight; + }); + } else { + item.height = Qt.binding(function () { + return root.barHeight; + }); + } + + // Apply remaining widget properties (screen is already set as initial prop) + for (var prop in widgetProps) { + if (item.hasOwnProperty(prop)) { + item[prop] = widgetProps[prop]; + } + } + + // Unregister any previous registration before registering the new instance + root._unregister(); + + // Register and store the key for reliable unregistration + BarService.registerWidget(widgetScreen.name, section, widgetId, sectionIndex, item); + root._regScreen = widgetScreen.name; + root._regSection = section; + root._regWidgetId = widgetId; + root._regIndex = sectionIndex; + + // Call custom onLoaded if it exists + if (item.hasOwnProperty("onLoaded")) { + item.onLoaded(); + } + } + + Component.onDestruction: { + root._unregister(); + } + } + + // Error handling + Component.onCompleted: { + if (!BarWidgetRegistry.hasWidget(widgetId)) { + Logger.w("BarWidgetLoader", "Widget not found in registry:", widgetId); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/TrayMenu.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/TrayMenu.qml new file mode 100644 index 0000000..ab11985 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/TrayMenu.qml @@ -0,0 +1,638 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +PopupWindow { + id: root + + property ShellScreen screen + + property var trayItem: null + property var anchorItem: null + property real anchorX + property real anchorY + property bool isSubMenu: false + property string widgetSection: "" + property int widgetIndex: -1 + + // Derive menu from trayItem (only used for non-submenus) + readonly property QsMenuHandle menu: isSubMenu ? null : (trayItem ? trayItem.menu : null) + + // Compute if current tray item is pinned + readonly property bool isPinned: { + if (!trayItem || widgetSection === "" || widgetIndex < 0) + return false; + var widgets = Settings.getBarWidgetsForScreen(root.screen?.name)[widgetSection]; + if (!widgets || widgetIndex >= widgets.length) + return false; + var widgetSettings = widgets[widgetIndex]; + if (!widgetSettings || widgetSettings.id !== "Tray") + return false; + var pinnedList = widgetSettings.pinned || []; + const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || ""; + for (var i = 0; i < pinnedList.length; i++) { + if (pinnedList[i] === itemName) + return true; + } + return false; + } + + readonly property int menuWidth: 220 + + implicitWidth: menuWidth + + // Use the content height of the Flickable for implicit height + implicitHeight: Math.min(screen?.height * 0.9, flickable.contentHeight + Style.margin2S) + + // When implicitHeight changes (menu content loads), force anchor recalculation + onImplicitHeightChanged: { + if (visible && anchorItem) { + Qt.callLater(() => { + anchor.updateAnchor(); + }); + } + } + + visible: false + color: "transparent" + anchor.item: anchorItem + anchor.rect.x: { + if (anchorItem && screen) { + let baseX = anchorX; + + // Calculate position relative to current screen + let menuScreenX; + if (isSubMenu && anchorItem.Window && anchorItem.Window.window) { + const posInPopup = anchorItem.mapToItem(null, 0, 0); + const parentWindow = anchorItem.Window.window; + const windowXOnScreen = parentWindow.x - screen.x; + menuScreenX = windowXOnScreen + posInPopup.x + baseX; + } else { + const anchorGlobalPos = anchorItem.mapToItem(null, 0, 0); + const anchorScreenX = anchorGlobalPos.x; + menuScreenX = anchorScreenX + baseX; + } + + const menuRight = menuScreenX + implicitWidth; + const screenRight = screen.width; + const menuLeft = menuScreenX; + + // Only adjust if menu would clip off screen boundaries + // Don't adjust if the positioning is intentional (e.g., negative offset for right bar) + if (menuRight > screenRight && menuLeft < screenRight) { + // Clipping on right edge - shift left + const overflow = menuRight - screenRight; + return baseX - overflow - Style.marginS; + } else if (menuLeft < 0 && menuRight > 0) { + // Clipping on left edge - shift right + return baseX - menuLeft + Style.marginS; + } + + return baseX; + } + return anchorX; + } + anchor.rect.y: { + if (anchorItem && screen) { + const barPosition = Settings.getBarPositionForScreen(root.screen?.name); + + let baseY = anchorY; + + // Only apply bottom bar special positioning if: + // 1. Not a submenu + // 2. Bar is at bottom + // 3. anchorY is not already negative (if negative, it's pre-calculated from drawer) + const shouldApplyBottomBarLogic = !isSubMenu && barPosition === "bottom" && anchorY >= 0; + + if (shouldApplyBottomBarLogic) { + // For bottom bar from the bar itself, position menu above the anchor with margin + baseY = -(implicitHeight + Style.marginS); + } else if (barPosition === "top" && !isSubMenu && anchorY >= 0) { + // For top bar: position menu below bar with margin + const barHeight = Style.getBarHeightForScreen(root.screen?.name); + baseY = barHeight + Style.marginS; + } + + // Use a robust way to get screen coordinates + const posInWindow = anchorItem.mapToItem(null, 0, 0); + const parentWindow = anchorItem.Window.window; + + // Calculate screen-relative Y of the window + let windowYOnScreen = (parentWindow && screen) ? (parentWindow.y - screen.y) : 0; + + // If window reported 0 but bar is at bottom, assume it's at screen bottom + if (windowYOnScreen === 0 && barPosition === "bottom" && screen) { + windowYOnScreen = screen.height - (parentWindow ? parentWindow.height : Style.getBarHeightForScreen(screen.name)); + } + + // Calculate the screen Y of the menu top + // Use a small guess for height if implicitHeight is 0 to avoid covering the bar on the first frame + const effectiveHeight = implicitHeight > 0 ? implicitHeight : 200; + const effectiveBaseY = shouldApplyBottomBarLogic ? -(effectiveHeight + Style.marginS) : baseY; + + const menuScreenY = windowYOnScreen + posInWindow.y + effectiveBaseY; + const menuBottom = menuScreenY + (implicitHeight > 0 ? implicitHeight : effectiveHeight); + const screenHeight = screen ? screen.height : 1080; + + // Adjust the final baseY (the actual value returned to anchor.rect.y) + let finalBaseY = shouldApplyBottomBarLogic ? -(implicitHeight + Style.marginS) : baseY; + + // Adjust if menu would clip off the bottom + if (menuBottom > screenHeight) { + const overflow = menuBottom - screenHeight; + finalBaseY -= (overflow + Style.marginS); + } + + // Adjust if menu would clip off the top + // menuScreenY < 0 means it's above the screen edge + if (menuScreenY < 0) { + finalBaseY -= (menuScreenY - Style.marginS); + } + + return finalBaseY; + } + + // Fallback if no anchor/screen + if (isSubMenu) { + return anchorY; + } + return anchorY + (Settings.getBarPositionForScreen(root.screen?.name) === "bottom" ? -implicitHeight : Style.getBarHeightForScreen(root.screen?.name)); + } + + function showAt(item, x, y) { + if (!item) { + Logger.w("TrayMenu", "anchorItem is undefined, won't show menu."); + return; + } + + if (!opener.children || opener.children.values.length === 0) { + //Logger.w("TrayMenu", "Menu not ready, delaying show") + Qt.callLater(() => showAt(item, x, y)); + return; + } + + anchorItem = item; + anchorX = x; + anchorY = y; + + visible = true; + forceActiveFocus(); + + // Force update after showing. + Qt.callLater(() => { + root.anchor.updateAnchor(); + }); + } + + function hideMenu() { + visible = false; + + // Clean up all submenus recursively + for (var i = 0; i < columnLayout.children.length; i++) { + const child = columnLayout.children[i]; + if (child?.subMenu) { + child.subMenu.hideMenu(); + child.subMenu.destroy(); + child.subMenu = null; + } + } + } + + Item { + anchors.fill: parent + Keys.onEscapePressed: root.hideMenu() + } + + QsMenuOpener { + id: opener + menu: root.menu + } + + Rectangle { + anchors.fill: parent + color: Color.mSurface + border.color: Color.mOutline + border.width: Math.max(1, Style.borderS) + radius: Style.radiusM + + // Fade-in animation + opacity: root.visible ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + } + + Flickable { + id: flickable + anchors.fill: parent + anchors.margins: Style.marginS + contentHeight: columnLayout.implicitHeight + interactive: true + + // Fade-in animation + opacity: root.visible ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + // Use a ColumnLayout to handle menu item arrangement + ColumnLayout { + id: columnLayout + width: flickable.width + spacing: 0 + + Repeater { + model: opener.children ? [...opener.children.values] : [] + + delegate: Rectangle { + id: entry + required property var modelData + + Layout.preferredWidth: parent.width + Layout.preferredHeight: { + if (modelData?.isSeparator) { + return 8; + } else { + // Calculate based on text content + const textHeight = text.contentHeight || (Style.fontSizeS * 1.2); + return Math.max(28, textHeight + Style.margin2S); + } + } + + color: "transparent" + property var subMenu: null + + NDivider { + anchors.centerIn: parent + width: parent.width - Style.margin2M + visible: modelData?.isSeparator ?? false + } + + Rectangle { + id: innerRect + anchors.fill: parent + color: mouseArea.containsMouse ? Color.mHover : "transparent" + radius: Style.radiusS + visible: !(modelData?.isSeparator ?? false) + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginM + anchors.rightMargin: Style.marginM + spacing: Style.marginS + + // Indicator Container + Item { + visible: (modelData?.buttonType ?? QsMenuButtonType.None) !== QsMenuButtonType.None + + implicitWidth: Math.round(Style.baseWidgetSize * 0.5) + implicitHeight: Math.round(Style.baseWidgetSize * 0.5) + Layout.alignment: Qt.AlignVCenter + + // Helper properties + readonly property int type: modelData?.buttonType ?? QsMenuButtonType.None + readonly property bool isRadio: type === QsMenuButtonType.RadioButton + readonly property bool isChecked: modelData?.checkState === Qt.Checked || (modelData?.checked ?? false) + + // Color Logic + readonly property color activeColor: mouseArea.containsMouse ? Color.mOnHover : Color.mPrimary + readonly property color checkMarkColor: mouseArea.containsMouse ? Color.mHover : Color.mOnPrimary + readonly property color borderColor: isChecked ? activeColor : (mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface) + + // Checkbox Visuals + Rectangle { + visible: !parent.isRadio + anchors.centerIn: parent + width: Math.round(Style.baseWidgetSize * 0.5) + height: Math.round(Style.baseWidgetSize * 0.5) + radius: Style.iRadiusXS + color: "transparent" // Transparent to match RadioButton style + border.color: parent.borderColor + border.width: Style.borderM + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + NIcon { + visible: parent.parent.isChecked + anchors.centerIn: parent + anchors.horizontalCenterOffset: -1 + icon: "check" + color: parent.parent.activeColor + pointSize: Math.max(Style.fontSizeXXS, parent.width * 0.6) + } + } + + // RadioButton Visuals + Rectangle { + visible: parent.isRadio + anchors.centerIn: parent + width: Style.toOdd(Style.baseWidgetSize * 0.5) + height: Style.toOdd(Style.baseWidgetSize * 0.5) + radius: width / 2 + color: "transparent" + border.color: parent.borderColor + border.width: Style.borderM // Slightly thicker for radio look + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + Rectangle { + visible: parent.parent.isChecked + anchors.centerIn: parent + width: Style.toOdd(parent.width * 0.5) + height: Style.toOdd(parent.height * 0.5) + radius: width / 2 + color: parent.parent.activeColor + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + } + + NText { + id: text + Layout.fillWidth: true + color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface) : Color.mOnSurfaceVariant + text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..." + pointSize: Style.fontSizeS + verticalAlignment: Text.AlignVCenter + wrapMode: Text.WordWrap + } + + Image { + Layout.preferredWidth: Style.marginL + Layout.preferredHeight: Style.marginL + source: modelData?.icon ?? "" + visible: (modelData?.icon ?? "") !== "" + fillMode: Image.PreserveAspectFit + } + + NIcon { + icon: modelData?.hasChildren ? "menu" : "" + pointSize: Style.fontSizeS + applyUiScale: false + verticalAlignment: Text.AlignVCenter + visible: modelData?.hasChildren ?? false + color: (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + enabled: (modelData?.enabled ?? true) && !(modelData?.isSeparator ?? false) && root.visible + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onClicked: mouse => { + if (modelData && !modelData.isSeparator) { + if (modelData.hasChildren) { + // Click on items with children toggles submenu + if (entry.subMenu) { + // Close existing submenu + entry.subMenu.hideMenu(); + entry.subMenu.destroy(); + entry.subMenu = null; + } else { + // Close any other open submenus first + for (var i = 0; i < columnLayout.children.length; i++) { + const sibling = columnLayout.children[i]; + if (sibling !== entry && sibling.subMenu) { + sibling.subMenu.hideMenu(); + sibling.subMenu.destroy(); + sibling.subMenu = null; + } + } + + // Determine submenu opening direction + let openLeft = false; + const barPosition = Settings.getBarPositionForScreen(root.screen?.name); + const globalPos = entry.mapToItem(null, 0, 0); + + if (barPosition === "right") { + openLeft = true; + } else if (barPosition === "left") { + openLeft = false; + } else { + openLeft = (root.widgetSection === "right"); + } + + // Open new submenu + entry.subMenu = Qt.createComponent("TrayMenu.qml").createObject(root, { + "menu": modelData, + "isSubMenu": true, + "screen": root.screen + }); + + if (entry.subMenu) { + const overlap = 60; + entry.subMenu.anchorItem = entry; + entry.subMenu.anchorX = openLeft ? -overlap : overlap; + entry.subMenu.anchorY = 0; + entry.subMenu.visible = true; + // Force anchor update with new position + Qt.callLater(() => { + entry.subMenu.anchor.updateAnchor(); + }); + } + } + } else { + // Click on regular items triggers them + modelData.triggered(); + root.hideMenu(); + + // Close the drawer if it's open + if (root.screen) { + const panel = PanelService.getPanel("trayDrawerPanel", root.screen); + if (panel && panel.visible) { + panel.close(); + } + } + } + } + } + } + } + + Component.onDestruction: { + if (subMenu) { + subMenu.destroy(); + subMenu = null; + } + } + } + } + + // PIN / UNPIN + Rectangle { + visible: { + if (widgetSection === "" || widgetIndex < 0) + return false; + var widgets = Settings.getBarWidgetsForScreen(root.screen?.name)[widgetSection]; + if (!widgets || widgetIndex >= widgets.length) + return false; + var widgetSettings = widgets[widgetIndex]; + if (!widgetSettings) + return false; + return widgetSettings.drawerEnabled ?? false; + } + Layout.preferredWidth: parent.width + Layout.preferredHeight: 28 + color: pinUnpinMouseArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.2) : Qt.alpha(Color.mPrimary, 0.08) + radius: Style.radiusS + border.color: Qt.alpha(Color.mPrimary, pinUnpinMouseArea.containsMouse ? 0.4 : 0.2) + border.width: Style.borderS + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginM + anchors.rightMargin: Style.marginM + spacing: Style.marginS + + NIcon { + icon: root.isPinned ? "unpin" : "pin" + pointSize: Style.fontSizeS + applyUiScale: false + verticalAlignment: Text.AlignVCenter + color: Color.mPrimary + } + + NText { + Layout.fillWidth: true + color: Color.mPrimary + text: root.isPinned ? I18n.tr("panels.bar.tray-unpin-application") : I18n.tr("panels.bar.tray-pin-application") + pointSize: Style.fontSizeS + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + } + + MouseArea { + id: pinUnpinMouseArea + anchors.fill: parent + hoverEnabled: true + + onClicked: { + if (root.isPinned) { + root.removeFromPinned(); + } else { + root.addToPinned(); + } + } + } + } + } + } + + function addToPinned() { + if (!trayItem || widgetSection === "" || widgetIndex < 0) { + Logger.w("TrayMenu", "Cannot pin: missing tray item or widget info"); + return; + } + const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || ""; + if (!itemName) { + Logger.w("TrayMenu", "Cannot pin: tray item has no name"); + return; + } + var screenName = root.screen?.name || ""; + var widgets = Settings.getBarWidgetsForScreen(screenName)[widgetSection]; + if (!widgets || widgetIndex >= widgets.length) { + Logger.w("TrayMenu", "Cannot pin: invalid widget index"); + return; + } + var widgetSettings = widgets[widgetIndex]; + if (!widgetSettings || widgetSettings.id !== "Tray") { + Logger.w("TrayMenu", "Cannot pin: widget is not a Tray widget"); + return; + } + var pinnedList = widgetSettings.pinned || []; + var newPinned = pinnedList.slice(); + newPinned.push(itemName); + var newSettings = Object.assign({}, widgetSettings); + newSettings.pinned = newPinned; + widgets[widgetIndex] = newSettings; + + // Write to the correct location: screen override or global + if (Settings.hasScreenOverride(screenName, "widgets")) { + var overrideWidgets = Settings.getBarWidgetsForScreen(screenName); + overrideWidgets[widgetSection] = widgets; + Settings.setScreenOverride(screenName, "widgets", overrideWidgets); + } else { + Settings.data.bar.widgets[widgetSection] = widgets; + } + Settings.saveImmediate(); + + // Close drawer when pinning (drawer needs to resize) + if (screen) { + const panel = PanelService.getPanel("trayDrawerPanel", screen); + if (panel) + panel.close(); + } + } + + function removeFromPinned() { + if (!trayItem || widgetSection === "" || widgetIndex < 0) { + Logger.w("TrayMenu", "Cannot unpin: missing tray item or widget info"); + return; + } + const itemName = trayItem.tooltipTitle || trayItem.name || trayItem.id || ""; + if (!itemName) { + Logger.w("TrayMenu", "Cannot unpin: tray item has no name"); + return; + } + var screenName = root.screen?.name || ""; + var widgets = Settings.getBarWidgetsForScreen(screenName)[widgetSection]; + if (!widgets || widgetIndex >= widgets.length) { + Logger.w("TrayMenu", "Cannot unpin: invalid widget index"); + return; + } + var widgetSettings = widgets[widgetIndex]; + if (!widgetSettings || widgetSettings.id !== "Tray") { + Logger.w("TrayMenu", "Cannot unpin: widget is not a Tray widget"); + return; + } + var pinnedList = widgetSettings.pinned || []; + var newPinned = []; + for (var i = 0; i < pinnedList.length; i++) { + if (pinnedList[i] !== itemName) { + newPinned.push(pinnedList[i]); + } + } + var newSettings = Object.assign({}, widgetSettings); + newSettings.pinned = newPinned; + widgets[widgetIndex] = newSettings; + + // Write to the correct location: screen override or global + if (Settings.hasScreenOverride(screenName, "widgets")) { + var overrideWidgets = Settings.getBarWidgetsForScreen(screenName); + overrideWidgets[widgetSection] = widgets; + Settings.setScreenOverride(screenName, "widgets", overrideWidgets); + } else { + Settings.data.bar.widgets[widgetSection] = widgets; + } + Settings.saveImmediate(); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/WorkspacePill.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/WorkspacePill.qml new file mode 100644 index 0000000..149879e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Extras/WorkspacePill.qml @@ -0,0 +1,228 @@ +import QtQuick +import qs.Commons +import qs.Services.Compositor +import qs.Widgets + +Item { + id: pillContainer + + required property var workspace + required property bool isVertical + + // These must be provided by the parent Workspace widget + required property real baseDimensionRatio + required property real capsuleHeight + required property real barHeight + required property string labelMode + required property int fontWeight + required property int characterCount + required property real textRatio + required property bool showLabelsOnlyWhenOccupied + required property string focusedColor + required property string occupiedColor + required property string emptyColor + required property real masterProgress + required property bool effectsActive + required property color effectColor + required property var getWorkspaceWidth + required property var getWorkspaceHeight + + // Fixed dimension (cross-axis) for visual pill + readonly property real fixedDimension: Style.toOdd(capsuleHeight * baseDimensionRatio) + + // Animated pill dimensions (for visual pill, not container) + property real pillWidth: isVertical ? fixedDimension : getWorkspaceWidth(workspace, false) + property real pillHeight: isVertical ? getWorkspaceHeight(workspace, false) : fixedDimension + + // Container uses full barHeight on cross-axis for larger click area + width: isVertical ? barHeight : getWorkspaceWidth(workspace, false) + height: isVertical ? getWorkspaceHeight(workspace, false) : barHeight + + states: [ + State { + name: "active" + when: workspace.isActive + PropertyChanges { + target: pillContainer + width: isVertical ? barHeight : getWorkspaceWidth(workspace, true) + height: isVertical ? getWorkspaceHeight(workspace, true) : barHeight + pillWidth: isVertical ? fixedDimension : getWorkspaceWidth(workspace, true) + pillHeight: isVertical ? getWorkspaceHeight(workspace, true) : fixedDimension + } + } + ] + + transitions: [ + Transition { + from: "inactive" + to: "active" + NumberAnimation { + properties: isVertical ? "height,pillHeight" : "width,pillWidth" + duration: Style.animationNormal + easing.type: Easing.OutBack + } + }, + Transition { + from: "active" + to: "inactive" + NumberAnimation { + properties: isVertical ? "height,pillHeight" : "width,pillWidth" + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + ] + + Rectangle { + id: pill + width: pillContainer.pillWidth + height: pillContainer.pillHeight + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, height) + radius: Style.radiusM + z: 0 + + color: { + if (pillMouseArea.containsMouse) + return Color.mHover; + if (workspace.isFocused) + return Color.resolveColorKey(focusedColor); + if (workspace.isUrgent) + return Color.mError; + if (workspace.isOccupied) + return Color.resolveColorKey(occupiedColor); + return Qt.alpha(Color.resolveColorKey(emptyColor), 0.3); + } + + Loader { + active: (labelMode !== "none") && (!showLabelsOnlyWhenOccupied || workspace.isOccupied || workspace.isFocused) + anchors.fill: parent + sourceComponent: Component { + NText { + text: { + if (workspace.name && workspace.name.length > 0) { + if (labelMode === "name") { + return workspace.name.substring(0, characterCount); + } + if (labelMode === "index+name") { + // Vertical mode: compact format (no space, first char only) + // Horizontal mode: full format (space, more chars) + if (isVertical) { + return workspace.idx.toString() + workspace.name.substring(0, 1); + } + return workspace.idx.toString() + " " + workspace.name.substring(0, characterCount); + } + } + return workspace.idx.toString(); + } + family: Settings.data.ui.fontFixed + // Size based on the fixed dimension (cross-axis) of the visual pill + pointSize: (isVertical ? pillContainer.pillWidth : pillContainer.pillHeight) * textRatio + applyUiScale: false + font.capitalization: Font.AllUppercase + font.weight: fontWeight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + wrapMode: Text.Wrap + color: { + if (pillMouseArea.containsMouse) + return Color.mOnHover; + if (workspace.isFocused) + return Color.resolveOnColorKey(focusedColor); + if (workspace.isUrgent) + return Color.mOnError; + if (workspace.isOccupied) + return Color.resolveOnColorKey(occupiedColor); + return Color.resolveOnColorKey(emptyColor); + } + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + } + } + } + + // Material 3-inspired smooth animations + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.InOutCubic + } + } + Behavior on radius { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + } + + Behavior on width { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + Behavior on height { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + Behavior on pillWidth { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + Behavior on pillHeight { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + + // Full-height click area + MouseArea { + id: pillMouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: { + CompositorService.switchToWorkspace(workspace); + } + } + + // Burst effect overlay for focused pill + Rectangle { + id: pillBurst + anchors.centerIn: pill + width: pillContainer.pillWidth + 18 * masterProgress * scale + height: pillContainer.pillHeight + 18 * masterProgress * scale + radius: width / 2 + color: "transparent" + border.color: effectColor + border.width: Math.max(1, Math.round((2 + 6 * (1.0 - masterProgress)))) + opacity: effectsActive && workspace.isFocused ? (1.0 - masterProgress) * 0.7 : 0 + visible: effectsActive && workspace.isFocused + z: 1 + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/ActiveWindow.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/ActiveWindow.qml new file mode 100644 index 0000000..12f197d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/ActiveWindow.qml @@ -0,0 +1,385 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Services.Compositor +import qs.Services.UI +import qs.Widgets + +Item { + id: root + Layout.preferredHeight: isVerticalBar ? -1 : Style.getBarHeightForScreen(screenName) + Layout.preferredWidth: isVerticalBar ? Style.getBarHeightForScreen(screenName) : -1 + Layout.fillHeight: false + Layout.fillWidth: false + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] || {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length && widgets[sectionWidgetIndex]) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + // Widget settings - matching MediaMini pattern + readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : (widgetMetadata.showIcon || false) + readonly property bool showText: (widgetSettings.showText !== undefined) ? widgetSettings.showText : (widgetMetadata.showText || false) + readonly property string hideMode: (widgetSettings.hideMode !== undefined) ? widgetSettings.hideMode : (widgetMetadata.hideMode || "hidden") + readonly property string scrollingMode: (widgetSettings.scrollingMode !== undefined) ? widgetSettings.scrollingMode : (widgetMetadata.scrollingMode || "hover") + + // Maximum widget width with user settings support + readonly property real maxWidth: (widgetSettings.maxWidth !== undefined) ? widgetSettings.maxWidth : Math.max(widgetMetadata.maxWidth || 0, screen ? screen.width * 0.06 : 0) + readonly property bool useFixedWidth: (widgetSettings.useFixedWidth !== undefined) ? widgetSettings.useFixedWidth : (widgetMetadata.useFixedWidth || false) + readonly property string textColorKey: (widgetSettings.textColor !== undefined) ? widgetSettings.textColor : widgetMetadata.textColor + readonly property color textColor: Color.resolveColorKey(textColorKey) + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" + readonly property real barHeight: Style.getBarHeightForScreen(screenName) + readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName) + readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName) + readonly property bool hasFocusedWindow: CompositorService.getFocusedWindow() !== null + readonly property string windowTitle: CompositorService.getFocusedWindowTitle() || "No active window" + readonly property string fallbackIcon: "user-desktop" + + readonly property int iconSize: Style.toOdd(capsuleHeight * 0.75) + readonly property int verticalSize: Style.toOdd(capsuleHeight * 0.85) + + // For horizontal bars, height is always barHeight (no animation needed) + // For vertical bars, collapse to 0 when hidden + implicitHeight: isVerticalBar ? (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : verticalSize) : barHeight + implicitWidth: isVerticalBar ? (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : verticalSize) : (((!hasFocusedWindow) && hideMode === "hidden") ? 0 : dynamicWidth) + + // "visible": Always Visible, "hidden": Hide When Empty, "transparent": Transparent When Empty + visible: (hideMode !== "hidden" || hasFocusedWindow) || opacity > 0 + opacity: ((hideMode !== "hidden" || hasFocusedWindow) && (hideMode !== "transparent" || hasFocusedWindow)) ? 1.0 : 0.0 + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + Behavior on implicitWidth { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutCubic + } + } + + Behavior on implicitHeight { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutCubic + } + } + + function calculateContentWidth() { + // Calculate the actual content width based on visible elements + var contentWidth = 0; + var margins = Style.margin2S; // Left and right margins + + // Icon width (if visible) + if (showIcon) { + contentWidth += iconSize; + if (showText) { + contentWidth += Style.marginS; // Spacing after icon + } + } + + // Text width (use the measured width) + if (showText) { + contentWidth += titleContainer.measuredWidth; + // Additional small margin for text + contentWidth += Style.margin2XXS; + } + + // Add container margins + contentWidth += margins; + + return Math.ceil(contentWidth); + } + + // Dynamic width: adapt to content but respect maximum width setting + readonly property real dynamicWidth: { + // If using fixed width mode, always use maxWidth + if (useFixedWidth) { + return maxWidth; + } + // Otherwise, adapt to content + if (!hasFocusedWindow) { + return Math.min(calculateContentWidth(), maxWidth); + } + // Use content width but don't exceed user-set maximum width + return Math.min(calculateContentWidth(), maxWidth); + } + + function getAppIcon() { + try { + // Try CompositorService first + const focusedWindow = CompositorService.getFocusedWindow(); + if (focusedWindow && focusedWindow.appId) { + try { + const idValue = focusedWindow.appId; + const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue); + const iconResult = ThemeIcons.iconForAppId(normalizedId.toLowerCase()); + if (iconResult && iconResult !== "") { + return iconResult; + } + } catch (iconError) { + Logger.w("ActiveWindow", "Error getting icon from CompositorService:", iconError); + } + } + + if (CompositorService.isHyprland) { + // Fallback to ToplevelManager + if (ToplevelManager && ToplevelManager.activeToplevel) { + try { + const activeToplevel = ToplevelManager.activeToplevel; + if (activeToplevel.appId) { + const idValue2 = activeToplevel.appId; + const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2); + const iconResult2 = ThemeIcons.iconForAppId(normalizedId2.toLowerCase()); + if (iconResult2 && iconResult2 !== "") { + return iconResult2; + } + } + } catch (fallbackError) { + Logger.w("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError); + } + } + } + + return ThemeIcons.iconFromName(fallbackIcon); + } catch (e) { + Logger.w("ActiveWindow", "Error in getAppIcon:", e); + return ThemeIcons.iconFromName(fallbackIcon); + } + } + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + Rectangle { + id: windowActiveRect + visible: root.visible + x: isVerticalBar ? Style.pixelAlignCenter(parent.width, width) : 0 + y: isVerticalBar ? 0 : Style.pixelAlignCenter(parent.height, height) + width: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : verticalSize) : ((!hasFocusedWindow) && (hideMode === "hidden") ? 0 : dynamicWidth) + height: isVerticalBar ? ((!hasFocusedWindow) && hideMode === "hidden" ? 0 : verticalSize) : capsuleHeight + radius: Style.radiusM + color: Style.capsuleColor + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + // Smooth width transition + Behavior on width { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutCubic + } + } + + Item { + id: mainContainer + anchors.fill: parent + anchors.leftMargin: isVerticalBar ? 0 : Style.marginS + anchors.rightMargin: isVerticalBar ? 0 : Style.marginS + + // Horizontal layout for top/bottom bars + RowLayout { + id: rowLayout + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS + visible: !isVerticalBar + z: 1 + + // Window icon + Item { + Layout.preferredWidth: iconSize + Layout.preferredHeight: iconSize + Layout.alignment: Qt.AlignVCenter + visible: showIcon + + IconImage { + id: windowIcon + anchors.fill: parent + source: getAppIcon() + asynchronous: true + smooth: true + visible: source !== "" + + // Apply dock shader to active window icon (always themed) + layer.enabled: widgetSettings.colorizeIcons !== false + layer.effect: ShaderEffect { + property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant + property real colorizeMode: 0.0 // Dock mode (grayscale) + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") + } + } + } + + NScrollText { + id: titleContainer + text: windowTitle + Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: root.capsuleHeight + fadeRoundLeftCorners: !showIcon + visible: showText + + maxWidth: { + // Calculate available width based on other elements + var iconWidth = (showIcon && windowIcon.visible ? (iconSize + Style.marginS) : 0); + var totalMargins = Style.margin2XXS; + var availableWidth = mainContainer.width - iconWidth - totalMargins; + return Math.max(20, availableWidth); + } + scrollMode: { + if (scrollingMode === "always") + return NScrollText.ScrollMode.Always; + if (scrollingMode === "hover") + return NScrollText.ScrollMode.Hover; + return NScrollText.ScrollMode.Never; + } + forcedHover: mainMouseArea.containsMouse + fadeExtent: 0.1 + fadeCornerRadius: Style.radiusM + + NText { + text: windowTitle + pointSize: barFontSize + applyUiScale: false + font.weight: Style.fontWeightMedium + color: root.textColor + } + } + } + + // Vertical layout for left/right bars - icon only + Item { + id: verticalLayout + width: parent.width - Style.margin2M + height: parent.height - Style.margin2M + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, height) + visible: isVerticalBar + z: 1 + + // Window icon + Item { + id: verticalIconContainer + width: root.iconSize + height: width + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, height) + visible: windowTitle !== "" + + IconImage { + id: windowIconVertical + anchors.fill: parent + source: getAppIcon() + asynchronous: true + smooth: true + visible: source !== "" + + // Apply dock shader to active window icon (always themed) + layer.enabled: widgetSettings.colorizeIcons !== false + layer.effect: ShaderEffect { + property color targetColor: Color.mOnSurface + property real colorizeMode: 0.0 // Dock mode (grayscale) + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") + } + } + } + } + + // Mouse area moved to root + } + } + + // Mouse area for hover detection + MouseArea { + id: mainMouseArea + anchors.fill: parent + + // Extend click area to screen edge if widget is at the start/end + anchors.leftMargin: (!isVerticalBar && section === "left" && sectionWidgetIndex === 0) ? -Style.marginS : 0 + anchors.rightMargin: (!isVerticalBar && section === "right" && sectionWidgetIndex === sectionWidgetsCount - 1) ? -Style.marginS : 0 + anchors.topMargin: (isVerticalBar && section === "left" && sectionWidgetIndex === 0) ? -Style.marginM : 0 + anchors.bottomMargin: (isVerticalBar && section === "right" && sectionWidgetIndex === sectionWidgetsCount - 1) ? -Style.marginM : 0 + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + onEntered: { + if ((windowTitle !== "") && isVerticalBar || (scrollingMode === "never")) { + TooltipService.show(root, windowTitle, BarService.getTooltipDirection(root.screen?.name)); + } + } + onExited: { + TooltipService.hide(); + } + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + PanelService.showContextMenu(contextMenu, root, screen); + } + } + } + + Connections { + target: CompositorService + function onActiveWindowChanged() { + try { + windowIcon.source = Qt.binding(getAppIcon); + windowIconVertical.source = Qt.binding(getAppIcon); + } catch (e) { + Logger.w("ActiveWindow", "Error in onActiveWindowChanged:", e); + } + } + function onWindowListChanged() { + try { + windowIcon.source = Qt.binding(getAppIcon); + windowIconVertical.source = Qt.binding(getAppIcon); + } catch (e) { + Logger.w("ActiveWindow", "Error in onWindowListChanged:", e); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/AudioVisualizer.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/AudioVisualizer.qml new file mode 100644 index 0000000..f13701d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/AudioVisualizer.qml @@ -0,0 +1,216 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Services.Media +import qs.Services.UI +import qs.Widgets +import qs.Widgets.AudioSpectrum + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" + readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName) + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + // Resolve settings: try user settings or defaults from BarWidgetRegistry + readonly property int visualizerWidth: widgetSettings.width !== undefined ? widgetSettings.width : widgetMetadata.width + readonly property bool hideWhenIdle: widgetSettings.hideWhenIdle !== undefined ? widgetSettings.hideWhenIdle : widgetMetadata.hideWhenIdle + readonly property string colorName: widgetSettings.colorName !== undefined ? widgetSettings.colorName : widgetMetadata.colorName + + readonly property color fillColor: Color.resolveColorKey(colorName) + + readonly property bool shouldShow: (currentVisualizerType !== "" && currentVisualizerType !== "none") && (!hideWhenIdle || MediaService.isPlaying) + + // Register/unregister with SpectrumService based on visibility (use screenName — screen can be null after DPMS/output changes) + readonly property string spectrumComponentId: "bar:audiovisualizer:" + screenName + ":" + root.section + ":" + root.sectionWidgetIndex + + onShouldShowChanged: { + if (root.shouldShow) { + SpectrumService.registerComponent(root.spectrumComponentId); + } else { + SpectrumService.unregisterComponent(root.spectrumComponentId); + } + } + + Component.onDestruction: { + if (root.shouldShow) { + SpectrumService.unregisterComponent(root.spectrumComponentId); + } + } + + // Content dimensions for implicit sizing + readonly property real contentWidth: !shouldShow ? 0 : isVerticalBar ? capsuleHeight : visualizerWidth + readonly property real contentHeight: !shouldShow ? 0 : isVerticalBar ? visualizerWidth : capsuleHeight + + implicitWidth: contentWidth + implicitHeight: contentHeight + visible: shouldShow + opacity: shouldShow ? 1.0 : 0.0 + + Behavior on implicitWidth { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutCubic + } + } + Behavior on implicitHeight { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutCubic + } + } + + // Store visualizer type to force re-evaluation + readonly property string currentVisualizerType: Settings.data.audio.visualizerType + + // Visual capsule centered in parent + Rectangle { + id: background + width: root.contentWidth + height: root.contentHeight + anchors.centerIn: parent + radius: Style.radiusS + color: Style.capsuleColor + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + // When visualizer type or playback changes, shouldShow updates automatically + // The Loader dynamically loads the appropriate visualizer based on settings + Loader { + id: visualizerLoader + anchors.fill: parent + anchors.margins: Style.marginS + active: shouldShow + asynchronous: true + + sourceComponent: { + switch (currentVisualizerType) { + case "linear": + return linearComponent; + case "mirrored": + return mirroredComponent; + case "wave": + return waveComponent; + default: + return null; + } + } + } + } + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.cycle-visualizer"), + "action": "cycle-visualizer", + "icon": "chart-column" + }, + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + if (screen) { + PanelService.closeContextMenu(screen); + } + + if (action === "cycle-visualizer") { + const types = ["linear", "mirrored", "wave"]; + const currentIndex = types.indexOf(currentVisualizerType); + const nextIndex = (currentIndex + 1) % types.length; + Settings.data.audio.visualizerType = types[nextIndex]; + } else if (action === "widget-settings" && screen) { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + // Click to cycle through visualizer types + MouseArea { + id: mouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + if (screen) { + PanelService.showContextMenu(contextMenu, root, screen); + } + } else { + const types = ["linear", "mirrored", "wave"]; + const currentIndex = types.indexOf(currentVisualizerType); + const nextIndex = (currentIndex + 1) % types.length; + Settings.data.audio.visualizerType = types[nextIndex]; + } + } + } + + Component { + id: linearComponent + NLinearSpectrum { + anchors.fill: parent + values: SpectrumService.values + fillColor: root.fillColor + showMinimumSignal: true + vertical: root.isVerticalBar + barPosition: root.barPosition + mirrored: Settings.data.audio.spectrumMirrored + } + } + + Component { + id: mirroredComponent + NMirroredSpectrum { + anchors.fill: parent + values: SpectrumService.values + fillColor: root.fillColor + showMinimumSignal: true + vertical: root.isVerticalBar + mirrored: Settings.data.audio.spectrumMirrored + } + } + + Component { + id: waveComponent + NWaveSpectrum { + anchors.fill: parent + values: SpectrumService.values + fillColor: root.fillColor + showMinimumSignal: true + vertical: root.isVerticalBar + mirrored: Settings.data.audio.spectrumMirrored + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Battery.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Battery.qml new file mode 100644 index 0000000..9e87cf8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Battery.qml @@ -0,0 +1,263 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.UPower +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Services.Hardware +import qs.Services.Networking +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isBarVertical: barPosition === "left" || barPosition === "right" + readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName) + + readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode + readonly property bool useGraphicMode: displayMode === "graphic" || displayMode === "graphic-clean" + + readonly property bool hideIfNotDetected: widgetSettings.hideIfNotDetected !== undefined ? widgetSettings.hideIfNotDetected : widgetMetadata.hideIfNotDetected + readonly property bool hideIfIdle: widgetSettings.hideIfIdle !== undefined ? widgetSettings.hideIfIdle : widgetMetadata.hideIfIdle + + // Check if selected device is actually present/connected + readonly property bool isReady: BatteryService.isDeviceReady(selectedDevice) + readonly property bool isPresent: BatteryService.isDevicePresent(selectedDevice) + readonly property real percent: isReady ? BatteryService.getPercentage(selectedDevice) : -1 + readonly property bool isCharging: isReady ? BatteryService.isCharging(selectedDevice) : false + readonly property bool isPluggedIn: isReady ? BatteryService.isPluggedIn(selectedDevice) : false + readonly property bool isLowBattery: isReady ? BatteryService.isLowBattery(selectedDevice) : false + readonly property bool isCriticalBattery: isReady ? BatteryService.isCriticalBattery(selectedDevice) : false + + // Visibility: show if hideIfNotDetected is false, or if battery is ready + readonly property bool shouldShow: !hideIfNotDetected || (isReady && (hideIfIdle ? !isPluggedIn : true)) + readonly property string deviceNativePath: widgetSettings.deviceNativePath !== undefined ? widgetSettings.deviceNativePath : widgetMetadata.deviceNativePath + readonly property var selectedDevice: BatteryService.isDevicePresent(BatteryService.findDevice(deviceNativePath)) ? BatteryService.findDevice(deviceNativePath) : null + + readonly property var tooltipContent: { + if (!isReady || !isPresent) { + return I18n.tr("battery.no-battery-detected"); + } + + let rows = []; + const isInternal = selectedDevice.isLaptopBattery; + if (isInternal) { + // Show charge percentage + rows.push([I18n.tr("battery.battery-level"), `${percent}%`]); + + let timeText = BatteryService.getTimeRemainingText(selectedDevice); + if (timeText) { + const colonIdx = timeText.indexOf(":"); + if (colonIdx >= 0) { + rows.push([timeText.substring(0, colonIdx).trim(), timeText.substring(colonIdx + 1).trim()]); + } else { + rows.push([timeText, ""]); + } + } + + let rateText = BatteryService.getRateText(selectedDevice); + if (!isPluggedIn && rateText) { + const colonIdx = rateText.indexOf(":"); + if (colonIdx >= 0) { + rows.push([rateText.substring(0, colonIdx).trim(), rateText.substring(colonIdx + 1).trim()]); + } else { + rows.push([rateText, ""]); + } + } + + // Show battery health if supported (check actual battery, not DisplayDevice) + let healthDevice = selectedDevice.healthSupported ? selectedDevice : (BatteryService.laptopBatteries.length > 0 ? BatteryService.laptopBatteries[0] : null); + if (healthDevice && healthDevice.healthSupported) { + rows.push([I18n.tr("battery.battery-health"), `${Math.round(healthDevice.healthPercentage)}%`]); + } + } else if (selectedDevice) { + // External / Peripheral Device (Phone, Keyboard, Mouse, Gamepad, Headphone etc.) + let name = BatteryService.getDeviceName(selectedDevice); + rows.push([name, `${percent}%`]); + } + + // If we are showing the main laptop battery, append external devices + if (isInternal) { + var external = BatteryService.bluetoothBatteries; + if (external.length > 0) { + if (rows.length > 0) { + rows.push(["---", "---"]); // Separator + } + for (var j = 0; j < external.length; j++) { + var dev = external[j]; + var dName = BatteryService.getDeviceName(dev); + var dPct = BatteryService.getPercentage(dev); + rows.push([dName, `${dPct}%`]); + } + } + } + + return rows; + } + + visible: shouldShow + opacity: shouldShow ? 1.0 : 0.0 + + implicitWidth: useGraphicMode ? capsule.width : pill.width + implicitHeight: useGraphicMode ? capsule.height : pill.height + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + // ==================== GRAPHIC MODE ==================== + + // Capsule background (graphic mode only) + Rectangle { + id: capsule + visible: root.useGraphicMode + anchors.centerIn: nBattery + width: root.isBarVertical ? root.capsuleHeight : nBattery.width + Style.margin2S + height: root.isBarVertical ? nBattery.height + Style.margin2S : root.capsuleHeight + radius: Math.min(Style.radiusL, width / 2) + color: graphicMouseArea.containsMouse ? Color.mHover : Style.capsuleColor + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + } + + NBattery { + id: nBattery + visible: root.useGraphicMode + anchors.centerIn: parent + baseSize: (Style.getBarHeightForScreen(root.screenName) / root.capsuleHeight) * Style.fontSizeXXS + showPercentageText: root.displayMode !== "graphic-clean" + vertical: root.isBarVertical + percentage: root.percent + ready: root.isReady + charging: root.isCharging + pluggedIn: root.isPluggedIn + low: root.isLowBattery + critical: root.isCriticalBattery + baseColor: graphicMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface + textColor: graphicMouseArea.containsMouse ? Color.mHover : Color.mSurface + } + + MouseArea { + id: graphicMouseArea + visible: root.useGraphicMode + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + cursorShape: Qt.PointingHandCursor + onEntered: { + if (!getBatteryPanel()?.isPanelOpen && root.tooltipContent) { + TooltipService.show(root, root.tooltipContent, BarService.getTooltipDirection(root.screen?.name)); + tooltipRefreshTimer.start(); + } + } + onExited: { + tooltipRefreshTimer.stop(); + TooltipService.hide(); + } + onClicked: mouse => { + TooltipService.hide(); + if (mouse.button === Qt.RightButton) { + PanelService.showContextMenu(contextMenu, nBattery, screen); + } else { + toggleBatteryPanel(); + } + } + } + + Timer { + id: tooltipRefreshTimer + interval: 1000 + repeat: true + onTriggered: { + if (graphicMouseArea.containsMouse) { + TooltipService.updateText(root.tooltipContent); + } + } + } + + // ==================== ICON MODE ==================== + + BarPill { + id: pill + visible: !root.useGraphicMode + screen: root.screen + oppositeDirection: BarService.getPillDirection(root) + icon: BatteryService.getIcon(root.percent, root.isCharging, root.isPluggedIn, root.isReady) + text: root.isReady ? root.percent : "-" + suffix: "%" + autoHide: false + forceOpen: root.isReady && root.displayMode === "icon-always" + forceClose: root.displayMode === "icon-only" || !root.isReady + customBackgroundColor: root.isCharging ? Color.mPrimary : ((root.isLowBattery || root.isCriticalBattery) ? Color.mError : "transparent") + customTextIconColor: root.isCharging ? Color.mOnPrimary : ((root.isLowBattery || root.isCriticalBattery) ? Color.mOnError : "transparent") + tooltipText: !getBatteryPanel()?.isPanelOpen ? root.tooltipContent : "" + onClicked: toggleBatteryPanel() + onRightClicked: PanelService.showContextMenu(contextMenu, pill, screen) + } + + // ==================== SHARED ==================== + + function getBatteryPanel() { + var panel = PanelService.getPanel("batteryPanel", screen); + if (panel) { + panel.panelID = { + showPowerProfiles: widgetSettings.showPowerProfiles !== undefined ? widgetSettings.showPowerProfiles : widgetMetadata.showPowerProfiles, + showNoctaliaPerformance: widgetSettings.showNoctaliaPerformance !== undefined ? widgetSettings.showNoctaliaPerformance : widgetMetadata.showNoctaliaPerformance + }; + } + return panel; + } + + function toggleBatteryPanel() { + getBatteryPanel()?.toggle(root); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Bluetooth.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Bluetooth.qml new file mode 100644 index 0000000..1b24d51 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Bluetooth.qml @@ -0,0 +1,122 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Modules.Panels.Settings // For SettingsPanel +import qs.Services.Networking +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isBarVertical: barPosition === "left" || barPosition === "right" + readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode + readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor + readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor + + implicitWidth: pill.width + implicitHeight: pill.height + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": BluetoothService.enabled ? I18n.tr("actions.disable-bluetooth") : I18n.tr("actions.enable-bluetooth"), + "action": "toggle-bluetooth", + "icon": BluetoothService.enabled ? "bluetooth-off" : "bluetooth", + "enabled": !NetworkService.airplaneModeEnabled && BluetoothService.bluetoothAvailable + }, + { + "label": I18n.tr("common.bluetooth") + " " + I18n.tr("tooltips.open-settings"), + "action": "bluetooth-settings", + "icon": "settings" + }, + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "toggle-bluetooth") { + BluetoothService.setBluetoothEnabled(!BluetoothService.enabled); + } else if (action === "bluetooth-settings") { + SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 1, screen); + } else if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + BarPill { + id: pill + + screen: root.screen + oppositeDirection: BarService.getPillDirection(root) + customIconColor: Color.resolveColorKeyOptional(root.iconColorKey) + customTextColor: Color.resolveColorKeyOptional(root.textColorKey) + icon: !BluetoothService.enabled ? "bluetooth-off" : ((BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 0) ? "bluetooth-connected" : "bluetooth") + text: { + if (BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 0) { + const firstDevice = BluetoothService.connectedDevices[0]; + return firstDevice.name || firstDevice.deviceName; + } + return ""; + } + suffix: { + if (BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 1) { + return ` + ${BluetoothService.connectedDevices.length - 1}`; + } + return ""; + } + autoHide: false + forceOpen: !isBarVertical && root.displayMode === "alwaysShow" + forceClose: isBarVertical || root.displayMode === "alwaysHide" || text === "" + onClicked: { + var p = PanelService.getPanel("bluetoothPanel", screen); + if (p) + p.toggle(this); + } + onRightClicked: { + PanelService.showContextMenu(contextMenu, pill, screen); + } + tooltipText: { + if (PanelService.getPanel("bluetoothPanel", screen)?.isPanelOpen) { + return ""; + } + if (pill.text !== "") { + return pill.text; + } + return I18n.tr("tooltips.bluetooth-devices"); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Brightness.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Brightness.qml new file mode 100644 index 0000000..b82e0b4 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Brightness.qml @@ -0,0 +1,211 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Modules.Panels.Settings +import qs.Services.Hardware +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isBarVertical: barPosition === "left" || barPosition === "right" + readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode + readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor + readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor + readonly property bool applyToAllMonitors: widgetSettings.applyToAllMonitors !== undefined ? widgetSettings.applyToAllMonitors : (Settings.data.brightness.syncAllMonitors !== undefined ? Settings.data.brightness.syncAllMonitors : widgetMetadata.applyToAllMonitors) + readonly property bool reverseScroll: Settings.data.general.reverseScroll + + // Used to avoid opening the pill on Quickshell startup + property bool firstBrightnessReceived: false + + implicitWidth: pill.width + implicitHeight: pill.height + + // Track the brightness monitor reactively via declarative binding so it + // updates atomically when monitors change, avoiding a transient undefined + // state that occurs when Monitor QtObjects are destroyed before the + // imperative updateMonitor() call would run. + property var brightnessMonitor: { + var _ = BrightnessService.monitors; // reactive dependency + var __ = BrightnessService.ddcMonitors; // reactive dependency + if (!screen) + return null; + return BrightnessService.getMonitorForScreen(screen) ?? null; + } + + function getControllableMonitorCount() { + var monitors = BrightnessService.monitors || []; + var count = 0; + for (var i = 0; i < monitors.length; i++) { + if (monitors[i] && monitors[i].brightnessControlAvailable) + count++; + } + return count; + } + + visible: brightnessMonitor !== null + opacity: brightnessMonitor !== null ? 1.0 : 0.0 + + function getIcon() { + var monitor = brightnessMonitor; + if (!monitor || !monitor.brightnessControlAvailable || isNaN(monitor.brightness)) + return "sun-off"; + var brightness = monitor.brightness; + if (brightness <= 0.001) + return "sun-off"; + return brightness <= 0.5 ? "brightness-low" : "brightness-high"; + } + + // Connection used to open the pill when brightness changes + Connections { + target: brightnessMonitor + ignoreUnknownSignals: true + function onBrightnessUpdated() { + // Ignore if this is the first time we receive an update. + // Most likely service just kicked off. + if (!firstBrightnessReceived) { + firstBrightnessReceived = true; + return; + } + + pill.show(); + hideTimerAfterChange.restart(); + } + } + + Timer { + id: hideTimerAfterChange + interval: 2500 + running: false + repeat: false + onTriggered: pill.hide() + } + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.open-display-settings"), + "action": "open-display-settings", + "icon": "sun" + }, + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "open-display-settings") { + var settingsPanel = PanelService.getPanel("settingsPanel", screen); + settingsPanel.requestedTab = SettingsPanel.Tab.Display; + settingsPanel.open(); + } else if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + BarPill { + id: pill + + screen: root.screen + oppositeDirection: BarService.getPillDirection(root) + customIconColor: Color.resolveColorKeyOptional(root.iconColorKey) + customTextColor: Color.resolveColorKeyOptional(root.textColorKey) + icon: getIcon() + autoHide: false // Important to be false so we can hover as long as we want + text: { + var monitor = brightnessMonitor; + if (!monitor || !monitor.brightnessControlAvailable || isNaN(monitor.brightness)) + return ""; + return Math.round(monitor.brightness * 100); + } + suffix: text.length > 0 ? "%" : "-" + forceOpen: displayMode === "alwaysShow" + forceClose: displayMode === "alwaysHide" + tooltipText: { + var monitor = brightnessMonitor; + var panel = PanelService.getPanel("brightnessPanel", screen); + if (panel?.isPanelOpen || !monitor || !monitor.brightnessControlAvailable || isNaN(monitor.brightness)) + return ""; + return I18n.tr("tooltips.brightness-at", { + "brightness": Math.round(monitor.brightness * 100) + }); + } + + onWheel: function (angle) { + var monitor = brightnessMonitor; + if (!monitor || !monitor.brightnessControlAvailable) + return; + + if (root.reverseScroll) + angle *= -1; + + if (angle === 0) + return; + + var shouldApplyToAll = root.applyToAllMonitors && root.getControllableMonitorCount() > 1; + if (shouldApplyToAll) { + var direction = angle > 0 ? 1 : -1; + var baseValue = !isNaN(monitor.queuedBrightness) ? monitor.queuedBrightness : monitor.brightness; + var step = monitor.stepSize; + var minValue = monitor.minBrightnessValue; + + if (direction > 0 && Settings.data.brightness.enforceMinimum && baseValue < minValue) { + baseValue = Math.max(step, minValue); + } else { + baseValue = baseValue + direction * step; + } + + var targetValue = Math.max(minValue, Math.min(1, baseValue)); + + BrightnessService.monitors.forEach(function (m) { + if (m && m.brightnessControlAvailable) { + m.setBrightnessDebounced(targetValue); + } + }); + } else if (angle > 0) { + monitor.increaseBrightness(); + } else if (angle < 0) { + monitor.decreaseBrightness(); + } + } + + onClicked: PanelService.getPanel("brightnessPanel", screen)?.toggle(this) + + onRightClicked: { + PanelService.showContextMenu(contextMenu, pill, screen); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Clock.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Clock.qml new file mode 100644 index 0000000..7dbafa6 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Clock.qml @@ -0,0 +1,219 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isBarVertical: barPosition === "left" || barPosition === "right" + readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName) + readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName) + readonly property var now: Time.now + + // Resolve settings: try user settings or defaults from BarWidgetRegistry + readonly property string clockColor: widgetSettings.clockColor !== undefined ? widgetSettings.clockColor : widgetMetadata.clockColor + readonly property bool useCustomFont: widgetSettings.useCustomFont !== undefined ? widgetSettings.useCustomFont : widgetMetadata.useCustomFont + readonly property string customFont: widgetSettings.customFont !== undefined ? widgetSettings.customFont : widgetMetadata.customFont + readonly property string formatHorizontal: widgetSettings.formatHorizontal !== undefined ? widgetSettings.formatHorizontal : widgetMetadata.formatHorizontal + readonly property string formatVertical: widgetSettings.formatVertical !== undefined ? widgetSettings.formatVertical : widgetMetadata.formatVertical + readonly property string tooltipFormat: widgetSettings.tooltipFormat !== undefined ? widgetSettings.tooltipFormat : widgetMetadata.tooltipFormat + + readonly property color textColor: Color.resolveColorKey(clockColor) + + // Content dimensions for implicit sizing + readonly property real contentWidth: isBarVertical ? capsuleHeight : Math.round((isBarVertical ? verticalLoader.implicitWidth : horizontalLoader.implicitWidth) + Style.margin2M) + readonly property real contentHeight: isBarVertical ? Math.round(verticalLoader.implicitHeight + Style.margin2S) : capsuleHeight + + // Size: use implicit width/height + // BarWidgetLoader sets explicit width/height to extend click area + implicitWidth: contentWidth + implicitHeight: contentHeight + + // Visual clock capsule - stays at content size, centered in parent + Rectangle { + id: visualClock + width: root.contentWidth + height: root.contentHeight + anchors.centerIn: parent + + radius: Style.radiusL + color: Style.capsuleColor + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + Item { + id: clockContainer + anchors.centerIn: parent + + // Horizontal + Loader { + id: horizontalLoader + active: !isBarVertical + anchors.centerIn: parent + sourceComponent: ColumnLayout { + anchors.centerIn: parent + spacing: Settings.data.bar.showCapsule ? -5 : -3 + Repeater { + id: repeater + model: I18n.locale.toString(now, formatHorizontal.trim()).split("\\n") + NText { + visible: text !== "" + text: modelData + family: useCustomFont && customFont ? customFont : Settings.data.ui.fontDefault + Binding on pointSize { + value: { + if (repeater.model.length == 1) { + // Single line: Full size + return barFontSize; + } else if (repeater.model.length == 2) { + // Two lines: First line is bigger than the second + return (index == 0) ? Math.round(barFontSize * 0.9) : Math.round(barFontSize * 0.75); + } else { + // More than two lines: Make it small! + return Math.round(barFontSize * 0.75); + } + } + } + applyUiScale: false + color: textColor + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + features: ({ + "tnum": 1 + }) + } + } + } + } + + // Vertical + Loader { + id: verticalLoader + active: isBarVertical + anchors.centerIn: parent // Now this works without layout conflicts + sourceComponent: ColumnLayout { + anchors.centerIn: parent + spacing: -2 + Repeater { + model: I18n.locale.toString(now, formatVertical.trim()).split(" ") + delegate: NText { + visible: text !== "" + text: modelData + family: useCustomFont && customFont ? customFont : Settings.data.ui.fontDefault + pointSize: barFontSize + applyUiScale: false + color: textColor + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + features: ({ + "tnum": 1 + }) + } + } + } + } + } + } + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.open-calendar"), + "action": "open-calendar", + "icon": "calendar" + }, + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + // Close the context menu + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "open-calendar") { + PanelService.getPanel("clockPanel", screen)?.toggle(root); + } else if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + // Build tooltip text with formatted time/date + function buildTooltipText() { + if (tooltipFormat && tooltipFormat.trim() !== "") { + return I18n.locale.toString(now, tooltipFormat.trim()); + } + // Fallback to default if no format is set + return I18n.tr("common.calendar"); // Defaults to "Calendar" + } + + MouseArea { + id: clockMouseArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + onEntered: { + if (!PanelService.getPanel("clockPanel", screen)?.isPanelOpen) { + TooltipService.show(root, buildTooltipText(), BarService.getTooltipDirection(root.screen?.name)); + tooltipRefreshTimer.start(); + } + } + onExited: { + tooltipRefreshTimer.stop(); + TooltipService.hide(); + } + onClicked: mouse => { + TooltipService.hide(); + if (mouse.button === Qt.RightButton) { + PanelService.showContextMenu(contextMenu, root, screen); + } else { + PanelService.getPanel("clockPanel", screen)?.toggle(this); + } + } + } + + Timer { + id: tooltipRefreshTimer + interval: 1000 + repeat: true + onTriggered: { + if (clockMouseArea.containsMouse && !PanelService.getPanel("clockPanel", screen)?.isPanelOpen) { + TooltipService.updateText(buildTooltipText()); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/ControlCenter.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/ControlCenter.qml new file mode 100644 index 0000000..0621bcf --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/ControlCenter.qml @@ -0,0 +1,144 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Widgets +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Modules.Panels.Settings +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +NIconButton { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string customIcon: widgetSettings.icon !== undefined ? widgetSettings.icon : widgetMetadata.icon + readonly property bool useDistroLogo: widgetSettings.useDistroLogo !== undefined ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo + readonly property string customIconPath: widgetSettings.customIconPath !== undefined ? widgetSettings.customIconPath : widgetMetadata.customIconPath + readonly property bool enableColorization: widgetSettings.enableColorization !== undefined ? widgetSettings.enableColorization : widgetMetadata.enableColorization + readonly property string colorizeSystemIcon: widgetSettings.colorizeSystemIcon !== undefined ? widgetSettings.colorizeSystemIcon : widgetMetadata.colorizeSystemIcon + + readonly property color iconColor: { + if (!enableColorization) + return Color.mOnSurface; + return Color.resolveColorKey(colorizeSystemIcon); + } + + // If we have a custom path and not using distro logo, use the theme icon. + // If using distro logo, don't use theme icon. + icon: (customIconPath === "" && !useDistroLogo) ? customIcon : "" + tooltipText: { + if (!screen || PanelService.getPanel("controlCenterPanel", screen)?.isPanelOpen) { + return ""; + } else { + return I18n.tr("tooltips.open-control-center"); + } + } + tooltipDirection: BarService.getTooltipDirection(screen?.name) + baseSize: Style.getCapsuleHeightForScreen(screen?.name) + applyUiScale: false + customRadius: Style.radiusL + colorBg: Style.capsuleColor + colorFg: iconColor + colorBgHover: Color.mHover + colorFgHover: Color.mOnHover + colorBorder: Style.capsuleBorderColor + colorBorderHover: Style.capsuleBorderColor + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.open-launcher"), + "action": "open-launcher", + "icon": "search" + }, + { + "label": I18n.tr("actions.open-settings"), + "action": "open-settings", + "icon": "adjustments" + }, + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "open-launcher") { + PanelService.toggleLauncher(screen); + } else if (action === "open-settings") { + var panel = PanelService.getPanel("settingsPanel", screen); + panel.requestedTab = SettingsPanel.Tab.General; + panel.toggle(); + } else if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + onClicked: { + var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen); + if (Settings.data.controlCenter.position === "close_to_bar_button") { + // Will open the panel next to the bar button. + controlCenterPanel?.toggle(this); + } else { + controlCenterPanel?.toggle(); + } + } + onRightClicked: { + PanelService.showContextMenu(contextMenu, root, screen); + } + onMiddleClicked: PanelService.toggleLauncher(screen) + + IconImage { + id: customOrDistroLogo + anchors.centerIn: parent + width: root.buttonSize * 0.8 + height: width + source: { + if (useDistroLogo) + return HostService.osLogo; + if (customIconPath !== "") + return customIconPath.startsWith("file://") ? customIconPath : "file://" + customIconPath; + return ""; + } + visible: source !== "" + smooth: true + asynchronous: true + layer.enabled: (enableColorization) && (useDistroLogo || customIconPath !== "") + layer.effect: ShaderEffect { + property color targetColor: !hovering ? iconColor : Color.mOnHover + property real colorizeMode: 2.0 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/CustomButton.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/CustomButton.qml new file mode 100644 index 0000000..ed5796c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/CustomButton.qml @@ -0,0 +1,676 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Modules.Panels.Settings +import qs.Services.Control +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" + + readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon + readonly property string leftClickExec: widgetSettings.leftClickExec || widgetMetadata.leftClickExec + readonly property bool leftClickUpdateText: widgetSettings.leftClickUpdateText ?? widgetMetadata.leftClickUpdateText + readonly property string rightClickExec: widgetSettings.rightClickExec || widgetMetadata.rightClickExec + readonly property bool rightClickUpdateText: widgetSettings.rightClickUpdateText ?? widgetMetadata.rightClickUpdateText + readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec + readonly property bool middleClickUpdateText: widgetSettings.middleClickUpdateText ?? widgetMetadata.middleClickUpdateText + readonly property string ipcIdentifier: widgetSettings.ipcIdentifier !== undefined ? widgetSettings.ipcIdentifier : (widgetMetadata.ipcIdentifier || "") + readonly property string wheelExec: widgetSettings.wheelExec || widgetMetadata.wheelExec + readonly property string wheelUpExec: widgetSettings.wheelUpExec || widgetMetadata.wheelUpExec + readonly property string wheelDownExec: widgetSettings.wheelDownExec || widgetMetadata.wheelDownExec + readonly property string wheelMode: widgetSettings.wheelMode || widgetMetadata.wheelMode + readonly property bool wheelUpdateText: widgetSettings.wheelUpdateText ?? widgetMetadata.wheelUpdateText + readonly property bool wheelUpUpdateText: widgetSettings.wheelUpUpdateText ?? widgetMetadata.wheelUpUpdateText + readonly property bool wheelDownUpdateText: widgetSettings.wheelDownUpdateText ?? widgetMetadata.wheelDownUpdateText + readonly property string textCommand: widgetSettings.textCommand !== undefined ? widgetSettings.textCommand : (widgetMetadata.textCommand || "") + readonly property bool textStream: widgetSettings.textStream !== undefined ? widgetSettings.textStream : (widgetMetadata.textStream || false) + readonly property int textIntervalMs: widgetSettings.textIntervalMs !== undefined ? widgetSettings.textIntervalMs : (widgetMetadata.textIntervalMs || 3000) + readonly property string textCollapse: widgetSettings.textCollapse !== undefined ? widgetSettings.textCollapse : (widgetMetadata.textCollapse || "") + readonly property bool parseJson: widgetSettings.parseJson !== undefined ? widgetSettings.parseJson : (widgetMetadata.parseJson || false) + readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec || (wheelMode === "unified" && wheelExec) || (wheelMode === "separate" && (wheelUpExec || wheelDownExec))) + readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : true + readonly property bool showExecTooltip: widgetSettings.showExecTooltip !== undefined ? widgetSettings.showExecTooltip : (widgetMetadata.showExecTooltip !== undefined ? widgetMetadata.showExecTooltip : true) + readonly property bool showTextTooltip: widgetSettings.showTextTooltip !== undefined ? widgetSettings.showTextTooltip : (widgetMetadata.showTextTooltip !== undefined ? widgetMetadata.showTextTooltip : true) + readonly property string generalTooltipText: widgetSettings.generalTooltipText !== undefined ? widgetSettings.generalTooltipText : (widgetMetadata.generalTooltipText || "") + readonly property bool _hasCustomTooltip: generalTooltipText.trim() !== "" + readonly property string hideMode: widgetSettings.hideMode || "alwaysExpanded" + readonly property bool hasOutput: _dynamicText !== "" + readonly property bool shouldForceOpen: textStream && (hideMode === "alwaysExpanded" || hideMode === "maxTransparent") + + readonly property bool _useNewHideLogic: textCommand && textCommand.length > 0 && textStream + readonly property bool _useTextCommandLogic: textCommand && textCommand.length > 0 && textStream + + readonly property bool _pillVisible: { + if (!_useTextCommandLogic) { + return true; + } + if (hideMode === "alwaysExpanded" || hideMode === "maxTransparent") { + return true; + } + var hasActualIcon = (_dynamicIcon !== "" || customIcon !== ""); + return hasOutput || (showIcon && hasActualIcon); + } + + readonly property real _pillOpacity: { + if (!_useTextCommandLogic) { + return 1.0; + } + if (hideMode === "maxTransparent" && !hasOutput) { + return 0.0; + } + return 1.0; + } + + readonly property bool _pillForceOpen: { + if (!_useTextCommandLogic) { + return _dynamicText !== "" || (textStream && currentMaxTextLength > 0); + } + if (currentMaxTextLength <= 0) { + return false; + } + + if (hideMode === "alwaysExpanded" || hideMode === "maxTransparent") { + return true; + } + return hasOutput; + } + + readonly property string _pillIcon: { + if (!_useTextCommandLogic) { + if (textCommand && textCommand.length > 0 && showIcon) { + return _dynamicIcon !== "" ? _dynamicIcon : customIcon; + } else if (!(textCommand && textCommand.length > 0)) { + return _dynamicIcon !== "" ? _dynamicIcon : customIcon; + } else { + return ""; + } + } + if (!showIcon) + return ""; + var actualIcon = _dynamicIcon !== "" ? _dynamicIcon : customIcon; + if (hideMode === "expandWithOutput" && actualIcon === "") { + return "question-mark"; + } + return actualIcon; + } + + readonly property string _pillText: { + if (!_useTextCommandLogic) { + return (!isVerticalBar || currentMaxTextLength > 0) ? _dynamicText : ""; + } + if (currentMaxTextLength <= 0) { + return ""; + } + if (hasOutput) { + return _dynamicText; + } + if (hideMode === "expandWithOutput") { + return ""; + } + return " ".repeat(currentMaxTextLength); + } + + readonly property bool enableColorization: widgetSettings.enableColorization || false + readonly property string colorizeSystemIcon: { + if (widgetSettings.colorizeSystemIcon !== undefined) + return widgetSettings.colorizeSystemIcon; + return widgetMetadata.colorizeSystemIcon !== undefined ? widgetMetadata.colorizeSystemIcon : "none"; + } + + readonly property bool isColorizing: enableColorization && colorizeSystemIcon !== "none" + + // Get color value from color name (returns null for invalid names) + function _getColorValue(colorName, forHover) { + const baseColor = (function () { + switch (colorName) { + case "primary": + return Color.mPrimary; + case "secondary": + return Color.mSecondary; + case "tertiary": + return Color.mTertiary; + case "error": + return Color.mError; + default: + return null; + } + })(); + return baseColor !== null ? (forHover ? Qt.darker(baseColor, 1.2) : baseColor) : null; + } + + // Resolve icon color with priority: dynamic > static > default + function _resolveIconColor(dynamicColorName, staticColorName, isHover) { + if (dynamicColorName && dynamicColorName !== "") { + if (dynamicColorName === "none") { + return isHover ? Color.mOnHover : Color.mOnSurface; + } + const color = _getColorValue(dynamicColorName, isHover); + if (color !== null) + return color; + } + + if (staticColorName && staticColorName !== "" && isColorizing) { + const color = _getColorValue(staticColorName, isHover); + if (color !== null) + return color; + } + + return isHover ? Color.mOnHover : Color.mOnSurface; + } + + readonly property color iconColor: _resolveIconColor(_dynamicColor, colorizeSystemIcon, false) + readonly property color iconHoverColor: _resolveIconColor(_dynamicColor, colorizeSystemIcon, true) + + implicitWidth: pill.width + implicitHeight: pill.height + + BarPill { + id: pill + + visible: _pillVisible + opacity: _pillOpacity + screen: root.screen + oppositeDirection: BarService.getPillDirection(root) + icon: _pillIcon + text: _pillText + rotateText: isVerticalBar && currentMaxTextLength > 0 + autoHide: false + forceOpen: _pillForceOpen + forceClose: !_pillForceOpen + customTextIconColor: iconColor + + // Helper function to build tooltip content + function _buildTooltipContent() { + var lines = []; + + // Add custom tooltip if set + if (_hasCustomTooltip) { + lines.push(generalTooltipText); + } + + // Add command details if enabled and available + if (showExecTooltip && hasExec) { + if (leftClickExec !== "") { + lines.push(I18n.tr("bar.custom-button.left-click-label") + `: ${leftClickExec}`); + } else if (!leftClickUpdateText) { + lines.push(I18n.tr("bar.custom-button.left-click-label") + ": " + I18n.tr("actions.widget-settings")); + } + + if (rightClickExec !== "") { + lines.push(I18n.tr("bar.custom-button.right-click-label") + `: ${rightClickExec}`); + } else if (!rightClickUpdateText) { + lines.push(I18n.tr("bar.custom-button.right-click-label") + ": " + I18n.tr("actions.widget-settings")); + } + + if (middleClickExec !== "") { + lines.push(I18n.tr("bar.custom-button.middle-click-label") + `: ${middleClickExec}`); + } else if (!middleClickUpdateText) { + lines.push(I18n.tr("bar.custom-button.middle-click-label") + ": " + I18n.tr("actions.widget-settings")); + } + + if (wheelMode === "unified" && wheelExec !== "") { + lines.push(I18n.tr("bar.custom-button.wheel-label") + `: ${wheelExec}`); + } else if (wheelMode === "separate") { + if (wheelUpExec !== "") { + lines.push(I18n.tr("bar.custom-button.wheel-up") + `: ${wheelUpExec}`); + } + if (wheelDownExec !== "") { + lines.push(I18n.tr("bar.custom-button.wheel-down") + `Wheel down: ${wheelDownExec}`); + } + } + } + + // Add dynamic text tooltip if enabled and available + if (showTextTooltip && _dynamicTooltip !== "") { + lines.push(_dynamicTooltip); + } + + return lines; + } + + tooltipText: { + var lines = _buildTooltipContent(); + + // If no custom tooltip and both switches are off, show default tooltip + if (!_hasCustomTooltip && !showExecTooltip && !showTextTooltip) { + return I18n.tr("bar.custom-button.default-tooltip"); + } + + // If there's content, join with newlines + if (lines.length > 0) { + return lines.join("\n"); + } + + // Fallback (shouldn't reach here normally) + return I18n.tr("bar.custom-button.default-tooltip"); + } + + onClicked: root.clicked() + onRightClicked: root.rightClicked() + onMiddleClicked: root.middleClicked() + onWheel: delta => root.wheeled(delta) + } + + // Internal state for dynamic text + property string _dynamicText: "" + property string _dynamicIcon: "" + property string _dynamicTooltip: "" + property string _dynamicColor: "" + + // Maximum length for text display before scrolling (different values for horizontal and vertical) + readonly property var maxTextLength: { + "horizontal": ((widgetSettings && widgetSettings.maxTextLength && widgetSettings.maxTextLength.horizontal !== undefined) ? widgetSettings.maxTextLength.horizontal : ((widgetMetadata && widgetMetadata.maxTextLength && widgetMetadata.maxTextLength.horizontal !== undefined) ? widgetMetadata.maxTextLength.horizontal : 10)), + "vertical": ((widgetSettings && widgetSettings.maxTextLength && widgetSettings.maxTextLength.vertical !== undefined) ? widgetSettings.maxTextLength.vertical : ((widgetMetadata && widgetMetadata.maxTextLength && widgetMetadata.maxTextLength.vertical !== undefined) ? widgetMetadata.maxTextLength.vertical : 10)) + } + readonly property int _staticDuration: 6 // How many cycles to stay static at start/end + + // Encapsulated state for scrolling text implementation + property var _scrollState: { + "originalText": "", + "needsScrolling": false, + "offset": 0, + "phase": 0 // 0=static start, 1=scrolling, 2=static end + , + "phaseCounter": 0 + } + + // Current max text length based on bar orientation + readonly property int currentMaxTextLength: isVerticalBar ? maxTextLength.vertical : maxTextLength.horizontal + + // Periodically run the text command (if set) + Timer { + id: refreshTimer + interval: Math.max(250, textIntervalMs) + repeat: true + running: (!isVerticalBar || currentMaxTextLength > 0) && !textStream && textCommand && textCommand.length > 0 + triggeredOnStart: true + onTriggered: root.runTextCommand() + } + + // Restart exited text stream commands after a delay + Timer { + id: restartTimer + interval: 1000 + running: (!isVerticalBar || currentMaxTextLength > 0) && textStream && !textProc.running + onTriggered: root.runTextCommand() + } + + // Timer for scrolling text display + Timer { + id: scrollTimer + interval: 300 + repeat: true + running: false + onTriggered: { + if (_scrollState.needsScrolling && _scrollState.originalText.length > currentMaxTextLength) { + // Traditional marquee with pause at beginning and end + if (_scrollState.phase === 0) { + // Static at beginning + _dynamicText = _scrollState.originalText.substring(0, Math.min(currentMaxTextLength, _scrollState.originalText.length)); + _scrollState.phaseCounter++; + if (_scrollState.phaseCounter >= _staticDuration) { + _scrollState.phaseCounter = 0; + _scrollState.phase = 1; // Move to scrolling + } + } else if (_scrollState.phase === 1) { + // Scrolling + _scrollState.offset++; + var start = _scrollState.offset; + var end = start + currentMaxTextLength; + + if (start >= _scrollState.originalText.length - currentMaxTextLength) { + // Reached or passed the end, ensure we show the last part + var textEnd = _scrollState.originalText.length; + var textStart = Math.max(0, textEnd - currentMaxTextLength); + _dynamicText = _scrollState.originalText.substring(textStart, textEnd); + _scrollState.phase = 2; // Move to static end phase + _scrollState.phaseCounter = 0; + } else { + _dynamicText = _scrollState.originalText.substring(start, end); + } + } else if (_scrollState.phase === 2) { + // Static at end + // Ensure end text is displayed correctly + var textEnd = _scrollState.originalText.length; + var textStart = Math.max(0, textEnd - currentMaxTextLength); + _dynamicText = _scrollState.originalText.substring(textStart, textEnd); + _scrollState.phaseCounter++; + if (_scrollState.phaseCounter >= _staticDuration) { + // Do NOT loop back to start, just stop scrolling + scrollTimer.stop(); + } + } + } else { + scrollTimer.stop(); + } + } + } + + SplitParser { + id: textStdoutSplit + onRead: line => root.parseDynamicContent(line) + } + + StdioCollector { + id: textStdoutCollect + onStreamFinished: () => root.parseDynamicContent(this.text) + } + + Process { + id: textProc + stdout: textStream ? textStdoutSplit : textStdoutCollect + stderr: StdioCollector {} + onExited: (exitCode, exitStatus) => { + if (textStream) { + Logger.w("CustomButton", `Streaming text command exited (code: ${exitCode}), restarting...`); + return; + } + } + } + + function parseDynamicContent(content) { + var contentStr = String(content || "").trim(); + + if (parseJson && contentStr) { + // Handle multi-line JSON by filtering empty lines and joining + var jsonStr = contentStr; + if (contentStr.includes('\n')) { + const lines = contentStr.split('\n').filter(line => line.trim() !== ''); + jsonStr = lines.join(''); + } + + try { + const parsed = JSON.parse(jsonStr); + const text = parsed.text || ""; + const icon = parsed.icon || ""; + let tooltip = parsed.tooltip || ""; + const color = parsed.color || ""; + + // Validate color value + const validColors = ["primary", "secondary", "tertiary", "error", "none"]; + const validColor = (color && validColors.includes(color)) ? color : ""; + + if (checkCollapse(text)) { + _scrollState.originalText = ""; + _dynamicText = ""; + _dynamicIcon = ""; + _dynamicTooltip = ""; + _dynamicColor = ""; + _scrollState.needsScrolling = false; + _scrollState.phase = 0; + _scrollState.phaseCounter = 0; + return; + } + + _scrollState.originalText = text; + _scrollState.needsScrolling = text.length > currentMaxTextLength && currentMaxTextLength > 0; + if (_scrollState.needsScrolling) { + _dynamicText = text.substring(0, currentMaxTextLength); + _scrollState.phase = 0; + _scrollState.phaseCounter = 0; + _scrollState.offset = 0; + scrollTimer.start(); + } else { + _dynamicText = text; + scrollTimer.stop(); + } + _dynamicIcon = icon; + _dynamicColor = validColor; + + _dynamicTooltip = toHtml(tooltip); + _scrollState.offset = 0; + return; + } catch (e) { + Logger.w("CustomButton", `Failed to parse JSON. Content: "${contentStr}"`); + } + } + + if (checkCollapse(contentStr)) { + _scrollState.originalText = ""; + _dynamicText = ""; + _dynamicIcon = ""; + _dynamicTooltip = ""; + _dynamicColor = ""; + _scrollState.needsScrolling = false; + _scrollState.phase = 0; + _scrollState.phaseCounter = 0; + return; + } + + _scrollState.originalText = contentStr; + _scrollState.needsScrolling = contentStr.length > currentMaxTextLength && currentMaxTextLength > 0; + if (_scrollState.needsScrolling) { + _dynamicText = contentStr.substring(0, currentMaxTextLength); + _scrollState.phase = 0; + _scrollState.phaseCounter = 0; + _scrollState.offset = 0; + scrollTimer.start(); + } else { + _dynamicText = contentStr; + scrollTimer.stop(); + } + _dynamicIcon = ""; + _dynamicColor = ""; + _dynamicTooltip = toHtml(contentStr); + _scrollState.offset = 0; + } + + function checkCollapse(text) { + if (!textCollapse || textCollapse.length === 0) { + return false; + } + + if (textCollapse.startsWith("/") && textCollapse.endsWith("/") && textCollapse.length > 1) { + // Treat as regex + var pattern = textCollapse.substring(1, textCollapse.length - 1); + try { + var regex = new RegExp(pattern); + return regex.test(text); + } catch (e) { + Logger.w("CustomButton", `Invalid regex for textCollapse: ${textCollapse} - ${e.message}`); + return (textCollapse === text); // Fallback to exact match on invalid regex + } + } else { + // Treat as plain string + return (textCollapse === text); + } + } + + function clicked() { + if (leftClickExec) { + Quickshell.execDetached(["sh", "-lc", leftClickExec]); + Logger.i("CustomButton", `Executing command: ${leftClickExec}`); + } else if (!leftClickUpdateText) { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + if (!textStream && leftClickUpdateText) { + runTextCommand(); + } + } + + function rightClicked() { + if (rightClickExec) { + Quickshell.execDetached(["sh", "-lc", rightClickExec]); + Logger.i("CustomButton", `Executing command: ${rightClickExec}`); + } else if (!rightClickUpdateText) { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + if (!textStream && rightClickUpdateText) { + runTextCommand(); + } + } + + function middleClicked() { + if (middleClickExec) { + Quickshell.execDetached(["sh", "-lc", middleClickExec]); + Logger.i("CustomButton", `Executing command: ${middleClickExec}`); + } else if (!middleClickUpdateText) { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + if (!textStream && middleClickUpdateText) { + runTextCommand(); + } + } + + function toHtml(str) { + const htmlTagRegex = /<\/?[a-zA-Z][^>]*>/g; + const placeholders = []; + let i = 0; + const protectedStr = str.replace(htmlTagRegex, tag => { + placeholders.push(tag); + return `___HTML_TAG_${i++}___`; + }); + + let escaped = protectedStr.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/\r\n|\r|\n/g, "
"); + + escaped = escaped.replace(/___HTML_TAG_(\d+)___/g, (_, index) => placeholders[Number(index)]); + + return escaped; + } + + function runTextCommand() { + if (!textCommand || textCommand.length === 0) + return; + if (textProc.running) + return; + textProc.command = ["sh", "-lc", textCommand]; + textProc.running = true; + } + + function wheeled(delta) { + if (wheelMode === "unified" && wheelExec) { + let normalizedDelta = delta > 0 ? 1 : -1; + + let command = wheelExec.replace(/\$delta([+\-*/]\d+)?/g, function (match, operation) { + if (operation) { + try { + let operator = operation.charAt(0); + let operand = parseInt(operation.substring(1)); + + let result; + switch (operator) { + case '+': + result = normalizedDelta + operand; + break; + case '-': + result = normalizedDelta - operand; + break; + case '*': + result = normalizedDelta * operand; + break; + case '/': + result = Math.floor(normalizedDelta / operand); + break; + default: + result = normalizedDelta; + } + + return result.toString(); + } catch (e) { + Logger.w("CustomButton", `Error evaluating expression: ${match}, using normalized value ${normalizedDelta}`); + return normalizedDelta.toString(); + } + } else { + return normalizedDelta.toString(); + } + }); + + Quickshell.execDetached(["sh", "-lc", command]); + Logger.i("CustomButton", `Executing command: ${command}`); + } else if (wheelMode === "separate") { + if ((delta > 0 && wheelUpExec) || (delta < 0 && wheelDownExec)) { + let commandExec = delta > 0 ? wheelUpExec : wheelDownExec; + let normalizedDelta = delta > 0 ? 1 : -1; + + let command = commandExec.replace(/\$delta([+\-*/]\d+)?/g, function (match, operation) { + if (operation) { + try { + let operator = operation.charAt(0); + let operand = parseInt(operation.substring(1)); + + let result; + switch (operator) { + case '+': + result = normalizedDelta + operand; + break; + case '-': + result = normalizedDelta - operand; + break; + case '*': + result = normalizedDelta * operand; + break; + case '/': + result = Math.floor(normalizedDelta / operand); + break; + default: + result = normalizedDelta; + } + + return result.toString(); + } catch (e) { + Logger.w("CustomButton", `Error evaluating expression: ${match}, using normalized value ${normalizedDelta}`); + return normalizedDelta.toString(); + } + } else { + return normalizedDelta.toString(); + } + }); + + Quickshell.execDetached(["sh", "-lc", command]); + Logger.i("CustomButton", `Executing command: ${command}`); + } + } + + if (!textStream) { + if (wheelMode === "unified" && wheelUpdateText) { + runTextCommand(); + } else if (wheelMode === "separate") { + if ((delta > 0 && wheelUpUpdateText) || (delta < 0 && wheelDownUpdateText)) { + runTextCommand(); + } + } + } + } + + // Register IPC button in onLoaded (called by BarWidgetLoader after properties are set). + // Component.onCompleted is too early — widgetSettings depends on section/index/screen + // which are set by BarWidgetLoader.onLoaded after the component is created. + function onLoaded() { + if (ipcIdentifier && ipcIdentifier.trim() !== "") + CustomButtonIPCService.registerButton(root); + } + + Component.onDestruction: { + if (ipcIdentifier && ipcIdentifier.trim() !== "") + CustomButtonIPCService.unregisterButton(root); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/DarkMode.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/DarkMode.qml new file mode 100644 index 0000000..11f8015 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/DarkMode.qml @@ -0,0 +1,68 @@ +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +NIconButton { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor + + icon: "dark-mode" + tooltipText: Settings.data.colorSchemes.darkMode ? I18n.tr("tooltips.switch-to-light-mode") : I18n.tr("tooltips.switch-to-dark-mode") + tooltipDirection: BarService.getTooltipDirection(screen?.name) + baseSize: Style.getCapsuleHeightForScreen(screen?.name) + applyUiScale: false + customRadius: Style.radiusL + colorBg: Style.capsuleColor + colorFg: Color.resolveColorKey(iconColorKey) + onClicked: Settings.data.colorSchemes.darkMode = !Settings.data.colorSchemes.darkMode + + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + onRightClicked: { + PanelService.showContextMenu(contextMenu, root, screen); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/KeepAwake.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/KeepAwake.qml new file mode 100644 index 0000000..03fe09e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/KeepAwake.qml @@ -0,0 +1,96 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Services.Power +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isBarVertical: barPosition === "left" || barPosition === "right" + readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor + readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor + + implicitWidth: pill.width + implicitHeight: pill.height + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + BarPill { + id: pill + + screen: root.screen + text: IdleInhibitorService.timeout == null ? "" : Time.formatVagueHumanReadableDuration(IdleInhibitorService.timeout) + oppositeDirection: BarService.getPillDirection(root) + customIconColor: Color.resolveColorKeyOptional(root.iconColorKey) + customTextColor: Color.resolveColorKeyOptional(root.textColorKey) + icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off" + tooltipText: IdleInhibitorService.isInhibited ? I18n.tr("tooltips.keep-awake") : I18n.tr("tooltips.keep-awake") + onClicked: IdleInhibitorService.manualToggle() + onRightClicked: { + PanelService.showContextMenu(contextMenu, pill, screen); + } + forceOpen: IdleInhibitorService.timeout !== null + forceClose: IdleInhibitorService.timeout == null + onWheel: function (delta) { + var sign = delta > 0 ? 1 : -1; + // the offset makes scrolling down feel symmetrical to scrolling up + var timeout = IdleInhibitorService.timeout - (delta < 0 ? 60 : 0); + if (timeout == null || timeout < 600) { + delta = 60; // <= 10m, increment at 1m interval + } else if (timeout >= 600 && timeout < 1800) { + delta = 300; // >= 10m, increment at 5m interval + } else if (timeout >= 1800 && timeout < 3600) { + delta = 600; // >= 30m, increment at 10m interval + } else if (timeout >= 3600) { + delta = 1800; // > 1h, increment at 30m interval + } + + IdleInhibitorService.changeTimeout(delta * sign); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/KeyboardLayout.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/KeyboardLayout.qml new file mode 100644 index 0000000..b9b0b48 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/KeyboardLayout.qml @@ -0,0 +1,92 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Services.Compositor +import qs.Services.Keyboard +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isBarVertical: barPosition === "left" || barPosition === "right" + + readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode + readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon + readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor + readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor + + // Use the shared service for keyboard layout + property string currentLayout: KeyboardLayoutService.currentLayout + + implicitWidth: pill.width + implicitHeight: pill.height + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + BarPill { + id: pill + anchors.verticalCenter: parent.verticalCenter + screen: root.screen + oppositeDirection: BarService.getPillDirection(root) + customIconColor: Color.resolveColorKeyOptional(root.iconColorKey) + customTextColor: Color.resolveColorKeyOptional(root.textColorKey) + icon: root.showIcon ? "keyboard" : "" + autoHide: false // Important to be false so we can hover as long as we want + text: isBarVertical ? currentLayout.substring(0, 3).toUpperCase() : currentLayout + tooltipText: KeyboardLayoutService.fullLayoutName + // When icon is disabled, always show the layout text + forceOpen: !root.showIcon || root.displayMode === "forceOpen" + forceClose: root.showIcon && root.displayMode === "alwaysHide" + onClicked: CompositorService.cycleKeyboardLayout() + onRightClicked: { + PanelService.showContextMenu(contextMenu, pill, screen); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Launcher.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Launcher.qml new file mode 100644 index 0000000..b12fb98 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Launcher.qml @@ -0,0 +1,119 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Commons +import qs.Modules.Panels.Settings +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +NIconButton { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string customIcon: widgetSettings.icon || (widgetMetadata ? widgetMetadata.icon : "rocket") + readonly property bool useDistroLogo: widgetSettings.useDistroLogo !== undefined ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo + readonly property string customIconPath: widgetSettings.customIconPath !== undefined ? widgetSettings.customIconPath : widgetMetadata.customIconPath + readonly property bool enableColorization: widgetSettings.enableColorization !== undefined ? widgetSettings.enableColorization : widgetMetadata.enableColorization + readonly property string colorizeSystemIcon: widgetSettings.colorizeSystemIcon !== undefined ? widgetSettings.colorizeSystemIcon : widgetMetadata.colorizeSystemIcon + + readonly property color iconColor: { + if (!enableColorization) + return Color.mOnSurface; + return Color.resolveColorKey(colorizeSystemIcon); + } + + // If we have a custom path or are using distro logo, don't show the theme icon. + icon: (customIconPath === "" && !useDistroLogo) ? customIcon : "" + tooltipText: I18n.tr("actions.open-launcher") + tooltipDirection: BarService.getTooltipDirection(screenName) + baseSize: Style.getCapsuleHeightForScreen(screenName) + applyUiScale: false + customRadius: Style.radiusL + colorBg: Style.capsuleColor + colorFg: iconColor + colorBgHover: Color.mHover + colorFgHover: Color.mOnHover + colorBorder: Style.capsuleBorderColor + colorBorderHover: Style.capsuleBorderColor + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.launcher-settings"), + "action": "launcher-settings", + "icon": "adjustments" + }, + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + } + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "launcher-settings") { + var panel = PanelService.getPanel("settingsPanel", screen); + panel.requestedTab = SettingsPanel.Tab.Launcher; + panel.toggle(); + } else if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + onClicked: PanelService.toggleLauncher(screen) + onMiddleClicked: PanelService.toggleLauncher(screen) + onRightClicked: { + PanelService.showContextMenu(contextMenu, root, screen); + } + + IconImage { + id: customOrDistroLogo + anchors.centerIn: parent + width: root.buttonSize * 0.8 + height: width + source: { + if (useDistroLogo) + return HostService.osLogo; + if (customIconPath !== "") + return customIconPath.startsWith("file://") ? customIconPath : "file://" + customIconPath; + return ""; + } + visible: source !== "" + smooth: true + asynchronous: true + layer.enabled: (enableColorization) && (useDistroLogo || customIconPath !== "") + layer.effect: ShaderEffect { + property color targetColor: !hovering ? iconColor : Color.mOnHover + property real colorizeMode: 2.0 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/LockKeys.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/LockKeys.qml new file mode 100644 index 0000000..f903a4b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/LockKeys.qml @@ -0,0 +1,165 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Modules.Panels.Settings +import qs.Services.Keyboard +import qs.Services.UI +import qs.Widgets + +//test +Item { + id: root + + property ShellScreen screen + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + // Settings + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isVertical: barPosition === "left" || barPosition === "right" + readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName) + + // Content dimensions for implicit sizing + readonly property real contentWidth: isVertical ? capsuleHeight : Math.round(layout.implicitWidth + Style.margin2M) + readonly property real contentHeight: isVertical ? Math.round(layout.implicitHeight + Style.margin2M) : capsuleHeight + + readonly property bool hideWhenOff: (widgetSettings.hideWhenOff !== undefined) ? widgetSettings.hideWhenOff : (widgetMetadata.hideWhenOff !== undefined ? widgetMetadata.hideWhenOff : false) + + readonly property string capsIcon: widgetSettings.capsLockIcon !== undefined ? widgetSettings.capsLockIcon : widgetMetadata.capsLockIcon + readonly property string numIcon: widgetSettings.numLockIcon !== undefined ? widgetSettings.numLockIcon : widgetMetadata.numLockIcon + readonly property string scrollIcon: widgetSettings.scrollLockIcon !== undefined ? widgetSettings.scrollLockIcon : widgetMetadata.scrollLockIcon + readonly property bool showCaps: (widgetSettings.showCapsLock !== undefined) ? widgetSettings.showCapsLock : widgetMetadata.showCapsLock + readonly property bool showNum: (widgetSettings.showNumLock !== undefined) ? widgetSettings.showNumLock : widgetMetadata.showNumLock + readonly property bool showScroll: (widgetSettings.showScrollLock !== undefined) ? widgetSettings.showScrollLock : widgetMetadata.showScrollLock + + visible: !root.hideWhenOff || (root.showCaps && LockKeysService.capsLockOn) || (root.showNum && LockKeysService.numLockOn) || (root.showScroll && LockKeysService.scrollLockOn) + + Component.onCompleted: LockKeysService.registerComponent("bar-lockkeys:" + (screen?.name || "unknown")) + Component.onDestruction: LockKeysService.unregisterComponent("bar-lockkeys:" + (screen?.name || "unknown")) + + implicitHeight: contentHeight + implicitWidth: contentWidth + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + // Visual capsule centered in parent + Rectangle { + id: visualCapsule + anchors.centerIn: parent + color: Style.capsuleColor + width: root.contentWidth + height: root.contentHeight + radius: Style.radiusM + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + Item { + id: layout + + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + implicitHeight: rowLayout.visible ? rowLayout.implicitHeight : colLayout.implicitHeight + implicitWidth: rowLayout.visible ? rowLayout.implicitWidth : colLayout.implicitWidth + + RowLayout { + id: rowLayout + + spacing: 0 + visible: !root.isVertical + + NIcon { + color: LockKeysService.capsLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3) + icon: root.capsIcon + visible: root.showCaps && (!root.hideWhenOff || LockKeysService.capsLockOn) + } + + NIcon { + color: LockKeysService.numLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3) + icon: root.numIcon + visible: root.showNum && (!root.hideWhenOff || LockKeysService.numLockOn) + } + + NIcon { + color: LockKeysService.scrollLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3) + icon: root.scrollIcon + visible: root.showScroll && (!root.hideWhenOff || LockKeysService.scrollLockOn) + } + } + + ColumnLayout { + id: colLayout + + spacing: 0 + visible: root.isVertical + + NIcon { + color: LockKeysService.capsLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3) + icon: root.capsIcon + visible: root.showCaps && (!root.hideWhenOff || LockKeysService.capsLockOn) + } + + NIcon { + color: LockKeysService.numLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3) + icon: root.numIcon + visible: root.showNum && (!root.hideWhenOff || LockKeysService.numLockOn) + } + + NIcon { + color: LockKeysService.scrollLockOn ? Color.mTertiary : Qt.alpha(Color.mOnSurfaceVariant, 0.3) + icon: root.scrollIcon + visible: root.showScroll && (!root.hideWhenOff || LockKeysService.scrollLockOn) + } + } + } + } + + // MouseArea at root level for extended click area + MouseArea { + acceptedButtons: Qt.RightButton + anchors.fill: parent + + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + PanelService.showContextMenu(contextMenu, root, screen); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/MediaMini.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/MediaMini.qml new file mode 100644 index 0000000..d7b6b74 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/MediaMini.qml @@ -0,0 +1,510 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Services.Media +import qs.Services.UI +import qs.Widgets +import qs.Widgets.AudioSpectrum + +Item { + id: root + + property ShellScreen screen + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + // Settings + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + // Bar orientation (per-screen) + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isVertical: barPosition === "left" || barPosition === "right" + readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName) + readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName) + + // Widget settings + readonly property string hideMode: widgetSettings.hideMode !== undefined ? widgetSettings.hideMode : widgetMetadata.hideMode + readonly property bool hideWhenIdle: widgetSettings.hideWhenIdle !== undefined ? widgetSettings.hideWhenIdle : widgetMetadata.hideWhenIdle + readonly property bool showAlbumArt: widgetSettings.showAlbumArt !== undefined ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt + readonly property bool showArtistFirst: widgetSettings.showArtistFirst !== undefined ? widgetSettings.showArtistFirst : widgetMetadata.showArtistFirst + readonly property bool showVisualizer: widgetSettings.showVisualizer !== undefined ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer + readonly property string visualizerType: widgetSettings.visualizerType !== undefined ? widgetSettings.visualizerType : widgetMetadata.visualizerType + readonly property string scrollingMode: widgetSettings.scrollingMode !== undefined ? widgetSettings.scrollingMode : widgetMetadata.scrollingMode + readonly property bool showProgressRing: widgetSettings.showProgressRing !== undefined ? widgetSettings.showProgressRing : widgetMetadata.showProgressRing + readonly property bool useFixedWidth: widgetSettings.useFixedWidth !== undefined ? widgetSettings.useFixedWidth : widgetMetadata.useFixedWidth + readonly property real maxWidth: widgetSettings.maxWidth !== undefined ? widgetSettings.maxWidth : Math.max(widgetMetadata.maxWidth, screen ? screen.width * 0.06 : 0) + readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor + readonly property color textColor: Color.resolveColorKey(textColorKey) + + // Dimensions + readonly property int artSize: Style.toOdd(capsuleHeight * 0.75) + readonly property int iconSize: Style.toOdd(capsuleHeight * 0.75) + readonly property int verticalSize: Style.toOdd(capsuleHeight * 0.85) + readonly property int progressWidth: 2 + + // State + readonly property bool hasPlayer: MediaService.currentPlayer !== null + readonly property bool shouldHideIdle: (hideMode === "idle" || hideWhenIdle) && !MediaService.isPlaying + readonly property bool shouldHideEmpty: !hasPlayer && hideMode === "hidden" + readonly property bool isHidden: shouldHideIdle || shouldHideEmpty + + // Title + readonly property string title: { + if (!hasPlayer) + return I18n.tr("bar.media-mini.no-active-player"); + var artist = MediaService.trackArtist; + var track = MediaService.trackTitle; + return showArtistFirst ? (artist ? `${artist} - ${track}` : track) : (artist ? `${track} - ${artist}` : track); + } + + // SpectrumService registration for visualizer + readonly property string spectrumComponentId: "bar:mediamini:" + root.screen?.name + ":" + root.section + ":" + root.sectionWidgetIndex + readonly property bool needsSpectrum: root.showVisualizer && root.visualizerType !== "" && root.visualizerType !== "none" && !root.isHidden + + Layout.preferredHeight: isVertical ? -1 : Style.getBarHeightForScreen(screenName) + Layout.preferredWidth: isVertical ? Style.getBarHeightForScreen(screenName) : -1 + Layout.fillHeight: false + Layout.fillWidth: false + + onNeedsSpectrumChanged: { + if (root.needsSpectrum) { + SpectrumService.registerComponent(root.spectrumComponentId); + } else { + SpectrumService.unregisterComponent(root.spectrumComponentId); + } + } + + Component.onCompleted: { + if (root.needsSpectrum) { + SpectrumService.registerComponent(root.spectrumComponentId); + } + } + + Component.onDestruction: { + SpectrumService.unregisterComponent(root.spectrumComponentId); + } + + // Layout + // For horizontal bars, height is always capsuleHeight (no animation needed to prevent jitter) + // For vertical bars, collapse to 0 when hidden + implicitWidth: isVertical ? (isHidden ? 0 : verticalSize) : (isHidden ? 0 : contentWidth) + implicitHeight: isVertical ? (isHidden ? 0 : verticalSize) : capsuleHeight + visible: !shouldHideIdle && (hideMode !== "hidden" || opacity > 0) + opacity: isHidden ? 0.0 : ((hideMode === "transparent" && !hasPlayer) ? 0.0 : 1.0) + + property real mainContentWidth: 0 + readonly property real contentWidth: { + if (useFixedWidth) + return maxWidth; + + // Calculate icon/art width (must match RowLayout visibility) + var iconWidth = 0; + if (!hasPlayer) { + iconWidth = iconSize; + } else if (showAlbumArt || showProgressRing) { + iconWidth = artSize; + } + + var margins = isVertical ? 0 : Style.margin2S; + + // Add spacing and text width + var textWidth = 0; + if (titleContainer.measuredWidth > 0) { + if (iconWidth > 0) + margins += Style.marginS; + textWidth = titleContainer.measuredWidth + Style.margin2XXS; + } + + var total = iconWidth + textWidth + margins; + + // calculate the width of all elements except the scrolling text + mainContentWidth = total - textWidth; + + return hasPlayer ? Math.min(total, maxWidth) : total; + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutCubic + } + } + Behavior on implicitWidth { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutCubic + } + } + Behavior on implicitHeight { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutCubic + } + } + + // Context menu + NPopupContextMenu { + id: contextMenu + model: { + var items = []; + if (hasPlayer && MediaService.canPlay) { + items.push({ + "label": MediaService.isPlaying ? I18n.tr("common.pause") : I18n.tr("common.play"), + "action": "play-pause", + "icon": MediaService.isPlaying ? "media-pause" : "media-play" + }); + } + if (hasPlayer && MediaService.canGoPrevious) { + items.push({ + "label": I18n.tr("common.previous"), + "action": "previous", + "icon": "media-prev" + }); + } + if (hasPlayer && MediaService.canGoNext) { + items.push({ + "label": I18n.tr("common.next"), + "action": "next", + "icon": "media-next" + }); + } + + // Append available players (like in Control Center) so user can switch from the bar + var players = MediaService.getAvailablePlayers ? MediaService.getAvailablePlayers() : []; + if (players && players.length > 1) { + for (var i = 0; i < players.length; i++) { + var isCurrent = (i === MediaService.selectedPlayerIndex); + items.push({ + "label": players[i].identity, + "action": "player-" + i, + "icon": isCurrent ? "check" : "disc", + "enabled": true, + "visible": true + }); + } + } + + items.push({ + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }); + return items; + } + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "play-pause") + MediaService.playPause(); + else if (action === "previous") + MediaService.previous(); + else if (action === "next") + MediaService.next(); + else if (action && action.indexOf("player-") === 0) { + var idx = parseInt(action.split("-")[1]); + if (!isNaN(idx)) { + MediaService.switchToPlayer(idx); + } + } else if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + // Main container - stays at content size, pixel-perfect centered in parent + Rectangle { + id: container + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, height) + width: Style.toOdd(isVertical ? (isHidden ? 0 : verticalSize) : (isHidden ? 0 : contentWidth)) + height: Style.toOdd(isVertical ? (isHidden ? 0 : verticalSize) : capsuleHeight) + radius: Style.radiusM + color: Style.capsuleColor + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + Behavior on width { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutCubic + } + } + Behavior on height { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutCubic + } + } + + Item { + anchors.fill: parent + anchors.leftMargin: isVertical ? 0 : Style.marginS + anchors.rightMargin: isVertical ? 0 : Style.marginS + + // Visualizer + Loader { + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, height) + width: Style.toOdd(parent.width) + height: Style.toOdd(parent.height) + active: showVisualizer + z: 0 + sourceComponent: { + if (!showVisualizer) + return null; + if (visualizerType === "linear") + return linearSpectrum; + if (visualizerType === "mirrored") + return mirroredSpectrum; + if (visualizerType === "wave") + return waveSpectrum; + return null; + } + } + + // Horizontal layout + RowLayout { + anchors.fill: parent + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginS + visible: !isVertical + z: 1 + + // Album art / Progress ring + Item { + visible: hasPlayer && (showAlbumArt || showProgressRing) + Layout.preferredWidth: visible ? artSize : 0 + Layout.preferredHeight: visible ? artSize : 0 + Layout.alignment: Qt.AlignVCenter + + ProgressRing { + id: progressRing + anchors.fill: parent + visible: showProgressRing + progress: MediaService.trackLength > 0 ? MediaService.currentPosition / MediaService.trackLength : 0 + lineWidth: root.progressWidth + } + + NImageRounded { + visible: showAlbumArt && hasPlayer + anchors.fill: parent + anchors.margins: showProgressRing ? root.progressWidth * 2 : 0 + radius: width / 2 + imagePath: MediaService.trackArtUrl + borderWidth: 0 + imageFillMode: Image.PreserveAspectCrop + } + } + + // Scrolling title + NScrollText { + id: titleContainer + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + Layout.preferredHeight: capsuleHeight + fadeRoundLeftCorners: !(showAlbumArt || showProgressRing) + + text: title + + scrollMode: { + if (scrollingMode === "always") + return NScrollText.ScrollMode.Always; + if (scrollingMode === "hover") + return NScrollText.ScrollMode.Hover; + return NScrollText.ScrollMode.Never; + } + cursorShape: hasPlayer ? Qt.PointingHandCursor : Qt.ArrowCursor + maxWidth: root.maxWidth - root.mainContentWidth + forcedHover: mainMouseArea.containsMouse + fadeExtent: 0.1 + fadeCornerRadius: Style.radiusM + + NText { + color: hasPlayer ? root.textColor : Color.mOnSurfaceVariant + pointSize: barFontSize + elide: Text.ElideNone + } + } + } + + // Vertical layout + Item { + id: verticalLayout + visible: isVertical + width: Style.toOdd(verticalSize) + height: Style.toOdd(width) + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, height) + z: 1 + + ProgressRing { + anchors.fill: parent + visible: showProgressRing + progress: MediaService.trackLength > 0 ? MediaService.currentPosition / MediaService.trackLength : 0 + lineWidth: root.progressWidth + } + + NImageRounded { + visible: showAlbumArt && hasPlayer + anchors.fill: parent + anchors.margins: showProgressRing ? root.progressWidth * 2 : 0 + radius: width / 2 + imagePath: MediaService.trackArtUrl + borderWidth: 0 + imageFillMode: Image.PreserveAspectCrop + } + } + + // Mouse interaction moved to root + } + } + + // Mouse interaction + MouseArea { + id: mainMouseArea + anchors.fill: parent + + // Extend click area to screen edge if widget is at the start/end + anchors.leftMargin: (!isVertical && section === "left" && sectionWidgetIndex === 0) ? -Style.marginS : 0 + anchors.rightMargin: (!isVertical && section === "right" && sectionWidgetIndex === sectionWidgetsCount - 1) ? -Style.marginS : 0 + anchors.topMargin: (isVertical && section === "left" && sectionWidgetIndex === 0) ? -Style.marginM : 0 + anchors.bottomMargin: (isVertical && section === "right" && sectionWidgetIndex === sectionWidgetsCount - 1) ? -Style.marginM : 0 + + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton | Qt.ForwardButton | Qt.BackButton + + onClicked: mouse => { + TooltipService.hide(); + if (mouse.button === Qt.LeftButton) { + PanelService.getPanel("mediaPlayerPanel", screen)?.toggle(container); + } else if (mouse.button === Qt.RightButton) { + PanelService.showContextMenu(contextMenu, container, screen); + } else if (mouse.button === Qt.MiddleButton && hasPlayer) { + MediaService.playPause(); + } else if (mouse.button === Qt.ForwardButton && hasPlayer) { + MediaService.next(); + } else if (mouse.button === Qt.BackButton && hasPlayer) { + MediaService.previous(); + } + } + + onEntered: { + if (!root || !screen) { + return; + } + var scrollMode = scrollingMode; + if ((isVertical || scrollMode === "never")) { + var panel = PanelService.getPanel("mediaPlayerPanel", screen); + if (panel && !panel.isPanelOpen) { + TooltipService.show(root, title, BarService.getTooltipDirection(root.screen?.name)); + } + } + } + onExited: TooltipService.hide() + } + + // Components + Component { + id: linearSpectrum + NLinearSpectrum { + width: parent.width - Style.marginS + height: 20 + values: SpectrumService.values + fillColor: Color.mPrimary + opacity: 0.4 + barPosition: root.barPosition + mirrored: Settings.data.audio.spectrumMirrored + } + } + + Component { + id: mirroredSpectrum + NMirroredSpectrum { + width: parent.width - Style.marginS + height: parent.height - Style.marginS + values: SpectrumService.values + fillColor: Color.mPrimary + opacity: 0.4 + mirrored: Settings.data.audio.spectrumMirrored + } + } + + Component { + id: waveSpectrum + NWaveSpectrum { + width: parent.width - Style.marginS + height: parent.height - Style.marginS + values: SpectrumService.values + fillColor: Color.mPrimary + opacity: 0.4 + mirrored: Settings.data.audio.spectrumMirrored + } + } + + // Progress Ring Component + component ProgressRing: Canvas { + property real progress: 0 + property real lineWidth: 2 + + function repaint() { + if (this.visible && this.opacity > 0) + requestPaint(); + } + + onProgressChanged: repaint() + Component.onCompleted: repaint() + + Connections { + target: Color + function onMPrimaryChanged() { + repaint(); + } + } + + onPaint: { + if (width <= 0 || height <= 0) + return; + + var ctx = getContext("2d"); + var centerX = width / 2; + var centerY = height / 2; + var radius = Math.min(width, height) / 2 - lineWidth; + + ctx.reset(); + + // Background + ctx.beginPath(); + ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI); + ctx.lineWidth = lineWidth; + ctx.strokeStyle = Qt.alpha(Color.mOnSurface, 0.4); + ctx.stroke(); + + // Progress + ctx.beginPath(); + ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progress * 2 * Math.PI); + ctx.lineWidth = lineWidth; + ctx.strokeStyle = Color.mPrimary; + ctx.lineCap = "round"; + ctx.stroke(); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Microphone.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Microphone.qml new file mode 100644 index 0000000..dab422d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Microphone.qml @@ -0,0 +1,185 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pipewire +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Modules.Panels.Settings +import qs.Services.Media +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isBarVertical: barPosition === "left" || barPosition === "right" + readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode + readonly property string middleClickCommand: (widgetSettings.middleClickCommand !== undefined) ? widgetSettings.middleClickCommand : widgetMetadata.middleClickCommand + readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor + readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor + + // Used to avoid opening the pill on Quickshell startup + property bool firstInputVolumeReceived: false + property int wheelAccumulator: 0 + + implicitWidth: pill.width + implicitHeight: pill.height + + // Connection used to open the pill when input volume changes + Connections { + target: AudioService.source?.audio ? AudioService.source?.audio : null + function onVolumeChanged() { + // Logger.i("Bar:Microphone", "onInputVolumeChanged") + if (!firstInputVolumeReceived) { + // Ignore the first volume change + firstInputVolumeReceived = true; + } else { + // If a tooltip is visible while we show the pill + // hide it so it doesn't overlap the volume slider. + TooltipService.hide(); + pill.show(); + externalHideTimer.restart(); + } + } + } + + // Connection used to open the pill when input mute state changes + Connections { + target: AudioService.source?.audio ? AudioService.source?.audio : null + function onMutedChanged() { + // Logger.i("Bar:Microphone", "onInputMutedChanged") + if (!firstInputVolumeReceived) { + // Ignore the first mute change + firstInputVolumeReceived = true; + } else { + TooltipService.hide(); + pill.show(); + externalHideTimer.restart(); + } + } + } + + Timer { + id: externalHideTimer + running: false + interval: 1500 + onTriggered: { + pill.hide(); + } + } + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.toggle-mute"), + "action": "toggle-mute", + "icon": AudioService.inputMuted ? "microphone-off" : "microphone" + }, + { + "label": I18n.tr("actions.run-custom-command"), + "action": "custom-command", + "icon": "adjustments" + }, + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "toggle-mute") { + AudioService.setInputMuted(!AudioService.inputMuted); + } else if (action === "custom-command") { + Quickshell.execDetached(["sh", "-c", middleClickCommand]); + } else if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + BarPill { + id: pill + + screen: root.screen + oppositeDirection: BarService.getPillDirection(root) + customIconColor: Color.resolveColorKeyOptional(root.iconColorKey) + customTextColor: Color.resolveColorKeyOptional(root.textColorKey) + icon: AudioService.getInputIcon() + autoHide: false // Important to be false so we can hover as long as we want + text: { + const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; + const displayVolume = Math.min(maxVolume, AudioService.inputVolume); + return Math.round(displayVolume * 100); + } + suffix: "%" + forceOpen: displayMode === "alwaysShow" + forceClose: displayMode === "alwaysHide" + tooltipText: { + if (PanelService.getPanel("audioPanel", screen)?.isPanelOpen) { + return ""; + } else { + const nick = AudioService.source?.nickname ?? ""; + const volumeText = I18n.tr("tooltips.microphone-volume-at", { + "volume": (() => { + const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; + const displayVolume = Math.min(maxVolume, AudioService.inputVolume); + return Math.round(displayVolume * 100); + })() + }); + return nick ? volumeText + "\n" + nick : volumeText; + } + } + + onWheel: function (delta) { + // As soon as we start scrolling to adjust volume, hide the tooltip + TooltipService.hide(); + + wheelAccumulator += delta; + if (wheelAccumulator >= 120) { + wheelAccumulator = 0; + AudioService.setInputVolume(AudioService.inputVolume + AudioService.stepVolume); + } else if (wheelAccumulator <= -120) { + wheelAccumulator = 0; + AudioService.setInputVolume(AudioService.inputVolume - AudioService.stepVolume); + } + } + onClicked: { + PanelService.getPanel("audioPanel", screen)?.toggle(this); + } + onRightClicked: { + PanelService.showContextMenu(contextMenu, pill, screen); + } + onMiddleClicked: { + Quickshell.execDetached(["sh", "-c", middleClickCommand]); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Network.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Network.qml new file mode 100644 index 0000000..e7687ab --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Network.qml @@ -0,0 +1,105 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Modules.Panels.Settings // For SettingsPanel +import qs.Services.Networking +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isBarVertical: barPosition === "left" || barPosition === "right" + readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode + readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor + readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor + + implicitWidth: pill.width + implicitHeight: pill.height + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": NetworkService.wifiEnabled ? I18n.tr("actions.disable-wifi") : I18n.tr("actions.enable-wifi"), + "action": "toggle-wifi", + "icon": NetworkService.wifiEnabled ? "wifi-off" : "wifi", + "enabled": !NetworkService.airplaneModeEnabled && NetworkService.wifiAvailable + }, + { + "label": I18n.tr("common.wifi") + " " + I18n.tr("tooltips.open-settings"), + "action": "wifi-settings", + "icon": "settings" + }, + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "toggle-wifi") { + NetworkService.setWifiEnabled(!NetworkService.wifiEnabled); + } else if (action === "wifi-settings") { + SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 0, screen); + } else if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + BarPill { + id: pill + screen: root.screen + oppositeDirection: BarService.getPillDirection(root) + customIconColor: Color.resolveColorKeyOptional(root.iconColorKey) + customTextColor: Color.resolveColorKeyOptional(root.textColorKey) + icon: NetworkService.getIcon() + text: NetworkService.getStatusText(false) + autoHide: false + forceOpen: !isBarVertical && root.displayMode === "alwaysShow" + forceClose: isBarVertical || root.displayMode === "alwaysHide" || text === "" + onClicked: { + var panel = PanelService.getPanel("networkPanel", screen); + panel?.toggle(this); + } + onRightClicked: { + PanelService.showContextMenu(contextMenu, pill, screen); + } + tooltipText: { + if (PanelService.getPanel("networkPanel", screen)?.isPanelOpen) { + return ""; + } + return NetworkService.getStatusText(true); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/NightLight.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/NightLight.qml new file mode 100644 index 0000000..8c5fe5b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/NightLight.qml @@ -0,0 +1,90 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Modules.Panels.Settings +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +NIconButton { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor + + baseSize: Style.getCapsuleHeightForScreen(screen?.name) + applyUiScale: false + customRadius: Style.radiusL + colorBg: Settings.data.nightLight.enabled ? Color.mPrimary : Style.capsuleColor + colorFg: Settings.data.nightLight.enabled ? Color.mOnPrimary : Color.resolveColorKey(iconColorKey) + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off" + tooltipText: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? I18n.tr("common.night-light") : I18n.tr("common.night-light")) : I18n.tr("common.night-light") + tooltipDirection: BarService.getTooltipDirection(screen?.name) + onClicked: { + // Check if wlsunset is available before enabling night light + if (!ProgramCheckerService.wlsunsetAvailable) { + ToastService.showWarning(I18n.tr("common.night-light"), I18n.tr("toast.night-light.not-installed")); + return; + } + + if (!Settings.data.nightLight.enabled) { + Settings.data.nightLight.enabled = true; + Settings.data.nightLight.forced = false; + } else if (Settings.data.nightLight.enabled && !Settings.data.nightLight.forced) { + Settings.data.nightLight.forced = true; + } else { + Settings.data.nightLight.enabled = false; + Settings.data.nightLight.forced = false; + } + } + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + onRightClicked: { + PanelService.showContextMenu(contextMenu, root, screen); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/NoctaliaPerformance.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/NoctaliaPerformance.qml new file mode 100644 index 0000000..ef70b3f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/NoctaliaPerformance.qml @@ -0,0 +1,74 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Modules.Panels.Settings +import qs.Services.Power +import qs.Services.UI +import qs.Widgets + +NIconButton { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor + + baseSize: Style.getCapsuleHeightForScreen(screen?.name) + applyUiScale: false + customRadius: Style.radiusL + colorBg: PowerProfileService.noctaliaPerformanceMode ? Color.mPrimary : Style.capsuleColor + colorFg: PowerProfileService.noctaliaPerformanceMode ? Color.mOnPrimary : Color.resolveColorKey(iconColorKey) + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + icon: PowerProfileService.noctaliaPerformanceMode ? "rocket" : "rocket-off" + tooltipText: PowerProfileService.noctaliaPerformanceMode ? I18n.tr("tooltips.noctalia-performance-enabled") : I18n.tr("tooltips.noctalia-performance-enabled") + tooltipDirection: BarService.getTooltipDirection(screen?.name) + onClicked: PowerProfileService.toggleNoctaliaPerformance() + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + onRightClicked: { + PanelService.showContextMenu(contextMenu, root, screen); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/NotificationHistory.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/NotificationHistory.qml new file mode 100644 index 0000000..13596e5 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/NotificationHistory.qml @@ -0,0 +1,139 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +NIconButton { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + readonly property bool showUnreadBadge: widgetSettings.showUnreadBadge !== undefined ? widgetSettings.showUnreadBadge : widgetMetadata.showUnreadBadge + readonly property bool hideWhenZero: widgetSettings.hideWhenZero !== undefined ? widgetSettings.hideWhenZero : widgetMetadata.hideWhenZero + readonly property bool hideWhenZeroUnread: widgetSettings.hideWhenZeroUnread !== undefined ? widgetSettings.hideWhenZeroUnread : widgetMetadata.hideWhenZeroUnread + readonly property string unreadBadgeColor: widgetSettings.unreadBadgeColor !== undefined ? widgetSettings.unreadBadgeColor : widgetMetadata.unreadBadgeColor + readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor + + readonly property color badgeColor: Color.resolveColorKey(unreadBadgeColor) + + function computeUnreadCount() { + var since = NotificationService.lastSeenTs; + var count = 0; + var model = NotificationService.historyModel; + for (var i = 0; i < model.count; i++) { + var item = model.get(i); + var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp; + if (ts > since) + count++; + } + return count; + } + + readonly property int count: computeUnreadCount() + + baseSize: Style.getCapsuleHeightForScreen(screen?.name) + applyUiScale: false + customRadius: Style.radiusL + icon: NotificationService.doNotDisturb ? "bell-off" : "bell" + tooltipText: { + if (PanelService.getPanel("notificationHistoryPanel", screen)?.isPanelOpen) { + return ""; + } else { + return I18n.tr("tooltips.open-notification-history-enable-dnd"); + } + } + tooltipDirection: BarService.getTooltipDirection(screen?.name) + colorBg: Style.capsuleColor + colorFg: Color.resolveColorKey(iconColorKey) + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + visible: !((hideWhenZero && NotificationService.historyModel.count === 0) || (hideWhenZeroUnread && count === 0)) + opacity: !((hideWhenZero && NotificationService.historyModel.count === 0) || (hideWhenZeroUnread && count === 0)) ? 1.0 : 0.0 + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": NotificationService.doNotDisturb ? I18n.tr("actions.disable-dnd") : I18n.tr("actions.enable-dnd"), + "action": "toggle-dnd", + "icon": NotificationService.doNotDisturb ? "bell" : "bell-off" + }, + { + "label": I18n.tr("actions.clear-history"), + "action": "clear-history", + "icon": "trash" + }, + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "toggle-dnd") { + NotificationService.doNotDisturb = !NotificationService.doNotDisturb; + } else if (action === "clear-history") { + NotificationService.clearHistory(); + } else if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + onClicked: { + var panel = PanelService.getPanel("notificationHistoryPanel", screen); + panel?.toggle(this); + } + + onRightClicked: { + PanelService.showContextMenu(contextMenu, root, screen); + } + + Loader { + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenterOffset: parent.baseSize / 4 + anchors.verticalCenterOffset: -parent.baseSize / 4 + z: 2 + active: showUnreadBadge + sourceComponent: Rectangle { + id: badge + height: 7 + width: height + radius: Style.radiusXS + color: root.hovering ? Color.mOnHover : (root.badgeColor || Color.mError) + border.color: Color.mSurface + border.width: Style.borderS + visible: count > 0 + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/PowerProfile.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/PowerProfile.qml new file mode 100644 index 0000000..9ec9742 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/PowerProfile.qml @@ -0,0 +1,74 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.UPower +import qs.Commons +import qs.Services.Power +import qs.Services.UI +import qs.Widgets + +NIconButton { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor + + baseSize: Style.getCapsuleHeightForScreen(screen?.name) + applyUiScale: false + customRadius: Style.radiusL + visible: PowerProfileService.available + icon: PowerProfileService.getIcon() + tooltipText: I18n.tr("tooltips.power-profile", { + "profile": PowerProfileService.getName() + }) + tooltipDirection: BarService.getTooltipDirection(screen?.name) + colorBg: (PowerProfileService.profile === PowerProfile.Balanced) ? Style.capsuleColor : Color.mPrimary + colorFg: (PowerProfileService.profile === PowerProfile.Balanced) ? Color.resolveColorKey(iconColorKey) : Color.mOnPrimary + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + onClicked: PowerProfileService.cycleProfile() + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + onRightClicked: { + PanelService.showContextMenu(contextMenu, root, screen); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/SessionMenu.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/SessionMenu.qml new file mode 100644 index 0000000..ba862bc --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/SessionMenu.qml @@ -0,0 +1,77 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Services.UI +import qs.Widgets + +NIconButton { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string iconColorKey: (widgetSettings.iconColor !== undefined) ? widgetSettings.iconColor : widgetMetadata.iconColor + + baseSize: Style.getCapsuleHeightForScreen(screenName) + applyUiScale: false + customRadius: Style.radiusL + icon: "power" + tooltipText: { + if (PanelService.getPanel("sessionMenuPanel", screen)?.isPanelOpen) + return ""; + else + return I18n.tr("tooltips.session-menu"); + } + tooltipDirection: BarService.getTooltipDirection(screenName) + colorBg: Style.capsuleColor + colorFg: Color.resolveColorKey(iconColorKey) + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + onClicked: PanelService.getPanel("sessionMenuPanel", screen)?.toggle() + onRightClicked: { + PanelService.showContextMenu(contextMenu, root, screen); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Settings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Settings.qml new file mode 100644 index 0000000..a7cecbf --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Settings.qml @@ -0,0 +1,86 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +NIconButton { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string valueIconColor: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor + + readonly property color iconColor: Color.resolveColorKey(valueIconColor) + + icon: "settings" + tooltipText: { + if (PanelService.getPanel("settingsPanel", screen)?.isPanelOpen) { + return ""; + } else { + return I18n.tr("tooltips.open-settings"); + } + } + tooltipDirection: BarService.getTooltipDirection(screen?.name) + baseSize: Style.getCapsuleHeightForScreen(screen?.name) + applyUiScale: false + customRadius: Style.radiusL + colorBg: Style.capsuleColor + colorFg: iconColor + colorBgHover: Color.mHover + colorFgHover: Color.mOnHover + colorBorder: Style.capsuleBorderColor + colorBorderHover: Style.capsuleBorderColor + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + onClicked: { + if (Settings.data.ui.settingsPanelMode === "attached") { + PanelService.getPanel("settingsPanel", screen)?.toggle(this); + } else { + PanelService.getPanel("settingsPanel", screen)?.toggle(); + } + } + onRightClicked: { + PanelService.showContextMenu(contextMenu, root, screen); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Spacer.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Spacer.qml new file mode 100644 index 0000000..7fe6c16 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Spacer.qml @@ -0,0 +1,41 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isBarVertical: barPosition === "left" || barPosition === "right" + readonly property real barHeight: Style.getBarHeightForScreen(screenName) + readonly property int spacerSize: widgetSettings.width !== undefined ? widgetSettings.width : widgetMetadata.width + + implicitWidth: isBarVertical ? barHeight : spacerSize + implicitHeight: isBarVertical ? spacerSize : barHeight + width: implicitWidth + height: implicitHeight +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/SystemMonitor.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/SystemMonitor.qml new file mode 100644 index 0000000..7bdfa63 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/SystemMonitor.qml @@ -0,0 +1,967 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Modules.Panels.Settings +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isVertical: barPosition === "left" || barPosition === "right" + readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName) + readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName) + + readonly property bool compactMode: widgetSettings.compactMode !== undefined ? widgetSettings.compactMode : widgetMetadata.compactMode + readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor + readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor + + readonly property bool useMonospaceFont: widgetSettings.useMonospaceFont !== undefined ? widgetSettings.useMonospaceFont : widgetMetadata.useMonospaceFont + readonly property bool usePadding: !compactMode && !isVertical && useMonospaceFont && ((widgetSettings.usePadding !== undefined) ? widgetSettings.usePadding : widgetMetadata.usePadding) + + readonly property bool showCpuUsage: (widgetSettings.showCpuUsage !== undefined) ? widgetSettings.showCpuUsage : widgetMetadata.showCpuUsage + readonly property bool showCpuCores: (widgetSettings.showCpuCores !== undefined) ? widgetSettings.showCpuCores : widgetMetadata.showCpuCores + readonly property bool showCpuFreq: (widgetSettings.showCpuFreq !== undefined) ? widgetSettings.showCpuFreq : widgetMetadata.showCpuFreq + readonly property bool showCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : widgetMetadata.showCpuTemp + readonly property bool showGpuTemp: (widgetSettings.showGpuTemp !== undefined) ? widgetSettings.showGpuTemp : widgetMetadata.showGpuTemp + readonly property bool showMemoryUsage: (widgetSettings.showMemoryUsage !== undefined) ? widgetSettings.showMemoryUsage : widgetMetadata.showMemoryUsage + readonly property bool showMemoryAsPercent: (widgetSettings.showMemoryAsPercent !== undefined) ? widgetSettings.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent + readonly property bool showSwapUsage: (widgetSettings.showSwapUsage !== undefined) ? widgetSettings.showSwapUsage : widgetMetadata.showSwapUsage + readonly property bool showNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats + readonly property bool showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage + readonly property bool showDiskUsageAsPercent: (widgetSettings.showDiskUsageAsPercent !== undefined) ? widgetSettings.showDiskUsageAsPercent : widgetMetadata.showDiskUsageAsPercent + readonly property bool showDiskAvailable: (widgetSettings.showDiskAvailable !== undefined) ? widgetSettings.showDiskAvailable : widgetMetadata.showDiskAvailable + readonly property bool showLoadAverage: (widgetSettings.showLoadAverage !== undefined) ? widgetSettings.showLoadAverage : widgetMetadata.showLoadAverage + readonly property string diskPath: (widgetSettings.diskPath !== undefined) ? widgetSettings.diskPath : widgetMetadata.diskPath + readonly property string fontFamily: useMonospaceFont ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault + + readonly property int paddingPercent: usePadding ? String("100%").length : 0 + readonly property int paddingTemp: usePadding ? String("999°").length : 0 + readonly property int paddingCpuFreq: usePadding ? String("9.9").length : 0 + readonly property int paddingSpeed: usePadding ? String("9999G").length : 0 + + readonly property real iconSize: Style.toOdd(capsuleHeight * 0.48) + readonly property real miniGaugeWidth: Math.max(3, Style.toOdd(root.iconSize * 0.25)) + + // Content dimensions for implicit sizing + readonly property real contentWidth: isVertical ? capsuleHeight : Math.round(mainGrid.implicitWidth + Style.margin2M) + readonly property real contentHeight: isVertical ? Math.round(mainGrid.implicitHeight + Style.margin2M) : capsuleHeight + + readonly property color iconColor: Color.resolveColorKey(iconColorKey) + readonly property color textColor: Color.resolveColorKey(textColorKey) + + // Size: use implicit width/height + // BarWidgetLoader sets explicit width/height to extend click area + implicitWidth: contentWidth + implicitHeight: contentHeight + + Component.onCompleted: SystemStatService.registerComponent("bar-sysmon:" + (screen?.name || "unknown")) + Component.onDestruction: SystemStatService.unregisterComponent("bar-sysmon:" + (screen?.name || "unknown")) + + function openExternalMonitor() { + Quickshell.execDetached(["sh", "-c", Settings.data.systemMonitor.externalMonitor]); + } + + // Build comprehensive tooltip text with all stats + function buildTooltipContent() { + let rows = []; + + // CPU + rows.push([I18n.tr("system-monitor.cpu-usage"), `${Math.round(SystemStatService.cpuUsage)}% (${SystemStatService.cpuFreq.replace(/[^0-9.]/g, "")} GHz)`]); + if (showCpuCores) { + SystemStatService.coresUsage.forEach((usage, core) => rows.push([" " + I18n.tr("system-monitor.core-usage", { + "id": core + }), `${Math.round(usage)}%`])); + } + + if (SystemStatService.cpuTemp > 0) { + rows.push([I18n.tr("system-monitor.cpu-temp"), `${Math.round(SystemStatService.cpuTemp)}°C`]); + } + + // GPU (if available) + if (SystemStatService.gpuAvailable) { + rows.push([I18n.tr("system-monitor.gpu-temp"), `${Math.round(SystemStatService.gpuTemp)}°C`]); + } + + // Load Average + if (SystemStatService.loadAvg1 >= 0) { + rows.push([I18n.tr("system-monitor.load-average"), `${SystemStatService.loadAvg1.toFixed(2)} · ${SystemStatService.loadAvg5.toFixed(2)} · ${SystemStatService.loadAvg15.toFixed(2)}`]); + } + + // Memory + rows.push([I18n.tr("common.memory"), `${Math.round(SystemStatService.memPercent)}% (${(SystemStatService.memGb).toFixed(1)} GiB)`]); + + // Swap (if available) + if (SystemStatService.swapTotalGb > 0) { + rows.push([I18n.tr("bar.system-monitor.swap-usage-label"), `${Math.round(SystemStatService.swapPercent)}% (${(SystemStatService.swapGb).toFixed(1)} GiB)`]); + } + + // Network + rows.push([I18n.tr("system-monitor.download-speed"), `${SystemStatService.formatSpeed(SystemStatService.rxSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")}` + "/s"]); + rows.push([I18n.tr("system-monitor.upload-speed"), `${SystemStatService.formatSpeed(SystemStatService.txSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2")}` + "/s"]); + + // Disk + const diskPercent = SystemStatService.diskPercents[diskPath]; + if (diskPercent !== undefined) { + const usedGb = SystemStatService.diskUsedGb[diskPath] || 0; + const sizeGb = SystemStatService.diskSizeGb[diskPath] || 0; + const availGb = SystemStatService.diskAvailableGb[diskPath] || 0; + rows.push([I18n.tr("system-monitor.disk"), `${diskPercent}% (${usedGb.toFixed(1)} / ${sizeGb.toFixed(1)} GB)`]); + rows.push([I18n.tr("common.available"), `${availGb.toFixed(1)} GB`]); + } + + return rows; + } + + // Visibility-aware warning/critical states (delegates to service) + readonly property bool cpuWarning: showCpuUsage && SystemStatService.cpuWarning + readonly property bool cpuCritical: showCpuUsage && SystemStatService.cpuCritical + readonly property bool tempWarning: showCpuTemp && SystemStatService.tempWarning + readonly property bool tempCritical: showCpuTemp && SystemStatService.tempCritical + readonly property bool gpuWarning: showGpuTemp && SystemStatService.gpuWarning + readonly property bool gpuCritical: showGpuTemp && SystemStatService.gpuCritical + readonly property bool memWarning: showMemoryUsage && SystemStatService.memWarning + readonly property bool memCritical: showMemoryUsage && SystemStatService.memCritical + readonly property bool swapWarning: showSwapUsage && SystemStatService.swapWarning + readonly property bool swapCritical: showSwapUsage && SystemStatService.swapCritical + readonly property bool diskWarning: showDiskUsage && SystemStatService.isDiskWarning(diskPath) + readonly property bool diskCritical: showDiskUsage && SystemStatService.isDiskCritical(diskPath) + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("system-monitor.title"), + "action": "sysmon-settings", + "icon": "settings" + }, + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "sysmon-settings") { + let monitorCmd = Settings.data.systemMonitor.externalMonitor; + if (monitorCmd && monitorCmd.trim() !== "") { + openExternalMonitor(); + } else { + SettingsPanelService.openToTab(SettingsPanel.Tab.System, 0, screen); + } + } else if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + // Visual capsule centered in parent + Rectangle { + id: visualCapsule + width: root.contentWidth + height: root.contentHeight + anchors.centerIn: parent + radius: Style.radiusM + color: Style.capsuleColor + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + // Mini gauge component for compact mode, vertical gauge that fills from bottom + Component { + id: miniGaugeComponent + + NLinearGauge { + ratio: 0 + orientation: Qt.Vertical + fillColor: Color.mPrimary + width: miniGaugeWidth + height: iconSize + } + } + + GridLayout { + id: mainGrid + anchors.centerIn: parent + flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: isVertical ? -1 : 1 + columns: isVertical ? 1 : -1 + rowSpacing: isVertical ? (compactMode ? Style.marginL : Style.marginXL) : 0 + columnSpacing: isVertical ? 0 : Style.marginM + + // CPU Usage Component + Item { + id: cpuUsageContainer + implicitWidth: cpuUsageContent.implicitWidth + implicitHeight: cpuUsageContent.implicitHeight + Layout.preferredWidth: isVertical ? root.width : implicitWidth + Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight + Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter + visible: showCpuUsage + + GridLayout { + id: cpuUsageContent + anchors.centerIn: parent + + property bool verticalDisplay: isVertical && (!compactMode || showCpuCores) + flow: verticalDisplay ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: verticalDisplay ? -1 : 1 + columns: verticalDisplay ? 1 : -1 + rowSpacing: compactMode ? 3 : Style.marginXS + columnSpacing: compactMode ? 3 : Style.marginXS + + Item { + Layout.preferredWidth: iconSize + Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight + Layout.alignment: Qt.AlignCenter + Layout.row: (isVertical && !compactMode) ? 1 : 0 + Layout.column: 0 + + NIcon { + icon: "cpu-usage" + pointSize: iconSize + applyUiScale: false + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, contentHeight) + color: (cpuWarning || cpuCritical) ? SystemStatService.cpuColor : root.iconColor + } + } + + // Text mode + NText { + visible: !compactMode + text: `${Math.round(SystemStatService.cpuUsage)}%`.padStart(paddingPercent, " ") + family: fontFamily + pointSize: barFontSize + applyUiScale: false + Layout.alignment: Qt.AlignCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: (cpuWarning || cpuCritical) ? SystemStatService.cpuColor : root.textColor + Layout.row: isVertical ? 0 : 0 + Layout.column: isVertical ? 0 : 1 + } + + // Compact mode general cpu + Loader { + active: compactMode && !showCpuCores + visible: compactMode && !showCpuCores + sourceComponent: miniGaugeComponent + Layout.alignment: Qt.AlignCenter + Layout.row: 0 + Layout.column: 1 + + onLoaded: { + item.ratio = Qt.binding(() => SystemStatService.cpuUsage / 100); + item.fillColor = Qt.binding(() => SystemStatService.cpuColor); + } + } + + // Compact mode for cores + Repeater { + model: (compactMode && showCpuCores) ? SystemStatService.coresUsage : [] + + delegate: NLinearGauge { + required property var modelData + width: isVertical ? iconSize : miniGaugeWidth + height: isVertical ? miniGaugeWidth : iconSize + orientation: isVertical ? Qt.Horizontal : Qt.Vertical + ratio: modelData / 100 + fillColor: SystemStatService.getCoreUsageColor(modelData) + } + } + } + } + + // CPU Frequency Component + Item { + id: cpuFreqContainer + implicitWidth: cpuFreqContent.implicitWidth + implicitHeight: cpuFreqContent.implicitHeight + Layout.preferredWidth: isVertical ? root.width : implicitWidth + Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight + Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter + visible: showCpuFreq && (!isVertical || compactMode) + + GridLayout { + id: cpuFreqContent + anchors.centerIn: parent + flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: (isVertical && !compactMode) ? 2 : 1 + columns: (isVertical && !compactMode) ? 1 : 2 + rowSpacing: Style.marginXXS + columnSpacing: compactMode ? 3 : Style.marginXS + + Item { + Layout.preferredWidth: iconSize + Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight + Layout.alignment: Qt.AlignCenter + Layout.row: (isVertical && !compactMode) ? 1 : 0 + Layout.column: 0 + + NIcon { + icon: "cpu-usage" + pointSize: iconSize + applyUiScale: false + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, contentHeight) + color: root.iconColor + } + } + + // Text mode + NText { + visible: !compactMode + text: SystemStatService.cpuFreq.replace("Hz", "").replace(" ", "").padStart(paddingCpuFreq, " ") + family: fontFamily + pointSize: barFontSize + applyUiScale: false + Layout.alignment: Qt.AlignCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: root.textColor + Layout.row: isVertical ? 0 : 0 + Layout.column: isVertical ? 0 : 1 + } + + // Compact mode + Loader { + active: compactMode + visible: compactMode + sourceComponent: miniGaugeComponent + Layout.alignment: Qt.AlignCenter + Layout.row: 0 + Layout.column: 1 + + onLoaded: { + item.ratio = Qt.binding(() => SystemStatService.cpuFreqRatio); + } + } + } + } + + // CPU Temperature Component + Item { + id: cpuTempContainer + implicitWidth: cpuTempContent.implicitWidth + implicitHeight: cpuTempContent.implicitHeight + Layout.preferredWidth: isVertical ? root.width : implicitWidth + Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight + Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter + visible: showCpuTemp + + GridLayout { + id: cpuTempContent + anchors.centerIn: parent + flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: (isVertical && !compactMode) ? 2 : 1 + columns: (isVertical && !compactMode) ? 1 : 2 + rowSpacing: Style.marginXXS + columnSpacing: compactMode ? 3 : Style.marginXS + + Item { + Layout.preferredWidth: iconSize + Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight + Layout.alignment: Qt.AlignCenter + Layout.row: (isVertical && !compactMode) ? 1 : 0 + Layout.column: 0 + + NIcon { + icon: "cpu-temperature" + pointSize: iconSize + applyUiScale: false + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, contentHeight) + color: (tempWarning || tempCritical) ? SystemStatService.tempColor : root.iconColor + } + } + + // Text mode + NText { + visible: !compactMode + text: `${Math.round(SystemStatService.cpuTemp)}°`.padStart(paddingTemp, " ") + family: fontFamily + pointSize: barFontSize + applyUiScale: false + Layout.alignment: Qt.AlignCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: (tempWarning || tempCritical) ? SystemStatService.tempColor : root.textColor + Layout.row: isVertical ? 0 : 0 + Layout.column: isVertical ? 0 : 1 + } + + // Compact mode, mini gauge (to the right of icon) + Loader { + active: compactMode + visible: compactMode + sourceComponent: miniGaugeComponent + Layout.alignment: Qt.AlignCenter + Layout.row: 0 + Layout.column: 1 + + onLoaded: { + item.ratio = Qt.binding(() => SystemStatService.cpuTemp / 100); + item.fillColor = Qt.binding(() => SystemStatService.tempColor); + } + } + } + } + + // GPU Temperature Component + Item { + id: gpuTempContainer + implicitWidth: gpuTempContent.implicitWidth + implicitHeight: gpuTempContent.implicitHeight + Layout.preferredWidth: isVertical ? root.width : implicitWidth + Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight + Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter + visible: showGpuTemp && SystemStatService.gpuAvailable + + GridLayout { + id: gpuTempContent + anchors.centerIn: parent + flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: (isVertical && !compactMode) ? 2 : 1 + columns: (isVertical && !compactMode) ? 1 : 2 + rowSpacing: Style.marginXXS + columnSpacing: compactMode ? 3 : Style.marginXS + + Item { + Layout.preferredWidth: iconSize + Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight + Layout.alignment: Qt.AlignCenter + Layout.row: (isVertical && !compactMode) ? 1 : 0 + Layout.column: 0 + + NIcon { + icon: "gpu-temperature" + pointSize: iconSize + applyUiScale: false + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, contentHeight) + color: (gpuWarning || gpuCritical) ? SystemStatService.gpuColor : root.iconColor + } + } + + // Text mode + NText { + visible: !compactMode + text: `${Math.round(SystemStatService.gpuTemp)}°`.padStart(paddingTemp, " ") + family: fontFamily + pointSize: barFontSize + applyUiScale: false + Layout.alignment: Qt.AlignCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: (gpuWarning || gpuCritical) ? SystemStatService.gpuColor : root.textColor + Layout.row: isVertical ? 0 : 0 + Layout.column: isVertical ? 0 : 1 + } + + // Compact mode + Loader { + active: compactMode + visible: compactMode + sourceComponent: miniGaugeComponent + Layout.alignment: Qt.AlignCenter + Layout.row: 0 + Layout.column: 1 + + onLoaded: { + item.ratio = Qt.binding(() => SystemStatService.gpuTemp / 100); + item.fillColor = Qt.binding(() => SystemStatService.gpuColor); + } + } + } + } + + // Load Average Component + Item { + id: loadAvgContainer + implicitWidth: loadAvgContent.implicitWidth + implicitHeight: loadAvgContent.implicitHeight + Layout.preferredWidth: isVertical ? root.width : implicitWidth + Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight + Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter + visible: showLoadAverage && SystemStatService.nproc > 0 && SystemStatService.loadAvg1 > 0 + + GridLayout { + id: loadAvgContent + anchors.centerIn: parent + flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: (isVertical && !compactMode) ? 2 : 1 + columns: (isVertical && !compactMode) ? 1 : 2 + rowSpacing: Style.marginXXS + columnSpacing: compactMode ? 3 : Style.marginXS + + Item { + Layout.preferredWidth: iconSize + Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight + Layout.alignment: Qt.AlignCenter + Layout.row: (isVertical && !compactMode) ? 1 : 0 + Layout.column: 0 + + NIcon { + icon: "weight" + pointSize: iconSize + applyUiScale: false + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, contentHeight) + color: root.iconColor + } + } + + // Text mode + NText { + visible: !compactMode + text: `${SystemStatService.loadAvg1.toFixed(1)}`.padStart(usePadding ? `${SystemStatService.nproc.toFixed(1)}`.length : 0, " ") + family: fontFamily + pointSize: barFontSize + applyUiScale: false + Layout.alignment: Qt.AlignCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: root.textColor + Layout.row: isVertical ? 0 : 0 + Layout.column: isVertical ? 0 : 1 + } + + // Compact mode + Loader { + active: compactMode + visible: compactMode + sourceComponent: miniGaugeComponent + Layout.alignment: Qt.AlignCenter + Layout.row: 0 + Layout.column: 1 + + onLoaded: { + item.ratio = Qt.binding(() => Math.min(1, SystemStatService.loadAvg1 / SystemStatService.nproc)); + } + } + } + } + + // Memory Usage Component + Item { + id: memoryContainer + implicitWidth: memoryContent.implicitWidth + implicitHeight: memoryContent.implicitHeight + Layout.preferredWidth: isVertical ? root.width : implicitWidth + Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight + Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter + visible: showMemoryUsage + + GridLayout { + id: memoryContent + anchors.centerIn: parent + flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: (isVertical && !compactMode) ? 2 : 1 + columns: (isVertical && !compactMode) ? 1 : 2 + rowSpacing: Style.marginXXS + columnSpacing: compactMode ? 3 : Style.marginXS + + Item { + Layout.preferredWidth: iconSize + Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight + Layout.alignment: Qt.AlignCenter + Layout.row: (isVertical && !compactMode) ? 1 : 0 + Layout.column: 0 + + NIcon { + icon: "memory" + pointSize: iconSize + applyUiScale: false + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, contentHeight) + color: (memWarning || memCritical) ? SystemStatService.memColor : root.iconColor + } + } + + // Text mode + NText { + visible: !compactMode + text: SystemStatService.formatRamDisplay({ + percent: showMemoryAsPercent, + padding: usePadding + }) + family: fontFamily + pointSize: barFontSize + applyUiScale: false + Layout.alignment: Qt.AlignCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: (memWarning || memCritical) ? SystemStatService.memColor : root.textColor + Layout.row: isVertical ? 0 : 0 + Layout.column: isVertical ? 0 : 1 + } + + // Compact mode + Loader { + active: compactMode + visible: compactMode + sourceComponent: miniGaugeComponent + Layout.alignment: Qt.AlignCenter + Layout.row: 0 + Layout.column: 1 + + onLoaded: { + item.ratio = Qt.binding(() => SystemStatService.memPercent / 100); + item.fillColor = Qt.binding(() => SystemStatService.memColor); + } + } + } + } + + // Swap Usage Component + Item { + id: swapContainer + implicitWidth: swapContent.implicitWidth + implicitHeight: swapContent.implicitHeight + Layout.preferredWidth: isVertical ? root.width : implicitWidth + Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight + Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter + visible: showSwapUsage && SystemStatService.swapTotalGb > 0 + + GridLayout { + id: swapContent + anchors.centerIn: parent + flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: (isVertical && !compactMode) ? 2 : 1 + columns: (isVertical && !compactMode) ? 1 : 2 + rowSpacing: Style.marginXXS + columnSpacing: compactMode ? 3 : Style.marginXS + + Item { + Layout.preferredWidth: iconSize + Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight + Layout.alignment: Qt.AlignCenter + Layout.row: (isVertical && !compactMode) ? 1 : 0 + Layout.column: 0 + + NIcon { + icon: "exchange" + pointSize: iconSize + applyUiScale: false + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, contentHeight) + color: (swapWarning || swapCritical) ? SystemStatService.swapColor : root.iconColor + } + } + + // Text mode + NText { + visible: !compactMode + text: SystemStatService.formatRamDisplay({ + swap: true, + percent: true, + padding: usePadding + }) + family: fontFamily + pointSize: barFontSize + applyUiScale: false + Layout.alignment: Qt.AlignCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: (swapWarning || swapCritical) ? SystemStatService.swapColor : root.textColor + Layout.row: isVertical ? 0 : 0 + Layout.column: isVertical ? 0 : 1 + } + + // Compact mode + Loader { + active: compactMode + visible: compactMode + sourceComponent: miniGaugeComponent + Layout.alignment: Qt.AlignCenter + Layout.row: 0 + Layout.column: 1 + + onLoaded: { + item.ratio = Qt.binding(() => SystemStatService.swapPercent / 100); + item.fillColor = Qt.binding(() => SystemStatService.swapColor); + } + } + } + } + + // Network Download Speed Component + Item { + implicitWidth: downloadContent.implicitWidth + implicitHeight: downloadContent.implicitHeight + Layout.preferredWidth: isVertical ? root.width : implicitWidth + Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight + Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter + visible: showNetworkStats + + GridLayout { + id: downloadContent + anchors.centerIn: parent + flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: (isVertical && !compactMode) ? 2 : 1 + columns: (isVertical && !compactMode) ? 1 : 2 + rowSpacing: Style.marginXXS + columnSpacing: compactMode ? 3 : Style.marginXS + + Item { + Layout.preferredWidth: iconSize + Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight + Layout.alignment: Qt.AlignCenter + Layout.row: (isVertical && !compactMode) ? 1 : 0 + Layout.column: 0 + + NIcon { + icon: "download-speed" + pointSize: iconSize + applyUiScale: false + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, contentHeight) + color: root.iconColor + } + } + + // Text mode + NText { + visible: !compactMode + text: isVertical ? SystemStatService.formatCompactSpeed(SystemStatService.rxSpeed) : SystemStatService.formatSpeed(SystemStatService.rxSpeed).padStart(paddingSpeed, " ") + family: fontFamily + pointSize: barFontSize + applyUiScale: false + Layout.alignment: Qt.AlignCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: root.textColor + Layout.row: isVertical ? 0 : 0 + Layout.column: isVertical ? 0 : 1 + } + + // Compact mode + Loader { + active: compactMode + visible: compactMode + sourceComponent: miniGaugeComponent + Layout.alignment: Qt.AlignCenter + Layout.row: 0 + Layout.column: 1 + + onLoaded: { + item.ratio = Qt.binding(() => SystemStatService.rxRatio); + } + } + } + } + + // Network Upload Speed Component + Item { + implicitWidth: uploadContent.implicitWidth + implicitHeight: uploadContent.implicitHeight + Layout.preferredWidth: isVertical ? root.width : implicitWidth + Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight + Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter + visible: showNetworkStats + + GridLayout { + id: uploadContent + anchors.centerIn: parent + flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: (isVertical && !compactMode) ? 2 : 1 + columns: (isVertical && !compactMode) ? 1 : 2 + rowSpacing: Style.marginXXS + columnSpacing: compactMode ? 3 : Style.marginXS + + Item { + Layout.preferredWidth: iconSize + Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight + Layout.alignment: Qt.AlignCenter + Layout.row: (isVertical && !compactMode) ? 1 : 0 + Layout.column: 0 + + NIcon { + icon: "upload-speed" + pointSize: iconSize + applyUiScale: false + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, contentHeight) + color: root.iconColor + } + } + + // Text mode + NText { + visible: !compactMode + text: isVertical ? SystemStatService.formatCompactSpeed(SystemStatService.txSpeed) : SystemStatService.formatSpeed(SystemStatService.txSpeed).padStart(paddingSpeed, " ") + family: fontFamily + pointSize: barFontSize + applyUiScale: false + Layout.alignment: Qt.AlignCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: root.textColor + Layout.row: isVertical ? 0 : 0 + Layout.column: isVertical ? 0 : 1 + } + + // Compact mode + Loader { + active: compactMode + visible: compactMode + sourceComponent: miniGaugeComponent + Layout.alignment: Qt.AlignCenter + Layout.row: 0 + Layout.column: 1 + + onLoaded: { + item.ratio = Qt.binding(() => SystemStatService.txRatio); + } + } + } + } + + // Disk Usage Component (primary drive) + Item { + id: diskContainer + implicitWidth: diskContent.implicitWidth + implicitHeight: diskContent.implicitHeight + Layout.preferredWidth: isVertical ? root.width : implicitWidth + Layout.preferredHeight: compactMode ? implicitHeight : capsuleHeight + Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter + visible: showDiskUsage + + GridLayout { + id: diskContent + anchors.centerIn: parent + flow: (isVertical && !compactMode) ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: (isVertical && !compactMode) ? 2 : 1 + columns: (isVertical && !compactMode) ? 1 : 2 + rowSpacing: Style.marginXXS + columnSpacing: compactMode ? 3 : Style.marginXS + + Item { + Layout.preferredWidth: iconSize + Layout.preferredHeight: (compactMode || isVertical) ? iconSize : capsuleHeight + Layout.alignment: Qt.AlignCenter + Layout.row: (isVertical && !compactMode) ? 1 : 0 + Layout.column: 0 + + NIcon { + icon: "storage" + pointSize: iconSize + applyUiScale: false + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, contentHeight) + color: (diskWarning || diskCritical) ? SystemStatService.getDiskColor(diskPath) : root.iconColor + } + } + + // Text mode + NText { + visible: !compactMode + text: SystemStatService.formatDiskDisplay(diskPath, { + percent: showDiskUsageAsPercent, + available: showDiskAvailable, + padding: usePadding + }) + family: fontFamily + pointSize: barFontSize + applyUiScale: false + Layout.alignment: Qt.AlignCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: (diskWarning || diskCritical) ? SystemStatService.getDiskColor(diskPath) : root.textColor + Layout.row: isVertical ? 0 : 0 + Layout.column: isVertical ? 0 : 1 + } + + // Compact mode + Loader { + active: compactMode + visible: compactMode + sourceComponent: miniGaugeComponent + Layout.alignment: Qt.AlignCenter + Layout.row: 0 + Layout.column: 1 + + onLoaded: { + item.ratio = Qt.binding(() => (showDiskAvailable ? SystemStatService.diskAvailPercents[diskPath] : SystemStatService.diskPercents[diskPath] ?? 0) / 100); + item.fillColor = Qt.binding(() => SystemStatService.getDiskColor(diskPath, showDiskAvailable)); + } + } + } + } + } + } + + // MouseArea at root level for extended click area + MouseArea { + id: tooltipArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + hoverEnabled: true + onClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + PanelService.getPanel("systemStatsPanel", screen)?.toggle(root); + TooltipService.hide(); + } else if (mouse.button === Qt.RightButton) { + TooltipService.hide(); + PanelService.showContextMenu(contextMenu, root, screen); + } else if (mouse.button === Qt.MiddleButton) { + TooltipService.hide(); + openExternalMonitor(); + } + } + onEntered: { + if (!PanelService.getPanel("systemStatsPanel", screen).isPanelOpen) { + TooltipService.show(root, buildTooltipContent(), BarService.getTooltipDirection(root.screen?.name)); + tooltipRefreshTimer.start(); + } + } + onExited: { + tooltipRefreshTimer.stop(); + TooltipService.hide(); + } + } + + Timer { + id: tooltipRefreshTimer + interval: 1000 + repeat: true + onTriggered: { + if (tooltipArea.containsMouse) { + TooltipService.updateText(buildTooltipContent()); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Taskbar.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Taskbar.qml new file mode 100644 index 0000000..339727e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Taskbar.qml @@ -0,0 +1,1026 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Commons +import qs.Services.Compositor +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" + readonly property real barHeight: Style.getBarHeightForScreen(screenName) + readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName) + readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName) + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + property bool hasWindow: false + readonly property string hideMode: (widgetSettings.hideMode !== undefined) ? widgetSettings.hideMode : widgetMetadata.hideMode + readonly property bool onlySameOutput: (widgetSettings.onlySameOutput !== undefined) ? widgetSettings.onlySameOutput : widgetMetadata.onlySameOutput + readonly property bool onlyActiveWorkspaces: (widgetSettings.onlyActiveWorkspaces !== undefined) ? widgetSettings.onlyActiveWorkspaces : widgetMetadata.onlyActiveWorkspaces + readonly property bool showTitle: isVerticalBar ? false : (widgetSettings.showTitle !== undefined) ? widgetSettings.showTitle : widgetMetadata.showTitle + readonly property bool smartWidth: (widgetSettings.smartWidth !== undefined) ? widgetSettings.smartWidth : widgetMetadata.smartWidth + readonly property int maxTaskbarWidthPercent: (widgetSettings.maxTaskbarWidth !== undefined) ? widgetSettings.maxTaskbarWidth : widgetMetadata.maxTaskbarWidth + readonly property real iconScale: (widgetSettings.iconScale !== undefined) ? widgetSettings.iconScale : widgetMetadata.iconScale + readonly property int itemSize: Style.toOdd(capsuleHeight * Math.max(0.1, iconScale)) + + // Maximum width for the taskbar widget to prevent overlapping with other widgets + readonly property real maxTaskbarWidth: { + if (!screen || isVerticalBar || !smartWidth || maxTaskbarWidthPercent <= 0) + return 0; + var barFloating = Settings.data.bar.barType === "floating"; + var barMarginH = barFloating ? Math.ceil(Settings.data.bar.marginHorizontal) : 0; + var availableWidth = screen.width - (barMarginH * 2); + return Math.round(availableWidth * (maxTaskbarWidthPercent / 100)); + } + + readonly property int titleWidth: { + // First, use user-defined title width if set + var calculatedWidth = (widgetSettings.titleWidth !== undefined) ? widgetSettings.titleWidth : widgetMetadata.titleWidth; + + // Second, shrink title width if it exceeds maxTaskbarWidth when smartWidth is enabled + if (smartWidth && combinedModel.length > 0) { + if (maxTaskbarWidth > 0) { + var entriesCount = combinedModel.length; + var maxWidthPerEntry = (maxTaskbarWidth / entriesCount) - itemSize - Style.marginS - Style.margin2M; + calculatedWidth = Math.min(calculatedWidth, maxWidthPerEntry); + } + + calculatedWidth = Math.max(Math.round(calculatedWidth), 20); + } + + return calculatedWidth; + } + readonly property bool showPinnedApps: (widgetSettings.showPinnedApps !== undefined) ? widgetSettings.showPinnedApps : widgetMetadata.showPinnedApps + + // Context menu state - store ID instead of object reference to avoid stale references + property string selectedWindowId: "" + property string selectedAppId: "" + + // Helper to get the current window object from ID + function getSelectedWindow() { + if (!selectedWindowId) + return null; + for (var i = 0; i < combinedModel.length; i++) { + // Using loose equality on purpose (==) + if (combinedModel[i].id == selectedWindowId && combinedModel[i].window) { + return combinedModel[i].window; + } + } + return null; + } + property int modelUpdateTrigger: 0 // Dummy property to force model re-evaluation + + // Hover state + property var hoveredWindowId: "" + // Combined model of running windows and pinned apps + property var combinedModel: [] + + // Wheel scroll handling + property int wheelAccumulatedDelta: 0 + property bool wheelCooldown: false + + // Drag and Drop state for visual feedback + property int dragSourceIndex: -1 + property int dragTargetIndex: -1 + + // Track the session order of apps (transient reordering) + property var sessionAppOrder: [] + + function getAppKey(appData) { + if (!appData) + return null; + // prefer window object identity for running apps to distinguish instances + if (appData.window) + return appData.window; + // fallback to appId for pinned-only apps + return appData.appId; + } + + function sortApps(apps) { + if (!sessionAppOrder || sessionAppOrder.length === 0) { + return apps; + } + + const sorted = []; + const remaining = [...apps]; + + // 1. Pick apps that are in the session order + for (let i = 0; i < sessionAppOrder.length; i++) { + const key = sessionAppOrder[i]; + const idx = remaining.findIndex(app => getAppKey(app) === key); + if (idx !== -1) { + sorted.push(remaining[idx]); + remaining.splice(idx, 1); + } + } + + // 2. Append any new/remaining apps + remaining.forEach(app => sorted.push(app)); + + return sorted; + } + + function reorderApps(fromIndex, toIndex) { + Logger.d("Taskbar", "Reordering apps from " + fromIndex + " to " + toIndex); + if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= combinedModel.length || toIndex >= combinedModel.length) + return; + + const list = [...combinedModel]; + const item = list.splice(fromIndex, 1)[0]; + list.splice(toIndex, 0, item); + + combinedModel = list; + sessionAppOrder = combinedModel.map(getAppKey); + savePinnedOrder(); + } + + function savePinnedOrder() { + const currentPinned = Settings.data.dock.pinnedApps || []; + const newPinned = []; + const seen = new Set(); + + // Extract pinned apps in their current visual order + combinedModel.forEach(app => { + if (app.appId && !seen.has(app.appId)) { + const isPinned = currentPinned.some(p => normalizeAppId(p) === normalizeAppId(app.appId)); + + if (isPinned) { + newPinned.push(app.appId); + seen.add(app.appId); + } + } + }); + + // Check if any pinned apps were missed (e.g. filtered out by workspace) + currentPinned.forEach(p => { + if (!seen.has(p)) { + newPinned.push(p); + seen.add(p); + } + }); + + if (JSON.stringify(currentPinned) !== JSON.stringify(newPinned)) { + Settings.data.dock.pinnedApps = newPinned; + } + } + + // Helper function to normalize app IDs for case-insensitive matching + function normalizeAppId(appId) { + if (!appId || typeof appId !== 'string') + return ""; + return appId.toLowerCase().trim(); + } + + // Helper function to check if an app ID matches a pinned app (case-insensitive) + function isAppIdPinned(appId, pinnedApps) { + if (!appId || !pinnedApps || pinnedApps.length === 0) + return false; + const normalizedId = normalizeAppId(appId); + // Direct match + if (pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId)) + return true; + // Resolve via desktop entry lookup (handles StartupWMClass != .desktop filename) + const resolved = resolveToDesktopEntryId(appId); + if (resolved !== appId) { + const normalizedResolved = normalizeAppId(resolved); + return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedResolved); + } + return false; + } + + // Desktop entry ID resolution cache (cleared when DesktopEntries change) + property var _desktopEntryIdCache: ({}) + + // Resolve a toplevel appId to its canonical .desktop entry ID via heuristic lookup. + function resolveToDesktopEntryId(appId) { + if (!appId) + return appId; + if (_desktopEntryIdCache.hasOwnProperty(appId)) + return _desktopEntryIdCache[appId]; + try { + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) { + const entry = DesktopEntries.heuristicLookup(appId); + if (entry && entry.id) { + _desktopEntryIdCache[appId] = entry.id; + return entry.id; + } + } + } catch (e) {} + _desktopEntryIdCache[appId] = appId; + return appId; + } + + // Helper function to get app name from desktop entry + function getAppNameFromDesktopEntry(appId) { + if (!appId) + return appId; + + try { + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) { + const entry = DesktopEntries.heuristicLookup(appId); + if (entry && entry.name) { + return entry.name; + } + } + + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId) { + const entry = DesktopEntries.byId(appId); + if (entry && entry.name) { + return entry.name; + } + } + } catch (e) + // Fall through to return original appId + {} + + // Return original appId if we can't find a desktop entry + return appId; + } + + // Helper function to get desktop entry ID from an app ID + function getDesktopEntryId(appId) { + if (!appId) + return appId; + + // Try to find the desktop entry using heuristic lookup + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) { + try { + const entry = DesktopEntries.heuristicLookup(appId); + if (entry && entry.id) { + return entry.id; + } + } catch (e) + // Fall through to return original appId + {} + } + + // Try direct lookup + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId) { + try { + const entry = DesktopEntries.byId(appId); + if (entry && entry.id) { + return entry.id; + } + } catch (e) + // Fall through to return original appId + {} + } + + // Return original appId if we can't find a desktop entry + return appId; + } + + // Helper function to check if an app is pinned + function isAppPinned(appId) { + if (!appId) + return false; + const pinnedApps = Settings.data.dock.pinnedApps || []; + const normalizedId = normalizeAppId(appId); + if (pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId)) + return true; + const resolved = resolveToDesktopEntryId(appId); + if (resolved !== appId) { + const normalizedResolved = normalizeAppId(resolved); + return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedResolved); + } + return false; + } + + // Helper function to toggle app pin/unpin + function toggleAppPin(appId) { + if (!appId) + return; + + // Get the desktop entry ID for consistent pinning + const desktopEntryId = getDesktopEntryId(appId); + const normalizedId = normalizeAppId(desktopEntryId); + + let pinnedApps = (Settings.data.dock.pinnedApps || []).slice(); // Create a copy + + // Find existing pinned app with case-insensitive matching + const existingIndex = pinnedApps.findIndex(pinnedId => normalizeAppId(pinnedId) === normalizedId); + const isPinned = existingIndex >= 0; + + if (isPinned) { + // Unpin: remove from array + pinnedApps.splice(existingIndex, 1); + } else { + // Pin: add desktop entry ID to array + pinnedApps.push(desktopEntryId); + } + + // Update the settings + Settings.data.dock.pinnedApps = pinnedApps; + } + + // Function to update the combined model + function updateCombinedModel() { + const runningWindows = []; + const pinnedApps = Settings.data.dock.pinnedApps || []; + const processedAppIds = new Set(); + + // First pass: Add all running windows + try { + const total = CompositorService.windows.count || 0; + const activeIds = CompositorService.getActiveWorkspaces().map(function (ws) { + return ws.id; + }); + + for (var i = 0; i < total; i++) { + var w = CompositorService.windows.get(i); + if (!w) + continue; + var passOutput = (!onlySameOutput) || (w.output == screen?.name); + var passWorkspace = (!onlyActiveWorkspaces) || (activeIds.includes(w.workspaceId)); + if (passOutput && passWorkspace) { + const isPinned = isAppIdPinned(w.appId, pinnedApps); + runningWindows.push({ + "id": w.id, + "type": isPinned ? "pinned-running" : "running", + "window": w, + "appId": w.appId, + "title": w.title || getAppNameFromDesktopEntry(w.appId) + }); + processedAppIds.add(normalizeAppId(w.appId)); + // Also track the resolved desktop entry ID so pinned app matching works + const resolvedId = resolveToDesktopEntryId(w.appId); + if (resolvedId !== w.appId) + processedAppIds.add(normalizeAppId(resolvedId)); + } + } + } catch (e) + // Ignore errors + {} + + // Second pass: Add non-running pinned apps (only if showPinnedApps is enabled) + if (showPinnedApps) { + pinnedApps.forEach(pinnedAppId => { + const normalizedPinnedId = normalizeAppId(pinnedAppId); + if (!processedAppIds.has(normalizedPinnedId)) { + const appName = getAppNameFromDesktopEntry(pinnedAppId); + runningWindows.push({ + "id": pinnedAppId, + "type": "pinned", + "window": null, + "appId": pinnedAppId, + "title": appName + }); + } + }); + } + + combinedModel = sortApps(runningWindows); + + // Sync session order if needed (e.g. first run or new apps added) + if (!sessionAppOrder || sessionAppOrder.length === 0 || sessionAppOrder.length !== combinedModel.length) { + sessionAppOrder = combinedModel.map(getAppKey); + } + updateHasWindow(); + } + + // Function to launch a pinned app + function launchPinnedApp(appId) { + if (!appId) + return; + + try { + const app = DesktopEntries.byId(appId); + + if (Settings.data.appLauncher.customLaunchPrefixEnabled && Settings.data.appLauncher.customLaunchPrefix) { + // Use custom launch prefix + const prefix = Settings.data.appLauncher.customLaunchPrefix.split(" "); + + if (app.runInTerminal) { + const terminal = Settings.data.appLauncher.terminalCommand.split(" "); + const command = prefix.concat(terminal.concat(app.command)); + Quickshell.execDetached(command); + } else { + const command = prefix.concat(app.command); + Quickshell.execDetached(command); + } + } else { + if (app.runInTerminal) { + Logger.d("Taskbar", "Executing terminal app manually: " + app.name); + const terminal = Settings.data.appLauncher.terminalCommand.split(" "); + const command = terminal.concat(app.command); + CompositorService.spawn(command); + } else if (app.command && app.command.length > 0) { + CompositorService.spawn(app.command); + } else if (app.execute) { + app.execute(); + } else { + Logger.w("Taskbar", `Could not launch: ${app.name}. No valid launch method.`); + } + } + } catch (e) { + Logger.e("Taskbar", "Failed to launch app: " + e); + } + } + + NPopupContextMenu { + id: contextMenu + model: { + // Reference modelUpdateTrigger to make binding reactive + const _ = root.modelUpdateTrigger; + + var items = []; + if (root.selectedWindowId) { + // Focus item (for running apps) + items.push({ + "label": I18n.tr("common.focus"), + "action": "focus", + "icon": "eye" + }); + + // Pin/Unpin item (always available when right-clicking an app) + const isPinned = root.isAppPinned(root.selectedAppId); + items.push({ + "label": !isPinned ? I18n.tr("common.pin") : I18n.tr("common.unpin"), + "action": "pin", + "icon": !isPinned ? "pin" : "unpin" + }); + + // Close item (for running apps) + items.push({ + "label": I18n.tr("common.close"), + "action": "close", + "icon": "x" + }); + + // Add desktop entry actions (like "New Window", "Private Window", etc.) + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId && root.selectedAppId) { + const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(root.selectedAppId) : DesktopEntries.byId(root.selectedAppId); + if (entry != null && entry.actions) { + entry.actions.forEach(function (action) { + items.push({ + "label": action.name, + "action": "desktop-action-" + action.name, + "icon": "chevron-right", + "desktopAction": action + }); + }); + } + } + } + items.push({ + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }); + return items; + } + onTriggered: (action, item) => { + contextMenu.close(); + PanelService.closeContextMenu(root.screen); + + // Look up the window fresh each time to avoid stale references + const selectedWindow = root.getSelectedWindow(); + + if (action === "focus" && selectedWindow) { + CompositorService.focusWindow(selectedWindow); + } else if (action === "pin" && root.selectedAppId) { + root.toggleAppPin(root.selectedAppId); + } else if (action === "close" && selectedWindow) { + CompositorService.closeWindow(selectedWindow); + } else if (action === "widget-settings") { + BarService.openWidgetSettings(root.screen, root.section, root.sectionWidgetIndex, root.widgetId, root.widgetSettings); + } else if (action.startsWith("desktop-action-") && item && item.desktopAction) { + if (item.desktopAction.command && item.desktopAction.command.length > 0) { + Quickshell.execDetached(item.desktopAction.command); + } else if (item.desktopAction.execute) { + item.desktopAction.execute(); + } + } + root.selectedWindowId = ""; + root.selectedAppId = ""; + } + } + + function updateHasWindow() { + // Check if we have any items in the combined model (windows or pinned apps) + hasWindow = combinedModel.length > 0; + } + + Connections { + target: CompositorService + function onActiveWindowChanged() { + updateCombinedModel(); + } + function onWindowListChanged() { + updateCombinedModel(); + } + function onWorkspaceChanged() { + updateCombinedModel(); + } + } + + Connections { + target: Settings.data.dock + function onPinnedAppsChanged() { + updateCombinedModel(); + } + } + + Component.onCompleted: { + updateCombinedModel(); + } + onScreenChanged: updateCombinedModel() + + // Debounce timer for wheel interactions + Timer { + id: wheelDebounce + interval: 150 + repeat: false + onTriggered: { + root.wheelCooldown = false; + root.wheelAccumulatedDelta = 0; + } + } + + // Scroll to switch between windows + WheelHandler { + id: wheelHandler + target: root + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onWheel: function (event) { + if (root.wheelCooldown || root.combinedModel.length === 0) + return; + var dy = event.angleDelta.y; + var dx = event.angleDelta.x; + var useDy = Math.abs(dy) >= Math.abs(dx); + var delta = useDy ? dy : dx; + root.wheelAccumulatedDelta += delta; + var step = 120; + if (Math.abs(root.wheelAccumulatedDelta) >= step) { + var direction = root.wheelAccumulatedDelta > 0 ? -1 : 1; + // Find the focused window or first running window + var currentIndex = -1; + for (var i = 0; i < root.combinedModel.length; i++) { + if (root.combinedModel[i].window && root.combinedModel[i].window.isFocused) { + currentIndex = i; + break; + } + } + if (currentIndex < 0) { + // No focused window, find first running window + for (var j = 0; j < root.combinedModel.length; j++) { + if (root.combinedModel[j].window) { + currentIndex = j; + break; + } + } + } + if (currentIndex >= 0) { + var nextIndex = (currentIndex + direction + root.combinedModel.length) % root.combinedModel.length; + var nextItem = root.combinedModel[nextIndex]; + if (nextItem && nextItem.window) { + try { + CompositorService.focusWindow(nextItem.window); + } catch (error) { + Logger.e("Taskbar", "Failed to focus window: " + error); + } + } + } + root.wheelCooldown = true; + wheelDebounce.restart(); + root.wheelAccumulatedDelta = 0; + event.accepted = true; + } + } + } + + // "visible": Always Visible, "hidden": Hide When Empty, "transparent": Transparent When Empty + visible: hideMode !== "hidden" || hasWindow + opacity: ((hideMode !== "hidden" && hideMode !== "transparent") || hasWindow) ? 1.0 : 0.0 + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + // Content dimensions for implicit sizing + readonly property real contentWidth: { + if (!visible) + return 0; + if (isVerticalBar) + return barHeight; + + var calculatedWidth = showTitle ? taskbarLayout.implicitWidth : taskbarLayout.implicitWidth + Style.margin2M; + + // Apply maximum width constraint when smartWidth is enabled + if (smartWidth && maxTaskbarWidth > 0) { + return Math.min(calculatedWidth, maxTaskbarWidth); + } + + return Math.round(calculatedWidth); + } + readonly property real contentHeight: visible ? (isVerticalBar ? Math.round(taskbarLayout.implicitHeight + Style.margin2S) : capsuleHeight) : 0 + + implicitWidth: contentWidth + implicitHeight: contentHeight + + // Visual capsule centered in parent + Rectangle { + id: visualCapsule + width: root.contentWidth + height: root.contentHeight + anchors.centerIn: parent + radius: Style.radiusM + color: Style.capsuleColor + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + GridLayout { + id: taskbarLayout + + // Pixel-perfect centering + x: isVerticalBar ? Style.pixelAlignCenter(parent.width, width) : ((root.showTitle) ? Style.pixelAlignCenter(parent.width, width) : Style.marginM) + y: Style.pixelAlignCenter(parent.height, height) + + // Configure GridLayout to behave like RowLayout or ColumnLayout + rows: isVerticalBar ? -1 : 1 // -1 means unlimited + columns: isVerticalBar ? 1 : -1 // -1 means unlimited + + rowSpacing: isVerticalBar ? Style.marginXXS : 0 + columnSpacing: isVerticalBar ? 0 : Style.marginXXS + + Repeater { + model: root.combinedModel + delegate: Item { + id: taskbarItem + required property var modelData + required property int index + property ShellScreen screen: root.screen + + readonly property bool isRunning: modelData.window !== null + readonly property bool isPinned: modelData.type === "pinned" || modelData.type === "pinned-running" + readonly property bool isFocused: isRunning && modelData.window && modelData.window.isFocused + readonly property bool isPinnedRunning: isPinned && isRunning && !isFocused + readonly property bool isHovered: root.hoveredWindowId === modelData.id + + readonly property bool shouldShowTitle: root.showTitle && modelData.type !== "pinned" + readonly property real itemSpacing: Style.marginS + readonly property real contentWidth: shouldShowTitle ? root.itemSize + itemSpacing + root.titleWidth : root.itemSize + + readonly property string title: modelData.title || modelData.appId || "Unknown application" + readonly property color titleBgColor: (isHovered || isFocused) ? Color.mHover : Style.capsuleColor + readonly property color titleFgColor: (isHovered || isFocused) ? Color.mOnHover : Color.mOnSurface + + Layout.preferredWidth: root.isVerticalBar ? root.barHeight : (root.showTitle ? Math.round(contentWidth + Style.margin2M) : Math.round(contentWidth)) // Add margins for both pinned and running apps + Layout.preferredHeight: root.isVerticalBar ? root.itemSize : root.barHeight + Layout.alignment: Qt.AlignCenter + + // Ensure dragged item is on top + z: (root.dragSourceIndex === index) ? 1000 : 1 + + property int modelIndex: index + objectName: "taskbarAppItem" + + DropArea { + anchors.fill: parent + keys: ["taskbar-app"] + onEntered: function (drag) { + if (drag.source && drag.source.objectName === "taskbarAppItem") { + root.dragTargetIndex = taskbarItem.modelIndex; + } + } + onExited: function () { + if (root.dragTargetIndex === taskbarItem.modelIndex) { + root.dragTargetIndex = -1; + } + } + onDropped: function (drop) { + root.dragSourceIndex = -1; + root.dragTargetIndex = -1; + Logger.d("Taskbar", "Dropped! Source: " + (drop.source ? drop.source.objectName : "null") + " Index: " + (drop.source ? drop.source.modelIndex : "?") + " -> Target Index: " + taskbarItem.modelIndex); + if (drop.source && drop.source.objectName === "taskbarAppItem" && drop.source !== taskbarItem) { + root.reorderApps(drop.source.modelIndex, taskbarItem.modelIndex); + } else { + Logger.d("Taskbar", "Drop ignored. Source objectName: " + (drop.source ? drop.source.objectName : "null")); + } + } + } + + Item { + id: draggableContent + width: parent.width + height: parent.height + anchors.centerIn: dragging ? undefined : parent + + // Visual shifting logic + readonly property bool isDragged: root.dragSourceIndex === index + property real shiftOffset: 0 + + // Calculate shift based on drag state + // If I am NOT the dragged item, but I am in the path of the drag + Binding on shiftOffset { + value: { + if (root.dragSourceIndex !== -1 && root.dragTargetIndex !== -1 && !draggableContent.isDragged) { + if (root.dragSourceIndex < root.dragTargetIndex) { + // Dragging Right: Items between source and target shift Left + if (index > root.dragSourceIndex && index <= root.dragTargetIndex) { + return -1 * (root.isVerticalBar ? root.itemSize : draggableContent.width); // Simple approximation, could be refined + } + } else if (root.dragSourceIndex > root.dragTargetIndex) { + // Dragging Left: Items between target and source shift Right + if (index >= root.dragTargetIndex && index < root.dragSourceIndex) { + return (root.isVerticalBar ? root.itemSize : draggableContent.width); + } + } + } + return 0; + } + } + + transform: Translate { + x: !root.isVerticalBar ? draggableContent.shiftOffset : 0 + y: root.isVerticalBar ? draggableContent.shiftOffset : 0 + + Behavior on x { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutQuad + } + } + Behavior on y { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutQuad + } + } + } + + property bool dragging: taskbarMouseArea.drag.active + onDraggingChanged: { + if (dragging) { + root.dragSourceIndex = index; + } else { + // Don't reset immediately on release to allow drop to handle it, + // or use a timer if needed, but drop handler usually fires. + // However, if dropped outside, we need to reset. + // Let's reset if not handled by drop area quickly? + // Actually, drag.active becomes false on release. + // We might want to clear it if no drop happened. + if (root.dragSourceIndex === index) { + // Slight delay/check? For now, let DropArea handle reset on success. + // If cancelled (dropped nowhere), we should reset. + Qt.callLater(() => { + if (!taskbarMouseArea.drag.active && root.dragSourceIndex === index) { + root.dragSourceIndex = -1; + root.dragTargetIndex = -1; + } + }); + } + } + } + + Drag.active: dragging + Drag.source: taskbarItem + Drag.hotSpot.x: width / 2 + Drag.hotSpot.y: height / 2 + Drag.keys: ["taskbar-app"] + + z: dragging ? 1000 : 0 + scale: dragging ? 1.05 : 1.0 + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + } + } + + Rectangle { + id: titleBackground + visible: shouldShowTitle + anchors.centerIn: parent + width: parent.width + height: root.capsuleHeight + color: titleBgColor + radius: Style.radiusM + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + } + + Rectangle { + anchors.centerIn: parent + width: taskbarItem.contentWidth + height: parent.height + color: "transparent" + + RowLayout { + id: itemLayout + anchors.fill: parent + spacing: taskbarItem.itemSpacing + + Item { + Layout.preferredWidth: root.itemSize + Layout.preferredHeight: root.itemSize + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + + IconImage { + id: appIcon + anchors.fill: parent + + source: ThemeIcons.iconForAppId(taskbarItem.modelData.appId) + smooth: true + asynchronous: true + + // Apply dock shader to all taskbar icons + layer.enabled: widgetSettings.colorizeIcons !== false + layer.effect: ShaderEffect { + property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant + property real colorizeMode: 0.0 // Dock mode (grayscale) + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") + } + } + + Rectangle { + id: iconBackground + visible: !shouldShowTitle + anchors.bottomMargin: -2 + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + width: Style.toOdd(root.itemSize * 0.25) + height: 4 + color: taskbarItem.isFocused ? Color.mPrimary : (taskbarItem.isHovered ? Color.mHover : "transparent") + radius: Math.min(Style.radiusXXS, width / 2) + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + } + + NText { + id: titleText + visible: shouldShowTitle + Layout.preferredWidth: root.titleWidth + Layout.preferredHeight: root.itemSize + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.fillWidth: false + + text: taskbarItem.title + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + + pointSize: barFontSize + color: titleFgColor + opacity: Style.opacityFull + } + } + } + } + + MouseArea { + id: taskbarMouseArea + objectName: "taskbarMouseArea" + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + + drag.target: draggableContent + drag.axis: root.isVerticalBar ? Drag.YAxis : Drag.XAxis + preventStealing: true + + onPressed: { + // Constrain drag to roughly the taskbar area but allow some freedom + // Or just let it be free since we only care about drops + } + + onReleased: { + if (draggableContent.Drag.active) { + draggableContent.Drag.drop(); + } + } + + onClicked: mouse => { + if (!modelData) + return; + if (mouse.button === Qt.LeftButton) { + if (isRunning && modelData.window) { + // Running app - focus it + try { + CompositorService.focusWindow(modelData.window); + } catch (error) { + Logger.e("Taskbar", "Failed to activate toplevel: " + error); + } + } else if (isPinned) { + // Pinned app not running - launch it + root.launchPinnedApp(modelData.appId); + } + } else if (mouse.button === Qt.RightButton) { + TooltipService.hide(); + // Only show context menu for running apps + if (isRunning && modelData.window) { + root.selectedWindowId = modelData.id; + root.selectedAppId = modelData.appId; + root.openTaskbarContextMenu(taskbarItem); + } + } + } + onEntered: { + root.hoveredWindowId = taskbarItem.modelData.id; + TooltipService.show(taskbarItem, taskbarItem.title, BarService.getTooltipDirection(root.screen?.name)); + } + onExited: { + root.hoveredWindowId = ""; + TooltipService.hide(); + } + } + } + } + } + } + + function openTaskbarContextMenu(item) { + // Build menu model directly + var items = []; + if (root.selectedWindowId) { + // Focus item (for running apps) + items.push({ + "label": I18n.tr("common.focus"), + "action": "focus", + "icon": "eye" + }); + + // Pin/Unpin item + const isPinned = root.isAppPinned(root.selectedAppId); + items.push({ + "label": !isPinned ? I18n.tr("common.pin") : I18n.tr("common.unpin"), + "action": "pin", + "icon": !isPinned ? "pin" : "unpin" + }); + + // Close item + items.push({ + "label": I18n.tr("common.close"), + "action": "close", + "icon": "x" + }); + + // Add desktop entry actions (like "New Window", "Private Window", etc.) + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId && root.selectedAppId) { + const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(root.selectedAppId) : DesktopEntries.byId(root.selectedAppId); + if (entry != null && entry.actions) { + entry.actions.forEach(function (action) { + items.push({ + "label": action.name, + "action": "desktop-action-" + action.name, + "icon": "chevron-right", + "desktopAction": action + }); + }); + } + } + } + items.push({ + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }); + + // Set the model directly + contextMenu.model = items; + + // Anchor to root (stable) but center horizontally on the clicked item + PanelService.showContextMenu(contextMenu, root, screen, item); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Tray.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Tray.qml new file mode 100644 index 0000000..ef33111 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Tray.qml @@ -0,0 +1,580 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.SystemTray +import Quickshell.Widgets +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Trigger re-evaluation when window is registered + property int popupMenuUpdateTrigger: 0 + + // Get shared popup menu window from PanelService (reactive to trigger changes) + readonly property var popupMenuWindow: { + // Reference trigger to force re-evaluation + var popupMenuUpdateTriggerRef = popupMenuUpdateTrigger; + return PanelService.getPopupMenuWindow(screen); + } + + readonly property var trayMenu: popupMenuWindow ? popupMenuWindow.trayMenuLoader : null + + Connections { + target: PanelService + function onPopupMenuWindowRegistered(registeredScreen) { + if (registeredScreen === screen) { + root.popupMenuUpdateTrigger++; + } + } + } + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isVertical: barPosition === "left" || barPosition === "right" + readonly property real barHeight: Style.getBarHeightForScreen(screenName) + readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName) + readonly property bool density: Settings.data.bar.density + readonly property int iconSize: Style.toOdd(capsuleHeight * 0.65) + + property var blacklist: widgetSettings.blacklist || widgetMetadata.blacklist || [] // Read from settings + property var pinned: widgetSettings.pinned || widgetMetadata.pinned || [] // Pinned items (shown inline) + property bool drawerEnabled: widgetSettings.drawerEnabled !== undefined ? widgetSettings.drawerEnabled : (widgetMetadata.drawerEnabled !== undefined ? widgetMetadata.drawerEnabled : true) // Enable drawer panel + property bool hidePassive: widgetSettings.hidePassive !== undefined ? widgetSettings.hidePassive : true // Hide passive status items + readonly property string chevronColorKey: widgetSettings.chevronColor !== undefined ? widgetSettings.chevronColor : widgetMetadata.chevronColor + readonly property color chevronColor: Color.resolveColorKey(chevronColorKey) + property var filteredItems: [] // Items to show inline (pinned) + property var dropdownItems: [] // Items to show in drawer (unpinned) + property int hoveredItemIndex: -1 // Track hovered item for dot indicator + + Timer { + id: updateDebounceTimer + interval: 100 // milliseconds + running: false + repeat: false + onTriggered: _performFilteredItemsUpdate() + } + + readonly property var statusSignature: { + if (!SystemTray.items || !SystemTray.items.values) { + return ""; + } + var sig = ""; + var items = SystemTray.items.values; + for (var i = 0; i < items.length; i++) { + var item = items[i]; + if (item) { + // Direct property access creates reactive binding + var s = item.status; + sig += (item.id || i) + ":" + (s !== undefined ? s : -1); + } + } + // Trigger update when signature changes (status changed) + if (root.hidePassive) { + Qt.callLater(root.updateFilteredItems); + } + return sig; + } + Repeater { + id: statusConnectionsRepeater + model: SystemTray.items && SystemTray.items.values ? SystemTray.items.values : [] + + delegate: Item { + Connections { + target: modelData + enabled: modelData !== null && modelData !== undefined + function onStatusChanged() { + if (root.hidePassive) { + root.updateFilteredItems(); + } + } + } + } + } + + function _performFilteredItemsUpdate() { + // Force a fresh read of settings to ensure we have the latest blacklist + var currentSettings = {}; + if (section && sectionWidgetIndex >= 0 && screenName) { + var w = Settings.getBarWidgetsForScreen(screenName)[section]; + if (w && sectionWidgetIndex < w.length) { + currentSettings = w[sectionWidgetIndex]; + } + } + + // Update local properties with fresh data + if (currentSettings.blacklist !== undefined) + root.blacklist = currentSettings.blacklist; + if (currentSettings.pinned !== undefined) + root.pinned = currentSettings.pinned; + + let newItems = []; + if (SystemTray.items && SystemTray.items.values) { + const trayItems = SystemTray.items.values; + for (var i = 0; i < trayItems.length; i++) { + const item = trayItems[i]; + if (!item) { + continue; + } + + const title = item.tooltipTitle || item.name || item.id || ""; + + // Skip passive items if hidePassive is enabled + if (root.hidePassive && item.status !== undefined && (item.status === SystemTray.Passive || item.status === 0)) { + continue; + } + + // Check if blacklisted + let isBlacklisted = false; + if (root.blacklist && root.blacklist.length > 0) { + for (var j = 0; j < root.blacklist.length; j++) { + const rule = root.blacklist[j]; + if (wildCardMatch(title, rule)) { + isBlacklisted = true; + break; + } + } + } + + if (!isBlacklisted) { + newItems.push(item); + } + } + } + + // If drawer is disabled, show all items inline + if (!root.drawerEnabled) { + filteredItems = newItems; + dropdownItems = []; + } else { + // Build inline (pinned) and drawer (unpinned) lists + // If pinned list is empty, all items go to drawer (none inline) + // If pinned list has items, pinned items are inline, rest go to drawer + if (pinned && pinned.length > 0) { + let pinnedItems = []; + for (var k = 0; k < newItems.length; k++) { + const item2 = newItems[k]; + const title2 = item2.tooltipTitle || item2.name || item2.id || ""; + for (var m = 0; m < pinned.length; m++) { + const rule2 = pinned[m]; + if (wildCardMatch(title2, rule2)) { + pinnedItems.push(item2); + break; + } + } + } + filteredItems = pinnedItems; + + // Unpinned items go to drawer + let unpinnedItems = []; + for (var v = 0; v < newItems.length; v++) { + const cand = newItems[v]; + let isPinned = false; + for (var f = 0; f < filteredItems.length; f++) { + if (filteredItems[f] === cand) { + isPinned = true; + break; + } + } + if (!isPinned) + unpinnedItems.push(cand); + } + dropdownItems = unpinnedItems; + } else { + // No pinned items: all items go to drawer (none inline) + filteredItems = []; + dropdownItems = newItems; + } + } + } + + function updateFilteredItems() { + updateDebounceTimer.restart(); + } + + function wildCardMatch(str, rule) { + if (!str || !rule) { + return false; + } + + // First, convert '*' to a placeholder to preserve it, then escape other special regex characters + // Use a unique placeholder that won't appear in normal strings + const placeholder = '\uE000'; // Private use character + let processedRule = rule.replace(/\*/g, placeholder); + // Escape all special regex characters (but placeholder won't match this) + let escapedRule = processedRule.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); + // Convert placeholder back to '.*' for wildcard matching + let pattern = escapedRule.replace(new RegExp(placeholder, 'g'), '.*'); + // Add ^ and $ to match the entire string + pattern = '^' + pattern + '$'; + + try { + const regex = new RegExp(pattern, 'i'); + // 'i' for case-insensitive + return regex.test(str); + } catch (e) { + Logger.w("Tray", "Invalid regex pattern for wildcard match:", rule, e.message); + return false; // If regex is invalid, it won't match + } + } + + function toggleDrawer(button) { + TooltipService.hideImmediately(); + + // Close the popup menu if it's open + if (popupMenuWindow && popupMenuWindow.visible) { + popupMenuWindow.close(); + } + + const panel = PanelService.getPanel("trayDrawerPanel", root.screen); + if (panel) { + panel.widgetSection = root.section; + panel.widgetIndex = root.sectionWidgetIndex; + panel.toggle(this); + } + } + + function onLoaded() { + // When the widget is fully initialized with its props set the screen for the trayMenu + if (trayMenu && trayMenu.item) { + trayMenu.item.screen = screen; + } + } + + Connections { + target: SystemTray.items + function onValuesChanged() { + root.updateFilteredItems(); + // Repeater will automatically update when items change + } + } + + Connections { + target: Settings + function onSettingsSaved() { + root.updateFilteredItems(); + } + } + + // Watch for hidePassive changes to update filtering immediately + onHidePassiveChanged: { + root.updateFilteredItems(); + } + + Component.onCompleted: { + root.updateFilteredItems(); // Initial update + } + + // Content dimensions for implicit sizing + readonly property int visibleItemCount: (root.drawerEnabled && dropdownItems.length > 0 ? 1 : 0) + filteredItems.length + readonly property real capsulePadding: 0 + readonly property real capsuleWidth: isVertical ? capsuleHeight : Math.round(trayFlow.implicitWidth + capsulePadding * 2) + readonly property real capsuleContentHeight: isVertical ? Math.round(trayFlow.implicitHeight + capsulePadding * 2) : capsuleHeight + + implicitWidth: isVertical ? barHeight : Math.round(trayFlow.implicitWidth + capsulePadding * 2) + implicitHeight: isVertical ? Math.round(trayFlow.implicitHeight + capsulePadding * 2) : barHeight + visible: filteredItems.length > 0 || dropdownItems.length > 0 + opacity: (filteredItems.length > 0 || dropdownItems.length > 0) ? 1.0 : 0.0 + + // Visual capsule centered in parent + Rectangle { + id: visualCapsule + width: capsuleWidth + height: capsuleContentHeight + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, height) + radius: Style.radiusM + color: Style.capsuleColor + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + } + + NPopupContextMenu { + id: chevronContextMenu + + model: [ + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + chevronContextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + Flow { + id: trayFlow + spacing: 0 + flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight + + // Position centered in capsule + anchors.centerIn: visualCapsule + + // Drawer opener (before items if opposite direction) + NIconButton { + id: chevronIconBefore + visible: root.drawerEnabled && dropdownItems.length > 0 && BarService.getPillDirection(root) + width: isVertical ? barHeight : capsuleHeight + height: isVertical ? capsuleHeight : barHeight + tooltipText: { + if (PanelService.getPanel("trayDrawerPanel", root.screen)?.isPanelOpen) { + return ""; + } else { + return I18n.tr("tooltips.open-tray-dropdown"); + } + } + tooltipDirection: BarService.getTooltipDirection(root.screen?.name) + baseSize: capsuleHeight + applyUiScale: false + customRadius: Style.radiusL + colorBg: "transparent" + colorFg: root.chevronColor + colorBorder: "transparent" + colorBorderHover: "transparent" + icon: { + switch (barPosition) { + case "bottom": + return "caret-up"; + case "left": + return "caret-right"; + case "right": + return "caret-left"; + case "top": + default: + return "caret-down"; + } + } + onClicked: toggleDrawer(this) + onRightClicked: PanelService.showContextMenu(chevronContextMenu, this, screen) + } + + // Pinned items + Repeater { + id: repeater + model: root.filteredItems + + delegate: Item { + id: trayDelegate + required property var modelData + required property int index + width: isVertical ? barHeight : capsuleHeight + height: isVertical ? capsuleHeight : barHeight + visible: modelData + readonly property bool isHovered: root.hoveredItemIndex === index + + // Tooltip anchor representing the visual area (for proper tooltip positioning) + Item { + id: tooltipAnchor + width: capsuleHeight + height: capsuleHeight + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, height) + } + + IconImage { + id: trayIcon + width: iconSize + height: iconSize + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, height) + asynchronous: true + backer.fillMode: Image.PreserveAspectFit + + source: { + let icon = modelData?.icon || ""; + if (!icon) { + return ""; + } + + // Process icon path + if (icon.includes("?path=")) { + const chunks = icon.split("?path="); + const name = chunks[0]; + const path = chunks[1]; + const fileName = name.substring(name.lastIndexOf("/") + 1); + return `file://${path}/${fileName}`; + } + return icon; + } + opacity: status === Image.Ready ? 1 : 0 + + layer.enabled: widgetSettings.colorizeIcons !== false + layer.effect: ShaderEffect { + property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant + property real colorizeMode: 1.0 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") + } + } + + Rectangle { + id: hoverIndicator + anchors.bottom: trayIcon.bottom + anchors.bottomMargin: -2 + anchors.horizontalCenter: trayIcon.horizontalCenter + width: Style.toOdd(iconSize * 0.25) + height: 4 + color: trayDelegate.isHovered ? Color.mHover : "transparent" + radius: Math.min(Style.radiusXXS, width / 2) + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + id: itemMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onContainsMouseChanged: { + if (containsMouse) { + if (popupMenuWindow) { + popupMenuWindow.close(); + } + root.hoveredItemIndex = trayDelegate.index; + TooltipService.show(tooltipAnchor, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection(root.screen?.name)); + } else if (root.hoveredItemIndex === trayDelegate.index) { + root.hoveredItemIndex = -1; + TooltipService.hide(tooltipAnchor); + } + } + onClicked: mouse => { + if (!modelData) { + return; + } + + if (mouse.button === Qt.LeftButton) { + // Close any open menu first + if (popupMenuWindow) { + popupMenuWindow.close(); + } + + if (!modelData.onlyMenu) { + modelData.activate(); + } + } else if (mouse.button === Qt.MiddleButton) { + // Close the menu if it was visible + if (popupMenuWindow && popupMenuWindow.visible) { + popupMenuWindow.close(); + return; + } + modelData.secondaryActivate && modelData.secondaryActivate(); + } else if (mouse.button === Qt.RightButton) { + TooltipService.hideImmediately(); + + // Close the menu if it was visible + if (popupMenuWindow && popupMenuWindow.visible) { + popupMenuWindow.close(); + return; + } + + // Close any opened panel + if ((PanelService.openedPanel !== null) && !PanelService.openedPanel.isClosing) { + PanelService.openedPanel.close(); + } + + if (modelData.hasMenu && modelData.menu && trayMenu && trayMenu.item) { + // Calculate menu position after ensuring menu is loaded + const calculateAndShow = () => { + // Position menu based on bar position, using tooltipAnchor for proper positioning + // Increased spacing for better alignment with other context menus + let menuX, menuY; + if (barPosition === "left") { + // For left bar: position menu to the right of the visual area + menuX = tooltipAnchor.width + Style.marginL; + menuY = 0; + } else if (barPosition === "right") { + // For right bar: position menu to the left of the visual area + menuX = -trayMenu.item.implicitWidth - Style.marginL; + menuY = 0; + } else { + // For horizontal bars: center horizontally and position below visual area + menuX = (tooltipAnchor.width / 2) - (trayMenu.item.implicitWidth / 2); + menuY = tooltipAnchor.height + Style.marginS; + } + + PanelService.showTrayMenu(root.screen, modelData, trayMenu.item, tooltipAnchor, menuX, menuY, root.section, root.sectionWidgetIndex); + }; + + // Use Qt.callLater to ensure menu dimensions are calculated + Qt.callLater(calculateAndShow); + } else { + Logger.d("Tray", "No menu available for", modelData.id, "or trayMenu not set"); + } + } + } + } + } + } + + // Drawer opener (after items if normal direction) + NIconButton { + id: chevronIconAfter + visible: root.drawerEnabled && dropdownItems.length > 0 && !BarService.getPillDirection(root) + width: isVertical ? barHeight : capsuleHeight + height: isVertical ? capsuleHeight : barHeight + tooltipText: I18n.tr("tooltips.open-tray-dropdown") + tooltipDirection: BarService.getTooltipDirection(root.screen?.name) + baseSize: capsuleHeight + applyUiScale: false + customRadius: Style.radiusL + colorBg: "transparent" + colorFg: root.chevronColor + colorBorder: "transparent" + colorBorderHover: "transparent" + icon: { + switch (barPosition) { + case "bottom": + return "caret-up"; + case "left": + return "caret-right"; + case "right": + return "caret-left"; + case "top": + default: + return "caret-down"; + } + } + onClicked: toggleDrawer(this) + onRightClicked: PanelService.showContextMenu(chevronContextMenu, this, screen) + } + } // closes Flow +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/VPN.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/VPN.qml new file mode 100644 index 0000000..eee5196 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/VPN.qml @@ -0,0 +1,139 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Services.Networking +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isBarVertical: barPosition === "left" || barPosition === "right" + readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode + readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor + readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor + + implicitWidth: pill.width + implicitHeight: pill.height + + NPopupContextMenu { + id: contextMenu + + model: { + const items = []; + const active = VPNService.activeConnections; + for (let i = 0; i < active.length; ++i) { + const conn = active[i]; + items.push({ + "label": I18n.tr("actions.disconnect-vpn", { + "name": conn.name + }), + "action": "disconnect:" + conn.uuid, + "icon": "shield-off" + }); + } + const inactive = VPNService.inactiveConnections; + for (let i = 0; i < inactive.length; ++i) { + const conn = inactive[i]; + items.push({ + "label": I18n.tr("actions.connect-vpn", { + "name": conn.name + }), + "action": "connect:" + conn.uuid, + "icon": "shield-lock" + }); + } + items.push({ + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }); + return items; + } + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (!action) { + return; + } + if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + return; + } + if (action.startsWith("connect:")) { + const uuid = action.substring("connect:".length); + VPNService.connect(uuid); + return; + } + if (action.startsWith("disconnect:")) { + const uuid = action.substring("disconnect:".length); + VPNService.disconnect(uuid); + } + } + } + + BarPill { + id: pill + + screen: root.screen + oppositeDirection: BarService.getPillDirection(root) + customIconColor: Color.resolveColorKeyOptional(root.iconColorKey) + customTextColor: Color.resolveColorKeyOptional(root.textColorKey) + icon: VPNService.hasActiveConnection ? "shield-lock" : "shield" + text: { + if (VPNService.activeConnections.length > 0) { + return VPNService.activeConnections[0].name; + } + if (VPNService.connectingUuid) { + const pending = VPNService.connections[VPNService.connectingUuid]; + if (pending) { + return pending.name; + } + } + return ""; + } + suffix: { + if (VPNService.activeConnections.length > 1) { + return ` + ${VPNService.activeConnections.length - 1}`; + } + return ""; + } + autoHide: false + forceOpen: !isBarVertical && root.displayMode === "alwaysShow" + forceClose: isBarVertical || root.displayMode === "alwaysHide" || !pill.text + onRightClicked: { + PanelService.showContextMenu(contextMenu, pill, screen); + } + tooltipText: { + if (pill.text !== "") { + return pill.text; + } + return I18n.tr("tooltips.manage-vpn"); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Volume.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Volume.qml new file mode 100644 index 0000000..03bd496 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Volume.qml @@ -0,0 +1,203 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pipewire +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Modules.Panels.Settings +import qs.Services.Media +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isBarVertical: barPosition === "left" || barPosition === "right" + readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode + readonly property string middleClickCommand: (widgetSettings.middleClickCommand !== undefined) ? widgetSettings.middleClickCommand : widgetMetadata.middleClickCommand + readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor + readonly property string textColorKey: widgetSettings.textColor !== undefined ? widgetSettings.textColor : widgetMetadata.textColor + readonly property bool reverseScroll: Settings.data.general.reverseScroll + + // Used to avoid opening the pill on Quickshell startup + property bool firstVolumeReceived: false + property int wheelAccumulator: 0 + + implicitWidth: pill.width + implicitHeight: pill.height + + // Connection used to open the pill when volume changes + Connections { + target: AudioService + function onVolumeChanged() { + // Logger.i("Bar:Volume", "onVolumeChanged") + if (!firstVolumeReceived) { + // Ignore the first volume change + firstVolumeReceived = true; + } else { + // Hide any tooltip while the pill is visible / being updated + TooltipService.hide(); + pill.show(); + externalHideTimer.restart(); + } + } + + function onMutedChanged() { + if (!firstVolumeReceived) { + firstVolumeReceived = true; + } else { + TooltipService.hide(); + pill.show(); + externalHideTimer.restart(); + } + } + + function onVolumeAtMaximum() { + if (!firstVolumeReceived) { + firstVolumeReceived = true; + } else { + // Hide any tooltip while the pill is visible / being updated + TooltipService.hide(); + pill.show(); + externalHideTimer.restart(); + } + } + + function onVolumeAtMinimum() { + if (!firstVolumeReceived) { + firstVolumeReceived = true; + } else { + // Hide any tooltip while the pill is visible / being updated + TooltipService.hide(); + pill.show(); + externalHideTimer.restart(); + } + } + } + + Timer { + id: externalHideTimer + running: false + interval: 1500 + onTriggered: { + pill.hide(); + } + } + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.toggle-mute"), + "action": "toggle-mute", + "icon": AudioService.muted ? "volume-off" : "volume" + }, + { + "label": I18n.tr("actions.run-custom-command"), + "action": "custom-command", + "icon": "adjustments" + }, + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "toggle-mute") { + AudioService.setOutputMuted(!AudioService.muted); + } else if (action === "custom-command") { + Quickshell.execDetached(["sh", "-c", middleClickCommand]); + } else if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + BarPill { + id: pill + + screen: root.screen + oppositeDirection: BarService.getPillDirection(root) + customIconColor: Color.resolveColorKeyOptional(root.iconColorKey) + customTextColor: Color.resolveColorKeyOptional(root.textColorKey) + icon: AudioService.getOutputIcon() + autoHide: false // Important to be false so we can hover as long as we want + text: { + const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; + const displayVolume = Math.min(maxVolume, AudioService.volume); + return Math.round(displayVolume * 100); + } + suffix: "%" + forceOpen: displayMode === "alwaysShow" + forceClose: displayMode === "alwaysHide" + tooltipText: { + if (PanelService.getPanel("audioPanel", screen)?.isPanelOpen) { + return ""; + } else { + const nick = AudioService.sink?.nickname ?? ""; + const volumeText = I18n.tr("tooltips.volume-at", { + "volume": (() => { + const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; + const displayVolume = Math.min(maxVolume, AudioService.volume); + return Math.round(displayVolume * 100); + })() + }); + return nick ? volumeText + "\n" + nick : volumeText; + } + } + + onWheel: function (delta) { + // Hide tooltip as soon as the user starts scrolling to adjust volume + TooltipService.hide(); + + if (root.reverseScroll) + delta *= -1; + + wheelAccumulator += delta; + if (wheelAccumulator >= 120) { + wheelAccumulator = 0; + AudioService.increaseVolume(); + } else if (wheelAccumulator <= -120) { + wheelAccumulator = 0; + AudioService.decreaseVolume(); + } + } + onClicked: { + PanelService.getPanel("audioPanel", screen)?.toggle(this); + } + onRightClicked: { + PanelService.showContextMenu(contextMenu, pill, screen); + } + onMiddleClicked: { + Quickshell.execDetached(["sh", "-c", middleClickCommand]); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/WallpaperSelector.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/WallpaperSelector.qml new file mode 100644 index 0000000..0c2c562 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/WallpaperSelector.qml @@ -0,0 +1,92 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Services.UI +import qs.Widgets + +NIconButton { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string iconColorKey: widgetSettings.iconColor !== undefined ? widgetSettings.iconColor : widgetMetadata.iconColor + + enabled: Settings.data.wallpaper.enabled + baseSize: Style.getCapsuleHeightForScreen(screen?.name) + applyUiScale: false + customRadius: Style.radiusL + icon: "wallpaper-selector" + tooltipText: { + if (PanelService.getPanel("wallpaperPanel", screen)?.isPanelOpen) { + return ""; + } else { + return I18n.tr("tooltips.wallpaper-selector"); + } + } + tooltipDirection: BarService.getTooltipDirection(screen?.name) + colorBg: Style.capsuleColor + colorFg: Color.resolveColorKey(iconColorKey) + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + NPopupContextMenu { + id: contextMenu + + model: [ + { + "label": I18n.tr("actions.random-wallpaper"), + "action": "random-wallpaper", + "icon": "dice" + }, + { + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }, + ] + + onTriggered: action => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + if (action === "random-wallpaper") { + WallpaperService.setRandomWallpaper(); + } else if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } + } + } + + onClicked: { + var wallpaperPanel = PanelService.getPanel("wallpaperPanel", screen); + if (Settings.data.wallpaper.panelPosition === "follow_bar") { + wallpaperPanel?.toggle(this); + } else { + wallpaperPanel?.toggle(); + } + } + onRightClicked: { + PanelService.showContextMenu(contextMenu, root, screen); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Workspace.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Workspace.qml new file mode 100644 index 0000000..c961036 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Bar/Widgets/Workspace.qml @@ -0,0 +1,1007 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import QtQuick.Window +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import qs.Commons +import qs.Modules.Bar.Extras +import qs.Services.Compositor +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + + // Widget properties passed from Bar.qml for per-instance settings + property string widgetId: "" + property string section: "" + property int sectionWidgetIndex: -1 + property int sectionWidgetsCount: 0 + + property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {} + // Explicit screenName property ensures reactive binding when screen changes + readonly property string screenName: screen ? screen.name : "" + property var widgetSettings: { + if (section && sectionWidgetIndex >= 0 && screenName) { + var widgets = Settings.getBarWidgetsForScreen(screenName)[section]; + if (widgets && sectionWidgetIndex < widgets.length) { + return widgets[sectionWidgetIndex]; + } + } + return {}; + } + + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool isVertical: barPosition === "left" || barPosition === "right" + readonly property real barHeight: Style.getBarHeightForScreen(screenName) + readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName) + readonly property real barFontSize: Style.getBarFontSizeForScreen(screenName) + + readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode + readonly property bool hasLabel: (labelMode !== "none") + readonly property string fontWeight: { + var fontWeightSetting = (widgetSettings.fontWeight !== undefined) ? widgetSettings.fontWeight : widgetMetadata.fontWeight; + + if (fontWeightSetting === "regular") + return Style.fontWeightRegular; + if (fontWeightSetting === "medium") + return Style.fontWeightMedium; + if (fontWeightSetting === "semibold") + return Style.fontWeightSemiBold; + if (fontWeightSetting === "bold") + return Style.fontWeightBold; + return Style.fontWeightBold; + } + readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : widgetMetadata.hideUnoccupied + readonly property bool followFocusedScreen: (widgetSettings.followFocusedScreen !== undefined) ? widgetSettings.followFocusedScreen : widgetMetadata.followFocusedScreen + readonly property int characterCount: { + const count = (widgetSettings.characterCount !== undefined) ? widgetSettings.characterCount : widgetMetadata.characterCount; + return isVertical ? Math.min(count, 2) : count; + } + + // Pill size setting (0.5-1.0 range) + readonly property real pillSize: (widgetSettings.pillSize !== undefined) ? widgetSettings.pillSize : widgetMetadata.pillSize + + // When no label the pills are smaller + readonly property real baseDimensionRatio: pillSize + + // Grouped mode (show applications) settings + readonly property bool showApplications: (widgetSettings.showApplications !== undefined) ? widgetSettings.showApplications : widgetMetadata.showApplications + readonly property bool showApplicationsHover: (widgetSettings.showApplicationsHover !== undefined) ? widgetSettings.showApplicationsHover : widgetMetadata.showApplicationsHover + readonly property bool showLabelsOnlyWhenOccupied: (widgetSettings.showLabelsOnlyWhenOccupied !== undefined) ? widgetSettings.showLabelsOnlyWhenOccupied : widgetMetadata.showLabelsOnlyWhenOccupied + readonly property bool colorizeIcons: (widgetSettings.colorizeIcons !== undefined) ? widgetSettings.colorizeIcons : widgetMetadata.colorizeIcons + readonly property real unfocusedIconsOpacity: (widgetSettings.unfocusedIconsOpacity !== undefined) ? widgetSettings.unfocusedIconsOpacity : widgetMetadata.unfocusedIconsOpacity + readonly property real groupedBorderOpacity: (widgetSettings.groupedBorderOpacity !== undefined) ? widgetSettings.groupedBorderOpacity : widgetMetadata.groupedBorderOpacity + readonly property bool enableScrollWheel: (widgetSettings.enableScrollWheel !== undefined) ? widgetSettings.enableScrollWheel : widgetMetadata.enableScrollWheel + readonly property bool reverseScroll: Settings.data.general.reverseScroll + readonly property real iconScale: (widgetSettings.iconScale !== undefined) ? widgetSettings.iconScale : widgetMetadata.iconScale + readonly property string focusedColor: (widgetSettings.focusedColor !== undefined) ? widgetSettings.focusedColor : widgetMetadata.focusedColor + readonly property string occupiedColor: (widgetSettings.occupiedColor !== undefined) ? widgetSettings.occupiedColor : widgetMetadata.occupiedColor + readonly property string emptyColor: (widgetSettings.emptyColor !== undefined) ? widgetSettings.emptyColor : widgetMetadata.emptyColor + readonly property bool showBadge: (widgetSettings.showBadge !== undefined) ? widgetSettings.showBadge : widgetMetadata.showBadge + + // Only for grouped mode / show apps + readonly property int baseItemSize: Style.toOdd(capsuleHeight * 0.8) + readonly property int iconSize: Style.toOdd(baseItemSize * iconScale) + readonly property real textRatio: 0.50 + + // Context menu state for grouped mode - store IDs instead of object references to avoid stale references + property string selectedWindowId: "" + property string selectedAppId: "" + + // Helper to get the current window object from ID + function getSelectedWindow() { + if (!selectedWindowId) + return null; + // Search directly in CompositorService to get the live window object + for (var i = 0; i < CompositorService.windows.count; i++) { + var win = CompositorService.windows.get(i); + // Using loose equality on purpose (==) + if (win && (win.id == selectedWindowId || win.address == selectedWindowId)) { + return win; + } + } + return null; + } + + property bool isDestroying: false + + // Revision counter to force icon re-evaluation + property int iconRevision: 0 + // Revision counter to force window list re-evaluation (for liveWindows binding in grouped mode) + property int windowRevision: 0 + + property ListModel localWorkspaces: ListModel {} + property int lastFocusedWorkspaceId: -1 + property real masterProgress: 0.0 + property bool effectsActive: false + property color effectColor: Color.mPrimary + + property int horizontalPadding: Style.marginS + property int spacingBetweenPills: Style.marginXS + + // Wheel scroll handling + property int wheelAccumulatedDelta: 0 + property bool wheelCooldown: false + + signal workspaceChanged(int workspaceId, color accentColor) + + property bool isHovered: false + + HoverHandler { + id: workspaceHoverHandler + enabled: showApplications && showApplicationsHover + onHoveredChanged: { + if (hovered) { + hoverEval.stop(); + isHovered = true; + } else { + hoverEval.restart(); + } + } + } + + Timer { + id: hoverEval + interval: 150 + repeat: false + onTriggered: { + isHovered = workspaceHoverHandler.hovered || contextMenu.visible; + } + } + + readonly property bool appVisible: showApplications && (!showApplicationsHover || root.isHovered) + + implicitWidth: appVisible ? (isVertical ? groupedGrid.implicitWidth : Math.round(groupedGrid.implicitWidth + horizontalPadding * hasLabel)) : (isVertical ? barHeight : computeWidth()) + implicitHeight: appVisible ? (isVertical ? Math.round(groupedGrid.implicitHeight + horizontalPadding * 0.6 * hasLabel) : barHeight) : (isVertical ? computeHeight() : barHeight) + + function getWorkspaceWidth(ws, activeOverride) { + const d = Math.round(capsuleHeight * root.baseDimensionRatio); + const isActive = activeOverride !== undefined ? activeOverride : ws.isActive; + const factor = isActive ? 2.2 : 1; + + // Don't calculate text width if labels are off + if (labelMode === "none") { + return Style.toOdd(d * factor); + } + + var displayText = ws.idx.toString(); + + if (ws.name && ws.name.length > 0) { + if (root.labelMode === "name") { + displayText = ws.name.substring(0, characterCount); + } else if (root.labelMode === "index+name") { + displayText = ws.idx.toString() + " " + ws.name.substring(0, characterCount); + } + } + + const textWidth = displayText.length * (d * 0.4); // Approximate width per character + const padding = d * 0.6; + return Style.toOdd(Math.max(d * factor, textWidth + padding)); + } + + function getWorkspaceHeight(ws, activeOverride) { + const d = Math.round(capsuleHeight * root.baseDimensionRatio); + const isActive = activeOverride !== undefined ? activeOverride : ws.isActive; + const factor = isActive ? 2.2 : 1; + return Style.toOdd(d * factor); + } + + function computeWidth() { + let total = 0; + for (var i = 0; i < localWorkspaces.count; i++) { + const ws = localWorkspaces.get(i); + total += getWorkspaceWidth(ws); + } + total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills; + total += horizontalPadding * 2; + return Style.toOdd(total); + } + + function computeHeight() { + let total = 0; + for (var i = 0; i < localWorkspaces.count; i++) { + const ws = localWorkspaces.get(i); + total += getWorkspaceHeight(ws); + } + total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills; + total += horizontalPadding * 2; + return Style.toOdd(total); + } + + function getFocusedLocalIndex() { + for (var i = 0; i < localWorkspaces.count; i++) { + if (localWorkspaces.get(i).isFocused === true) + return i; + } + return -1; + } + + function switchByOffset(offset) { + if (localWorkspaces.count <= 1) + return; + var current = getFocusedLocalIndex(); + if (current < 0) + current = 0; + var next = (current + offset) % localWorkspaces.count; + if (next < 0) + next = localWorkspaces.count - 1; + if (next === current) + return; + const ws = localWorkspaces.get(next); + if (ws && ws.idx !== undefined) + CompositorService.switchToWorkspace(ws); + } + + // Helper function to normalize app IDs for case-insensitive matching + function normalizeAppId(appId) { + if (!appId || typeof appId !== 'string') + return ""; + return appId.toLowerCase().trim(); + } + + // Helper function to check if an app is pinned + function isAppPinned(appId) { + if (!appId) + return false; + const pinnedApps = Settings.data.dock.pinnedApps || []; + const normalizedId = normalizeAppId(appId); + return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId); + } + + // Helper function to toggle app pin/unpin + function toggleAppPin(appId) { + if (!appId) + return; + + const normalizedId = normalizeAppId(appId); + let pinnedApps = (Settings.data.dock.pinnedApps || []).slice(); + + const existingIndex = pinnedApps.findIndex(pinnedId => normalizeAppId(pinnedId) === normalizedId); + const isPinned = existingIndex >= 0; + + if (isPinned) { + pinnedApps.splice(existingIndex, 1); + } else { + pinnedApps.push(appId); + } + + Settings.data.dock.pinnedApps = pinnedApps; + } + + // Deferred via Qt.callLater to avoid synchronous ListModel mutations during + // signal cascades. Qt.callLater deduplicates by function identity, so rapid + // calls from multiple signal handlers coalesce into a single refresh. + function scheduleRefresh() { + if (!root.isDestroying) + Qt.callLater(root.refreshWorkspaces); + } + + Component.onCompleted: scheduleRefresh() + + Component.onDestruction: { + root.isDestroying = true; + } + + onScreenChanged: scheduleRefresh() + onScreenNameChanged: scheduleRefresh() + onHideUnoccupiedChanged: scheduleRefresh() + onAppVisibleChanged: { + if (appVisible) { + scheduleRefresh(); + } + } + + Connections { + target: CompositorService + function onWorkspacesChanged() { + scheduleRefresh(); + } + function onWindowListChanged() { + if (appVisible || showLabelsOnlyWhenOccupied) { + root.windowRevision++; + scheduleRefresh(); + } + } + function onActiveWindowChanged() { + if (appVisible) { + root.windowRevision++; + scheduleRefresh(); + } + } + } + + // Refresh icons when DesktopEntries becomes available + Connections { + target: DesktopEntries.applications + function onValuesChanged() { + root.iconRevision++; + } + } + + function refreshWorkspaces() { + var targetList = []; + var focusedOutput = null; + if (followFocusedScreen) { + for (var i = 0; i < CompositorService.workspaces.count; i++) { + const ws = CompositorService.workspaces.get(i); + if (ws.isFocused) + focusedOutput = ws.output.toLowerCase(); + } + } + + if (screen !== null) { + const screenName = screen.name.toLowerCase(); + for (var i = 0; i < CompositorService.workspaces.count; i++) { + const ws = CompositorService.workspaces.get(i); + // For global workspaces (e.g., LabWC), show all workspaces on all screens + const matchesScreen = CompositorService.globalWorkspaces || (followFocusedScreen && ws.output.toLowerCase() == focusedOutput) || (!followFocusedScreen && ws.output.toLowerCase() == screenName); + + if (!matchesScreen) + continue; + if (hideUnoccupied && !ws.isOccupied && !ws.isFocused) + continue; + + // Create a plain JS object for the workspace data + var workspaceData = { + id: ws.id, + idx: ws.idx, + name: ws.name, + output: ws.output, + isFocused: ws.isFocused, + isActive: ws.isActive, + isUrgent: ws.isUrgent, + isOccupied: ws.isOccupied + }; + + if (ws.handle !== null && ws.handle !== undefined) { + workspaceData.handle = ws.handle; + } + + // Windows are fetched live via liveWindows property in grouped mode + // to avoid Qt 6.9 ListModel nested array serialization issues + + targetList.push(workspaceData); + } + } + + // In-place update to preserve delegates for animations + var i = 0; + while (i < localWorkspaces.count || i < targetList.length) { + if (i < localWorkspaces.count && i < targetList.length) { + var existing = localWorkspaces.get(i); + var target = targetList[i]; + if (existing.id === target.id) { + // Use set() to update all properties, including arrays like 'windows' + // This is more reliable than repeated setProperty calls for complex types + localWorkspaces.set(i, target); + i++; + } else { + // ID mismatch, remove existing and re-evaluate this index + localWorkspaces.remove(i); + } + } else if (i < localWorkspaces.count) { + // Excess items in local, remove them + localWorkspaces.remove(i); + } else { + // More items in target, append them + localWorkspaces.append(targetList[i]); + i++; + } + } + + updateWorkspaceFocus(); + } + + function triggerUnifiedWave() { + effectColor = Color.mPrimary; + masterAnimation.restart(); + } + + function updateWorkspaceFocus() { + for (var i = 0; i < localWorkspaces.count; i++) { + const ws = localWorkspaces.get(i); + if (ws.isFocused === true) { + if (root.lastFocusedWorkspaceId !== -1 && root.lastFocusedWorkspaceId !== ws.id) { + root.triggerUnifiedWave(); + } + root.lastFocusedWorkspaceId = ws.id; + root.workspaceChanged(ws.id, Color.mPrimary); + break; + } + } + } + + SequentialAnimation { + id: masterAnimation + PropertyAction { + target: root + property: "effectsActive" + value: true + } + NumberAnimation { + target: root + property: "masterProgress" + from: 0.0 + to: 1.0 + duration: Style.animationSlow * 2 + easing.type: Easing.OutQuint + } + PropertyAction { + target: root + property: "effectsActive" + value: false + } + PropertyAction { + target: root + property: "masterProgress" + value: 0.0 + } + } + + NPopupContextMenu { + id: contextMenu + + onVisibleChanged: { + if (visible) { + hoverEval.stop(); + root.isHovered = true; + } else { + hoverEval.restart(); + } + } + + model: { + var items = []; + if (root.selectedWindowId) { + // Focus item + items.push({ + "label": I18n.tr("common.focus"), + "action": "focus", + "icon": "eye" + }); + + // Pin/Unpin item + const isPinned = root.isAppPinned(root.selectedAppId); + items.push({ + "label": !isPinned ? I18n.tr("common.pin") : I18n.tr("common.unpin"), + "action": "pin", + "icon": !isPinned ? "pin" : "pinned-off" + }); + + // Close item + items.push({ + "label": I18n.tr("common.close"), + "action": "close", + "icon": "x" + }); + + // Add desktop entry actions + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId && root.selectedAppId) { + const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(root.selectedAppId) : DesktopEntries.byId(root.selectedAppId); + if (entry != null && entry.actions) { + entry.actions.forEach(function (action) { + items.push({ + "label": action.name, + "action": "desktop-action-" + action.name, + "icon": "chevron-right", + "desktopAction": action + }); + }); + } + } + } + items.push({ + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }); + return items; + } + + onTriggered: (action, item) => { + contextMenu.close(); + PanelService.closeContextMenu(screen); + + const selectedWindow = root.getSelectedWindow(); + + if (action === "focus" && selectedWindow) { + CompositorService.focusWindow(selectedWindow); + } else if (action === "pin" && selectedAppId) { + root.toggleAppPin(selectedAppId); + } else if (action === "close" && selectedWindow) { + CompositorService.closeWindow(selectedWindow); + } else if (action === "widget-settings") { + BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings); + } else if (action.startsWith("desktop-action-") && item && item.desktopAction) { + if (item.desktopAction.command && item.desktopAction.command.length > 0) { + Quickshell.execDetached(item.desktopAction.command); + } else if (item.desktopAction.execute) { + item.desktopAction.execute(); + } + } + selectedWindowId = ""; + selectedAppId = ""; + } + } + + Rectangle { + id: workspaceBackground + visible: !appVisible + width: isVertical ? capsuleHeight : parent.width + height: isVertical ? parent.height : capsuleHeight + radius: Style.radiusM + color: Style.capsuleColor + border.color: Style.capsuleBorderColor + border.width: Style.capsuleBorderWidth + + x: isVertical ? Style.pixelAlignCenter(parent.width, width) : 0 + y: isVertical ? 0 : Style.pixelAlignCenter(parent.height, height) + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + PanelService.showContextMenu(contextMenu, workspaceBackground, screen); + } + } + } + } + + // Debounce timer for wheel interactions + Timer { + id: wheelDebounce + interval: 150 + repeat: false + onTriggered: { + root.wheelCooldown = false; + root.wheelAccumulatedDelta = 0; + } + } + + // Scroll to switch workspaces + WheelHandler { + id: wheelHandler + target: root + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + enabled: root.enableScrollWheel + onWheel: function (event) { + if (root.wheelCooldown) + return; + // Prefer vertical delta, fall back to horizontal if needed + var dy = event.angleDelta.y; + var dx = event.angleDelta.x; + var useDy = Math.abs(dy) >= Math.abs(dx); + var delta = useDy ? dy : dx; + // One notch is typically 120 + root.wheelAccumulatedDelta += delta; + var step = 120; + if (Math.abs(root.wheelAccumulatedDelta) >= step) { + var direction = root.wheelAccumulatedDelta > 0 ? -1 : 1; + if (root.reverseScroll) + direction *= -1; + // For vertical layout, natural mapping: wheel up -> previous, down -> next (already handled by sign) + // For horizontal layout, same mapping using vertical wheel + root.switchByOffset(direction); + root.wheelCooldown = true; + wheelDebounce.restart(); + root.wheelAccumulatedDelta = 0; + event.accepted = true; + } + } + } + + // Horizontal layout for top/bottom bars + Row { + id: pillRow + spacing: spacingBetweenPills + x: horizontalPadding + y: 0 + visible: !isVertical && !appVisible + scale: visible ? 1.0 : 0.8 + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutBack + easing.overshoot: 1.2 + } + } + + Repeater { + id: workspaceRepeaterHorizontal + model: localWorkspaces + delegate: WorkspacePill { + required property var model + workspace: model + isVertical: false + baseDimensionRatio: root.baseDimensionRatio + capsuleHeight: root.capsuleHeight + barHeight: root.barHeight + labelMode: root.labelMode + fontWeight: root.fontWeight + characterCount: root.characterCount + textRatio: root.textRatio + showLabelsOnlyWhenOccupied: root.showLabelsOnlyWhenOccupied + focusedColor: root.focusedColor + occupiedColor: root.occupiedColor + emptyColor: root.emptyColor + masterProgress: root.masterProgress + effectsActive: root.effectsActive + effectColor: root.effectColor + getWorkspaceWidth: root.getWorkspaceWidth + getWorkspaceHeight: root.getWorkspaceHeight + } + } + } + + // Vertical layout for left/right bars + Column { + id: pillColumn + spacing: spacingBetweenPills + x: 0 + y: horizontalPadding + visible: isVertical && !appVisible + scale: visible ? 1.0 : 0.8 + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutBack + easing.overshoot: 1.2 + } + } + + Repeater { + id: workspaceRepeaterVertical + model: localWorkspaces + delegate: WorkspacePill { + required property var model + workspace: model + isVertical: true + baseDimensionRatio: root.baseDimensionRatio + capsuleHeight: root.capsuleHeight + barHeight: root.barHeight + labelMode: root.labelMode + fontWeight: root.fontWeight + characterCount: root.characterCount + textRatio: root.textRatio + showLabelsOnlyWhenOccupied: root.showLabelsOnlyWhenOccupied + focusedColor: root.focusedColor + occupiedColor: root.occupiedColor + emptyColor: root.emptyColor + masterProgress: root.masterProgress + effectsActive: root.effectsActive + effectColor: root.effectColor + getWorkspaceWidth: root.getWorkspaceWidth + getWorkspaceHeight: root.getWorkspaceHeight + } + } + } + + // ======================================== + // Grouped mode (appVisible = true) + // ======================================== + Component { + id: groupedWorkspaceDelegate + + Rectangle { + id: groupedContainer + + required property var model + property var workspaceModel: model + // Fetch windows directly from service to avoid Qt 6.9 ListModel nested array issues + property var liveWindows: [] + property bool hasWindows: liveWindows.length > 0 + + function updateWindows() { + var wsId = workspaceModel?.id; + if (wsId !== undefined && wsId !== null) { + liveWindows = CompositorService.getWindowsForWorkspace(wsId); + } else { + liveWindows = []; + } + } + + // Deferred to avoid re-entrant incubation: when localWorkspaces.append() + Component.onCompleted: Qt.callLater(updateWindows) + onWorkspaceModelChanged: Qt.callLater(updateWindows) + + Connections { + target: root + function onWindowRevisionChanged() { + Qt.callLater(groupedContainer.updateWindows); + } + } + + HoverHandler { + id: groupHoverHandler + } + + width: Style.toOdd((hasWindows ? groupedIconsFlow.implicitWidth : root.iconSize) + (root.isVertical ? (root.baseItemSize - root.iconSize + Style.marginXS) : Style.marginXL)) + height: Style.toOdd((hasWindows ? groupedIconsFlow.implicitHeight : root.iconSize) + (root.isVertical ? Style.marginL : (root.baseItemSize - root.iconSize + Style.marginXS))) + color: Style.capsuleColor + radius: Style.radiusS + border.color: Settings.data.bar.showOutline ? Style.capsuleBorderColor : Qt.alpha((workspaceModel.isFocused ? Color.mPrimary : (groupHoverHandler.hovered ? Color.mHover : Color.mOutline)), root.groupedBorderOpacity) + border.width: Style.borderS + + Behavior on width { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + Behavior on height { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + enabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + preventStealing: true + onPressed: mouse => { + if (mouse.button === Qt.LeftButton) { + CompositorService.switchToWorkspace(groupedContainer.workspaceModel); + } + } + onReleased: mouse => { + if (mouse.button === Qt.RightButton) { + mouse.accepted = true; + TooltipService.hide(); + root.selectedWindowId = ""; + root.selectedAppId = ""; + openGroupedContextMenu(groupedContainer); + } + } + } + + Flow { + id: groupedIconsFlow + + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, height) + spacing: 2 + flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight + + Repeater { + model: groupedContainer.liveWindows + + delegate: Item { + id: groupedTaskbarItem + + readonly property bool isFocused: modelData?.isFocused ?? false + + width: root.iconSize + height: root.iconSize + + HoverHandler { + id: windowHoverHandler + } + + IconImage { + id: groupedAppIcon + + width: parent.width + height: parent.height + + source: { + root.iconRevision; // Force re-evaluation when revision changes + return ThemeIcons.iconForAppId(modelData?.appId?.toLowerCase()); + } + smooth: true + asynchronous: true + opacity: groupedTaskbarItem.isFocused ? Style.opacityFull : unfocusedIconsOpacity + layer.enabled: root.colorizeIcons && !groupedTaskbarItem.isFocused + + Rectangle { + id: groupedFocusIndicator + visible: groupedTaskbarItem.isFocused || windowHoverHandler.hovered + anchors.bottomMargin: -2 + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + width: Style.toOdd(root.iconSize * 0.25) + height: 4 + color: groupedTaskbarItem.isFocused ? Color.mPrimary : Color.mHover + radius: Math.min(Style.radiusXXS, width / 2) + } + + layer.effect: ShaderEffect { + property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant + property real colorizeMode: 0 + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton + preventStealing: true + + onPressed: mouse => { + if (mouse.button === Qt.LeftButton && modelData) { + CompositorService.focusWindow(modelData); + } + } + + onReleased: mouse => { + if (mouse.button === Qt.RightButton && modelData) { + mouse.accepted = true; + TooltipService.hide(); + root.selectedWindowId = modelData.id || modelData.address || ""; + root.selectedAppId = modelData.appId; + openGroupedContextMenu(groupedTaskbarItem); + } + } + onEntered: { + if (!modelData) + return; + TooltipService.show(groupedTaskbarItem, modelData.title || modelData.appId || "Unknown app.", BarService.getTooltipDirection(root.screenName)); + } + onExited: { + TooltipService.hide(); + } + } + } + } + } + + Item { + id: groupedWorkspaceNumberContainer + + visible: root.labelMode !== "none" && root.showBadge && (!root.showLabelsOnlyWhenOccupied || groupedContainer.hasWindows || groupedContainer.workspaceModel.isFocused) + + anchors { + left: parent.left + top: parent.top + leftMargin: -Style.fontSizeXS * 0.55 + topMargin: -Style.fontSizeXS * 0.25 + } + + width: Math.max(groupedWorkspaceNumber.implicitWidth + Style.margin2XS, Style.fontSizeXXS * 2) + height: Math.max(groupedWorkspaceNumber.implicitHeight + Style.marginXS, Style.fontSizeXXS * 2) + + Rectangle { + id: groupedWorkspaceNumberBackground + + anchors.fill: parent + radius: Math.min(Style.radiusL, width / 2) + + color: { + if (groupedContainer.workspaceModel.isFocused) + return Color.resolveColorKey(root.focusedColor); + if (groupedContainer.workspaceModel.isUrgent) + return Color.mError; + if (groupedContainer.hasWindows) + return Color.resolveColorKey(root.occupiedColor); + + return Color.resolveColorKey(root.emptyColor); + } + + scale: groupedContainer.workspaceModel.isActive ? 1.0 : 0.8 + + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + } + } + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutCubic + } + } + } + + // Burst effect overlay for focused workspace number + Rectangle { + id: groupedWorkspaceNumberBurst + anchors.centerIn: groupedWorkspaceNumberContainer + width: groupedWorkspaceNumberContainer.width + 12 * root.masterProgress + height: groupedWorkspaceNumberContainer.height + 12 * root.masterProgress + radius: width / 2 + color: "transparent" + border.color: root.effectColor + border.width: Math.max(1, Math.round((2 + 4 * (1.0 - root.masterProgress)))) + opacity: root.effectsActive && groupedContainer.workspaceModel.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0 + visible: root.effectsActive && groupedContainer.workspaceModel.isFocused + z: 1 + } + + NText { + id: groupedWorkspaceNumber + + anchors.centerIn: parent + + text: { + if (groupedContainer.workspaceModel.name && groupedContainer.workspaceModel.name.length > 0) { + if (root.labelMode === "name") { + return groupedContainer.workspaceModel.name.substring(0, root.characterCount); + } + if (root.labelMode === "index+name") { + return (groupedContainer.workspaceModel.idx.toString() + groupedContainer.workspaceModel.name.substring(0, 1)); + } + } + return groupedContainer.workspaceModel.idx.toString(); + } + + family: Settings.data.ui.fontFixed + font { + pointSize: barFontSize * 0.75 + weight: fontWeight + capitalization: Font.AllUppercase + } + applyUiScale: false + + color: { + if (groupedContainer.workspaceModel.isFocused) + return Color.resolveOnColorKey(root.focusedColor); + if (groupedContainer.workspaceModel.isUrgent) + return Color.mOnError; + if (groupedContainer.hasWindows) + return Color.resolveOnColorKey(root.occupiedColor); + + return Color.resolveOnColorKey(root.emptyColor); + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.InOutCubic + } + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.InOutCubic + } + } + } + } + } + + Flow { + id: groupedGrid + visible: appVisible + scale: visible ? 1.0 : 0.8 + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + easing.overshoot: 1.2 + } + } + + x: root.isVertical ? Style.pixelAlignCenter(parent.width, width) : Math.round(horizontalPadding * root.hasLabel) + y: root.isVertical ? Math.round(horizontalPadding * 0.4 * root.hasLabel) : Style.pixelAlignCenter(parent.height, height) + + spacing: Style.marginS + flow: root.isVertical ? Flow.TopToBottom : Flow.LeftToRight + + Repeater { + model: appVisible ? localWorkspaces : null + delegate: groupedWorkspaceDelegate + } + } + + function openGroupedContextMenu(item) { + // Anchor to root (stable) but center horizontally on the clicked item + PanelService.showContextMenu(contextMenu, root, screen, item); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Cards/AudioCard.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/AudioCard.qml new file mode 100644 index 0000000..ccc1876 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/AudioCard.qml @@ -0,0 +1,276 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Media +import qs.Widgets + +// Audio controls card: output and input volume controls +NBox { + id: root + + property real localOutputVolume: 0 + property bool localOutputVolumeChanging: false + property int lastSinkId: -1 + + property real localInputVolume: 0 + property bool localInputVolumeChanging: false + property int lastSourceId: -1 + + readonly property bool outputVolumeGuard: outputVolumeSlider.sliderActive || localOutputVolumeChanging + readonly property bool inputVolumeGuard: inputVolumeSlider.sliderActive || localInputVolumeChanging + + Component.onCompleted: { + var vol = AudioService.volume; + localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; + var inputVol = AudioService.inputVolume; + localInputVolume = (inputVol !== undefined && !isNaN(inputVol)) ? inputVol : 0; + if (AudioService.sink) { + lastSinkId = AudioService.sink.id; + } + if (AudioService.source) { + lastSourceId = AudioService.source.id; + } + } + + // Reset local volume when device changes - use current device's volume + Connections { + target: AudioService + function onSinkChanged() { + if (AudioService.sink) { + const newSinkId = AudioService.sink.id; + if (newSinkId !== lastSinkId) { + lastSinkId = newSinkId; + // Immediately set local volume to current device's volume + var vol = AudioService.volume; + localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; + } + } else { + lastSinkId = -1; + localOutputVolume = 0; + } + } + } + + Connections { + target: AudioService + function onSourceChanged() { + if (AudioService.source) { + const newSourceId = AudioService.source.id; + if (newSourceId !== lastSourceId) { + lastSourceId = newSourceId; + // Immediately set local volume to current device's volume + var vol = AudioService.inputVolume; + localInputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; + } + } else { + lastSourceId = -1; + localInputVolume = 0; + } + } + } + + // Timer to debounce volume changes + // Only sync if the device hasn't changed (check by comparing IDs) + Timer { + interval: 100 + running: true + repeat: true + onTriggered: { + // Only sync if sink hasn't changed + if (AudioService.sink && AudioService.sink.id === lastSinkId) { + if (Math.abs(localOutputVolume - AudioService.volume) >= 0.01) { + AudioService.setVolume(localOutputVolume); + } + } + // Only sync if source hasn't changed + if (AudioService.source && AudioService.source.id === lastSourceId) { + if (Math.abs(localInputVolume - AudioService.inputVolume) >= 0.01) { + AudioService.setInputVolume(localInputVolume); + } + } + } + } + + Connections { + target: AudioService + function onVolumeChanged() { + if (!outputVolumeGuard && !AudioService.isSettingOutputVolume && AudioService.sink && AudioService.sink.id === lastSinkId) { + var vol = AudioService.volume; + localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; + } + } + } + + Connections { + target: AudioService + function onInputVolumeChanged() { + if (!inputVolumeGuard && !AudioService.isSettingInputVolume && AudioService.source && AudioService.source.id === lastSourceId) { + var vol = AudioService.inputVolume; + localInputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; + } + } + } + + Connections { + target: outputVolumeSlider + function onSliderActiveChanged() { + if (!outputVolumeSlider.sliderActive && AudioService.sink && AudioService.sink.id === lastSinkId) { + var vol = AudioService.volume; + localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; + } + } + } + + Connections { + target: inputVolumeSlider + function onSliderActiveChanged() { + if (!inputVolumeSlider.sliderActive && AudioService.source && AudioService.source.id === lastSourceId) { + var vol = AudioService.inputVolume; + localInputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; + } + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + // Output Volume Section + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + Layout.preferredWidth: 0 + opacity: AudioService.sink ? 1.0 : 0.5 + enabled: AudioService.sink + + // Output Volume Header + RowLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NIconButton { + icon: AudioService.muted ? "volume-off" : "volume-high" + baseSize: Style.baseWidgetSize * 0.5 + colorFg: AudioService.muted ? Color.mError : Color.mOnSurface + colorBg: "transparent" + colorBgHover: Color.mHover + colorFgHover: Color.mOnHover + onClicked: AudioService.setOutputMuted(!AudioService.muted) + } + + NText { + text: AudioService.sink ? AudioService.sink.description : "No output device" + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + elide: Text.ElideRight + Layout.fillWidth: true + Layout.preferredWidth: 0 + } + } + + // Output Volume Slider + NSlider { + id: outputVolumeSlider + Layout.fillWidth: true + from: 0 + to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0 + value: localOutputVolume + stepSize: 0.01 + heightRatio: 0.5 + onMoved: localOutputVolume = value + onPressedChanged: localOutputVolumeChanging = pressed + tooltipText: `${Math.round((outputVolumeGuard ? localOutputVolume : AudioService.volume) * 100)}%` + tooltipDirection: "bottom" + + // MouseArea to handle wheel events when hovering over the slider + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + + onWheel: wheel => { + if (outputVolumeSlider.enabled && AudioService.sink) { + const delta = wheel.angleDelta.y || wheel.angleDelta.x; + const step = Settings.data.audio.volumeStep / 100.0; // Convert percentage to 0-1 range + const increment = delta > 0 ? step : -step; + const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; + const newValue = Math.max(0, Math.min(maxVolume, localOutputVolume + increment)); + localOutputVolume = newValue; + } + } + } + } + } + + // Input Volume Section + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + Layout.preferredWidth: 0 + opacity: AudioService.source ? 1.0 : 0.5 + enabled: AudioService.source + + // Input Volume Header + RowLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NIconButton { + icon: AudioService.inputMuted ? "microphone-off" : "microphone" + baseSize: Style.baseWidgetSize * 0.5 + colorFg: AudioService.inputMuted ? Color.mError : Color.mOnSurface + colorBg: "transparent" + colorBgHover: Color.mHover + colorFgHover: Color.mOnHover + onClicked: AudioService.setInputMuted(!AudioService.inputMuted) + } + + NText { + text: AudioService.source ? AudioService.source.description : "No input device" + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + elide: Text.ElideRight + Layout.fillWidth: true + Layout.preferredWidth: 0 + } + } + + // Input Volume Slider + NSlider { + id: inputVolumeSlider + Layout.fillWidth: true + from: 0 + to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0 + value: localInputVolume + stepSize: 0.01 + heightRatio: 0.5 + onMoved: localInputVolume = value + onPressedChanged: localInputVolumeChanging = pressed + tooltipText: `${Math.round((inputVolumeGuard ? localInputVolume : AudioService.inputVolume) * 100)}%` + tooltipDirection: "bottom" + + // MouseArea to handle wheel events when hovering over the slider + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + + onWheel: wheel => { + if (inputVolumeSlider.enabled && AudioService.source) { + const delta = wheel.angleDelta.y || wheel.angleDelta.x; + const step = Settings.data.audio.volumeStep / 100.0; // Convert percentage to 0-1 range + const increment = delta > 0 ? step : -step; + const maxVolume = Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; + const newValue = Math.max(0, Math.min(maxVolume, localInputVolume + increment)); + localInputVolume = newValue; + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Cards/BrightnessCard.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/BrightnessCard.qml new file mode 100644 index 0000000..3254db6 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/BrightnessCard.qml @@ -0,0 +1,155 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Hardware +import qs.Widgets + +// Brightness control card for the ControlCenter +NBox { + id: root + + Layout.fillWidth: true + clip: true + + // Get the primary monitor (first screen) + readonly property var brightnessMonitor: { + if (Quickshell.screens.length > 0) { + return BrightnessService.getMonitorForScreen(Quickshell.screens[0]); + } + return null; + } + + property real localBrightness: 0 + property bool localBrightnessChanging: false + + Component.onCompleted: { + if (brightnessMonitor) { + localBrightness = brightnessMonitor.brightness || 0; + } + } + + // Update local brightness when monitor changes + Connections { + target: BrightnessService + function onMonitorBrightnessChanged(monitor, newBrightness) { + if (monitor === brightnessMonitor && !localBrightnessChanging) { + localBrightness = newBrightness; + } + } + } + + // Update local brightness when monitor's brightness property changes + Connections { + target: brightnessMonitor + ignoreUnknownSignals: true + function onBrightnessUpdated() { + if (brightnessMonitor && !localBrightnessChanging) { + localBrightness = brightnessMonitor.brightness || 0; + } + } + } + + // Timer to debounce brightness changes - only runs when user is changing slider + Timer { + id: debounceTimer + interval: 100 + running: false + repeat: false + onTriggered: { + if (brightnessMonitor && Math.abs(localBrightness - brightnessMonitor.brightness) >= 0.01) { + brightnessMonitor.setBrightness(localBrightness); + } + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + // Brightness Section + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + Layout.preferredWidth: 0 + opacity: brightnessMonitor && brightnessMonitor.brightnessControlAvailable ? 1.0 : 0.5 + enabled: brightnessMonitor && brightnessMonitor.brightnessControlAvailable + + // Brightness Header + RowLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NIconButton { + icon: { + if (!brightnessMonitor) + return "brightness-low"; + const brightness = brightnessMonitor.brightness || 0; + if (brightness <= 0.001) + return "sun-off"; + return brightness <= 0.5 ? "brightness-low" : "brightness-high"; + } + baseSize: Style.baseWidgetSize * 0.5 + colorFg: Color.mOnSurface + colorBg: "transparent" + colorBgHover: Color.mHover + colorFgHover: Color.mOnHover + } + + NText { + text: brightnessMonitor ? I18n.tr("common.brightness") : "No display" + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + elide: Text.ElideRight + Layout.fillWidth: true + Layout.preferredWidth: 0 + } + + NText { + text: brightnessMonitor ? Math.round(localBrightness * 100) + "%" : "N/A" + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + opacity: brightnessMonitor && brightnessMonitor.brightnessControlAvailable ? 1.0 : 0.5 + } + } + + // Brightness Slider + NSlider { + id: brightnessSlider + Layout.fillWidth: true + from: 0 + to: 1 + value: localBrightness + stepSize: 0.01 + heightRatio: 0.5 + onMoved: { + localBrightness = value; + debounceTimer.restart(); + } + onPressedChanged: localBrightnessChanging = pressed + tooltipText: `${Math.round(localBrightness * 100)}%` + tooltipDirection: "bottom" + + // MouseArea to handle wheel events when hovering over the slider + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + + onWheel: wheel => { + if (brightnessSlider.enabled && brightnessMonitor && brightnessMonitor.brightnessControlAvailable) { + const delta = wheel.angleDelta.y || wheel.angleDelta.x; + const step = Settings.data.brightness.brightnessStep / 100.0; // Convert percentage to 0-1 range + const increment = delta > 0 ? step : -step; + const newValue = Math.max(0, Math.min(1, localBrightness + increment)); + localBrightness = newValue; + debounceTimer.restart(); + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Cards/CalendarHeaderCard.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/CalendarHeaderCard.qml new file mode 100644 index 0000000..23c5fbe --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/CalendarHeaderCard.qml @@ -0,0 +1,134 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Location +import qs.Widgets + +// Calendar header with date, month/year, location, and clock +Rectangle { + id: root + Layout.fillWidth: true + Layout.minimumHeight: (60 * Style.uiScaleRatio) + Style.margin2M + Layout.preferredHeight: (60 * Style.uiScaleRatio) + Style.margin2M + implicitHeight: (60 * Style.uiScaleRatio) + Style.margin2M + radius: Style.radiusL + color: Color.mPrimary + + // Internal state + readonly property var now: Time.now + readonly property bool weatherReady: Settings.data.location.weatherEnabled && (LocationService.data.weather !== null) + + // Expose current month/year for potential synchronization with CalendarMonthCard + readonly property int currentMonth: now.getMonth() + readonly property int currentYear: now.getFullYear() + + ColumnLayout { + id: capsuleColumn + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.topMargin: Style.marginM + anchors.bottomMargin: Style.marginM + anchors.rightMargin: clockLoader.width + Style.margin2XL + anchors.leftMargin: Style.marginXL + spacing: 0 + + // Combined layout for date, month year, location and time-zone + RowLayout { + Layout.fillWidth: true + height: 60 * Style.uiScaleRatio + clip: true + spacing: Style.marginS + + // Today day number + NText { + Layout.preferredWidth: implicitWidth + elide: Text.ElideNone + clip: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + text: root.now.getDate() + pointSize: Style.fontSizeXXXL * 1.5 + font.weight: Style.fontWeightBold + color: Color.mOnPrimary + } + + // Month, year, location + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.bottomMargin: Style.marginXXS + Layout.topMargin: -Style.marginXXS + spacing: -Style.marginXS + + RowLayout { + spacing: Style.marginS + + NText { + text: I18n.locale.monthName(root.currentMonth, Locale.LongFormat).toUpperCase() + pointSize: Style.fontSizeXL * 1.1 + font.weight: Style.fontWeightBold + color: Color.mOnPrimary + Layout.alignment: Qt.AlignBaseline + elide: Text.ElideRight + } + + NText { + text: `${root.currentYear}` + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + color: Qt.alpha(Color.mOnPrimary, 0.7) + Layout.alignment: Qt.AlignBaseline + } + } + + RowLayout { + spacing: 0 + + NText { + text: { + if (!Settings.data.location.weatherEnabled) + return ""; + if (!LocationService.locationConfigured) + return I18n.tr("common.weather-no-location"); + if (!root.weatherReady) + return I18n.tr("common.weather-loading"); + if (Settings.data.location.hideWeatherCityName) + return ""; + const chunks = Settings.data.location.name.split(","); + return chunks[0]; + } + pointSize: Style.fontSizeM + color: Color.mOnPrimary + Layout.maximumWidth: 150 + elide: Text.ElideRight + } + + NText { + text: root.weatherReady && !Settings.data.location.hideWeatherTimezone ? `${Settings.data.location.hideWeatherCityName ? "" : " "}(${LocationService.data.weather.timezone_abbreviation})` : "" + pointSize: Style.fontSizeXS + color: Qt.alpha(Color.mOnPrimary, 0.7) + } + } + } + + // Spacer + Item { + Layout.fillWidth: true + } + } + } + + // Analog/Digital clock + NClock { + id: clockLoader + anchors.right: parent.right + anchors.rightMargin: Style.marginXL + anchors.verticalCenter: parent.verticalCenter + clockStyle: Settings.data.location.analogClockInCalendar ? "analog" : "digital" + progressColor: Color.mOnPrimary + Layout.alignment: Qt.AlignVCenter + now: root.now + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Cards/CalendarMonthCard.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/CalendarMonthCard.qml new file mode 100644 index 0000000..6008a28 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/CalendarMonthCard.qml @@ -0,0 +1,429 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Location +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +// Calendar month grid with navigation +NBox { + id: root + Layout.fillWidth: true + implicitHeight: calendarContent.implicitHeight + Style.margin2M + + // Internal state - independent from header + readonly property var now: Time.now + property int calendarMonth: now.getMonth() + property int calendarYear: now.getFullYear() + readonly property int firstDayOfWeek: Settings.data.location.firstDayOfWeek === -1 ? I18n.locale.firstDayOfWeek : Settings.data.location.firstDayOfWeek + + // Helper function to calculate ISO week number + function getISOWeekNumber(date) { + const target = new Date(date.valueOf()); + const dayNr = (date.getDay() + 6) % 7; + target.setDate(target.getDate() - dayNr + 3); + const firstThursday = new Date(target.getFullYear(), 0, 4); + const diff = target - firstThursday; + const oneWeek = 1000 * 60 * 60 * 24 * 7; + const weekNumber = 1 + Math.round(diff / oneWeek); + return weekNumber; + } + + // Helper function to check if an event is all-day + function isAllDayEvent(event) { + const duration = event.end - event.start; + const startDate = new Date(event.start * 1000); + const isAtMidnight = startDate.getHours() === 0 && startDate.getMinutes() === 0; + return duration === 86400 && isAtMidnight; + } + + // Navigation functions + function navigateToPreviousMonth() { + let newDate = new Date(root.calendarYear, root.calendarMonth - 1, 1); + root.calendarYear = newDate.getFullYear(); + root.calendarMonth = newDate.getMonth(); + const now = new Date(); + const monthStart = new Date(root.calendarYear, root.calendarMonth, 1); + const monthEnd = new Date(root.calendarYear, root.calendarMonth + 1, 0); + const daysBehind = Math.max(0, Math.ceil((now - monthStart) / (24 * 60 * 60 * 1000))); + const daysAhead = Math.max(0, Math.ceil((monthEnd - now) / (24 * 60 * 60 * 1000))); + CalendarService.loadEvents(daysAhead + 30, daysBehind + 30); + } + + function navigateToNextMonth() { + let newDate = new Date(root.calendarYear, root.calendarMonth + 1, 1); + root.calendarYear = newDate.getFullYear(); + root.calendarMonth = newDate.getMonth(); + const now = new Date(); + const monthStart = new Date(root.calendarYear, root.calendarMonth, 1); + const monthEnd = new Date(root.calendarYear, root.calendarMonth + 1, 0); + const daysBehind = Math.max(0, Math.ceil((now - monthStart) / (24 * 60 * 60 * 1000))); + const daysAhead = Math.max(0, Math.ceil((monthEnd - now) / (24 * 60 * 60 * 1000))); + CalendarService.loadEvents(daysAhead + 30, daysBehind + 30); + } + + // Wheel handler for month navigation + WheelHandler { + id: wheelHandler + target: root + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onWheel: function (event) { + if (event.angleDelta.y > 0) { + // Scroll up - go to previous month + root.navigateToPreviousMonth(); + event.accepted = true; + } else if (event.angleDelta.y < 0) { + // Scroll down - go to next month + root.navigateToNextMonth(); + event.accepted = true; + } + } + } + + ColumnLayout { + id: calendarContent + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + // Navigation row + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + Item { + Layout.preferredWidth: Style.marginS + } + + NText { + text: I18n.locale.monthName(root.calendarMonth, Locale.LongFormat).toUpperCase() + " " + root.calendarYear + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NDivider { + Layout.fillWidth: true + } + + NIconButton { + icon: "chevron-left" + onClicked: root.navigateToPreviousMonth() + } + + NIconButton { + icon: "calendar" + onClicked: { + root.calendarMonth = root.now.getMonth(); + root.calendarYear = root.now.getFullYear(); + CalendarService.loadEvents(); + } + } + + NIconButton { + icon: "chevron-right" + onClicked: root.navigateToNextMonth() + } + } + + // Day names header + RowLayout { + Layout.fillWidth: true + spacing: 0 + + Item { + visible: Settings.data.location.showWeekNumberInCalendar + Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0 + } + + GridLayout { + Layout.fillWidth: true + columns: 7 + rows: 1 + columnSpacing: 0 + rowSpacing: 0 + + Repeater { + model: 7 + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.fontSizeS * 2 + + NText { + anchors.centerIn: parent + text: { + let dayIndex = (root.firstDayOfWeek + index) % 7; + const dayName = I18n.locale.dayName(dayIndex, Locale.ShortFormat); + return dayName.substring(0, 2).toUpperCase(); + } + color: Color.mPrimary + pointSize: Style.fontSizeS + font.weight: Style.fontWeightBold + horizontalAlignment: Text.AlignHCenter + } + } + } + } + } + + // Calendar grid with week numbers + RowLayout { + Layout.fillWidth: true + spacing: 0 + + // Helper functions + function hasEventsOnDate(year, month, day) { + if (!CalendarService.available || CalendarService.events.length === 0) + return false; + const targetDate = new Date(year, month, day); + const targetStart = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000; + const targetEnd = targetStart + 86400; + return CalendarService.events.some(event => { + return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd); + }); + } + + function getEventsForDate(year, month, day) { + if (!CalendarService.available || CalendarService.events.length === 0) + return []; + const targetDate = new Date(year, month, day); + const targetStart = Math.floor(new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()).getTime() / 1000); + const targetEnd = targetStart + 86400; + return CalendarService.events.filter(event => { + return (event.start >= targetStart && event.start < targetEnd) || (event.end > targetStart && event.end <= targetEnd) || (event.start < targetStart && event.end > targetEnd); + }); + } + + function isMultiDayEvent(event) { + if (root.isAllDayEvent(event)) { + return false; + } + const startDate = new Date(event.start * 1000); + const endDate = new Date(event.end * 1000); + const startDateOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate()); + const endDateOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()); + return startDateOnly.getTime() !== endDateOnly.getTime(); + } + + function getEventColor(event, isToday) { + if (isMultiDayEvent(event)) { + return isToday ? Color.mOnSecondary : Color.mTertiary; + } else if (root.isAllDayEvent(event)) { + return isToday ? Color.mOnSecondary : Color.mSecondary; + } else { + return isToday ? Color.mOnSecondary : Color.mPrimary; + } + } + + // Week numbers column + ColumnLayout { + visible: Settings.data.location.showWeekNumberInCalendar + Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.7 : 0 + Layout.alignment: Qt.AlignTop + spacing: Style.marginXXS + + property var weekNumbers: { + if (!grid.daysModel || grid.daysModel.length === 0) + return []; + const weeks = []; + const numWeeks = Math.ceil(grid.daysModel.length / 7); + for (var i = 0; i < numWeeks; i++) { + const dayIndex = i * 7; + if (dayIndex < grid.daysModel.length) { + const weekDay = grid.daysModel[dayIndex]; + const date = new Date(weekDay.year, weekDay.month, weekDay.day); + let thursday = new Date(date); + if (root.firstDayOfWeek === 0) { + thursday.setDate(date.getDate() + 4); + } else if (root.firstDayOfWeek === 1) { + thursday.setDate(date.getDate() + 3); + } else { + let daysToThursday = (4 - root.firstDayOfWeek + 7) % 7; + thursday.setDate(date.getDate() + daysToThursday); + } + weeks.push(root.getISOWeekNumber(thursday)); + } + } + return weeks; + } + + Repeater { + model: parent.weekNumbers + Item { + Layout.preferredWidth: Style.baseWidgetSize * 0.7 + Layout.preferredHeight: Style.baseWidgetSize * 0.9 + + NText { + anchors.centerIn: parent + color: Qt.alpha(Color.mPrimary, 0.7) + pointSize: Style.fontSizeXXS + text: modelData + } + } + } + } + + // Calendar grid + GridLayout { + id: grid + Layout.fillWidth: true + columns: 7 + columnSpacing: Style.marginXXS + rowSpacing: Style.marginXXS + + property int month: root.calendarMonth + property int year: root.calendarYear + + property var daysModel: { + const firstOfMonth = new Date(year, month, 1); + const lastOfMonth = new Date(year, month + 1, 0); + const daysInMonth = lastOfMonth.getDate(); + const firstDayOfWeek = root.firstDayOfWeek; + const firstOfMonthDayOfWeek = firstOfMonth.getDay(); + let daysBefore = (firstOfMonthDayOfWeek - firstDayOfWeek + 7) % 7; + const lastOfMonthDayOfWeek = lastOfMonth.getDay(); + const daysAfter = (firstDayOfWeek - lastOfMonthDayOfWeek - 1 + 7) % 7; + const days = []; + const today = new Date(); + + // Previous month days + const prevMonth = new Date(year, month, 0); + const prevMonthDays = prevMonth.getDate(); + for (var i = daysBefore - 1; i >= 0; i--) { + const day = prevMonthDays - i; + days.push({ + "day": day, + "month": month - 1, + "year": month === 0 ? year - 1 : year, + "today": false, + "currentMonth": false + }); + } + + // Current month days + for (var day = 1; day <= daysInMonth; day++) { + const date = new Date(year, month, day); + const isToday = date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate(); + days.push({ + "day": day, + "month": month, + "year": year, + "today": isToday, + "currentMonth": true + }); + } + + // Next month days + for (var i = 1; i <= daysAfter; i++) { + days.push({ + "day": i, + "month": month + 1, + "year": month === 11 ? year + 1 : year, + "today": false, + "currentMonth": false + }); + } + + return days; + } + + Repeater { + model: grid.daysModel + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize * 0.9 + + Rectangle { + width: Style.baseWidgetSize * 0.9 + height: Style.baseWidgetSize * 0.9 + anchors.centerIn: parent + radius: Style.radiusM + color: modelData.today ? Color.mSecondary : "transparent" + + NText { + anchors.centerIn: parent + text: modelData.day + color: { + if (modelData.today) + return Color.mOnSecondary; + if (modelData.currentMonth) + return Color.mOnSurface; + return Color.mOnSurfaceVariant; + } + opacity: modelData.currentMonth ? 1.0 : 0.4 + pointSize: Style.fontSizeM + font.weight: modelData.today ? Style.fontWeightBold : Style.fontWeightMedium + } + + // Event indicator dots + Row { + visible: Settings.data.location.showCalendarEvents && parent.parent.parent.parent.hasEventsOnDate(modelData.year, modelData.month, modelData.day) + spacing: 2 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: Style.marginXS + + Repeater { + model: parent.parent.parent.parent.parent.getEventsForDate(modelData.year, modelData.month, modelData.day) + + Rectangle { + width: 4 + height: width + radius: Style.radiusXXS + color: parent.parent.parent.parent.parent.getEventColor(modelData, modelData.today) + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + enabled: Settings.data.location.showCalendarEvents + + onEntered: { + const events = parent.parent.parent.parent.getEventsForDate(modelData.year, modelData.month, modelData.day); + if (events.length > 0) { + const summaries = events.map(event => { + if (root.isAllDayEvent(event)) { + return event.summary; + } else { + const timeFormat = Settings.data.location.use12hourFormat ? "hh:mm AP" : "HH:mm"; + const start = new Date(event.start * 1000); + const startFormatted = I18n.locale.toString(start, timeFormat); + const end = new Date(event.end * 1000); + const endFormatted = I18n.locale.toString(end, timeFormat); + return `${startFormatted}-${endFormatted} ${event.summary}`; + } + }).join('\n'); + TooltipService.show(parent, summaries, "auto", Style.tooltipDelay, Settings.data.ui.fontFixed); + } + } + + onClicked: { + const dateWithSlashes = `${(modelData.month + 1).toString().padStart(2, '0')}/${modelData.day.toString().padStart(2, '0')}/${modelData.year.toString().substring(2)}`; + if (ProgramCheckerService.gnomeCalendarAvailable) { + Quickshell.execDetached(["gnome-calendar", "--date", dateWithSlashes]); + } + } + + onExited: { + TooltipService.hide(); + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Cards/MediaCard.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/MediaCard.qml new file mode 100644 index 0000000..ab604be --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/MediaCard.qml @@ -0,0 +1,456 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Media +import qs.Services.UI +import qs.Widgets +import qs.Widgets.AudioSpectrum + +NBox { + id: root + + // Track whether we have an active media player + readonly property bool hasActivePlayer: MediaService.currentPlayer && MediaService.canPlay + + // SpectrumService registration for visualizer + readonly property bool needsSpectrum: Settings.data.audio.visualizerType !== "" && Settings.data.audio.visualizerType !== "none" + + onNeedsSpectrumChanged: { + if (root.needsSpectrum) { + SpectrumService.registerComponent("mediacard"); + } else { + SpectrumService.unregisterComponent("mediacard"); + } + } + + Component.onCompleted: { + if (root.needsSpectrum) { + SpectrumService.registerComponent("mediacard"); + } + updateCachedWallpaper(); + } + + Component.onDestruction: { + SpectrumService.unregisterComponent("mediacard"); + } + + property string wallpaper: WallpaperService.getWallpaper(screen.name) + property string cachedWallpaper: "" + + // External state management + Connections { + target: WallpaperService + function onWallpaperChanged(screenName, path) { + if (screenName === screen.name) { + wallpaper = path; + updateCachedWallpaper(); + } + } + } + + function updateCachedWallpaper() { + // Handle solid color mode - no wallpaper to cache + if (Settings.data.wallpaper.useSolidColor || WallpaperService.isSolidColorPath(wallpaper)) { + cachedWallpaper = ""; + return; + } + + if (!wallpaper) { + cachedWallpaper = ""; + return; + } + + if (!ImageCacheService.initialized) { + cachedWallpaper = wallpaper; + return; + } + + ImageCacheService.getThumbnail(wallpaper, function (cachedPath, success) { + if (!root) + return; + cachedWallpaper = success ? cachedPath : wallpaper; + }); + } + + // Wrapper - rounded rect clipper + Item { + anchors.fill: parent + layer.enabled: true + layer.smooth: true + layer.effect: MultiEffect { + maskEnabled: true + maskThresholdMin: 0.95 + maskSpreadAtMin: 0.15 + maskSource: ShaderEffectSource { + sourceItem: Rectangle { + width: root.width + height: root.height + radius: Style.radiusM + color: "white" + } + } + } + + // Solid color background (always present as base layer) + Rectangle { + anchors.fill: parent + color: Settings.data.wallpaper.useSolidColor ? Settings.data.wallpaper.solidColor : Color.mSurface + } + + // Background image that covers everything + Image { + id: bgImage + readonly property int dim: Math.round(256 * Style.uiScaleRatio) + anchors.fill: parent + visible: source.toString() !== "" + source: MediaService.trackArtUrl || (Settings.data.wallpaper.enabled && !Settings.data.wallpaper.useSolidColor ? root.cachedWallpaper : "") + sourceSize: Qt.size(dim, dim) + fillMode: Image.PreserveAspectCrop + layer.enabled: true + layer.smooth: true + layer.effect: MultiEffect { + blurEnabled: true + blurMax: 8 + blur: 0.33 + } + } + + // Dark overlay for readability + Rectangle { + anchors.fill: parent + color: Color.mSurface + opacity: 0.65 + radius: Style.radiusM + } + + // Border + Rectangle { + anchors.fill: parent + color: "transparent" + border.color: Style.boxBorderColor + border.width: Style.borderS + radius: Style.radiusM + } + + // Background visualizer on top of the artwork + Loader { + anchors.fill: parent + active: Settings.data.audio.visualizerType !== "" && Settings.data.audio.visualizerType !== "none" + + sourceComponent: { + switch (Settings.data.audio.visualizerType) { + case "linear": + return linearComponent; + case "mirrored": + return mirroredComponent; + case "wave": + return waveComponent; + default: + return null; + } + } + + Component { + id: linearComponent + NLinearSpectrum { + anchors.fill: parent + values: SpectrumService.values + fillColor: Color.mPrimary + opacity: 0.8 + mirrored: Settings.data.audio.spectrumMirrored + } + } + + Component { + id: mirroredComponent + NMirroredSpectrum { + anchors.fill: parent + values: SpectrumService.values + fillColor: Color.mPrimary + opacity: 0.8 + mirrored: Settings.data.audio.spectrumMirrored + } + } + + Component { + id: waveComponent + NWaveSpectrum { + anchors.fill: parent + values: SpectrumService.values + fillColor: Color.mPrimary + opacity: 0.8 + mirrored: Settings.data.audio.spectrumMirrored + } + } + } + } + + // Player selector + Rectangle { + id: playerSelectorButton + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: Style.marginXS + anchors.leftMargin: Style.marginM + anchors.rightMargin: Style.marginM + height: Style.baseWidgetSize + visible: MediaService.getAvailablePlayers().length > 1 + radius: Style.radiusM + color: "transparent" + + property var currentPlayer: MediaService.getAvailablePlayers()[MediaService.selectedPlayerIndex] + + RowLayout { + anchors.fill: parent + spacing: Style.marginS + + NIcon { + icon: "caret-down" + pointSize: Style.fontSizeXXL + color: Color.mOnSurfaceVariant + } + + NText { + text: playerSelectorButton.currentPlayer ? playerSelectorButton.currentPlayer.identity : "" + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + Layout.fillWidth: true + } + } + + MouseArea { + id: playerSelectorMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + var menuItems = []; + var players = MediaService.getAvailablePlayers(); + for (var i = 0; i < players.length; i++) { + menuItems.push({ + "label": players[i].identity, + "action": i.toString(), + "icon": "disc", + "enabled": true, + "visible": true + }); + } + playerContextMenu.model = menuItems; + playerContextMenu.openAtItem(playerSelectorButton, playerSelectorButton.width - playerContextMenu.width, playerSelectorButton.height); + } + } + + NContextMenu { + id: playerContextMenu + parent: root + width: 200 + verticalPolicy: ScrollBar.AlwaysOff + + onTriggered: function (action) { + var index = parseInt(action); + if (!isNaN(index)) { + MediaService.switchToPlayer(index); + } + } + } + } + + // Content container that adjusts for player selector + Item { + anchors.fill: parent + anchors.topMargin: playerSelectorButton.visible ? (playerSelectorButton.height + Style.marginXS + Style.marginM) : Style.marginM + anchors.leftMargin: Style.marginM + anchors.rightMargin: Style.marginM + anchors.bottomMargin: Style.marginM + + // No media player detected - centered disc icon + NIcon { + anchors.centerIn: parent + visible: !root.hasActivePlayer && SpectrumService.isIdle + icon: "disc" + pointSize: Style.fontSizeXXXL * 3 + color: Color.mOnSurfaceVariant + opacity: 1.0 + } + + // MediaPlayer Main Content - use Loader for performance + Loader { + id: mainLoader + anchors.fill: parent + active: root.hasActivePlayer + + sourceComponent: Item { + Layout.fillWidth: true + Layout.fillHeight: true + + // Exceptionaly we put shadow on text and controls to ease readability + NDropShadow { + anchors.fill: main + source: main + autoPaddingEnabled: true + shadowBlur: 1.0 + shadowOpacity: 0.9 + shadowHorizontalOffset: 0 + shadowVerticalOffset: 0 + shadowColor: Settings.data.colorSchemes.darkMode ? "black" : "white" + } + + ColumnLayout { + id: main + anchors.fill: parent + spacing: Style.marginS + + // Spacer to push content down + Item { + Layout.preferredHeight: Style.marginM + } + + // Metadata + ColumnLayout { + id: metadata + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + spacing: Style.marginXS + + NText { + visible: MediaService.trackTitle !== "" + text: MediaService.trackTitle + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + elide: Text.ElideRight + wrapMode: Text.Wrap + maximumLineCount: 2 + Layout.fillWidth: true + } + + NText { + visible: MediaService.trackArtist !== "" + text: MediaService.trackArtist + color: Color.mSecondary + pointSize: Style.fontSizeS + elide: Text.ElideRight + Layout.fillWidth: true + } + + NText { + visible: MediaService.trackAlbum !== "" + text: MediaService.trackAlbum + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeM + elide: Text.ElideRight + Layout.fillWidth: true + } + } + + // Progress slider + Item { + id: progressWrapper + visible: (MediaService.currentPlayer && MediaService.trackLength > 0) + Layout.fillWidth: true + height: Style.baseWidgetSize * 0.5 + + property real localSeekRatio: -1 + property real lastSentSeekRatio: -1 + property real seekEpsilon: 0.01 + property real progressRatio: { + if (!MediaService.currentPlayer || MediaService.trackLength <= 0) + return 0; + const r = MediaService.currentPosition / MediaService.trackLength; + if (isNaN(r) || !isFinite(r)) + return 0; + return Math.max(0, Math.min(1, r)); + } + property real effectiveRatio: (MediaService.isSeeking && localSeekRatio >= 0) ? Math.max(0, Math.min(1, localSeekRatio)) : progressRatio + + Timer { + id: seekDebounce + interval: 75 + repeat: false + onTriggered: { + if (MediaService.isSeeking && progressWrapper.localSeekRatio >= 0) { + const next = Math.max(0, Math.min(1, progressWrapper.localSeekRatio)); + if (progressWrapper.lastSentSeekRatio < 0 || Math.abs(next - progressWrapper.lastSentSeekRatio) >= progressWrapper.seekEpsilon) { + MediaService.seekByRatio(next); + progressWrapper.lastSentSeekRatio = next; + } + } + } + } + + NSlider { + id: progressSlider + anchors.fill: parent + from: 0 + to: 1 + stepSize: 0 + snapAlways: false + enabled: MediaService.trackLength > 0 && MediaService.canSeek + heightRatio: 0.6 + + onMoved: { + progressWrapper.localSeekRatio = value; + seekDebounce.restart(); + } + onPressedChanged: { + if (pressed) { + MediaService.isSeeking = true; + progressWrapper.localSeekRatio = value; + MediaService.seekByRatio(value); + progressWrapper.lastSentSeekRatio = value; + } else { + seekDebounce.stop(); + MediaService.seekByRatio(value); + MediaService.isSeeking = false; + progressWrapper.localSeekRatio = -1; + progressWrapper.lastSentSeekRatio = -1; + } + } + } + + Binding { + target: progressSlider + property: "value" + value: progressWrapper.progressRatio + when: !MediaService.isSeeking + } + } + + // Spacer to push media controls down + Item { + Layout.preferredHeight: Style.marginL + } + + // Media controls + RowLayout { + spacing: Style.marginS + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + + NIconButton { + icon: "media-prev" + visible: MediaService.canGoPrevious + onClicked: MediaService.canGoPrevious ? MediaService.previous() : {} + } + + NIconButton { + icon: MediaService.isPlaying ? "media-pause" : "media-play" + visible: (MediaService.canPlay || MediaService.canPause) + onClicked: (MediaService.canPlay || MediaService.canPause) ? MediaService.playPause() : {} + } + + NIconButton { + icon: "media-next" + visible: MediaService.canGoNext + onClicked: MediaService.canGoNext ? MediaService.next() : {} + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Cards/ProfileCard.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/ProfileCard.qml new file mode 100644 index 0000000..2c49613 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/ProfileCard.qml @@ -0,0 +1,123 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import qs.Commons +import qs.Modules.Panels.Settings +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +// Header card with avatar, user and quick actions +NBox { + id: root + + property string uptimeText: "--" + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.margins: Style.marginM + spacing: Style.marginM + + NImageRounded { + Layout.preferredWidth: Math.round(Style.baseWidgetSize * 1.25 * Style.uiScaleRatio) + Layout.preferredHeight: Math.round(Style.baseWidgetSize * 1.25 * Style.uiScaleRatio) + radius: Layout.preferredWidth / 2 + imagePath: Settings.preprocessPath(Settings.data.general.avatarImage) + fallbackIcon: "person" + borderColor: Color.mPrimary + borderWidth: Style.borderS * 1.5 + } + + ColumnLayout { + Layout.fillWidth: true + Layout.minimumWidth: 0 + spacing: Style.marginXXS + NText { + Layout.fillWidth: true + Layout.minimumWidth: 0 + text: HostService.displayName + font.weight: Style.fontWeightBold + } + NText { + Layout.fillWidth: true + Layout.minimumWidth: 0 + text: I18n.tr("system.uptime", { + "uptime": uptimeText + }) + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + + RowLayout { + spacing: Style.marginS + Layout.fillWidth: false + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + Item { + Layout.fillWidth: true + } + NIconButton { + icon: "settings" + tooltipText: I18n.tr("tooltips.open-settings") + onClicked: { + // Better close the control center in case the settings open in a separate window + PanelService.openedPanel?.close(); + + var panel = PanelService.getPanel("settingsPanel", screen); + panel.requestedTab = SettingsPanel.Tab.General; + panel.open(); + } + } + + NIconButton { + icon: "power" + tooltipText: I18n.tr("tooltips.session-menu") + onClicked: { + PanelService.getPanel("sessionMenuPanel", screen)?.open(); + PanelService.getPanel("controlCenterPanel", screen)?.close(); + } + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + onClicked: { + PanelService.getPanel("controlCenterPanel", screen)?.close(); + } + } + } + } + + // ---------------------------------- + // Uptime + Timer { + interval: 60000 + repeat: true + running: true + onTriggered: uptimeProcess.running = true + } + + Process { + id: uptimeProcess + command: ["cat", "/proc/uptime"] + running: true + + stdout: StdioCollector { + onStreamFinished: { + var uptimeSeconds = parseFloat(this.text.trim().split(' ')[0]); + uptimeText = Time.formatVagueHumanReadableDuration(uptimeSeconds); + uptimeProcess.running = false; + } + } + } + + function updateSystemInfo() { + uptimeProcess.running = true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Cards/ShortcutsCard.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/ShortcutsCard.qml new file mode 100644 index 0000000..104569c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/ShortcutsCard.qml @@ -0,0 +1,92 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.Panels.ControlCenter +import qs.Widgets + +RowLayout { + Layout.fillWidth: true + spacing: Style.marginL + + NBox { + Layout.fillWidth: true + Layout.preferredHeight: root.shortcutsHeight + visible: Settings.data.controlCenter.shortcuts.left.length > 0 + + RowLayout { + id: leftContent + anchors.fill: parent + spacing: Style.marginS + + Item { + Layout.fillWidth: true + } + + Repeater { + model: Settings.data.controlCenter.shortcuts.left + delegate: ControlCenterWidgetLoader { + required property var modelData + required property int index + + Layout.fillWidth: false + widgetId: (modelData.id !== undefined ? modelData.id : "") + widgetScreen: root.screen + widgetProps: { + "widgetId": modelData.id, + "section": "quickSettings", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.controlCenter.shortcuts.left.length, + "widgetSettings": modelData + } + Layout.alignment: Qt.AlignVCenter + } + } + + Item { + Layout.fillWidth: true + } + } + } + + NBox { + Layout.fillWidth: true + Layout.preferredHeight: root.shortcutsHeight + visible: Settings.data.controlCenter.shortcuts.right.length > 0 + + RowLayout { + id: rightContent + anchors.fill: parent + spacing: Style.marginS + + Item { + Layout.fillWidth: true + } + + Repeater { + model: Settings.data.controlCenter.shortcuts.right + delegate: ControlCenterWidgetLoader { + required property var modelData + required property int index + + Layout.fillWidth: false + widgetId: (modelData.id !== undefined ? modelData.id : "") + widgetScreen: root.screen + widgetProps: { + "widgetId": modelData.id, + "section": "quickSettings", + "sectionWidgetIndex": index, + "sectionWidgetsCount": Settings.data.controlCenter.shortcuts.right.length, + "widgetSettings": modelData + } + Layout.alignment: Qt.AlignVCenter + } + } + + Item { + Layout.fillWidth: true + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Cards/SystemMonitorCard.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/SystemMonitorCard.qml new file mode 100644 index 0000000..6ce5af6 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/SystemMonitorCard.qml @@ -0,0 +1,125 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +// Unified system card: monitors CPU, temp, memory, disk +NBox { + id: root + + Component.onCompleted: SystemStatService.registerComponent("card-sysmonitor") + Component.onDestruction: SystemStatService.unregisterComponent("card-sysmonitor") + + readonly property string diskPath: Settings.data.controlCenter.diskPath || "/" + readonly property real contentScale: 0.95 * Style.uiScaleRatio + + Item { + id: content + anchors.fill: parent + anchors.margins: Style.marginS + + Column { + anchors.fill: parent + + Item { + width: parent.width + height: parent.height / 4 + + NCircleStat { + id: cpuUsageGauge + anchors.centerIn: parent + ratio: SystemStatService.cpuUsage / 100 + icon: "cpu-usage" + contentScale: root.contentScale + fillColor: SystemStatService.cpuColor + tooltipText: I18n.tr("system-monitor.cpu-usage") + `: ${Math.round(SystemStatService.cpuUsage)}%` + } + + Connections { + target: SystemStatService + function onCpuUsageChanged() { + if (TooltipService.activeTooltip && TooltipService.activeTooltip.targetItem === cpuUsageGauge) { + TooltipService.updateText(I18n.tr("system-monitor.cpu-usage") + `: ${Math.round(SystemStatService.cpuUsage)}%`); + } + } + } + } + + Item { + width: parent.width + height: parent.height / 4 + + NCircleStat { + id: cpuTempGauge + anchors.centerIn: parent + ratio: SystemStatService.cpuTemp / 100 + suffix: "°C" + icon: "cpu-temperature" + contentScale: root.contentScale + fillColor: SystemStatService.tempColor + tooltipText: I18n.tr("system-monitor.cpu-temp") + `: ${Math.round(SystemStatService.cpuTemp)}°C` + } + + Connections { + target: SystemStatService + function onCpuTempChanged() { + if (TooltipService.activeTooltip && TooltipService.activeTooltip.targetItem === cpuTempGauge) { + TooltipService.updateText(I18n.tr("system-monitor.cpu-temp") + `: ${Math.round(SystemStatService.cpuTemp)}°C`); + } + } + } + } + + Item { + width: parent.width + height: parent.height / 4 + + NCircleStat { + id: memPercentGauge + anchors.centerIn: parent + ratio: SystemStatService.memPercent / 100 + icon: "memory" + contentScale: root.contentScale + fillColor: SystemStatService.memColor + tooltipText: I18n.tr("common.memory") + `: ${Math.round(SystemStatService.memPercent)}%` + } + + Connections { + target: SystemStatService + function onMemPercentChanged() { + if (TooltipService.activeTooltip && TooltipService.activeTooltip.targetItem === memPercentGauge) { + TooltipService.updateText(I18n.tr("common.memory") + `: ${Math.round(SystemStatService.memPercent)}%`); + } + } + } + } + + Item { + width: parent.width + height: parent.height / 4 + + NCircleStat { + id: diskPercentsGauge + anchors.centerIn: parent + ratio: (SystemStatService.diskPercents[root.diskPath] ?? 0) / 100 + icon: "storage" + contentScale: root.contentScale + fillColor: SystemStatService.getDiskColor(root.diskPath) + tooltipText: I18n.tr("system-monitor.disk") + `: ${SystemStatService.diskPercents[root.diskPath] || 0}%\n${root.diskPath}` + } + + Connections { + target: SystemStatService + function onDiskPercentsChanged() { + if (TooltipService.activeTooltip && TooltipService.activeTooltip.targetItem === diskPercentsGauge) { + TooltipService.updateText(I18n.tr("system-monitor.disk") + `: ${SystemStatService.diskPercents[root.diskPath] || 0}%\n${root.diskPath}`); + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Cards/WeatherCard.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/WeatherCard.qml new file mode 100644 index 0000000..0dd5cb4 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Cards/WeatherCard.qml @@ -0,0 +1,279 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Location +import qs.Widgets + +// Weather overview card (placeholder data) +NBox { + id: root + + property int forecastDays: 6 + property bool showLocation: true + property bool showEffects: Settings.data.location.weatherShowEffects + readonly property bool weatherReady: Settings.data.location.weatherEnabled && (LocationService.data.weather !== null) + + // Test mode: set to "clear_day", "clear_night", "rain", "snow", "cloud" or "fog" + property string testEffects: "" + + // Weather condition detection + readonly property int currentWeatherCode: weatherReady ? LocationService.data.weather.current_weather.weathercode : 0 + readonly property bool isDayTime: weatherReady ? LocationService.data.weather.current_weather.is_day : true + readonly property bool isRaining: testEffects === "rain" || (testEffects === "" && ((currentWeatherCode >= 51 && currentWeatherCode <= 67) || (currentWeatherCode >= 80 && currentWeatherCode <= 82))) + readonly property bool isSnowing: testEffects === "snow" || (testEffects === "" && ((currentWeatherCode >= 71 && currentWeatherCode <= 77) || (currentWeatherCode >= 85 && currentWeatherCode <= 86))) + readonly property bool isCloudy: testEffects === "cloud" || (testEffects === "" && (currentWeatherCode === 3)) + readonly property bool isFoggy: testEffects === "fog" || (testEffects === "" && (currentWeatherCode >= 40 && currentWeatherCode <= 49)) + readonly property bool isClearDay: testEffects === "clear_day" || (testEffects === "" && (currentWeatherCode === 0 && isDayTime)) + readonly property bool isClearNight: testEffects === "clear_night" || (testEffects === "" && (currentWeatherCode === 0 && !isDayTime)) + + visible: Settings.data.location.weatherEnabled + implicitHeight: Math.max(100 * Style.uiScaleRatio, content.implicitHeight + Style.margin2XL) + + // Weather effect layer (rain/snow) + Loader { + id: weatherEffectLoader + anchors.fill: parent + active: root.showEffects && (root.isRaining || root.isSnowing || root.isCloudy || root.isFoggy || root.isClearDay || root.isClearNight) + + sourceComponent: Item { + anchors.fill: parent + + // Animated time for shaders + property real shaderTime: 0 + NumberAnimation on shaderTime { + loops: Animation.Infinite + from: 0 + to: root.isSnowing ? 900 : 3000 + duration: 300000 + } + + ShaderEffect { + id: weatherEffect + anchors.fill: parent + // Rain matches content margins, everything else fills the box + anchors.margins: root.isRaining ? Style.marginXL : root.border.width + + property var source: ShaderEffectSource { + sourceItem: content + hideSource: root.isRaining // Only hide for rain (distortion), show for snow + } + + // Limit update rate to avoid using too much processing power + property real time: parent.shaderTime - (parent.shaderTime % (1 / (root.isRaining ? 3 : root.isSnowing ? 6 : 2))) + property real itemWidth: weatherEffect.width + property real itemHeight: weatherEffect.height + property color bgColor: root.color + property real cornerRadius: root.isRaining ? 0 : (root.radius - root.border.width) + property real alternative: root.isFoggy + + fragmentShader: { + let shaderName; + if (root.isSnowing) + shaderName = "weather_snow"; + else if (root.isRaining) + shaderName = "weather_rain"; + else if (root.isCloudy || root.isFoggy) + shaderName = "weather_cloud"; + else if (root.isClearDay) + shaderName = "weather_sun"; + else if (root.isClearNight) + shaderName = "weather_stars"; + else + shaderName = ""; + + return Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/" + shaderName + ".frag.qsb"); + } + } + } + } + + ColumnLayout { + id: content + anchors.fill: parent + anchors.margins: Style.marginXL + spacing: Style.marginM + clip: true + + RowLayout { + visible: weatherReady + Layout.fillWidth: true + spacing: Style.marginS + + Item { + Layout.preferredWidth: Style.marginXXS + } + + RowLayout { + spacing: Style.marginL + Layout.fillWidth: true + + Item { + Layout.preferredWidth: mainWeatherIconSide + Layout.preferredHeight: mainWeatherIconSide + Layout.alignment: Qt.AlignVCenter + readonly property int mainWeatherIconSide: Math.round(Style.fontSizeXXXL * 1.75 * Style.uiScaleRatio * 1.6) + + NIcon { + visible: !LocationService.taliaWeatherMascotActive + anchors.centerIn: parent + icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : "" + pointSize: Style.fontSizeXXXL * 1.75 + color: Color.mPrimary + } + Loader { + active: LocationService.taliaWeatherMascotActive + anchors.fill: parent + asynchronous: true + sourceComponent: Component { + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectFit + smooth: true + mipmap: true + asynchronous: true + source: Qt.resolvedUrl(LocationService.taliaWeatherImageFromCode(currentWeatherCode)) + } + } + } + } + + ColumnLayout { + spacing: Style.marginXXS + NText { + text: { + // Ensure the name is not too long if one had to specify the country + const chunks = Settings.data.location.name.split(","); + return chunks[0]; + } + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + visible: showLocation && !Settings.data.location.hideWeatherCityName + } + + RowLayout { + NText { + text: { + if (!weatherReady) + return ""; + var temp = LocationService.data.weather.current_weather.temperature; + var suffix = "C"; + if (Settings.data.location.useFahrenheit) { + temp = LocationService.celsiusToFahrenheit(temp); + var suffix = "F"; + } + temp = Math.round(temp); + return `${temp}°${suffix}`; + } + pointSize: showLocation ? Style.fontSizeXL : Style.fontSizeXL * 1.6 + font.weight: Style.fontWeightBold + } + + NText { + text: weatherReady ? `(${LocationService.data.weather.timezone_abbreviation})` : "" + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + visible: LocationService.data.weather && showLocation && !Settings.data.location.hideWeatherTimezone + } + } + } + } + } + + NDivider { + visible: weatherReady + Layout.fillWidth: true + } + + RowLayout { + visible: weatherReady + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: Style.marginM + + Repeater { + model: weatherReady ? Math.min(root.forecastDays, LocationService.data.weather.daily.time.length) : 0 + delegate: ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXS + Item { + Layout.fillWidth: true + } + NText { + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + text: { + var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/")); + return I18n.locale.toString(weatherDate, "ddd"); + } + color: Color.mOnSurface + } + Item { + Layout.preferredWidth: forecastWeatherIconSide + Layout.preferredHeight: forecastWeatherIconSide + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + readonly property int forecastWeatherIconSide: Math.round(Style.fontSizeXXL * 1.6 * Style.uiScaleRatio * 1.6) + + NIcon { + visible: !LocationService.taliaWeatherMascotActive + anchors.centerIn: parent + icon: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index]) + pointSize: Style.fontSizeXXL * 1.6 + color: Color.mPrimary + } + Loader { + active: LocationService.taliaWeatherMascotActive + anchors.fill: parent + asynchronous: true + sourceComponent: Component { + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectFit + smooth: true + mipmap: true + asynchronous: true + source: Qt.resolvedUrl(LocationService.taliaWeatherImageFromCode(LocationService.data.weather.daily.weathercode[index])) + } + } + } + } + NText { + Layout.alignment: Qt.AlignHCenter + text: { + var max = LocationService.data.weather.daily.temperature_2m_max[index]; + var min = LocationService.data.weather.daily.temperature_2m_min[index]; + if (Settings.data.location.useFahrenheit) { + max = LocationService.celsiusToFahrenheit(max); + min = LocationService.celsiusToFahrenheit(min); + } + max = Math.round(max); + min = Math.round(min); + return `${max}°/${min}°`; + } + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + } + } + } + + ColumnLayout { + visible: !weatherReady + Layout.alignment: Qt.AlignCenter + spacing: Style.marginS + + NBusyIndicator { + Layout.alignment: Qt.AlignCenter + visible: LocationService.locationConfigured + } + + NText { + visible: !LocationService.locationConfigured + Layout.alignment: Qt.AlignCenter + text: I18n.tr("common.weather-no-location") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/DesktopWidgets.qml b/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/DesktopWidgets.qml new file mode 100644 index 0000000..e137d1e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/DesktopWidgets.qml @@ -0,0 +1,547 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Modules.Panels.Settings +import qs.Services.Compositor +import qs.Services.Noctalia +import qs.Services.Power +import qs.Services.UI +import qs.Widgets + +Variants { + id: root + model: Quickshell.screens + + // Direct binding to registry's widgets property for reactivity + readonly property var registeredWidgets: DesktopWidgetRegistry.widgets + + // Force reload counter - incremented when plugin widget registry changes + property int pluginReloadCounter: 0 + + Connections { + target: DesktopWidgetRegistry + + function onPluginWidgetRegistryUpdated() { + root.pluginReloadCounter++; + Logger.d("DesktopWidgets", "Plugin widget registry updated, reload counter:", root.pluginReloadCounter); + } + } + + delegate: Loader { + id: screenLoader + required property ShellScreen modelData + + // Reactive property for widgets on this specific screen + // Returns a fresh array whenever Settings changes + property var screenWidgets: { + if (!modelData || !modelData.name) { + return []; + } + var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || []; + for (var i = 0; i < monitorWidgets.length; i++) { + if (monitorWidgets[i].name === modelData.name) { + return monitorWidgets[i].widgets || []; + } + } + return []; + } + + // Only create PanelWindow if enabled AND (screen has widgets OR in edit mode) + // During compositor overview, show widgets only when overviewEnabled is true. + active: modelData && Settings.data.desktopWidgets.enabled && (screenWidgets.length > 0 || DesktopWidgetRegistry.editMode) && (!CompositorService.overviewActive || Settings.data.desktopWidgets.overviewEnabled) && (!PowerProfileService.noctaliaPerformanceMode || !Settings.data.noctaliaPerformance.disableDesktopWidgets) && !PanelService.lockScreen?.active + + sourceComponent: PanelWindow { + id: window + color: "transparent" + screen: screenLoader.modelData + mask: DesktopWidgetRegistry.editMode ? null : widgetsMask + + // Dynamic mask: combine clickable regions for each loaded widget + property var _maskRegions: [] + + Component { + id: maskRegionComponent + Region {} + } + + Region { + id: widgetsMask + regions: window._maskRegions + } + + WlrLayershell.layer: WlrLayer.Bottom + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "noctalia-desktop-widgets-" + (screen?.name || "unknown") + + anchors { + top: true + bottom: true + right: true + left: true + } + + Component.onCompleted: { + Logger.d("DesktopWidgets", "Created panel window for", screen?.name); + } + + // Add a new widget to the current screen + function addWidgetToCurrentScreen(widgetId) { + var monitorName = window.screen.name; + var newWidget = { + "id": widgetId + }; + + // Load default metadata if available + var metadata = DesktopWidgetRegistry.widgetMetadata[widgetId]; + if (metadata) { + Object.keys(metadata).forEach(function (key) { + newWidget[key] = metadata[key]; + }); + } + + // Place at screen center + newWidget.x = (window.screen.width / 2) - 100; + newWidget.y = (window.screen.height / 2) - 100; + newWidget.scale = 1.0; + + // Get current widgets and add new one + var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || []; + var newMonitorWidgets = monitorWidgets.slice(); + var found = false; + + for (var i = 0; i < newMonitorWidgets.length; i++) { + if (newMonitorWidgets[i].name === monitorName) { + var widgets = (newMonitorWidgets[i].widgets || []).slice(); + widgets.push(newWidget); + newMonitorWidgets[i] = { + "name": monitorName, + "widgets": widgets + }; + found = true; + break; + } + } + + if (!found) { + newMonitorWidgets.push({ + "name": monitorName, + "widgets": [newWidget] + }); + } + + Settings.data.desktopWidgets.monitorWidgets = newMonitorWidgets; + Logger.i("DesktopWidgets", "Added widget", widgetId, "to", monitorName); + } + + Item { + id: widgetsContainer + anchors.fill: parent + + // Visual grid overlay - shown when grid snap is enabled in edit mode + // Using Loader to properly unload Canvas when not needed + Loader { + id: gridOverlayLoader + active: DesktopWidgetRegistry.editMode && Settings.data.desktopWidgets.enabled && Settings.data.desktopWidgets.gridSnap + anchors.fill: parent + z: -1 // Behind widgets but above background + asynchronous: false + + sourceComponent: Canvas { + id: gridOverlay + anchors.fill: parent + opacity: 0.3 + + // Grid size calculated based on screen resolution - matches DraggableDesktopWidget + // Ensures grid lines pass through the screen center on both axes + readonly property int gridSize: { + if (!window.screen) + return 30; // Fallback + var baseSize = Math.round(window.screen.width * 0.015); + baseSize = Math.max(20, Math.min(60, baseSize)); + + // Calculate center coordinates + var centerX = window.screen.width / 2; + var centerY = window.screen.height / 2; + + // Find a grid size that divides evenly into both center coordinates + // This ensures a grid line crosses through the center on both axes + var bestSize = baseSize; + var bestDistance = Infinity; + + // Try values around baseSize to find one that divides evenly into both centers + for (var offset = -10; offset <= 10; offset++) { + var candidate = baseSize + offset; + if (candidate < 20 || candidate > 60) + continue; + + // Check if this size divides evenly into both center coordinates + var remainderX = centerX % candidate; + var remainderY = centerY % candidate; + + // If both remainders are 0, this is perfect - center is on grid lines + if (remainderX === 0 && remainderY === 0) { + return candidate; // Perfect match, use it immediately + } + + // Otherwise, find the closest to perfect alignment + var distance = Math.abs(remainderX) + Math.abs(remainderY); + if (distance < bestDistance) { + bestDistance = distance; + bestSize = candidate; + } + } + + // If we found a perfect match, it would have returned already + // Otherwise, try to find a divisor of both centerX and centerY + // that's close to our best size + var gcd = function (a, b) { + while (b !== 0) { + var temp = b; + b = a % b; + a = temp; + } + return a; + }; + + // Find common divisors of centerX and centerY + var centerGcd = gcd(Math.round(centerX), Math.round(centerY)); + if (centerGcd > 0) { + // Find a divisor of centerGcd that's close to bestSize + for (var divisor = Math.floor(centerGcd / 60); divisor <= Math.ceil(centerGcd / 20); divisor++) { + if (centerGcd % divisor !== 0) + continue; + var candidate = centerGcd / divisor; + if (candidate >= 20 && candidate <= 60) { + if (Math.abs(candidate - baseSize) < Math.abs(bestSize - baseSize)) { + bestSize = candidate; + } + } + } + } + + return bestSize; + } + + onPaint: { + const ctx = getContext("2d"); + ctx.reset(); + ctx.strokeStyle = Color.mPrimary; + ctx.lineWidth = 1; + + // Draw vertical lines + for (let x = 0; x <= width; x += gridSize) { + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, height); + ctx.stroke(); + } + + // Draw horizontal lines + for (let y = 0; y <= height; y += gridSize) { + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(width, y); + ctx.stroke(); + } + } + + // Repaint when size changes + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + + Component.onCompleted: { + requestPaint(); + } + + Connections { + target: Settings.data.desktopWidgets + function onGridSnapChanged() { + if (gridOverlayLoader.active) { + gridOverlay.requestPaint(); + } + } + } + + Connections { + target: DesktopWidgetRegistry + function onEditModeChanged() { + if (gridOverlayLoader.active) { + gridOverlay.requestPaint(); + } + } + } + } + } + + // Load widgets dynamically from per-monitor array + Repeater { + model: screenLoader.screenWidgets + + delegate: Loader { + id: widgetLoader + // Bind to registeredWidgets and pluginReloadCounter to re-evaluate when plugins register/unregister + active: (modelData.id in root.registeredWidgets) && (root.pluginReloadCounter >= 0) + + required property var modelData + required property int index + property var _maskRegion: null + readonly property bool _isPlugin: DesktopWidgetRegistry.isPluginWidget(modelData.id) + + // All widgets use setSource() so that screen, widgetData, and + // widgetIndex are set as initial properties, available during + // Component.onCompleted. This prevents registration-key + // mismatches in widgets that build IDs from screen.name. + Component.onCompleted: _loadWidget() + + onActiveChanged: { + if (active) + _loadWidget(); + } + + function _loadWidget() { + var widgetId = modelData.id; + var comp = root.registeredWidgets[widgetId]; + if (!comp) + return; + + var props = { + "screen": window.screen, + "widgetData": modelData, + "widgetIndex": index + }; + + if (_isPlugin) { + var pluginId = widgetId.replace("plugin:", ""); + var api = PluginService.getPluginAPI(pluginId); + if (api) + props.pluginApi = api; + setSource(comp.url, props); + } else { + // Core widgets: use explicit URL (inline Component.url + // returns the registry file, not the widget file) + var url = DesktopWidgetRegistry.widgetUrls[widgetId]; + if (url) + setSource(url, props); + } + } + + onLoaded: { + if (item) { + item.parent = widgetsContainer; + + // Create mask region so this widget receives mouse input + _maskRegion = maskRegionComponent.createObject(window); + _maskRegion.item = item; + var newRegions = window._maskRegions.slice(); + newRegions.push(_maskRegion); + window._maskRegions = newRegions; + } + } + + // Clean up mask region when widget unloads + onItemChanged: { + if (!item && _maskRegion) { + var region = _maskRegion; + _maskRegion = null; + window._maskRegions = window._maskRegions.filter(function (r) { + return r !== region; + }); + region.destroy(); + } + } + } + } + + // Edit mode controls panel + Rectangle { + id: editModeControlsPanel + visible: DesktopWidgetRegistry.editMode && Settings.data.desktopWidgets.enabled + + readonly property string barPos: Settings.getBarPositionForScreen(window.screen?.name) + readonly property bool barFloating: Settings.data.bar.barType === "floating" + readonly property real barHeight: Style.getBarHeightForScreen(window.screen?.name) + + readonly property int barOffsetTop: { + if (barPos !== "top") + return Style.marginM; + const floatMarginV = barFloating ? Math.ceil(Settings.data.bar.marginVertical) : 0; + return barHeight + floatMarginV + Style.marginM; + } + readonly property int barOffsetRight: { + if (barPos !== "right") + return Style.marginM; + const floatMarginH = barFloating ? Math.ceil(Settings.data.bar.marginHorizontal) : 0; + return barHeight + floatMarginH + Style.marginM; + } + + // Internal state for drag tracking (session-only, resets on restart) + QtObject { + id: panelInternal + property bool isDragging: false + property real dragOffsetX: 0 + property real dragOffsetY: 0 + // Default position: top-right corner accounting for bar + property real baseX: widgetsContainer.width - editModeControlsPanel.width - editModeControlsPanel.barOffsetRight + property real baseY: editModeControlsPanel.barOffsetTop + } + + // Reset position when bar position changes + Connections { + target: Settings.data.bar + function onPositionChanged() { + panelInternal.baseX = widgetsContainer.width - editModeControlsPanel.width - editModeControlsPanel.barOffsetRight; + panelInternal.baseY = editModeControlsPanel.barOffsetTop; + } + } + + x: panelInternal.isDragging ? panelInternal.dragOffsetX : panelInternal.baseX + y: panelInternal.isDragging ? panelInternal.dragOffsetY : panelInternal.baseY + + width: controlsLayout.implicitWidth + Style.margin2XL + height: controlsLayout.implicitHeight + Style.margin2XL + + color: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b, 0.85) + radius: Style.radiusL + border { + width: Style.borderS + color: Color.mOutline + } + z: 9999 + + // Drag area for relocating the panel + MouseArea { + id: dragArea + anchors.fill: parent + cursorShape: panelInternal.isDragging ? Qt.ClosedHandCursor : Qt.OpenHandCursor + + property point pressPos: Qt.point(0, 0) + + onPressed: mouse => { + pressPos = mapToItem(widgetsContainer, mouse.x, mouse.y); + panelInternal.dragOffsetX = editModeControlsPanel.x; + panelInternal.dragOffsetY = editModeControlsPanel.y; + panelInternal.isDragging = true; + } + + onPositionChanged: mouse => { + if (panelInternal.isDragging && pressed) { + var currentPos = mapToItem(widgetsContainer, mouse.x, mouse.y); + var deltaX = currentPos.x - pressPos.x; + var deltaY = currentPos.y - pressPos.y; + + var newX = panelInternal.baseX + deltaX; + var newY = panelInternal.baseY + deltaY; + + // Boundary clamping + newX = Math.max(0, Math.min(newX, widgetsContainer.width - editModeControlsPanel.width)); + newY = Math.max(0, Math.min(newY, widgetsContainer.height - editModeControlsPanel.height)); + + panelInternal.dragOffsetX = newX; + panelInternal.dragOffsetY = newY; + } + } + + onReleased: { + if (panelInternal.isDragging) { + panelInternal.baseX = panelInternal.dragOffsetX; + panelInternal.baseY = panelInternal.dragOffsetY; + panelInternal.isDragging = false; + } + } + + onCanceled: { + panelInternal.isDragging = false; + } + } + + ColumnLayout { + id: controlsLayout + anchors { + fill: parent + margins: Style.marginXL + } + spacing: Style.marginL + + RowLayout { + Layout.alignment: Qt.AlignRight + spacing: Style.marginS + + NIconButton { + id: addWidgetButton + icon: "layout-grid-add" + tooltipText: I18n.tr("tooltips.add-widget") + onClicked: { + var popupMenuWindow = PanelService.getPopupMenuWindow(window.screen); + if (popupMenuWindow) { + // Build menu items from registry + var items = []; + var widgets = DesktopWidgetRegistry.widgets; + for (var id in widgets) { + items.push({ + action: id, + text: DesktopWidgetRegistry.getWidgetDisplayName(id), + icon: "layout-grid-add" + }); + } + var globalPos = addWidgetButton.mapToItem(null, 0, addWidgetButton.height + Style.marginS); + popupMenuWindow.showDynamicContextMenu(items, globalPos.x, globalPos.y, function (widgetId) { + addWidgetToCurrentScreen(widgetId); + return false; + }); + } + } + } + + NIconButton { + icon: "grid-3x3" + visible: Settings.data.desktopWidgets.gridSnap + tooltipText: I18n.tr("panels.desktop-widgets.edit-mode-grid-snap-scale-label") + colorBg: Settings.data.desktopWidgets.gridSnapScale ? Color.mPrimary : Color.mSurfaceVariant + colorFg: Settings.data.desktopWidgets.gridSnapScale ? Color.mOnPrimary : Color.mPrimary + onClicked: Settings.data.desktopWidgets.gridSnapScale = !Settings.data.desktopWidgets.gridSnapScale + } + + NIconButton { + icon: "grid-4x4" + tooltipText: I18n.tr("panels.desktop-widgets.edit-mode-grid-snap-label") + colorBg: Settings.data.desktopWidgets.gridSnap ? Color.mPrimary : Color.mSurfaceVariant + colorFg: Settings.data.desktopWidgets.gridSnap ? Color.mOnPrimary : Color.mPrimary + onClicked: Settings.data.desktopWidgets.gridSnap = !Settings.data.desktopWidgets.gridSnap + } + + NIconButton { + icon: "settings" + tooltipText: I18n.tr("actions.open-settings") + onClicked: { + SettingsPanelService.toggle(SettingsPanel.Tab.DesktopWidgets, -1, screenLoader.modelData); + } + } + + NButton { + text: I18n.tr("panels.desktop-widgets.edit-mode-exit-button") + icon: "logout" + outlined: false + fontSize: Style.fontSizeS + iconSize: Style.fontSizeM + onClicked: DesktopWidgetRegistry.editMode = false + } + } + + NText { + Layout.alignment: Qt.AlignRight + Layout.maximumWidth: 300 * Style.uiScaleRatio + text: I18n.tr("panels.desktop-widgets.edit-mode-controls-explanation") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignRight + wrapMode: Text.WordWrap + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/DraggableDesktopWidget.qml b/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/DraggableDesktopWidget.qml new file mode 100644 index 0000000..c3c544c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/DraggableDesktopWidget.qml @@ -0,0 +1,634 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import qs.Commons +import qs.Services.Noctalia +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property ShellScreen screen + property var widgetData: null + property int widgetIndex: -1 + + property real defaultX: 100 + property real defaultY: 100 + + default property alias content: contentContainer.data + + readonly property bool isDragging: internal.isDragging + readonly property bool isScaling: internal.isScaling + + // All Desktop widgets have these settings, but fallback just in case + readonly property var _metadata: widgetData?.id ? DesktopWidgetRegistry.widgetMetadata[widgetData.id] : null + property bool showBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : (_metadata?.showBackground ?? true) + property bool roundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : (_metadata?.roundedCorners ?? true) + + property real widgetScale: 1.0 + property real minScale: 0.5 + property real maxScale: 5.0 + + readonly property real scaleSensitivity: 0.0015 + readonly property real scaleUpdateThreshold: 0.015 + readonly property real cornerScaleSensitivity: 0.0003 // Much lower sensitivity for corner handles + + // Grid size ensures lines pass through screen center on both axes + readonly property int gridSize: { + if (!screen) + return 30; + var baseSize = Math.round(screen.width * 0.015); + baseSize = Math.max(20, Math.min(60, baseSize)); + + var centerX = screen.width / 2; + var centerY = screen.height / 2; + var bestSize = baseSize; + var bestDistance = Infinity; + + for (var offset = -10; offset <= 10; offset++) { + var candidate = baseSize + offset; + if (candidate < 20 || candidate > 60) + continue; + + var remainderX = centerX % candidate; + var remainderY = centerY % candidate; + + if (remainderX === 0 && remainderY === 0) { + return candidate; + } + + var distance = Math.abs(remainderX) + Math.abs(remainderY); + if (distance < bestDistance) { + bestDistance = distance; + bestSize = candidate; + } + } + + var gcd = function (a, b) { + while (b !== 0) { + var temp = b; + b = a % b; + a = temp; + } + return a; + }; + + var centerGcd = gcd(Math.round(centerX), Math.round(centerY)); + if (centerGcd > 0) { + for (var divisor = Math.floor(centerGcd / 60); divisor <= Math.ceil(centerGcd / 20); divisor++) { + if (centerGcd % divisor !== 0) + continue; + var candidate = centerGcd / divisor; + if (candidate >= 20 && candidate <= 60) { + if (Math.abs(candidate - baseSize) < Math.abs(bestSize - baseSize)) { + bestSize = candidate; + } + } + } + } + + return bestSize; + } + + QtObject { + id: internal + property bool isDragging: false + property bool isScaling: false + property real dragOffsetX: 0 + property real dragOffsetY: 0 + property real baseX: (root.widgetData && root.widgetData.x !== undefined) ? root.widgetData.x : root.defaultX + property real baseY: (root.widgetData && root.widgetData.y !== undefined) ? root.widgetData.y : root.defaultY + property real initialWidth: 0 + property real initialHeight: 0 + property point initialMousePos: Qt.point(0, 0) + property real initialScale: 1.0 + property real lastScale: 1.0 + // Locks operation type to prevent switching between drag/scale mid-operation + property string operationType: "" // "drag" or "scale" or "" + } + + function snapToGrid(coord) { + if (!Settings.data.desktopWidgets.gridSnap) { + return coord; + } + return Math.round(coord / root.gridSize) * root.gridSize; + } + + function snapScaleToGrid(scale) { + if (!Settings.data.desktopWidgets.gridSnap || !Settings.data.desktopWidgets.gridSnapScale) { + return scale; + } + + // Get widget's base width + var initialWidth = internal.initialWidth; + var initialScale = internal.initialScale; + if (initialWidth <= 0 || initialScale <= 0) { + return scale; + } + + // Since initialWidth = baseWidth * initialScale + var baseWidth = initialWidth / initialScale; + + // Snap the resulting width with the scale + var resultingWidth = baseWidth * scale; + var snappedWidth = root.snapToGrid(resultingWidth); + + // Check that the snappedWidth isn't smaller than one grid size + if (snappedWidth < root.gridSize) { + snappedWidth = root.gridSize; + } + + // Return the ratio of the snappedWidth and the baseWidth, which is the new snapped scale + var snappedScale = snappedWidth / baseWidth; + return Math.max(minScale, Math.min(maxScale, snappedScale)); + } + + function updateWidgetData(properties) { + if (widgetIndex < 0 || !screen || !screen.name) { + return; + } + DesktopWidgetRegistry.updateWidgetData(screen.name, widgetIndex, properties); + } + + function removeWidget() { + if (widgetIndex < 0 || !screen || !screen.name) { + return; + } + + var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || []; + var newMonitorWidgets = monitorWidgets.slice(); + + for (var i = 0; i < newMonitorWidgets.length; i++) { + if (newMonitorWidgets[i].name === screen.name) { + var widgets = (newMonitorWidgets[i].widgets || []).slice(); + if (widgetIndex >= 0 && widgetIndex < widgets.length) { + widgets.splice(widgetIndex, 1); + newMonitorWidgets[i] = Object.assign({}, newMonitorWidgets[i], { + "widgets": widgets + }); + Settings.data.desktopWidgets.monitorWidgets = newMonitorWidgets; + } + break; + } + } + } + + function raiseToTop() { + if (widgetIndex < 0 || !screen || !screen.name) { + return; + } + + var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || []; + var newMonitorWidgets = monitorWidgets.slice(); + + for (var i = 0; i < newMonitorWidgets.length; i++) { + if (newMonitorWidgets[i].name === screen.name) { + var widgets = (newMonitorWidgets[i].widgets || []).slice(); + if (widgetIndex < widgets.length && widgetIndex < widgets.length - 1) { + var widget = widgets.splice(widgetIndex, 1)[0]; + widgets.push(widget); + newMonitorWidgets[i] = Object.assign({}, newMonitorWidgets[i], { + "widgets": widgets + }); + Settings.data.desktopWidgets.monitorWidgets = newMonitorWidgets; + } + break; + } + } + } + + function lowerToBottom() { + if (widgetIndex < 0 || !screen || !screen.name) { + return; + } + + var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || []; + var newMonitorWidgets = monitorWidgets.slice(); + + for (var i = 0; i < newMonitorWidgets.length; i++) { + if (newMonitorWidgets[i].name === screen.name) { + var widgets = (newMonitorWidgets[i].widgets || []).slice(); + if (widgetIndex < widgets.length && widgetIndex > 0) { + var widget = widgets.splice(widgetIndex, 1)[0]; + widgets.unshift(widget); + newMonitorWidgets[i] = Object.assign({}, newMonitorWidgets[i], { + "widgets": widgets + }); + Settings.data.desktopWidgets.monitorWidgets = newMonitorWidgets; + } + break; + } + } + } + + function openWidgetSettings() { + DesktopWidgetRegistry.openWidgetSettings(screen, widgetIndex, widgetData.id, widgetData); + } + + function handleContextMenuAction(action) { + if (action === "widget-settings") { + // Don't close - openWidgetSettings will use the popup window for the dialog + root.openWidgetSettings(); + return true; // Signal that we're handling close ourselves + } else if (action === "reset") { + // Reset scale and position to defaults + root.widgetScale = 1.0; + internal.baseX = root.defaultX; + internal.baseY = root.defaultY; + root.updateWidgetData({ + "scale": 1.0, + "x": Math.round(root.defaultX), + "y": Math.round(root.defaultY) + }); + return false; + } else if (action === "raise-to-top") { + root.raiseToTop(); + return false; + } else if (action === "lower-to-bottom") { + root.lowerToBottom(); + return false; + } else if (action === "delete") { + root.removeWidget(); + return false; // Let caller close the popup + } + return false; + } + + x: Math.round(internal.isDragging ? internal.dragOffsetX : internal.baseX) + y: Math.round(internal.isDragging ? internal.dragOffsetY : internal.baseY) + + // Note: We no longer use transform-based scaling (scale property) + // Instead, child widgets multiply their dimensions by widgetScale + // This prevents blurry text at fractional scale values + + Component.onCompleted: { + // Initialize scale from widgetData when component is first created + if (widgetData && widgetData.scale !== undefined) { + widgetScale = widgetData.scale; + } + } + + onWidgetDataChanged: { + if (!internal.isDragging && !internal.isScaling) { + internal.baseX = (widgetData && widgetData.x !== undefined) ? widgetData.x : defaultX; + internal.baseY = (widgetData && widgetData.y !== undefined) ? widgetData.y : defaultY; + if (widgetData && widgetData.scale !== undefined) { + widgetScale = widgetData.scale; + } else if (widgetData) { + // If widgetData exists but scale is not set, default to 1.0 + widgetScale = 1.0; + } + } + } + + Rectangle { + id: decorationRect + anchors.fill: parent + anchors.margins: -outlineMargin + color: DesktopWidgetRegistry.editMode ? Qt.rgba(Color.mPrimary.r, Color.mPrimary.g, Color.mPrimary.b, 0.1) : "transparent" + border.color: (DesktopWidgetRegistry.editMode || internal.isDragging) ? (internal.isDragging ? Color.mOutline : Color.mPrimary) : "transparent" + border.width: DesktopWidgetRegistry.editMode ? 3 : 0 + radius: Math.min(Math.round(Style.radiusL * root.widgetScale), Style.radiusL, width / 2, height / 2) + z: -1 + } + + Rectangle { + id: container + anchors.fill: parent + radius: root.roundedCorners ? Math.min(Math.round(Style.radiusL * root.widgetScale), Style.radiusL, width / 2, height / 2) : 0 + color: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity) + border { + width: 1 + color: Qt.alpha(Color.mOutline, 0.12) + } + clip: true + visible: root.showBackground + + layer.enabled: Settings.data.general.enableShadows && !internal.isDragging && root.showBackground + layer.effect: MultiEffect { + shadowEnabled: true + shadowBlur: Style.shadowBlur * 1.5 + shadowOpacity: Style.shadowOpacity * 0.6 + shadowColor: "black" + shadowHorizontalOffset: Settings.data.general.shadowOffsetX + shadowVerticalOffset: Settings.data.general.shadowOffsetY + blurMax: Style.shadowBlurMax + } + } + + Item { + id: contentContainer + anchors.fill: parent + z: 1 + clip: true + } + + // Context menu model and handler - menu is created dynamically in PopupMenuWindow + property var contextMenuModel: { + var hasSettings = false; + if (widgetData && widgetData.id) { + var widgetId = widgetData.id; + if (DesktopWidgetRegistry.isPluginWidget(widgetId)) { + var pluginId = widgetId.replace("plugin:", ""); + var manifest = PluginRegistry.getPluginManifest(pluginId); + hasSettings = manifest && manifest.entryPoints && (manifest.entryPoints.settings || manifest.entryPoints.desktopWidgetSettings); + } else { + hasSettings = DesktopWidgetRegistry.widgetSettingsMap[widgetId] !== undefined; + } + } + + var items = []; + if (hasSettings) { + items.push({ + "label": I18n.tr("actions.widget-settings"), + "action": "widget-settings", + "icon": "settings" + }); + } + items.push({ + "label": I18n.tr("common.reset"), + "action": "reset", + "icon": "restore" + }); + items.push({ + "label": I18n.tr("actions.raise-to-top"), + "action": "raise-to-top", + "icon": "stack-front" + }); + items.push({ + "label": I18n.tr("actions.lower-to-bottom"), + "action": "lower-to-bottom", + "icon": "stack-back" + }); + items.push({ + "label": I18n.tr("common.delete"), + "action": "delete", + "icon": "trash" + }); + return items; + } + + // Drag MouseArea - handles dragging (left-click) + MouseArea { + id: dragArea + anchors.fill: parent + z: 1000 + visible: DesktopWidgetRegistry.editMode + cursorShape: internal.isDragging ? Qt.ClosedHandCursor : Qt.OpenHandCursor + hoverEnabled: true + acceptedButtons: Qt.LeftButton + + property point pressPos: Qt.point(0, 0) + + onPressed: mouse => { + // Prevent starting new operation if one is already in progress + if (internal.operationType !== "") { + return; + } + + pressPos = Qt.point(mouse.x, mouse.y); + internal.operationType = "drag"; + internal.dragOffsetX = root.x; + internal.dragOffsetY = root.y; + internal.isDragging = true; + } + + onPositionChanged: mouse => { + if (internal.isDragging && pressed && internal.operationType === "drag") { + var globalPressPos = mapToItem(root.parent, pressPos.x, pressPos.y); + var globalCurrentPos = mapToItem(root.parent, mouse.x, mouse.y); + + var deltaX = globalCurrentPos.x - globalPressPos.x; + var deltaY = globalCurrentPos.y - globalPressPos.y; + + var newX = internal.dragOffsetX + deltaX; + var newY = internal.dragOffsetY + deltaY; + + // Boundary clamping - allow widgets to go partially off-screen (75% can clip) + // This gives users more control over positioning while keeping widgets accessible + var scaledWidth = root.width; + var scaledHeight = root.height; + if (root.parent && scaledWidth > 0 && scaledHeight > 0) { + var minVisibleX = scaledWidth * 0.25; + var minVisibleY = scaledHeight * 0.25; + newX = Math.max(-scaledWidth + minVisibleX, Math.min(newX, root.parent.width - minVisibleX)); + newY = Math.max(-scaledHeight + minVisibleY, Math.min(newY, root.parent.height - minVisibleY)); + } + + if (Settings.data.desktopWidgets.gridSnap) { + newX = root.snapToGrid(newX); + newY = root.snapToGrid(newY); + // Re-clamp after snapping + if (root.parent && scaledWidth > 0 && scaledHeight > 0) { + var minVisibleX = scaledWidth * 0.25; + var minVisibleY = scaledHeight * 0.25; + newX = Math.max(-scaledWidth + minVisibleX, Math.min(newX, root.parent.width - minVisibleX)); + newY = Math.max(-scaledHeight + minVisibleY, Math.min(newY, root.parent.height - minVisibleY)); + } + } + + internal.dragOffsetX = newX; + internal.dragOffsetY = newY; + } + } + + onReleased: mouse => { + if (internal.isDragging && internal.operationType === "drag" && widgetIndex >= 0 && screen && screen.name) { + var roundedX = Math.round(internal.dragOffsetX); + var roundedY = Math.round(internal.dragOffsetY); + root.updateWidgetData({ + "x": roundedX, + "y": roundedY + }); + + internal.baseX = roundedX; + internal.baseY = roundedY; + internal.isDragging = false; + internal.operationType = ""; + } + } + + onCanceled: { + internal.isDragging = false; + internal.operationType = ""; + } + } + + // Right-click MouseArea for context menu + MouseArea { + id: contextMenuArea + anchors.fill: parent + z: 1001 + visible: DesktopWidgetRegistry.editMode + acceptedButtons: Qt.RightButton + hoverEnabled: true + + onPressed: mouse => { + if (mouse.button === Qt.RightButton) { + var popupMenuWindow = PanelService.getPopupMenuWindow(root.screen); + if (popupMenuWindow) { + // Map click position to screen coordinates + var globalPos = root.mapToItem(null, mouse.x, mouse.y); + // Use dynamic context menu (created in PopupMenuWindow's Top layer) + // This ensures input events work correctly for desktop widgets (Bottom layer) + popupMenuWindow.showDynamicContextMenu(root.contextMenuModel, globalPos.x, globalPos.y, root.handleContextMenuAction); + } + } + } + } + + // Corner handles for scaling - using Repeater to avoid code duplication + readonly property real cornerHandleSize: 8 * widgetScale + readonly property real outlineMargin: Style.marginS * widgetScale + readonly property color colorHandle: Color.mSecondary + + // Corner handle model: defines position, direction, cursor, and triangle points for each corner + // xMult/yMult: multipliers for position (0 = left/top edge, 1 = right/bottom edge) + // xDir/yDir: direction multiplier for scaling (-1 = left/up increases, 1 = right/down increases) + // cursor: resize cursor type (FDiag for TL-BR diagonal, BDiag for TR-BL diagonal) + // points: triangle vertices as [x, y] pairs normalized to cornerHandleSize + readonly property var cornerHandleModel: [ + { + xMult: 0, + yMult: 0, + xDir: -1, + yDir: -1, + cursor: Qt.SizeFDiagCursor, + points: [[0, 0], [1, 0], [0, 1]] + }, + { + xMult: 1, + yMult: 0, + xDir: 1, + yDir: -1, + cursor: Qt.SizeBDiagCursor, + points: [[1, 0], [1, 1], [0, 0]] + }, + { + xMult: 0, + yMult: 1, + xDir: -1, + yDir: 1, + cursor: Qt.SizeBDiagCursor, + points: [[0, 1], [0, 0], [1, 1]] + }, + { + xMult: 1, + yMult: 1, + xDir: 1, + yDir: 1, + cursor: Qt.SizeFDiagCursor, + points: [[1, 1], [1, 0], [0, 1]] + } + ] + + Repeater { + model: root.cornerHandleModel + + delegate: Canvas { + id: cornerHandle + required property var modelData + required property int index + + visible: DesktopWidgetRegistry.editMode && !internal.isDragging + // Position handles at corners of decoration rectangle (which extends by outlineMargin) + x: modelData.xMult === 0 ? -outlineMargin : (root.width + outlineMargin - cornerHandleSize) + y: modelData.yMult === 0 ? -outlineMargin : (root.height + outlineMargin - cornerHandleSize) + width: cornerHandleSize + height: cornerHandleSize + z: 2000 + + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + ctx.fillStyle = colorHandle; + ctx.beginPath(); + ctx.moveTo(modelData.points[0][0] * cornerHandleSize, modelData.points[0][1] * cornerHandleSize); + ctx.lineTo(modelData.points[1][0] * cornerHandleSize, modelData.points[1][1] * cornerHandleSize); + ctx.lineTo(modelData.points[2][0] * cornerHandleSize, modelData.points[2][1] * cornerHandleSize); + ctx.closePath(); + ctx.fill(); + } + + Component.onCompleted: requestPaint() + onVisibleChanged: if (visible) + requestPaint() + + Connections { + target: root + function onWidthChanged() { + if (cornerHandle.visible) + cornerHandle.requestPaint(); + } + function onHeightChanged() { + if (cornerHandle.visible) + cornerHandle.requestPaint(); + } + } + + MouseArea { + id: scaleMouseArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton + cursorShape: cornerHandle.modelData.cursor + property point pressPos: Qt.point(0, 0) + + onPressed: mouse => { + if (internal.operationType !== "") { + return; + } + pressPos = mapToItem(root.parent, mouse.x, mouse.y); + internal.operationType = "scale"; + internal.isScaling = true; + internal.initialScale = root.widgetScale; + internal.lastScale = root.widgetScale; + internal.initialWidth = root.width; + internal.initialHeight = root.height; + } + + onPositionChanged: mouse => { + if (internal.isScaling && pressed && internal.operationType === "scale") { + var currentPos = mapToItem(root.parent, mouse.x, mouse.y); + var deltaX = currentPos.x - pressPos.x; + var deltaY = currentPos.y - pressPos.y; + + // Project delta onto the diagonal direction for this corner + // xDir/yDir indicate which direction increases scale + var diagonalDelta = (deltaX * cornerHandle.modelData.xDir + deltaY * cornerHandle.modelData.yDir) / Math.sqrt(2); + + // Scale sensitivity: pixels of drag per 1.0 scale change + var sensitivity = 150; + var scaleDelta = diagonalDelta / sensitivity; + var newScale = Math.max(root.minScale, Math.min(root.maxScale, internal.initialScale + scaleDelta)); + + newScale = root.snapScaleToGrid(newScale); + + if (!isNaN(newScale) && newScale > 0) { + root.widgetScale = newScale; + internal.lastScale = newScale; + } + } + } + + onReleased: mouse => { + if (internal.isScaling && internal.operationType === "scale") { + root.updateWidgetData({ + "scale": root.widgetScale + }); + internal.isScaling = false; + internal.operationType = ""; + root.widgetScale = root.snapScaleToGrid(root.widgetScale); + internal.lastScale = root.widgetScale; + } + } + + onCanceled: { + internal.isScaling = false; + internal.operationType = ""; + internal.lastScale = root.widgetScale; + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/Widgets/DesktopAudioVisualizer.qml b/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/Widgets/DesktopAudioVisualizer.qml new file mode 100644 index 0000000..a462df8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/Widgets/DesktopAudioVisualizer.qml @@ -0,0 +1,119 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import qs.Commons +import qs.Modules.DesktopWidgets +import qs.Services.Media +import qs.Services.UI +import qs.Widgets +import qs.Widgets.AudioSpectrum + +DraggableDesktopWidget { + id: root + + defaultY: 280 + + readonly property var widgetMetadata: DesktopWidgetRegistry.widgetMetadata["AudioVisualizer"] + + readonly property int visualizerWidth: (widgetData && widgetData.width !== undefined) ? widgetData.width : (widgetMetadata?.width ?? 320) + readonly property int visualizerHeight: (widgetData && widgetData.height !== undefined) ? widgetData.height : (widgetMetadata?.height ?? 72) + readonly property string visualizerType: (widgetData && widgetData.visualizerType !== undefined) ? widgetData.visualizerType : (widgetMetadata?.visualizerType ?? "linear") + readonly property bool hideWhenIdle: (widgetData && widgetData.hideWhenIdle !== undefined) ? widgetData.hideWhenIdle : (widgetMetadata?.hideWhenIdle ?? false) + readonly property string colorName: (widgetData && widgetData.colorName !== undefined) ? widgetData.colorName : (widgetMetadata?.colorName ?? "primary") + + readonly property color fillColor: Color.resolveColorKey(colorName) + + readonly property bool shouldShow: visualizerType !== "" && visualizerType !== "none" && (!hideWhenIdle || MediaService.isPlaying) + readonly property bool isHidden: !shouldShow + readonly property bool shouldRegisterSpectrum: shouldShow + + // Keep widget visible in edit mode so users can move/configure it + visible: !root.isHidden || DesktopWidgetRegistry.editMode + + readonly property string spectrumComponentId: "desktop:audiovisualizer:" + (root.screen ? root.screen.name : "unknown") + ":" + root.widgetIndex + + onShouldRegisterSpectrumChanged: { + if (root.shouldRegisterSpectrum) { + SpectrumService.registerComponent(root.spectrumComponentId); + } else { + SpectrumService.unregisterComponent(root.spectrumComponentId); + } + } + + Component.onCompleted: { + if (root.shouldRegisterSpectrum) { + SpectrumService.registerComponent(root.spectrumComponentId); + } + } + + Component.onDestruction: { + SpectrumService.unregisterComponent(root.spectrumComponentId); + } + + implicitWidth: Math.round(visualizerWidth * widgetScale) + implicitHeight: Math.round(visualizerHeight * widgetScale) + width: implicitWidth + height: implicitHeight + + Rectangle { + id: visualizerMask + anchors.fill: parent + color: "transparent" + radius: root.roundedCorners ? Math.min(Math.round(Style.radiusL * root.widgetScale), Style.radiusL, width / 2, height / 2) : 0 + clip: true + + Loader { + id: visualizerLoader + anchors.fill: parent + anchors.margins: root.showBackground ? Math.round(Style.marginXS * root.widgetScale) : 0 + active: root.shouldShow + asynchronous: true + + sourceComponent: { + switch (root.visualizerType) { + case "linear": + return linearComponent; + case "mirrored": + return mirroredComponent; + case "wave": + return waveComponent; + default: + return null; + } + } + } + } + + Component { + id: linearComponent + NLinearSpectrum { + anchors.fill: parent + values: SpectrumService.values + fillColor: root.fillColor + showMinimumSignal: true + mirrored: Settings.data.audio.spectrumMirrored + } + } + + Component { + id: mirroredComponent + NMirroredSpectrum { + anchors.fill: parent + values: SpectrumService.values + fillColor: root.fillColor + showMinimumSignal: true + mirrored: Settings.data.audio.spectrumMirrored + } + } + + Component { + id: waveComponent + NWaveSpectrum { + anchors.fill: parent + values: SpectrumService.values + fillColor: root.fillColor + showMinimumSignal: true + mirrored: Settings.data.audio.spectrumMirrored + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/Widgets/DesktopClock.qml b/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/Widgets/DesktopClock.qml new file mode 100644 index 0000000..e72d4f0 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/Widgets/DesktopClock.qml @@ -0,0 +1,81 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.DesktopWidgets +import qs.Services.UI +import qs.Widgets + +DraggableDesktopWidget { + id: root + + readonly property var now: Time.now + readonly property var widgetMetadata: DesktopWidgetRegistry.widgetMetadata["Clock"] + + readonly property color clockTextColor: Color.resolveColorKey(clockColor) + readonly property real fontSize: Math.round(Style.fontSizeXXXL * 2.5 * widgetScale) + readonly property real widgetOpacity: widgetData.opacity !== undefined ? widgetData.opacity : 1.0 + readonly property string clockStyle: widgetData.clockStyle !== undefined ? widgetData.clockStyle : widgetMetadata.clockStyle + readonly property string clockColor: widgetData.clockColor !== undefined ? widgetData.clockColor : widgetMetadata.clockColor + readonly property bool useCustomFont: widgetData.useCustomFont !== undefined ? widgetData.useCustomFont : widgetMetadata.useCustomFont + readonly property string customFont: widgetData.customFont !== undefined ? widgetData.customFont : widgetMetadata.customFont + readonly property string format: widgetData.format !== undefined ? widgetData.format : widgetMetadata.format + + readonly property real contentPadding: Math.round((clockStyle === "minimal" ? Style.marginL : Style.marginXL) * widgetScale) + implicitWidth: contentLoader.item ? Math.round((contentLoader.item.implicitWidth || contentLoader.item.width || 0) + contentPadding * 2) : 0 + implicitHeight: contentLoader.item ? Math.round((contentLoader.item.implicitHeight || contentLoader.item.height || 0) + contentPadding * 2) : 0 + width: implicitWidth + height: implicitHeight + + Component { + id: nclockComponent + NClock { + now: root.now + clockStyle: root.clockStyle + backgroundColor: "transparent" + clockColor: clockTextColor + progressColor: Color.mPrimary + opacity: root.widgetOpacity + height: Math.round(fontSize * 1.9) + width: height + hoursFontSize: fontSize * 0.6 + minutesFontSize: fontSize * 0.4 + scaleRatio: root.widgetScale + } + } + + Component { + id: minimalClockComponent + ColumnLayout { + spacing: -2 + opacity: root.widgetOpacity + + Repeater { + model: I18n.locale.toString(root.now, root.format.trim()).split("\\n") + delegate: NText { + visible: text !== "" + text: modelData + family: root.useCustomFont && root.customFont ? root.customFont : Settings.data.ui.fontDefault + pointSize: { + if (model.length == 1) { + return Math.round(Style.fontSizeXXL * root.widgetScale); + } else { + return Math.round((index == 0) ? Style.fontSizeXXL * root.widgetScale : Style.fontSizeM * root.widgetScale); + } + } + font.weight: Style.fontWeightBold + color: root.clockTextColor + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter + } + } + } + } + + Loader { + id: contentLoader + anchors.centerIn: parent + z: 2 + sourceComponent: clockStyle === "minimal" ? minimalClockComponent : nclockComponent + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/Widgets/DesktopMediaPlayer.qml b/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/Widgets/DesktopMediaPlayer.qml new file mode 100644 index 0000000..52b5cbb --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/Widgets/DesktopMediaPlayer.qml @@ -0,0 +1,311 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.DesktopWidgets +import qs.Services.Media +import qs.Services.UI +import qs.Widgets +import qs.Widgets.AudioSpectrum + +DraggableDesktopWidget { + id: root + + defaultY: 200 + + // Widget settings - check widgetData exists before accessing properties + readonly property string hideMode: (widgetData && widgetData.hideMode !== undefined) ? widgetData.hideMode : "visible" + readonly property bool showButtons: (widgetData && widgetData.showButtons !== undefined) ? widgetData.showButtons : true + readonly property bool showAlbumArt: (widgetData && widgetData.showAlbumArt !== undefined) ? widgetData.showAlbumArt : true + readonly property bool showVisualizer: (widgetData && widgetData.showVisualizer !== undefined) ? widgetData.showVisualizer : true + readonly property string visualizerType: (widgetData && widgetData.visualizerType && widgetData.visualizerType !== "") ? widgetData.visualizerType : "linear" + readonly property bool roundedCorners: (widgetData && widgetData.roundedCorners !== undefined) ? widgetData.roundedCorners : true + readonly property bool hasPlayer: MediaService.currentPlayer !== null + readonly property bool isPlaying: MediaService.isPlaying + readonly property bool hasActiveTrack: hasPlayer && (MediaService.trackTitle || MediaService.trackArtist) + + // State + // Hide when idle when playback is not active + readonly property bool shouldHideIdle: (hideMode === "idle") && !isPlaying + readonly property bool shouldHideEmpty: !hasPlayer && hideMode === "hidden" + readonly property bool isHidden: (shouldHideIdle || shouldHideEmpty) && !DesktopWidgetRegistry.editMode + visible: !isHidden + + // SpectrumService registration for visualizer + readonly property string spectrumComponentId: "desktopmediaplayer:" + (root.screen ? root.screen.name : "unknown") + readonly property bool needsSpectrum: root.shouldShowVisualizer && !root.isHidden + + onNeedsSpectrumChanged: { + if (root.needsSpectrum) { + SpectrumService.registerComponent(root.spectrumComponentId); + } else { + SpectrumService.unregisterComponent(root.spectrumComponentId); + } + } + + Component.onCompleted: { + if (root.needsSpectrum) { + SpectrumService.registerComponent(root.spectrumComponentId); + } + } + + Component.onDestruction: { + SpectrumService.unregisterComponent(root.spectrumComponentId); + } + + readonly property bool showPrev: hasPlayer && MediaService.canGoPrevious + readonly property bool showNext: hasPlayer && MediaService.canGoNext + readonly property int visibleButtonCount: root.showButtons ? (1 + (showPrev ? 1 : 0) + (showNext ? 1 : 0)) : 0 + + implicitWidth: Math.round(400 * widgetScale) + implicitHeight: Math.round(64 * widgetScale + Style.margin2M * widgetScale) + width: implicitWidth + height: implicitHeight + + // Visualizer visibility mode + readonly property bool shouldShowVisualizer: { + if (!root.showVisualizer) + return false; + if (root.visualizerType === "" || root.visualizerType === "none") + return false; + return true; + } + + // Visualizer overlay (visibility controlled by visualizerVisibility setting) + // Completely disabled during scaling to avoid expensive canvas redraws + Loader { + anchors.fill: parent + anchors.leftMargin: Math.round(Style.marginXS * widgetScale) + anchors.rightMargin: Math.round(Style.marginXS * widgetScale) + anchors.topMargin: Math.round(Style.marginXS * widgetScale) + anchors.bottomMargin: 0 + z: 0 + clip: true + active: needsSpectrum + layer.enabled: true + layer.smooth: true + layer.effect: MultiEffect { + maskEnabled: root.roundedCorners + maskSource: ShaderEffectSource { + sourceItem: Rectangle { + width: root.width - Math.round(Style.marginXS * widgetScale) * 2 + height: root.height - Math.round(Style.marginXS * widgetScale) + radius: root.roundedCorners ? Math.round(Math.max(0, (Style.radiusL - Style.marginXS) * widgetScale)) : 0 + color: "white" + } + } + } + + sourceComponent: { + switch (root.visualizerType) { + case "linear": + return linearComponent; + case "mirrored": + return mirroredComponent; + case "wave": + return waveComponent; + default: + return null; + } + } + + Component { + id: linearComponent + NLinearSpectrum { + anchors.fill: parent + values: SpectrumService.values + fillColor: Color.mPrimary + opacity: 0.5 + mirrored: Settings.data.audio.spectrumMirrored + } + } + + Component { + id: mirroredComponent + NMirroredSpectrum { + anchors.fill: parent + values: SpectrumService.values + fillColor: Color.mPrimary + opacity: 0.5 + mirrored: Settings.data.audio.spectrumMirrored + } + } + + Component { + id: waveComponent + NWaveSpectrum { + anchors.fill: parent + values: SpectrumService.values + fillColor: Color.mPrimary + opacity: 0.5 + mirrored: Settings.data.audio.spectrumMirrored + } + } + } + + // Drop shadow for text and controls readability over visualizer + // Disabled during scaling to avoid expensive recomputations + NDropShadow { + visible: !root.isScaling + anchors.fill: contentLayout + source: contentLayout + z: 1 + autoPaddingEnabled: true + shadowBlur: 1.0 + shadowOpacity: 0.9 + shadowHorizontalOffset: 0 + shadowVerticalOffset: 0 + } + + RowLayout { + id: contentLayout + states: [ + State { + when: root.showButtons + AnchorChanges { + target: contentLayout + anchors.horizontalCenter: undefined + anchors.verticalCenter: undefined + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + } + }, + State { + when: !root.showButtons + AnchorChanges { + target: contentLayout + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + anchors.top: undefined + anchors.bottom: undefined + anchors.left: undefined + anchors.right: undefined + } + } + ] + anchors.margins: Math.round(Style.marginM * widgetScale) + spacing: Math.round(Style.marginS * widgetScale) + z: 2 + + Item { + visible: root.showAlbumArt + Layout.preferredWidth: Math.round(48 * widgetScale) + Layout.preferredHeight: Math.round(48 * widgetScale) + Layout.alignment: Qt.AlignVCenter + + NImageRounded { + visible: hasPlayer + anchors.fill: parent + radius: Math.round(Style.radiusM * widgetScale) + imagePath: MediaService.trackArtUrl + imageFillMode: Image.PreserveAspectCrop + fallbackIcon: isPlaying ? "media-pause" : "media-play" + fallbackIconSize: Math.round(20 * widgetScale) + borderWidth: 0 + } + + NIcon { + visible: !hasPlayer + anchors.centerIn: parent + icon: "disc" + pointSize: Math.round(24 * widgetScale) + color: Color.mOnSurfaceVariant + } + } + + ColumnLayout { + visible: root.showAlbumArt + Layout.fillWidth: true + Layout.alignment: root.showButtons ? Qt.AlignVCenter : Qt.AlignCenter + spacing: 0 + + NText { + Layout.fillWidth: true + text: hasPlayer ? (MediaService.trackTitle || "Unknown Track") : "No media playing" + pointSize: Math.round(Style.fontSizeS * widgetScale) + font.weight: Style.fontWeightSemiBold + color: Color.mOnSurface + elide: Text.ElideRight + maximumLineCount: 1 + } + + NText { + visible: hasPlayer && MediaService.trackArtist + Layout.fillWidth: true + text: MediaService.trackArtist || "" + pointSize: Math.round(Style.fontSizeXS * widgetScale) + font.weight: Style.fontWeightRegular + color: Color.mSecondary + elide: Text.ElideRight + maximumLineCount: 1 + } + } + + RowLayout { + id: controlsRow + spacing: Math.round(Style.marginXS * widgetScale) + z: 10 + visible: root.showButtons + Layout.alignment: root.showAlbumArt ? Qt.AlignVCenter : Qt.AlignCenter + + NIconButton { + opacity: showPrev ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.InOutQuad + } + } + baseSize: Math.round(32 * widgetScale) + icon: "media-prev" + enabled: hasPlayer && MediaService.canGoPrevious + colorBg: Color.mSurfaceVariant + colorFg: enabled ? Color.mPrimary : Color.mOnSurfaceVariant + customRadius: Math.round(Style.radiusS * widgetScale) + onClicked: { + if (enabled) + MediaService.previous(); + } + } + + NIconButton { + baseSize: Math.round(36 * widgetScale) + icon: isPlaying ? "media-pause" : "media-play" + enabled: hasPlayer && (MediaService.canPlay || MediaService.canPause) + colorBg: Color.mPrimary + colorFg: Color.mOnPrimary + colorBgHover: Qt.lighter(Color.mPrimary, 1.1) + colorFgHover: Color.mOnPrimary + customRadius: Math.round(Style.radiusS * widgetScale) + onClicked: { + if (enabled) { + MediaService.playPause(); + } + } + } + + NIconButton { + opacity: showNext ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: Style.animationSlow + easing.type: Easing.InOutQuad + } + } + baseSize: Math.round(32 * widgetScale) + icon: "media-next" + enabled: hasPlayer && MediaService.canGoNext + colorBg: Color.mSurfaceVariant + colorFg: enabled ? Color.mPrimary : Color.mOnSurfaceVariant + customRadius: Math.round(Style.radiusS * widgetScale) + onClicked: { + if (enabled) + MediaService.next(); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/Widgets/DesktopSystemStat.qml b/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/Widgets/DesktopSystemStat.qml new file mode 100644 index 0000000..5d14aaf --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/Widgets/DesktopSystemStat.qml @@ -0,0 +1,305 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.DesktopWidgets +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +DraggableDesktopWidget { + id: root + + // Widget settings + readonly property var widgetMetadata: DesktopWidgetRegistry.widgetMetadata["SystemStat"] + readonly property string statType: (widgetData && widgetData.statType !== undefined) ? widgetData.statType : (widgetMetadata.statType !== undefined ? widgetMetadata.statType : "CPU") + readonly property string diskPath: (widgetData && widgetData.diskPath !== undefined) ? widgetData.diskPath : "/" + readonly property string layout: (widgetData && widgetData.layout !== undefined) ? widgetData.layout : (widgetMetadata.layout !== undefined ? widgetMetadata.layout : "side") + + // Fixed colors + readonly property color color: Color.mPrimary + readonly property color color2: Color.mSecondary + + // Legend items model - each item has: text, color, icon (optional), bold (optional), opacity (optional), elide (optional) + readonly property var legendItems: { + switch (root.statType) { + case "CPU": + return [ + { + icon: "cpu-usage", + text: Math.round(SystemStatService.cpuUsage) + "%", + color: root.color + }, + { + text: "cpu-usage", + text: SystemStatService.cpuFreq, + color: root.color, + opacity: 0.8 + }, + { + icon: "cpu-temperature", + text: SystemStatService.cpuTemp + "°C", + color: root.color2 + } + ]; + case "GPU": + return [ + { + icon: "gpu-temperature", + text: Math.round(SystemStatService.gpuTemp) + "°C", + color: root.color + } + ]; + case "Memory": + return [ + { + icon: "memory", + text: Math.round(SystemStatService.memPercent) + "%", + color: root.color + } + ]; + case "Disk": + var items = [ + { + icon: "storage", + text: Math.round(SystemStatService.diskPercents[root.diskPath] || 0) + "%", + color: root.color + } + ]; + if (root.diskPath !== "/") { + items.push({ + text: root.diskPath, + color: root.color, + opacity: 0.8, + elide: true + }); + } + return items; + case "Network": + return [ + { + icon: "download-speed", + text: SystemStatService.formatSpeed(SystemStatService.rxSpeed), + color: root.color + }, + { + icon: "upload-speed", + text: SystemStatService.formatSpeed(SystemStatService.txSpeed), + color: root.color2 + } + ]; + default: + return []; + } + } + + // History from service + readonly property var history: { + switch (root.statType) { + case "CPU": + return SystemStatService.cpuHistory; + case "GPU": + return SystemStatService.gpuTempHistory; + case "Memory": + return SystemStatService.memHistory; + case "Disk": + return SystemStatService.diskHistories[root.diskPath] || []; + case "Network": + return SystemStatService.rxSpeedHistory; + default: + return []; + } + } + + // Secondary history (CPU temp for CPU, Tx for Network) + readonly property var history2: { + switch (root.statType) { + case "CPU": + return SystemStatService.cpuTempHistory; + case "Network": + return SystemStatService.txSpeedHistory; + default: + return []; + } + } + + // Graph min/max values + readonly property real graphMinValue: root.statType === "GPU" ? Math.max(SystemStatService.gpuTempHistoryMin - 5, 0) : 0 + readonly property real graphMaxValue: { + switch (root.statType) { + case "CPU": + case "Memory": + case "Disk": + return 100; // Percentage-based stats use fixed 0-100 range + case "GPU": + return Math.max(SystemStatService.gpuTempHistoryMax + 5, 1); + case "Network": + return SystemStatService.rxMaxSpeed; + default: + return 100; + } + } + readonly property real graphMinValue2: { + switch (root.statType) { + case "CPU": + return Math.max(SystemStatService.cpuTempHistoryMin - 5, 0); + default: + return graphMinValue; + } + } + readonly property real graphMaxValue2: { + switch (root.statType) { + case "CPU": + return Math.max(SystemStatService.cpuTempHistoryMax + 5, 1); + case "Network": + return SystemStatService.txMaxSpeed; + default: + return graphMaxValue; + } + } + + Component.onCompleted: SystemStatService.registerComponent("desktop-sysstat:" + root.statType) + Component.onDestruction: SystemStatService.unregisterComponent("desktop-sysstat:" + root.statType) + + implicitWidth: Math.round(240 * widgetScale) + implicitHeight: Math.round(120 * widgetScale) + width: implicitWidth + height: implicitHeight + + // Update interval per stat type + readonly property int graphUpdateInterval: { + switch (root.statType) { + case "CPU": + return SystemStatService.cpuUsageIntervalMs; + case "GPU": + return SystemStatService.gpuIntervalMs; + case "Memory": + return SystemStatService.memIntervalMs; + case "Disk": + return SystemStatService.diskIntervalMs; + case "Network": + return SystemStatService.networkIntervalMs; + default: + return 1000; + } + } + + // Graph component (shared between layouts) + Component { + id: graphComponent + NGraph { + values: root.history + values2: root.history2 + minValue: root.graphMinValue + maxValue: root.graphMaxValue + minValue2: root.graphMinValue2 + maxValue2: root.graphMaxValue2 + color: root.color + color2: root.color2 + fill: true + updateInterval: root.graphUpdateInterval + strokeWidth: Math.max(1, root.widgetScale) + animateScale: root.statType === "Network" + } + } + + // Side layout: icon + legend on left, graph on right + RowLayout { + anchors.fill: parent + anchors.margins: Math.round(Style.marginM * widgetScale) + spacing: Math.round(Style.marginL * widgetScale) + visible: root.layout === "side" + + ColumnLayout { + Layout.alignment: Qt.AlignVCenter + Layout.fillHeight: true + Layout.preferredWidth: Math.round(64 * widgetScale) + spacing: Style.marginXS * root.widgetScale + + Repeater { + model: root.legendItems + delegate: RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Math.round(Style.marginXXS * root.widgetScale) + + NIcon { + visible: !!modelData.icon + icon: modelData.icon || "" + color: modelData.color + pointSize: Style.fontSizeS * root.widgetScale + opacity: modelData.opacity !== undefined ? modelData.opacity : 1.0 + } + + NText { + text: modelData.text + color: modelData.color + pointSize: Style.fontSizeS * root.widgetScale + font.family: Settings.data.ui.fontFixed + font.weight: modelData.bold ? Style.fontWeightBold : Style.fontWeightRegular + opacity: modelData.opacity !== undefined ? modelData.opacity : 1.0 + elide: modelData.elide ? Text.ElideMiddle : Text.ElideNone + Layout.maximumWidth: modelData.elide ? Math.round(56 * root.widgetScale) : -1 + horizontalAlignment: Text.AlignHCenter + } + } + } + } + + Loader { + active: root.layout === "side" + Layout.fillWidth: true + Layout.fillHeight: true + sourceComponent: graphComponent + } + } + + // Bottom layout: full-width graph, horizontal legend at bottom + ColumnLayout { + anchors.fill: parent + anchors.margins: Math.round(Style.marginM * widgetScale) + spacing: Math.round(Style.marginS * widgetScale) + visible: root.layout === "bottom" + + Loader { + active: root.layout === "bottom" + Layout.fillWidth: true + Layout.fillHeight: true + sourceComponent: graphComponent + } + + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + spacing: Math.round(Style.marginM * widgetScale) + + Repeater { + model: root.legendItems + delegate: RowLayout { + Layout.alignment: Qt.AlignVCenter + spacing: Math.round(Style.marginXXS * root.widgetScale) + + NIcon { + Layout.alignment: Qt.AlignVCenter + visible: !!modelData.icon + icon: modelData.icon || "" + color: modelData.color + pointSize: Style.fontSizeS * root.widgetScale + opacity: modelData.opacity !== undefined ? modelData.opacity : 1.0 + } + + NText { + Layout.alignment: Qt.AlignVCenter + text: modelData.text + color: modelData.color + pointSize: Style.fontSizeS * root.widgetScale + font.family: Settings.data.ui.fontFixed + font.weight: modelData.bold ? Style.fontWeightBold : Style.fontWeightRegular + opacity: modelData.opacity !== undefined ? modelData.opacity : 1.0 + elide: modelData.elide ? Text.ElideMiddle : Text.ElideNone + Layout.maximumWidth: modelData.elide ? Math.round(56 * root.widgetScale) : -1 + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/Widgets/DesktopWeather.qml b/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/Widgets/DesktopWeather.qml new file mode 100644 index 0000000..21a11eb --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/DesktopWidgets/Widgets/DesktopWeather.qml @@ -0,0 +1,146 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.DesktopWidgets +import qs.Services.Location +import qs.Widgets + +DraggableDesktopWidget { + id: root + + readonly property bool weatherReady: Settings.data.location.weatherEnabled && (LocationService.data.weather !== null) + readonly property int currentWeatherCode: weatherReady ? LocationService.data.weather.current_weather.weathercode : 0 + readonly property real currentTemp: { + if (!weatherReady) + return 0; + var temp = LocationService.data.weather.current_weather.temperature; + if (Settings.data.location.useFahrenheit) { + temp = LocationService.celsiusToFahrenheit(temp); + } + return Math.round(temp); + } + readonly property real todayMax: { + if (!weatherReady || !LocationService.data.weather.daily || LocationService.data.weather.daily.temperature_2m_max.length === 0) + return 0; + var temp = LocationService.data.weather.daily.temperature_2m_max[0]; + if (Settings.data.location.useFahrenheit) { + temp = LocationService.celsiusToFahrenheit(temp); + } + return Math.round(temp); + } + readonly property real todayMin: { + if (!weatherReady || !LocationService.data.weather.daily || LocationService.data.weather.daily.temperature_2m_min.length === 0) + return 0; + var temp = LocationService.data.weather.daily.temperature_2m_min[0]; + if (Settings.data.location.useFahrenheit) { + temp = LocationService.celsiusToFahrenheit(temp); + } + return Math.round(temp); + } + readonly property string tempUnit: Settings.data.location.useFahrenheit ? "F" : "C" + readonly property string locationName: { + const chunks = Settings.data.location.name.split(","); + return chunks[0]; + } + + implicitWidth: Math.round(Math.max(240 * widgetScale, contentLayout.implicitWidth + Style.margin2M * widgetScale)) + implicitHeight: Math.round(64 * widgetScale + Style.margin2M * widgetScale) + width: implicitWidth + height: implicitHeight + + RowLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Math.round(Style.marginM * widgetScale) + spacing: Math.round(Style.marginM * widgetScale) + z: 2 + + Item { + Layout.preferredWidth: Math.round(64 * widgetScale) + Layout.preferredHeight: Math.round(64 * widgetScale) + Layout.alignment: Qt.AlignVCenter + + NIcon { + visible: !LocationService.taliaWeatherMascotActive || !weatherReady + anchors.centerIn: parent + icon: weatherReady ? LocationService.weatherSymbolFromCode(currentWeatherCode) : (LocationService.locationConfigured ? "weather-cloud-off" : "map-pin-off") + pointSize: Math.round(Style.fontSizeXXXL * 2 * widgetScale) + color: weatherReady ? Color.mPrimary : Color.mOnSurfaceVariant + } + Loader { + active: LocationService.taliaWeatherMascotActive && weatherReady + anchors.fill: parent + asynchronous: true + sourceComponent: Component { + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectFit + smooth: true + mipmap: true + asynchronous: true + source: Qt.resolvedUrl(LocationService.taliaWeatherImageFromCode(currentWeatherCode)) + } + } + } + } + + NText { + text: weatherReady ? `${currentTemp}°${tempUnit}` : "--" + pointSize: Math.round(Style.fontSizeXXXL * widgetScale) + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Math.round(Style.marginXXS * widgetScale) + Layout.alignment: Qt.AlignVCenter + + NText { + Layout.fillWidth: true + text: locationName || I18n.tr("common.weather-no-location") + pointSize: Math.round(Style.fontSizeS * widgetScale) + font.weight: Style.fontWeightRegular + color: Color.mOnSurfaceVariant + elide: Text.ElideRight + maximumLineCount: 1 + visible: !Settings.data.location.hideWeatherCityName + } + + RowLayout { + spacing: Math.round(Style.marginXS * widgetScale) + visible: weatherReady && todayMax > 0 && todayMin > 0 + + NText { + text: "H:" + pointSize: Math.round(Style.fontSizeXS * widgetScale) + color: Color.mOnSurfaceVariant + } + NText { + text: `${todayMax}°` + pointSize: Math.round(Style.fontSizeXS * widgetScale) + color: Color.mOnSurface + } + + NText { + text: "•" + pointSize: Math.round(Style.fontSizeXXS * widgetScale) + color: Color.mOnSurfaceVariant + opacity: 0.5 + } + + NText { + text: "L:" + pointSize: Math.round(Style.fontSizeXS * widgetScale) + color: Color.mOnSurfaceVariant + } + NText { + text: `${todayMin}°` + pointSize: Math.round(Style.fontSizeXS * widgetScale) + color: Color.mOnSurfaceVariant + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Dock/Dock.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Dock/Dock.qml new file mode 100644 index 0000000..73dec8d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Dock/Dock.qml @@ -0,0 +1,951 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Commons +import qs.Services.Compositor +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +Loader { + + active: Settings.data.dock.enabled + sourceComponent: Variants { + model: Quickshell.screens + + delegate: Item { + id: root + + required property ShellScreen modelData + + property bool barIsReady: modelData ? BarService.isBarReady(modelData.name) : false + + Connections { + target: BarService + function onBarReadyChanged(screenName) { + if (screenName === modelData.name) { + barIsReady = true; + } + } + } + + // Update dock apps when window list change + Connections { + target: CompositorService + function onWindowListChanged() { + updateDockApps(); + } + } + + // Update dock apps when toplevels change + Connections { + target: ToplevelManager ? ToplevelManager.toplevels : null + function onValuesChanged() { + updateDockApps(); + } + } + + // Update dock apps when pinned apps change + Connections { + target: Settings.data.dock + function onPinnedAppsChanged() { + updateDockApps(); + } + function onOnlySameOutputChanged() { + updateDockApps(); + } + function onGroupAppsChanged() { + updateDockApps(); + } + } + + // Initial update when component is ready + Component.onCompleted: { + if (ToplevelManager) { + updateDockApps(); + } + } + + // Refresh icons and names when DesktopEntries becomes available (or updates) + Connections { + target: DesktopEntries.applications + function onValuesChanged() { + root.iconRevision++; + root._desktopEntryIdCache = {}; + updateDockApps(); + } + } + + // Shared properties between peek and dock windows + readonly property string displayMode: Settings.data.dock.displayMode + readonly property bool autoHide: displayMode === "auto_hide" + readonly property bool exclusive: displayMode === "exclusive" + readonly property bool isAttachedMode: Settings.data.dock.dockType === "attached" + readonly property int hideDelay: 500 + readonly property int showDelay: 100 + readonly property int hideAnimationDuration: Math.max(0, Math.round(Style.animationFast / (Settings.data.dock.animationSpeed || 1.0))) + readonly property int showAnimationDuration: Math.max(0, Math.round(Style.animationFast / (Settings.data.dock.animationSpeed || 1.0))) + readonly property int peekThickness: 1 + readonly property int indicatorThickness: Settings.data.dock.indicatorThickness || 3 + readonly property string indicatorColorKey: Settings.data.dock.indicatorColor || "primary" + readonly property real indicatorOpacity: Settings.data.dock.indicatorOpacity !== undefined ? Settings.data.dock.indicatorOpacity : 0.6 + readonly property int iconSize: Math.round(12 + 24 * (Settings.data.dock.size ?? 1)) + readonly property int floatingMargin: Settings.data.dock.floatingRatio * Style.marginL + readonly property int maxWidth: modelData ? modelData.width * 0.8 : 1000 + readonly property int maxHeight: modelData ? modelData.height * 0.8 : 1000 + + // Dock position properties + readonly property string dockPosition: Settings.data.dock.position + readonly property bool isVertical: dockPosition === "left" || dockPosition === "right" + + // Bar detection and positioning properties + readonly property bool hasBar: modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false + readonly property bool barAtSameEdge: hasBar && Settings.getBarPositionForScreen(modelData?.name) === dockPosition + readonly property string barPosition: Settings.getBarPositionForScreen(modelData?.name) + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property bool barIsFramed: Settings.data.bar.barType === "framed" && hasBar + readonly property bool barFloating: Settings.data.bar.barType === "floating" + readonly property real barMarginH: barFloating ? Math.ceil(Settings.data.bar.marginHorizontal) : 0 + readonly property real barMarginV: barFloating ? Math.ceil(Settings.data.bar.marginVertical) : 0 + readonly property int barHeight: Style.getBarHeightForScreen(modelData?.name) + readonly property bool staticPanelOpen: { + if (!isAttachedMode) + return false; + var panel = getStaticDockPanel(); + if (panel && panel.isPanelOpen !== undefined) + return panel.isPanelOpen; + return false; + } + readonly property int peekEdgeLength: { + const edgeSize = isVertical ? Math.round(modelData?.height || maxHeight) : Math.round(modelData?.width || maxWidth); + const minLength = Math.max(1, Math.round(edgeSize * (Settings.data.dock.showDockIndicator ? 0.1 : 0.25))); + return Math.max(minLength, dockIndicatorLength); + } + readonly property int peekCenterOffsetX: { + if (isVertical) + return 0; + const edgeSize = Math.round(modelData?.width || maxWidth); + if (barIsVertical) { + if (barPosition === "left") { + const availableStart = (barIsFramed ? 0 : barMarginH) + barHeight; + const availableWidth = edgeSize - availableStart - (barIsFramed ? Settings.data.bar.frameThickness : 0); + return Math.max(0, Math.round(availableStart + (availableWidth - peekEdgeLength) / 2)); + } + if (barPosition === "right") { + const availableWidth = edgeSize - (barIsFramed ? 0 : barMarginH) - barHeight - (barIsFramed ? Settings.data.bar.frameThickness : 0); + return Math.max(0, Math.round((barIsFramed ? Settings.data.bar.frameThickness : 0) + (availableWidth - peekEdgeLength) / 2)); + } + } + return Math.max(0, Math.round((edgeSize - peekEdgeLength) / 2)); + } + readonly property int peekCenterOffsetY: { + if (!isVertical) + return 0; + const edgeSize = Math.round(modelData?.height || maxHeight); + if (!barIsVertical) { + if (barPosition === "top") { + const availableStart = (barIsFramed ? 0 : barMarginV) + barHeight; + const availableHeight = edgeSize - availableStart - (barIsFramed ? Settings.data.bar.frameThickness : 0); + return Math.max(0, Math.round(availableStart + (availableHeight - peekEdgeLength) / 2)); + } + if (barPosition === "bottom") { + const availableHeight = edgeSize - (barIsFramed ? 0 : barMarginV) - barHeight - (barIsFramed ? Settings.data.bar.frameThickness : 0); + return Math.max(0, Math.round((barIsFramed ? Settings.data.bar.frameThickness : 0) + (availableHeight - peekEdgeLength) / 2)); + } + } + return Math.max(0, Math.round((edgeSize - peekEdgeLength) / 2)); + } + readonly property bool showDockIndicator: { + if (!Settings.data.dock.showDockIndicator || (!autoHide && !isAttachedMode) || !hidden) + return false; + return !staticPanelOpen; + } + readonly property int dockItemCount: dockApps.length + (Settings.data.dock.showLauncherIcon ? 1 : 0) + readonly property bool indicatorVisible: showDockIndicator && dockIndicatorLength > 0 + readonly property int dockIndicatorLength: { + if (dockItemCount <= 0) + return 0; + const spacing = Style.marginS; + const layoutLength = (iconSize * dockItemCount) + (spacing * Math.max(0, dockItemCount - 1)); + const padded = layoutLength + Style.marginXL; + return Math.min(padded, isVertical ? maxHeight : maxWidth); + } + + // Shared state between windows + property bool dockHovered: false + property bool anyAppHovered: false + property bool menuHovered: false + property bool hidden: autoHide + property bool peekHovered: false + + // Separate property to control Loader - stays true during animations + property bool dockLoaded: !autoHide // Start loaded if autoHide is off + + // Track the currently open context menu + property var currentContextMenu: null + + // Combined model of running apps and pinned apps + property var dockApps: [] + property var groupCycleIndices: ({}) + + // Track the session order of apps (transient reordering) + property var sessionAppOrder: [] + + // Drag and Drop state for visual feedback + property int dragSourceIndex: -1 + property int dragTargetIndex: -1 + + // when dragging ended but the cursor is outside the dock area, restart the timer + onDragSourceIndexChanged: { + if (dragSourceIndex === -1) { + if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) { + hideTimer.restart(); + } + } + } + + // Revision counter to force icon re-evaluation + property int iconRevision: 0 + + // Function to close any open context menu + function closeAllContextMenus() { + if (currentContextMenu && currentContextMenu.visible) { + currentContextMenu.hide(); + } + } + + function getStaticDockPanel() { + return PanelService.getPanel("staticDockPanel", modelData, false); + } + + function getAppKey(appData) { + if (!appData) + return null; + + if (Settings.data.dock.groupApps) { + return appData.appId; + } + + // Use stable appId for pinned apps to maintain their slot regardless of running state + if (appData.type === "pinned" || appData.type === "pinned-running") { + return appData.appId; + } + + // prefer toplevel object identity for unpinned running apps to distinguish instances + if (appData.toplevel) + return appData.toplevel; + + // fallback to appId + return appData.appId; + } + + function sortDockApps(apps) { + if (!sessionAppOrder || sessionAppOrder.length === 0) { + return apps; + } + + const sorted = []; + const remaining = [...apps]; + + // Pick apps that are in the session order + for (let i = 0; i < sessionAppOrder.length; i++) { + const key = sessionAppOrder[i]; + + // Pick ALL matching apps (e.g. all instances of a pinned app) + while (true) { + const idx = remaining.findIndex(app => getAppKey(app) === key); + if (idx !== -1) { + sorted.push(remaining[idx]); + remaining.splice(idx, 1); + } else { + break; + } + } + } + + // Append any new/remaining apps + remaining.forEach(app => sorted.push(app)); + + return sorted; + } + + function reorderApps(fromIndex, toIndex) { + if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= dockApps.length || toIndex >= dockApps.length) + return; + + const list = [...dockApps]; + const item = list.splice(fromIndex, 1)[0]; + list.splice(toIndex, 0, item); + + dockApps = list; + sessionAppOrder = dockApps.map(getAppKey); + savePinnedOrder(); + } + + function savePinnedOrder() { + const currentPinned = Settings.data.dock.pinnedApps || []; + const newPinned = []; + const seen = new Set(); + + // Extract pinned apps in their current visual order + dockApps.forEach(app => { + if (app.appId && !seen.has(app.appId)) { + const isPinned = currentPinned.some(p => normalizeAppId(p) === normalizeAppId(app.appId)); + + if (isPinned) { + newPinned.push(app.appId); + seen.add(app.appId); + } + } + }); + + // Check if any pinned apps were missed (unlikely if dockApps is correct) + currentPinned.forEach(p => { + if (!seen.has(p)) { + newPinned.push(p); + seen.add(p); + } + }); + + if (JSON.stringify(currentPinned) !== JSON.stringify(newPinned)) { + Settings.data.dock.pinnedApps = newPinned; + } + } + + // Helper function to normalize app IDs for case-insensitive matching + function normalizeAppId(appId) { + if (!appId || typeof appId !== 'string') + return ""; + let id = appId.toLowerCase().trim(); + if (id.endsWith(".desktop")) + id = id.substring(0, id.length - 8); + return id; + } + + // Helper function to check if an app ID matches a pinned app (case-insensitive) + function isAppIdPinned(appId, pinnedApps) { + if (!appId || !pinnedApps || pinnedApps.length === 0) + return false; + const normalizedId = normalizeAppId(appId); + // Direct match + if (pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId)) + return true; + // Resolve via desktop entry lookup (handles StartupWMClass != .desktop filename) + const resolved = resolveToDesktopEntryId(appId); + if (resolved !== appId) { + const normalizedResolved = normalizeAppId(resolved); + return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedResolved); + } + return false; + } + + // Desktop entry ID resolution cache (cleared when DesktopEntries change) + property var _desktopEntryIdCache: ({}) + + // Resolve a toplevel appId to its canonical .desktop entry ID via heuristic lookup. + // This handles cases where the Wayland appId (e.g. "zen" from StartupWMClass) + // differs from the .desktop filename (e.g. "zen-browser-bin"). + function resolveToDesktopEntryId(appId) { + if (!appId) + return appId; + if (_desktopEntryIdCache.hasOwnProperty(appId)) + return _desktopEntryIdCache[appId]; + try { + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) { + const entry = DesktopEntries.heuristicLookup(appId); + if (entry && entry.id) { + _desktopEntryIdCache[appId] = entry.id; + return entry.id; + } + } + } catch (e) {} + _desktopEntryIdCache[appId] = appId; + return appId; + } + + // Helper function to get app name from desktop entry + function getAppNameFromDesktopEntry(appId) { + if (!appId) + return appId; + + try { + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) { + const entry = DesktopEntries.heuristicLookup(appId); + if (entry && entry.name) { + return entry.name; + } + } + + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId) { + const entry = DesktopEntries.byId(appId); + if (entry && entry.name) { + return entry.name; + } + } + } catch (e) + // Fall through to return original appId + {} + + // Return original appId if we can't find a desktop entry + return appId; + } + + function getToplevelsForEntry(appData) { + if (!appData) + return []; + + if (appData.toplevels && appData.toplevels.length > 0) { + return appData.toplevels.filter(toplevel => toplevel && (!Settings.data.dock.onlySameOutput || !toplevel.screens || toplevel.screens.includes(modelData))); + } + + if (!appData.toplevel) + return []; + + if (Settings.data.dock.onlySameOutput && appData.toplevel.screens && !appData.toplevel.screens.includes(modelData)) + return []; + + return [appData.toplevel]; + } + + function getPrimaryToplevelForEntry(appData) { + const toplevels = getToplevelsForEntry(appData); + if (toplevels.length === 0) + return null; + + if (ToplevelManager && ToplevelManager.activeToplevel && toplevels.includes(ToplevelManager.activeToplevel)) + return ToplevelManager.activeToplevel; + + return toplevels[0]; + } + + // Build grouped render model without mutating the raw toplevel list. + function buildGroupedDockApps(apps) { + if (!Settings.data.dock.groupApps) { + return apps.map(app => { + const entry = Object.assign({}, app); + entry.toplevels = getToplevelsForEntry(app); + return entry; + }); + } + + const grouped = []; + const groupedById = new Map(); + + apps.forEach(app => { + const appId = app.appId; + const toplevels = getToplevelsForEntry(app); + const existing = groupedById.get(appId); + + if (existing) { + toplevels.forEach(toplevel => { + if (!existing.toplevels.includes(toplevel)) { + existing.toplevels.push(toplevel); + } + }); + if (app.type === "pinned" || app.type === "pinned-running") { + existing.isPinned = true; + } + } else { + const entry = { + "type": app.type, + "appId": appId, + "title": app.title, + "toplevels": toplevels.slice(), + "isPinned": app.type === "pinned" || app.type === "pinned-running" + }; + grouped.push(entry); + groupedById.set(appId, entry); + } + }); + + grouped.forEach(entry => { + entry.toplevel = getPrimaryToplevelForEntry(entry); + if (entry.toplevels.length > 0 && entry.isPinned) { + entry.type = "pinned-running"; + } else if (entry.toplevels.length > 0) { + entry.type = "running"; + } else { + entry.type = "pinned"; + } + if (entry.toplevel && entry.toplevel.title && entry.toplevel.title.trim() !== "") { + entry.title = entry.toplevel.title; + } + }); + + return grouped; + } + + // Function to update the combined dock apps model + function updateDockApps() { + const runningApps = ToplevelManager ? (ToplevelManager.toplevels.values || []) : []; + const pinnedApps = Settings.data.dock.pinnedApps || []; + const combined = []; + const processedToplevels = new Set(); + const processedPinnedAppIds = new Set(); + + //push an app onto combined with the given appType + function pushApp(appType, toplevel, appId, title) { + // Use canonical ID for pinned apps to ensure key stability + const canonicalId = isAppIdPinned(appId, pinnedApps) ? (pinnedApps.find(p => normalizeAppId(p) === normalizeAppId(appId)) || appId) : appId; + + // For running apps, track by toplevel object to allow multiple instances + if (toplevel) { + if (processedToplevels.has(toplevel)) { + return; // Already processed this toplevel instance + } + if (Settings.data.dock.onlySameOutput && toplevel.screens && !toplevel.screens.includes(modelData)) { + return; // Filtered out by onlySameOutput setting + } + combined.push({ + "type": appType, + "toplevel": toplevel, + "toplevels": toplevel ? [toplevel] : [], + "appId": canonicalId, + "title": title + }); + processedToplevels.add(toplevel); + } else { + // For pinned apps that aren't running, track by appId to avoid duplicates + if (processedPinnedAppIds.has(canonicalId)) { + return; // Already processed this pinned app + } + combined.push({ + "type": appType, + "toplevel": toplevel, + "toplevels": [], + "appId": canonicalId, + "title": title + }); + processedPinnedAppIds.add(canonicalId); + } + } + + function pushRunning(first) { + runningApps.forEach(toplevel => { + if (toplevel) { + // Use robust matching to check if pinned + const isPinned = isAppIdPinned(toplevel.appId, pinnedApps); + if (!first && isPinned && processedToplevels.has(toplevel)) { + return; // Already added by pushPinned() + } + pushApp((first && isPinned) ? "pinned-running" : "running", toplevel, toplevel.appId, toplevel.title); + } + }); + } + + function pushPinned() { + pinnedApps.forEach(pinnedAppId => { + // Find all running instances of this pinned app using robust matching + // Also resolve toplevel appId via desktop entry lookup to handle + // StartupWMClass != .desktop filename (e.g. zen -> zen-browser-bin) + const normalizedPinned = normalizeAppId(pinnedAppId); + const matchingToplevels = runningApps.filter(app => { + if (!app) + return false; + if (normalizeAppId(app.appId) === normalizedPinned) + return true; + const resolved = resolveToDesktopEntryId(app.appId); + return resolved !== app.appId && normalizeAppId(resolved) === normalizedPinned; + }); + + if (matchingToplevels.length > 0) { + // Add all running instances as pinned-running + matchingToplevels.forEach(toplevel => { + pushApp("pinned-running", toplevel, pinnedAppId, toplevel.title); + }); + } else { + // App is pinned but not running - add once + pushApp("pinned", null, pinnedAppId, getAppNameFromDesktopEntry(pinnedAppId) || pinnedAppId); + } + }); + } + + //if pinnedStatic then push all pinned and then all remaining running apps + if (Settings.data.dock.pinnedStatic) { + pushPinned(); + pushRunning(false); + + //else add all running apps and then remaining pinned apps + } else { + pushRunning(true); + pushPinned(); + } + + const sortedApps = sortDockApps(combined); + dockApps = buildGroupedDockApps(sortedApps); + const cycleState = root.groupCycleIndices || {}; + const nextCycleState = {}; + dockApps.forEach(app => { + if (app && app.appId && cycleState[app.appId] !== undefined) { + nextCycleState[app.appId] = cycleState[app.appId]; + } + }); + root.groupCycleIndices = nextCycleState; + + // Sync session order if needed + // Instead of resetting everything when length changes, we reconcile the keys + if (!sessionAppOrder || sessionAppOrder.length === 0) { + sessionAppOrder = dockApps.map(getAppKey); + } else { + const currentKeys = new Set(dockApps.map(getAppKey)); + const existingKeys = new Set(); + const newOrder = []; + + // Keep existing keys that are still present + sessionAppOrder.forEach(key => { + if (currentKeys.has(key)) { + newOrder.push(key); + existingKeys.add(key); + } + }); + + // Add new keys at the end + dockApps.forEach(app => { + const key = getAppKey(app); + if (!existingKeys.has(key)) { + newOrder.push(key); + existingKeys.add(key); + } + }); + + if (JSON.stringify(newOrder) !== JSON.stringify(sessionAppOrder)) { + sessionAppOrder = newOrder; + } + } + } + + // Timer to unload dock after hide animation completes + Timer { + id: unloadTimer + interval: hideAnimationDuration + 50 // Add small buffer + onTriggered: { + if (hidden && autoHide) { + dockLoaded = false; + } + } + } + + property alias hideTimer: hideTimer + property alias showTimer: showTimer + property alias unloadTimer: unloadTimer + + // Timer for auto-hide delay + Timer { + id: hideTimer + interval: hideDelay + onTriggered: { + // do not hide if dragging + if (root.dragSourceIndex !== -1) { + return; + } + + // Force menuHovered to false if no menu is current or visible + if (!root.currentContextMenu || !root.currentContextMenu.visible) { + menuHovered = false; + } + if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) { + if (isAttachedMode) { + const panel = getStaticDockPanel(); + if (panel && (panel.menuHovered || (panel.currentContextMenu && panel.currentContextMenu.visible))) { + restart(); + return; + } + if (panel && (panel.isDockHovered || panel.dockHovered || panel.anyAppHovered)) { + restart(); + return; + } + if (panel) + panel.close(); + } else { + closeAllContextMenus(); + } + hidden = true; + unloadTimer.restart(); // Start unload timer when hiding + } else if (autoHide && !dockHovered && !peekHovered) { + // Restart timer if menu is closing (handles race condition) + restart(); + } + } + } + + // Timer for show delay + Timer { + id: showTimer + interval: showDelay + onTriggered: { + if (autoHide) { + if (!isAttachedMode) { + dockLoaded = true; // Load dock immediately + } + hidden = false; // Then trigger show animation + unloadTimer.stop(); // Cancel any pending unload + } + } + } + + // Watch for autoHide setting changes + onAutoHideChanged: { + if (!autoHide) { + hidden = false; + dockLoaded = true; + hideTimer.stop(); + showTimer.stop(); + unloadTimer.stop(); + } else { + hidden = true; + unloadTimer.restart(); // Schedule unload after animation + } + } + + // PEEK WINDOW — only needed when dock can auto-hide or is in attached mode + Loader { + active: (autoHide || isAttachedMode) && (barIsReady || !hasBar) && modelData && (Settings.data.dock.monitors.length === 0 || Settings.data.dock.monitors.includes(modelData.name)) + + sourceComponent: PanelWindow { + id: peekWindow + + screen: modelData + // Dynamic anchors based on dock position + anchors.top: dockPosition === "top" || isVertical + anchors.bottom: dockPosition === "bottom" + anchors.left: dockPosition === "left" || !isVertical + anchors.right: dockPosition === "right" + focusable: false + color: "transparent" + + margins.top: peekCenterOffsetY + margins.left: peekCenterOffsetX + + WlrLayershell.namespace: "noctalia-dock-peek-" + (screen?.name || "unknown") + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.exclusionMode: ExclusionMode.Ignore + // Larger peek area when bar is at same edge, normal 1px otherwise + implicitHeight: isVertical ? peekEdgeLength : peekThickness + implicitWidth: isVertical ? peekThickness : peekEdgeLength + + MouseArea { + id: peekArea + anchors.fill: parent + hoverEnabled: true + + onEntered: { + peekHovered = true; + if (isAttachedMode) { + if (dockItemCount <= 0) + return; + const panel = getStaticDockPanel(); + if (panel && !panel.isPanelOpen) + panel.open(); + return; + } + if (hidden) { + showTimer.start(); + } + } + + onExited: { + peekHovered = false; + showTimer.stop(); + if (isAttachedMode) { + // Start hideTimer which checks panel.isDockHovered before closing + if (!dockHovered && !anyAppHovered && !menuHovered) { + hideTimer.restart(); + } + } else if (!hidden && !dockHovered && !anyAppHovered && !menuHovered) { + hideTimer.restart(); + } + } + } + } + } + + // DOCK INDICATOR WINDOW — only needed when dock can auto-hide/attach and indicator is enabled + Loader { + active: (autoHide || isAttachedMode) && Settings.data.dock.showDockIndicator && (barIsReady || !hasBar) && modelData && (Settings.data.dock.monitors.length === 0 || Settings.data.dock.monitors.includes(modelData.name)) + + sourceComponent: PanelWindow { + id: dockIndicatorWindow + + screen: modelData + // Dynamic anchors based on dock position + anchors.top: dockPosition === "top" || isVertical + anchors.bottom: dockPosition === "bottom" + anchors.left: dockPosition === "left" || !isVertical + anchors.right: dockPosition === "right" + focusable: false + color: "transparent" + + margins.top: peekCenterOffsetY + margins.left: peekCenterOffsetX + + WlrLayershell.namespace: "noctalia-dock-indicator-" + (screen?.name || "unknown") + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + implicitHeight: isVertical ? peekEdgeLength : indicatorThickness + implicitWidth: isVertical ? indicatorThickness : peekEdgeLength + + // Hide the window surface when indicator is not visible, so the compositor + // can skip compositing this layer-shell surface entirely (saves GPU on NVIDIA) + visible: indicatorRect.opacity > 0 || indicatorVisible + + Rectangle { + id: indicatorRect + anchors.fill: parent + radius: indicatorThickness + color: Qt.alpha(Color.resolveColorKey(indicatorColorKey), indicatorOpacity) + opacity: indicatorVisible ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + } + } + } + + // Force dock reload when position changes to fix anchor/layout issues + // Force dock reload when position/mode changes to fix anchor/layout issues + property bool _reloading: false + function handleReload() { + if (!autoHide && dockLoaded && !_reloading) { + _reloading = true; + // Brief unload/reload cycle to reset layout + Qt.callLater(() => { + dockLoaded = false; + Qt.callLater(() => { + dockLoaded = true; + _reloading = false; + }); + }); + } + } + + onDockPositionChanged: handleReload() + onExclusiveChanged: handleReload() + + Loader { + id: dockWindowLoader + active: Settings.data.dock.enabled && !isAttachedMode && (barIsReady || !hasBar) && modelData && (Settings.data.dock.monitors.length === 0 || Settings.data.dock.monitors.includes(modelData.name)) && dockLoaded && ToplevelManager && (dockApps.length > 0) + + sourceComponent: PanelWindow { + id: dockWindow + + screen: modelData + + focusable: false + color: "transparent" + + WlrLayershell.namespace: "noctalia-dock-" + (screen?.name || "unknown") + WlrLayershell.exclusionMode: exclusive ? ExclusionMode.Auto : ExclusionMode.Ignore + + // Slide animation: content slides inside a fixed window, no margin animation. + // Only reserve extra space for sliding when auto-hide is enabled + property int slideDistance: autoHide ? ((isVertical ? dockContainerWrapper.contentWidth : dockContainerWrapper.contentHeight) + floatingMargin + 10) : 0 + property real slideOffset: hidden ? slideDistance : 0 + + Behavior on slideOffset { + NumberAnimation { + duration: hidden ? hideAnimationDuration : showAnimationDuration + easing.type: hidden ? Easing.InCubic : Easing.OutCubic + } + } + + // Signed slide: positive pushes content toward its edge (off-screen) + readonly property real slideX: dockPosition === "left" ? -slideOffset : dockPosition === "right" ? slideOffset : 0 + readonly property real slideY: dockPosition === "top" ? -slideOffset : dockPosition === "bottom" ? slideOffset : 0 + + // Blur behind dock — offset by slide so it follows the content + BackgroundEffect.blurRegion: Settings.data.general.enableBlurBehind ? dockBlurRegion : null + Region { + id: dockBlurRegion + Region { + x: Math.round(dockContainerWrapper.x + dockContent.dockContainer.x + dockWindow.slideX) + y: Math.round(dockContainerWrapper.y + dockContent.dockContainer.y + dockWindow.slideY) + width: Math.round(dockContent.dockContainer.width) + height: Math.round(dockContent.dockContainer.height) + radius: Style.radiusL + } + } + + // Window sized to fit content + slide distance so content can slide off-edge. + // When auto-hide is disabled, slideDistance is 0 so the window (and thus + // the exclusion zone) matches the dock content size. + implicitWidth: dockContainerWrapper.width + (isVertical ? slideDistance : 0) + implicitHeight: dockContainerWrapper.height + (!isVertical ? slideDistance : 0) + + // Position based on dock setting + anchors.top: dockPosition === "top" + anchors.bottom: dockPosition === "bottom" + anchors.left: dockPosition === "left" + anchors.right: dockPosition === "right" + + // Static margins — no animation, window stays put + margins.top: dockPosition === "top" ? (barAtSameEdge && !exclusive ? barHeight + (barFloating ? Settings.data.bar.marginVertical : 0) + floatingMargin : floatingMargin) : 0 + margins.bottom: dockPosition === "bottom" ? (barAtSameEdge && !exclusive ? barHeight + (barFloating ? Settings.data.bar.marginVertical : 0) + floatingMargin : floatingMargin) : 0 + margins.left: dockPosition === "left" ? (barAtSameEdge && !exclusive ? barHeight + (barFloating ? Settings.data.bar.marginHorizontal : 0) + floatingMargin : floatingMargin) : 0 + margins.right: dockPosition === "right" ? (barAtSameEdge && !exclusive ? barHeight + (barFloating ? Settings.data.bar.marginHorizontal : 0) + floatingMargin : floatingMargin) : 0 + + // Container wrapper for animations + Item { + id: dockContainerWrapper + + // Helper properties for orthogonal bar detection + readonly property string screenBarPosition: Settings.getBarPositionForScreen(modelData?.name) + readonly property bool barOnLeft: hasBar && screenBarPosition === "left" && !barFloating + readonly property bool barOnRight: hasBar && screenBarPosition === "right" && !barFloating + readonly property bool barOnTop: hasBar && screenBarPosition === "top" && !barFloating + readonly property bool barOnBottom: hasBar && screenBarPosition === "bottom" && !barFloating + + // Calculate padding needed to shift center to match exclusive mode + readonly property int extraTop: (isVertical && !exclusive && barOnTop) ? barHeight : 0 + readonly property int extraBottom: (isVertical && !exclusive && barOnBottom) ? barHeight : 0 + readonly property int extraLeft: (!isVertical && !exclusive && barOnLeft) ? barHeight : 0 + readonly property int extraRight: (!isVertical && !exclusive && barOnRight) ? barHeight : 0 + + // Expose content size for window sizing (before slide padding) + readonly property int contentWidth: dockContent.dockContainer.width + extraLeft + extraRight + 2 + readonly property int contentHeight: dockContent.dockContainer.height + extraTop + extraBottom + 2 + + // Add +2 buffer for fractional scaling issues + width: contentWidth + height: contentHeight + + anchors.horizontalCenter: isVertical ? undefined : parent.horizontalCenter + anchors.verticalCenter: isVertical ? parent.verticalCenter : undefined + + anchors.top: dockPosition === "top" ? parent.top : undefined + anchors.bottom: dockPosition === "bottom" ? parent.bottom : undefined + anchors.left: dockPosition === "left" ? parent.left : undefined + anchors.right: dockPosition === "right" ? parent.right : undefined + + // Slide content inside the fixed window + transform: Translate { + x: dockWindow.slideX + y: dockWindow.slideY + } + + // Enable layer caching to reduce GPU usage from continuous animations + layer.enabled: true + + DockContent { + id: dockContent + anchors.fill: parent + dockRoot: root + extraTop: dockContainerWrapper.extraTop + extraBottom: dockContainerWrapper.extraBottom + extraLeft: dockContainerWrapper.extraLeft + extraRight: dockContainerWrapper.extraRight + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Dock/DockContent.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Dock/DockContent.qml new file mode 100644 index 0000000..09b4953 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Dock/DockContent.qml @@ -0,0 +1,895 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Commons +import qs.Services.Compositor +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +Item { + required property var dockRoot + required property int extraTop + required property int extraBottom + required property int extraLeft + required property int extraRight + property alias dockContainer: dockContainer + readonly property bool isAttachedMode: Settings.data.dock.dockType === "attached" + readonly property string tooltipDirection: dockRoot.dockPosition === "left" ? "right" : (dockRoot.dockPosition === "right" ? "left" : (dockRoot.dockPosition === "top" ? "bottom" : "top")) + + Rectangle { + id: dockContainer + // For vertical dock, swap width and height logic + width: dockRoot.isVertical ? Math.round(dockRoot.iconSize * 1.5) : Math.min(dockLayout.implicitWidth + Style.marginXL, dockRoot.maxWidth) + height: dockRoot.isVertical ? Math.min(dockLayout.implicitHeight + Style.marginXL, dockRoot.maxHeight) : Math.round(dockRoot.iconSize * 1.5) + color: Qt.alpha(Color.mSurface, (isAttachedMode ? 0 : Color.adaptiveOpacity(Settings.data.dock.backgroundOpacity))) + + // Anchor based on padding to achieve centering shift + anchors.horizontalCenter: extraLeft > 0 || extraRight > 0 ? undefined : parent.horizontalCenter + anchors.right: extraLeft > 0 ? parent.right : undefined + anchors.left: extraRight > 0 ? parent.left : undefined + + anchors.verticalCenter: extraTop > 0 || extraBottom > 0 ? undefined : parent.verticalCenter + anchors.bottom: extraTop > 0 ? parent.bottom : undefined + anchors.top: extraBottom > 0 ? parent.top : undefined + + radius: Style.radiusL + border.width: Style.borderS + border.color: Qt.alpha(Color.mOutline, (isAttachedMode ? 0 : Color.adaptiveOpacity(Settings.data.dock.backgroundOpacity))) + + MouseArea { + id: dockMouseArea + anchors.fill: parent + hoverEnabled: true + + onEntered: { + dockRoot.dockHovered = true; + if (dockRoot.autoHide) { + dockRoot.showTimer.stop(); + dockRoot.hideTimer.stop(); + dockRoot.unloadTimer.stop(); // Cancel unload if hovering + dockRoot.hidden = false; // Make sure dock is visible + } + } + + onExited: { + dockRoot.dockHovered = false; + if (dockRoot.autoHide && !dockRoot.anyAppHovered && !dockRoot.peekHovered && !dockRoot.menuHovered && dockRoot.dragSourceIndex === -1) { + dockRoot.hideTimer.restart(); + } + } + + onClicked: { + // Close any open context menu when clicking on the dock background + dockRoot.closeAllContextMenus(); + } + } + + Flickable { + id: dock + // Use parent dimensions more directly to avoid clipping + width: dockRoot.isVertical ? parent.width : Math.min(dockLayout.implicitWidth, parent.width - Style.marginXL) + height: !dockRoot.isVertical ? parent.height : Math.min(dockLayout.implicitHeight, parent.height - Style.marginXL) + contentWidth: dockLayout.implicitWidth + contentHeight: dockLayout.implicitHeight + anchors.centerIn: parent + clip: true + + flickableDirection: dockRoot.isVertical ? Flickable.VerticalFlick : Flickable.HorizontalFlick + + // Keep interactive dependent on overflow + interactive: dockRoot.isVertical ? contentHeight > height : contentWidth > width + + // Centering margins + contentX: dockRoot.isVertical && contentWidth < width ? (contentWidth - width) / 2 : 0 + contentY: !dockRoot.isVertical && contentHeight < height ? (contentHeight - height) / 2 : 0 + + WheelHandler { + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onWheel: event => { + var delta = (event.angleDelta.y !== 0) ? event.angleDelta.y : event.angleDelta.x; + if (dockRoot.isVertical) { + dock.contentY = Math.max(-dock.topMargin, Math.min(dock.contentHeight - dock.height + dock.bottomMargin, dock.contentY - delta)); + } else { + // For horizontal dock, we want to scroll contentX with BOTH x and y wheels + var hDelta = (event.angleDelta.x !== 0) ? event.angleDelta.x : event.angleDelta.y; + dock.contentX = Math.max(-dock.leftMargin, Math.min(dock.contentWidth - dock.width + dock.rightMargin, dock.contentX - hDelta)); + } + event.accepted = true; + } + } + + ScrollBar.horizontal: ScrollBar { + visible: !dockRoot.isVertical && dock.interactive + policy: ScrollBar.AsNeeded + } + ScrollBar.vertical: ScrollBar { + visible: dockRoot.isVertical && dock.interactive + policy: ScrollBar.AsNeeded + } + + function getAppIcon(appData): string { + if (!appData || !appData.appId) + return ""; + return ThemeIcons.iconForAppId(appData.appId?.toLowerCase()); + } + + function getValidToplevels(appData) { + if (!appData || !ToplevelManager || !ToplevelManager.toplevels) + return []; + const source = appData.toplevels && appData.toplevels.length > 0 ? appData.toplevels : (appData.toplevel ? [appData.toplevel] : []); + const allToplevels = ToplevelManager.toplevels.values || []; + return source.filter(toplevel => toplevel && allToplevels.includes(toplevel)); + } + + function getPrimaryToplevel(appData) { + const toplevels = getValidToplevels(appData); + if (toplevels.length === 0) + return null; + if (ToplevelManager && ToplevelManager.activeToplevel && toplevels.includes(ToplevelManager.activeToplevel)) + return ToplevelManager.activeToplevel; + return toplevels[0]; + } + + function launchAppById(appId) { + if (!appId) + return; + + const app = ThemeIcons.findAppEntry(appId); + if (!app) { + Logger.w("Dock", `Could not find desktop entry for pinned app: ${appId}`); + return; + } + + if (Settings.data.appLauncher.customLaunchPrefixEnabled && Settings.data.appLauncher.customLaunchPrefix) { + const prefix = Settings.data.appLauncher.customLaunchPrefix.split(" "); + + if (app.runInTerminal) { + const terminal = Settings.data.appLauncher.terminalCommand.split(" "); + const command = prefix.concat(terminal.concat(app.command)); + Quickshell.execDetached(command); + } else { + const command = prefix.concat(app.command); + Quickshell.execDetached(command); + } + } else { + if (app.runInTerminal) { + Logger.d("Dock", "Executing terminal app manually: " + app.name); + const terminal = Settings.data.appLauncher.terminalCommand.split(" "); + const command = terminal.concat(app.command); + CompositorService.spawn(command); + } else if (app.command && app.command.length > 0) { + CompositorService.spawn(app.command); + } else if (app.execute) { + app.execute(); + } else { + Logger.w("Dock", `Could not launch: ${app.name}. No valid launch method.`); + } + } + } + + // Use GridLayout for flexible horizontal/vertical arrangement + GridLayout { + id: dockLayout + columns: dockRoot.isVertical ? 1 : -1 + rows: dockRoot.isVertical ? -1 : 1 + rowSpacing: Style.marginS + columnSpacing: Style.marginS + + // Ensure the layout takes its full implicit size + width: implicitWidth + height: implicitHeight + + Component { + id: launcherButtonComponent + + Item { + id: launcherButton + anchors.fill: parent + readonly property string screenName: dockRoot.modelData ? dockRoot.modelData.name : (dockRoot.screen ? dockRoot.screen.name : "") + readonly property var launcherWidgetSettings: { + const widgetsBySection = screenName ? Settings.getBarWidgetsForScreen(screenName) : Settings.data.bar.widgets; + if (!widgetsBySection) + return {}; + const sections = ["left", "center", "right"]; + for (let i = 0; i < sections.length; i++) { + const sectionWidgets = widgetsBySection[sections[i]] || []; + for (let j = 0; j < sectionWidgets.length; j++) { + const widget = sectionWidgets[j]; + if (widget && widget.id === "Launcher") + return widget; + } + } + return {}; + } + readonly property string launcherWidgetSection: { + const widgetsBySection = screenName ? Settings.getBarWidgetsForScreen(screenName) : Settings.data.bar.widgets; + if (!widgetsBySection) + return ""; + const sections = ["left", "center", "right"]; + for (let i = 0; i < sections.length; i++) { + const sectionWidgets = widgetsBySection[sections[i]] || []; + for (let j = 0; j < sectionWidgets.length; j++) { + const widget = sectionWidgets[j]; + if (widget && widget.id === "Launcher") + return sections[i]; + } + } + return ""; + } + readonly property int launcherWidgetIndex: { + const widgetsBySection = screenName ? Settings.getBarWidgetsForScreen(screenName) : Settings.data.bar.widgets; + if (!widgetsBySection) + return -1; + const sections = ["left", "center", "right"]; + for (let i = 0; i < sections.length; i++) { + const sectionWidgets = widgetsBySection[sections[i]] || []; + for (let j = 0; j < sectionWidgets.length; j++) { + const widget = sectionWidgets[j]; + if (widget && widget.id === "Launcher") + return j; + } + } + return -1; + } + readonly property var launcherMetadata: BarWidgetRegistry.widgetMetadata["Launcher"] + readonly property string launcherIcon: { + if (Settings.data.dock.launcherIcon !== undefined && Settings.data.dock.launcherIcon !== "") + return Settings.data.dock.launcherIcon; + if (launcherWidgetSettings.icon !== undefined && launcherWidgetSettings.icon !== "") + return launcherWidgetSettings.icon; + return (launcherMetadata && launcherMetadata.icon) ? launcherMetadata.icon : "search"; + } + readonly property string launcherIconColorKey: { + if (Settings.data.dock.launcherIconColor !== undefined) + return Settings.data.dock.launcherIconColor; + if (launcherWidgetSettings.iconColor !== undefined) + return launcherWidgetSettings.iconColor; + if (launcherMetadata && launcherMetadata.iconColor !== undefined) + return launcherMetadata.iconColor; + return "none"; + } + readonly property bool launcherUseDistroLogo: { + if (Settings.data.dock.launcherUseDistroLogo !== undefined) + return Settings.data.dock.launcherUseDistroLogo; + if (launcherWidgetSettings.useDistroLogo !== undefined) + return launcherWidgetSettings.useDistroLogo; + if (launcherMetadata && launcherMetadata.useDistroLogo !== undefined) + return launcherMetadata.useDistroLogo; + return false; + } + + Item { + id: launcherIconContainer + width: dockRoot.iconSize + height: dockRoot.iconSize + anchors.centerIn: parent + + scale: launcherMouseArea.containsMouse ? 1.15 : 1.0 + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + easing.overshoot: 1.2 + } + } + + NIcon { + anchors.centerIn: parent + icon: launcherButton.launcherIcon + pointSize: dockRoot.iconSize * 0.7 + color: Color.resolveColorKey(launcherButton.launcherIconColorKey) + visible: !launcherButton.launcherUseDistroLogo + } + + IconImage { + anchors.centerIn: parent + width: dockRoot.iconSize * 0.8 + height: width + source: launcherButton.launcherUseDistroLogo ? HostService.osLogo : "" + visible: source !== "" + smooth: true + asynchronous: true + layer.enabled: visible + layer.effect: ShaderEffect { + property color targetColor: Color.resolveColorKey(launcherButton.launcherIconColorKey) + property real colorizeMode: 2.0 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") + } + } + } + + MouseArea { + id: launcherMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton + + onEntered: { + dockRoot.anyAppHovered = true; + TooltipService.show(launcherButton, I18n.tr("actions.open-launcher"), tooltipDirection); + if (dockRoot.autoHide) { + dockRoot.showTimer.stop(); + dockRoot.hideTimer.stop(); + dockRoot.unloadTimer.stop(); + dockRoot.hidden = false; + } + } + + onExited: { + dockRoot.anyAppHovered = false; + TooltipService.hide(); + if (dockRoot.autoHide && !dockRoot.dockHovered && !dockRoot.peekHovered && !dockRoot.menuHovered && dockRoot.dragSourceIndex === -1) { + dockRoot.hideTimer.restart(); + } + } + + onClicked: mouse => { + const targetScreen = dockRoot.modelData || dockRoot.screen || null; + if (!targetScreen) { + return; + } + + if (mouse.button === Qt.RightButton) { + if (dockRoot.currentContextMenu === launcherContextMenu && launcherContextMenu.visible) { + dockRoot.closeAllContextMenus(); + return; + } + dockRoot.closeAllContextMenus(); + TooltipService.hideImmediately(); + launcherContextMenu.show(launcherButton, null, targetScreen); + return; + } + + if (mouse.button === Qt.LeftButton || mouse.button === Qt.MiddleButton) { + dockRoot.closeAllContextMenus(); + PanelService.toggleLauncher(targetScreen); + } + } + } + + DockMenu { + id: launcherContextMenu + dockPosition: dockRoot.dockPosition + menuMode: "launcher" + launcherWidgetSection: launcherButton.launcherWidgetSection + launcherWidgetIndex: launcherButton.launcherWidgetIndex + launcherWidgetSettings: launcherButton.launcherWidgetSettings + + onHoveredChanged: { + if (dockRoot.currentContextMenu === launcherContextMenu && launcherContextMenu.visible) { + dockRoot.menuHovered = hovered; + } else { + dockRoot.menuHovered = false; + } + } + + Connections { + target: launcherContextMenu + function onRequestClose() { + dockRoot.currentContextMenu = null; + dockRoot.hideTimer.stop(); + launcherContextMenu.hide(); + dockRoot.menuHovered = false; + dockRoot.anyAppHovered = false; + } + } + + onVisibleChanged: { + if (visible) { + dockRoot.currentContextMenu = launcherContextMenu; + } else if (dockRoot.currentContextMenu === launcherContextMenu) { + dockRoot.currentContextMenu = null; + dockRoot.hideTimer.stop(); + dockRoot.menuHovered = false; + if (dockRoot.autoHide && !dockRoot.dockHovered && !dockRoot.anyAppHovered && !dockRoot.peekHovered && !dockRoot.menuHovered) { + dockRoot.hideTimer.restart(); + } + } + } + } + } + } + + Loader { + id: launcherButtonStart + active: Settings.data.dock.showLauncherIcon && Settings.data.dock.launcherPosition === "start" + visible: active + sourceComponent: launcherButtonComponent + readonly property real indicatorMargin: Math.max(3, Math.round(dockRoot.iconSize * 0.18)) + Layout.preferredWidth: active ? (dockRoot.isVertical ? dockRoot.iconSize + indicatorMargin * 2 : dockRoot.iconSize) : 0 + Layout.preferredHeight: active ? (dockRoot.isVertical ? dockRoot.iconSize : dockRoot.iconSize + indicatorMargin * 2) : 0 + Layout.minimumWidth: active ? Layout.preferredWidth : 0 + Layout.minimumHeight: active ? Layout.preferredHeight : 0 + Layout.maximumWidth: active ? Layout.preferredWidth : 0 + Layout.maximumHeight: active ? Layout.preferredHeight : 0 + Layout.alignment: Qt.AlignCenter + } + + Repeater { + model: dockRoot.dockApps + + delegate: Item { + id: appButton + readonly property real indicatorMargin: Math.max(3, Math.round(dockRoot.iconSize * 0.18)) + Layout.preferredWidth: dockRoot.isVertical ? dockRoot.iconSize + indicatorMargin * 2 : dockRoot.iconSize + Layout.preferredHeight: dockRoot.isVertical ? dockRoot.iconSize : dockRoot.iconSize + indicatorMargin * 2 + Layout.alignment: Qt.AlignCenter + + property var toplevels: dock.getValidToplevels(modelData) + property bool isActive: ToplevelManager && ToplevelManager.activeToplevel && toplevels.includes(ToplevelManager.activeToplevel) + property bool hovered: appMouseArea.containsMouse + property string appId: modelData ? modelData.appId : "" + property int groupedCount: toplevels.length + property int focusedWindowIndex: { + if (!ToplevelManager || !ToplevelManager.activeToplevel) + return -1; + return toplevels.indexOf(ToplevelManager.activeToplevel); + } + property string groupedIndicatorText: focusedWindowIndex >= 0 ? (focusedWindowIndex + 1) + "/" + groupedCount : groupedCount.toString() + property string appTitle: { + if (!modelData) + return ""; + const primaryToplevel = dock.getPrimaryToplevel(modelData); + if (primaryToplevel) { + const toplevelTitle = primaryToplevel.title || ""; + // If title is "Loading..." or empty, use desktop entry name + if (!toplevelTitle || toplevelTitle === "Loading..." || toplevelTitle.trim() === "") { + return dockRoot.getAppNameFromDesktopEntry(modelData.appId) || modelData.appId; + } + return toplevelTitle; + } + // For pinned apps that aren't running, use the stored title + return modelData.title || modelData.appId || ""; + } + property bool isRunning: toplevels.length > 0 + readonly property bool baseIndicatorVisible: Settings.data.dock.inactiveIndicators ? isRunning : isActive + // Grouped indicators should be visible whenever grouped windows are running, even if none is focused. + readonly property bool showGroupedIndicator: Settings.data.dock.groupApps && groupedCount > 1 && isRunning + + // Store index for drag-and-drop + property int modelIndex: index + objectName: "dockAppButton" + + DropArea { + anchors.fill: parent + keys: ["dock-app"] + onEntered: function (drag) { + if (drag.source && drag.source.objectName === "dockAppButton") { + dockRoot.dragTargetIndex = appButton.modelIndex; + } + } + onExited: function () { + if (dockRoot.dragTargetIndex === appButton.modelIndex) { + dockRoot.dragTargetIndex = -1; + } + } + onDropped: function (drop) { + dockRoot.dragSourceIndex = -1; + dockRoot.dragTargetIndex = -1; + if (drop.source && drop.source.objectName === "dockAppButton" && drop.source !== appButton) { + dockRoot.reorderApps(drop.source.modelIndex, appButton.modelIndex); + } + } + } + + // Listen for the toplevel being closed + Connections { + target: modelData?.toplevel + function onClosed() { + Qt.callLater(dockRoot.updateDockApps); + } + } + + // Draggable container for the icon + Item { + id: iconContainer + width: dockRoot.iconSize + height: dockRoot.iconSize + + // When dragging, remove anchors so MouseArea can position it + anchors.centerIn: dragging ? undefined : parent + + property bool dragging: appMouseArea.drag.active + onDraggingChanged: { + if (dragging) { + dockRoot.dragSourceIndex = index; + } else { + // Reset if not handled by drop (e.g. dropped outside) + Qt.callLater(() => { + if (!appMouseArea.drag.active && dockRoot.dragSourceIndex === index) { + dockRoot.dragSourceIndex = -1; + dockRoot.dragTargetIndex = -1; + } + }); + } + } + + Drag.active: dragging + Drag.source: appButton + Drag.hotSpot.x: width / 2 + Drag.hotSpot.y: height / 2 + Drag.keys: ["dock-app"] + + z: (dockRoot.dragSourceIndex === index) ? 1000 : ((dragging ? 1000 : 0)) + scale: dragging ? 1.1 : (appButton.hovered ? 1.15 : 1.0) + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + easing.overshoot: 1.2 + } + } + + // Visual shifting logic + readonly property bool isDragged: dockRoot.dragSourceIndex === index + property real shiftOffset: 0 + + Binding on shiftOffset { + value: { + if (dockRoot.dragSourceIndex !== -1 && dockRoot.dragTargetIndex !== -1 && !iconContainer.isDragged) { + if (dockRoot.dragSourceIndex < dockRoot.dragTargetIndex) { + // Dragging Forward: Items between source and target shift Backward + if (index > dockRoot.dragSourceIndex && index <= dockRoot.dragTargetIndex) { + return -1 * (dockRoot.isVertical ? dockRoot.iconSize + Style.marginS : dockRoot.iconSize + Style.marginS); + } + } else if (dockRoot.dragSourceIndex > dockRoot.dragTargetIndex) { + // Dragging Backward: Items between target and source shift Forward + if (index >= dockRoot.dragTargetIndex && index < dockRoot.dragSourceIndex) { + return (dockRoot.isVertical ? dockRoot.iconSize + Style.marginS : dockRoot.iconSize + Style.marginS); + } + } + } + return 0; + } + } + + transform: Translate { + x: !dockRoot.isVertical ? iconContainer.shiftOffset : 0 + y: dockRoot.isVertical ? iconContainer.shiftOffset : 0 + + Behavior on x { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutQuad + } + } + Behavior on y { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutQuad + } + } + } + + IconImage { + id: appIcon + anchors.fill: parent + source: { + dockRoot.iconRevision; // Force re-evaluation when revision changes + return dock.getAppIcon(modelData); + } + visible: source.toString() !== "" + smooth: true + asynchronous: true + + // Dim pinned apps that aren't running + opacity: appButton.isRunning ? 1.0 : Settings.data.dock.deadOpacity + + // Apply dock-specific colorization shader only to non-focused apps + layer.enabled: !appButton.isActive && Settings.data.dock.colorizeIcons + layer.effect: ShaderEffect { + property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant + property real colorizeMode: 0.0 // Dock mode (grayscale) + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutQuad + } + } + } + + // Fall back if no icon + NIcon { + anchors.centerIn: parent + visible: !appIcon.visible + icon: "question-mark" + pointSize: dockRoot.iconSize * 0.7 + color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant + opacity: appButton.isRunning ? 1.0 : 0.6 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutQuad + } + } + } + } + + // Context menu popup + DockMenu { + id: contextMenu + dockPosition: dockRoot.dockPosition // Pass dock position for menu placement + onHoveredChanged: { + // Only update menuHovered if this menu is current and visible + if (dockRoot.currentContextMenu === contextMenu && contextMenu.visible) { + dockRoot.menuHovered = hovered; + } else { + dockRoot.menuHovered = false; + } + } + + Connections { + target: contextMenu + function onRequestClose() { + // Clear current menu immediately to prevent hover updates + dockRoot.currentContextMenu = null; + dockRoot.hideTimer.stop(); + contextMenu.hide(); + dockRoot.menuHovered = false; + dockRoot.anyAppHovered = false; + } + } + onAppClosed: dockRoot.updateDockApps // Force immediate dock update when app is closed + onVisibleChanged: { + if (visible) { + dockRoot.currentContextMenu = contextMenu; + } else if (dockRoot.currentContextMenu === contextMenu) { + dockRoot.currentContextMenu = null; + dockRoot.hideTimer.stop(); + dockRoot.menuHovered = false; + // Restart hide timer after menu closes + if (dockRoot.autoHide && !dockRoot.dockHovered && !dockRoot.anyAppHovered && !dockRoot.peekHovered && !dockRoot.menuHovered) { + dockRoot.hideTimer.restart(); + } + } + } + } + + MouseArea { + id: appMouseArea + objectName: "appMouseArea" + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton + + // Only allow left-click dragging via axis control + drag.target: iconContainer + drag.axis: (pressedButtons & Qt.LeftButton) ? (dockRoot.isVertical ? Drag.YAxis : Drag.XAxis) : Drag.None + + onPressed: { + var p1 = appButton.mapFromItem(dockContainer, 0, 0); + var p2 = appButton.mapFromItem(dockContainer, dockContainer.width, dockContainer.height); + drag.minimumX = p1.x; + drag.maximumX = p2.x - iconContainer.width; + drag.minimumY = p1.y; + drag.maximumY = p2.y - iconContainer.height; + } + + onReleased: { + if (iconContainer.Drag.active) { + iconContainer.Drag.drop(); + } + } + + onEntered: { + dockRoot.anyAppHovered = true; + const appName = appButton.appTitle || appButton.appId || "Unknown"; + const tooltipText = appName.length > 40 ? appName.substring(0, 37) + "..." : appName; + if (!contextMenu.visible) { + TooltipService.show(appButton, tooltipText, tooltipDirection); + } + if (dockRoot.autoHide) { + dockRoot.showTimer.stop(); + dockRoot.hideTimer.stop(); + dockRoot.unloadTimer.stop(); // Cancel unload if hovering app + dockRoot.hidden = false; // Make sure dock is visible + } + } + + onExited: { + dockRoot.anyAppHovered = false; + TooltipService.hide(); + // Clear menuHovered if no current menu or menu not visible + if (!dockRoot.currentContextMenu || !dockRoot.currentContextMenu.visible) { + dockRoot.menuHovered = false; + } + if (dockRoot.autoHide && !dockRoot.dockHovered && !dockRoot.peekHovered && !dockRoot.menuHovered && dockRoot.dragSourceIndex === -1) { + dockRoot.hideTimer.restart(); + } + } + + onClicked: mouse => { + if (mouse.button === Qt.RightButton) { + const targetScreen = dockRoot.modelData || dockRoot.screen || null; + // If right-clicking on the same app with an open context menu, close it + if (dockRoot.currentContextMenu === contextMenu && contextMenu.visible) { + dockRoot.closeAllContextMenus(); + return; + } + // Close any other existing context menu first + dockRoot.closeAllContextMenus(); + // Hide tooltip when showing context menu + TooltipService.hideImmediately(); + contextMenu.show(appButton, modelData, targetScreen); + return; + } + + // Close any existing context menu for non-right-click actions + dockRoot.closeAllContextMenus(); + + const runningToplevels = dock.getValidToplevels(modelData); + const primaryToplevel = dock.getPrimaryToplevel(modelData); + + if (mouse.button === Qt.MiddleButton) { + if (primaryToplevel && primaryToplevel.close) { + primaryToplevel.close(); + Qt.callLater(dockRoot.updateDockApps); + } + } else if (mouse.button === Qt.LeftButton) { + if (runningToplevels.length === 0) { + dock.launchAppById(modelData?.appId); + return; + } + + if (!Settings.data.dock.groupApps || runningToplevels.length <= 1) { + if (primaryToplevel && primaryToplevel.activate) { + primaryToplevel.activate(); + } + return; + } + + const clickAction = Settings.data.dock.groupClickAction || "cycle"; + if (clickAction === "list") { + const targetScreen = dockRoot.modelData || dockRoot.screen || null; + TooltipService.hideImmediately(); + // Left-click list should always open the grouped window list view. + contextMenu.show(appButton, modelData, targetScreen, "list"); + } else { + const appKey = modelData?.appId || ""; + const state = dockRoot.groupCycleIndices || {}; + const nextIndex = (state[appKey] || 0) % runningToplevels.length; + const nextToplevel = runningToplevels[nextIndex]; + if (nextToplevel && nextToplevel.activate) { + nextToplevel.activate(); + } + state[appKey] = (nextIndex + 1) % runningToplevels.length; + dockRoot.groupCycleIndices = Object.assign({}, state); + } + } + } + } + + // Active indicator - positioned at the edge of the delegate area + Rectangle { + visible: baseIndicatorVisible && !showGroupedIndicator + width: dockRoot.isVertical ? indicatorMargin * 0.6 : dockRoot.iconSize * 0.2 + height: dockRoot.isVertical ? dockRoot.iconSize * 0.2 : indicatorMargin * 0.6 + color: Color.mPrimary + radius: Style.radiusXS + + // Anchor to the edge facing the screen center + anchors.bottom: !dockRoot.isVertical && dockRoot.dockPosition === "bottom" ? parent.bottom : undefined + anchors.top: !dockRoot.isVertical && dockRoot.dockPosition === "top" ? parent.top : undefined + anchors.left: dockRoot.isVertical && dockRoot.dockPosition === "left" ? parent.left : undefined + anchors.right: dockRoot.isVertical && dockRoot.dockPosition === "right" ? parent.right : undefined + + anchors.horizontalCenter: dockRoot.isVertical ? undefined : parent.horizontalCenter + anchors.verticalCenter: dockRoot.isVertical ? parent.verticalCenter : undefined + + // Offset slightly from the edge + anchors.bottomMargin: !dockRoot.isVertical && dockRoot.dockPosition === "bottom" ? 2 : 0 + anchors.topMargin: !dockRoot.isVertical && dockRoot.dockPosition === "top" ? 2 : 0 + anchors.leftMargin: dockRoot.isVertical && dockRoot.dockPosition === "left" ? 2 : 0 + anchors.rightMargin: dockRoot.isVertical && dockRoot.dockPosition === "right" ? 2 : 0 + } + + Loader { + id: groupedIndicatorLoader + active: showGroupedIndicator + anchors.bottom: !dockRoot.isVertical && dockRoot.dockPosition === "bottom" ? parent.bottom : undefined + anchors.top: !dockRoot.isVertical && dockRoot.dockPosition === "top" ? parent.top : undefined + anchors.left: dockRoot.isVertical && dockRoot.dockPosition === "left" ? parent.left : undefined + anchors.right: dockRoot.isVertical && dockRoot.dockPosition === "right" ? parent.right : undefined + anchors.horizontalCenter: dockRoot.isVertical ? undefined : parent.horizontalCenter + anchors.verticalCenter: dockRoot.isVertical ? parent.verticalCenter : undefined + anchors.bottomMargin: !dockRoot.isVertical && dockRoot.dockPosition === "bottom" ? 1 : 0 + anchors.topMargin: !dockRoot.isVertical && dockRoot.dockPosition === "top" ? 1 : 0 + anchors.leftMargin: dockRoot.isVertical && dockRoot.dockPosition === "left" ? 1 : 0 + anchors.rightMargin: dockRoot.isVertical && dockRoot.dockPosition === "right" ? 1 : 0 + + sourceComponent: Settings.data.dock.groupIndicatorStyle === "dots" ? groupDotsIndicatorComponent : groupNumberIndicatorComponent + } + + Component { + id: groupNumberIndicatorComponent + Rectangle { + radius: Style.radiusS + color: Qt.alpha(Color.mSurface, 0.9) + border.color: Qt.alpha(Color.mOutline, 0.7) + border.width: Style.borderS + width: Math.max(14, numberLabel.implicitWidth + Style.marginXS) + height: Math.max(10, numberLabel.implicitHeight + 2) + + NText { + id: numberLabel + anchors.centerIn: parent + text: appButton.groupedIndicatorText + pointSize: Style.fontSizeXS + color: appButton.focusedWindowIndex >= 0 ? Color.mPrimary : Color.mOnSurfaceVariant + } + } + } + + Component { + id: groupDotsIndicatorComponent + Item { + readonly property int maxVisibleDots: 5 + readonly property int totalCount: Math.max(0, appButton.groupedCount) + readonly property int focusedIndex: appButton.focusedWindowIndex >= 0 ? appButton.focusedWindowIndex : 0 + readonly property int visibleCount: Math.min(totalCount, maxVisibleDots) + readonly property int dotSize: Math.max(2, Math.round(dockRoot.iconSize * 0.1)) + readonly property int dotSpacing: Math.max(1, Math.round(dotSize * 0.7)) + readonly property int pitch: dotSize + dotSpacing + readonly property int windowStart: { + if (totalCount <= maxVisibleDots) + return 0; + const centeredStart = focusedIndex - Math.floor(maxVisibleDots / 2); + const maxStart = totalCount - maxVisibleDots; + return Math.max(0, Math.min(maxStart, centeredStart)); + } + readonly property bool hasHiddenLeft: windowStart > 0 + readonly property bool hasHiddenRight: (windowStart + visibleCount) < totalCount + width: dockRoot.isVertical ? dotSize : (visibleCount * dotSize + Math.max(0, visibleCount - 1) * dotSpacing) + height: dockRoot.isVertical ? (visibleCount * dotSize + Math.max(0, visibleCount - 1) * dotSpacing) : dotSize + + Repeater { + model: parent.visibleCount + delegate: Rectangle { + readonly property int absoluteIndex: parent.windowStart + index + readonly property bool isFocusedDot: appButton.focusedWindowIndex >= 0 && absoluteIndex === appButton.focusedWindowIndex + readonly property bool isOverflowHint: (index === 0 && parent.hasHiddenLeft) || (index === parent.visibleCount - 1 && parent.hasHiddenRight) + width: isOverflowHint && !isFocusedDot ? Math.max(2, Math.round(parent.dotSize * 0.72)) : parent.dotSize + height: width + radius: width / 2 + x: dockRoot.isVertical ? Math.round((parent.dotSize - width) / 2) : (index * parent.pitch + Math.round((parent.dotSize - width) / 2)) + y: dockRoot.isVertical ? (index * parent.pitch + Math.round((parent.dotSize - width) / 2)) : Math.round((parent.dotSize - width) / 2) + color: isFocusedDot ? Color.mPrimary : Qt.alpha(Color.mOutline, 0.9) + opacity: isOverflowHint && !isFocusedDot ? 0.55 : 1.0 + } + } + } + } + } + } + + Loader { + id: launcherButtonEnd + active: Settings.data.dock.showLauncherIcon && Settings.data.dock.launcherPosition === "end" + visible: active + sourceComponent: launcherButtonComponent + readonly property real indicatorMargin: Math.max(3, Math.round(dockRoot.iconSize * 0.18)) + Layout.preferredWidth: active ? (dockRoot.isVertical ? dockRoot.iconSize + indicatorMargin * 2 : dockRoot.iconSize) : 0 + Layout.preferredHeight: active ? (dockRoot.isVertical ? dockRoot.iconSize : dockRoot.iconSize + indicatorMargin * 2) : 0 + Layout.minimumWidth: active ? Layout.preferredWidth : 0 + Layout.minimumHeight: active ? Layout.preferredHeight : 0 + Layout.maximumWidth: active ? Layout.preferredWidth : 0 + Layout.maximumHeight: active ? Layout.preferredHeight : 0 + Layout.alignment: Qt.AlignCenter + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Dock/DockMenu.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Dock/DockMenu.qml new file mode 100644 index 0000000..2146b34 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Dock/DockMenu.qml @@ -0,0 +1,804 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Commons +import qs.Modules.Panels.Settings +import qs.Services.UI +import qs.Widgets + +PopupWindow { + id: root + + property var toplevel: null + property var appData: null + property Item anchorItem: null + property ShellScreen targetScreen: null + + property string menuMode: "app" // "app" or "launcher" + property string launcherWidgetSection: "" + property int launcherWidgetIndex: -1 + property var launcherWidgetSettings: ({}) + + property bool hovered: menuHoverHandler.hovered + property var onAppClosed: null // Callback function for when an app is closed + property bool canAutoClose: false + + // Track which menu item is hovered + property int hoveredItem: -1 // -1: none, otherwise the index of the item in `items` + + property var items: [] + + signal requestClose + + property real menuContentWidth: 160 + property real menuMinWidth: 120 + property real menuMaxWidth: 360 + property real menuMaxHeight: Math.max(180, Math.min(420, Math.round((targetScreen ? targetScreen.height : 600) * 0.3))) + property int separatorCompactHeight: Style.borderS + Style.margin2S + property string forcedGroupMenuMode: "" + readonly property int separatorIndex: { + for (let i = 0; i < root.items.length; i++) { + if (root.items[i] && root.items[i].separator === true) + return i; + } + return -1; + } + readonly property bool splitExtendedLayout: separatorIndex >= 0 + readonly property var scrollItems: splitExtendedLayout ? root.items.slice(0, separatorIndex) : root.items + readonly property var fixedItems: splitExtendedLayout ? root.items.slice(separatorIndex + 1) : [] + readonly property real menuInnerHeight: Math.max(0, implicitHeight - Style.margin2M) + readonly property real fixedActionsHeight: listHeight(fixedItems) + readonly property real separatorBlockHeight: splitExtendedLayout ? separatorCompactHeight : 0 + readonly property real scrollAreaHeight: splitExtendedLayout ? Math.max(0, menuInnerHeight - fixedActionsHeight - separatorBlockHeight) : menuInnerHeight + readonly property bool listOverflowing: menuFlick && menuFlick.contentHeight > menuFlick.height + readonly property real menuBodyHeight: { + if (splitExtendedLayout) { + return listHeight(scrollItems) + separatorBlockHeight + fixedActionsHeight; + } + return listHeight(root.items); + } + + implicitWidth: menuContentWidth + Style.margin2M + implicitHeight: Math.min(menuBodyHeight + Style.margin2M, menuMaxHeight) + color: "transparent" + visible: false + + // Hidden text element for measuring text width + NText { + id: textMeasure + visible: false + pointSize: Style.fontSizeS + family: "Sans Serif" // Match your NText font if different + wrapMode: Text.NoWrap + elide: Text.ElideNone + } + + // Calculate the maximum width needed for all menu items + function calculateMenuWidth() { + let maxWidth = 0; // Start with 0, we'll apply minimum later + if (root.items && root.items.length > 0) { + for (let i = 0; i < root.items.length; i++) { + const item = root.items[i]; + if (item && item.text) { + // Calculate width: margins + icon (if present) + spacing + text width + let itemWidth = Style.margin2S; // left and right margins + + if (item.icon && item.icon !== "") { + itemWidth += Style.fontSizeL + Style.marginS; // icon + spacing + } + + // Measure actual text width + textMeasure.text = item.text; + const textWidth = textMeasure.contentWidth; + itemWidth += textWidth; + + if (itemWidth > maxWidth) { + maxWidth = itemWidth; + } + } + } + } + // Keep menu readable without allowing extremely wide labels. + menuContentWidth = Math.max(menuMinWidth, Math.min(menuMaxWidth, Math.ceil(maxWidth))); + } + + function getCurrentAppId() { + return appData?.appId || toplevel?.appId || ""; + } + + function getValidToplevels() { + if (!ToplevelManager || !ToplevelManager.toplevels) + return []; + const source = appData?.toplevels && appData.toplevels.length > 0 ? appData.toplevels : (toplevel ? [toplevel] : []); + const allToplevels = ToplevelManager.toplevels.values || []; + return source.filter(window => window && allToplevels.includes(window)); + } + + function getPrimaryToplevel() { + const windows = getValidToplevels(); + if (windows.length === 0) + return null; + if (ToplevelManager && ToplevelManager.activeToplevel && windows.includes(ToplevelManager.activeToplevel)) + return ToplevelManager.activeToplevel; + return windows[0]; + } + + function isItemActionable(index) { + if (index < 0 || index >= root.items.length) + return false; + const item = root.items[index]; + return item && typeof item.action === "function"; + } + + function rowHeightForItem(item) { + return item && item.separator === true ? 16 : 32; + } + + function listHeight(items) { + let total = 0; + if (!items) + return total; + for (let i = 0; i < items.length; i++) { + total += rowHeightForItem(items[i]); + } + return total; + } + + function initItems() { + if (menuMode === "launcher") { + root.items = [ + { + "icon": "adjustments", + "text": I18n.tr("actions.dock-settings"), + "action": function () { + handleDockSettings(); + } + }, + { + "icon": "adjustments", + "text": I18n.tr("actions.launcher-settings"), + "action": function () { + handleLauncherSettings(); + } + } + ]; + calculateMenuWidth(); + return; + } + + const windows = getValidToplevels(); + const primaryToplevel = getPrimaryToplevel(); + const appId = getCurrentAppId(); + const isRunning = windows.length > 0; + const isPinned = isAppPinned(appId); + const grouped = Settings.data.dock.groupApps && windows.length > 1; + const rawGroupMenuMode = forcedGroupMenuMode || Settings.data.dock.groupContextMenuMode || "extended"; + const menuModeForGroup = grouped ? ((rawGroupMenuMode === "list" || rawGroupMenuMode === "extended") ? rawGroupMenuMode : "extended") : "single"; + + var next = []; + + if (!grouped || menuModeForGroup === "single") { + if (isRunning) { + next.push({ + "icon": "eye", + "text": I18n.tr("common.focus"), + "action": function () { + handleFocus(primaryToplevel); + } + }); + } + + next.push({ + "icon": !isPinned ? "pin" : "unpin", + "text": !isPinned ? I18n.tr("common.pin") : I18n.tr("common.unpin"), + "action": function () { + handlePin(appId); + } + }); + + if (isRunning) { + next.push({ + "icon": "close", + "text": I18n.tr("common.close"), + "action": function () { + handleClose(primaryToplevel); + } + }); + } + } else { + windows.forEach((window, index) => { + const windowTitle = (window.title && window.title.trim() !== "") ? window.title : (appId || ("Window " + (index + 1))); + next.push({ + "icon": window === ToplevelManager?.activeToplevel ? "circle-filled" : "square-rounded", + "text": windowTitle, + "action": function () { + handleFocus(window); + } + }); + }); + + if (menuModeForGroup === "extended") { + next.push({ + "separator": true + }); + next.push({ + "icon": "eye", + "text": I18n.tr("common.focus"), + "action": function () { + handleFocus(primaryToplevel); + } + }); + next.push({ + "icon": !isPinned ? "pin" : "unpin", + "text": !isPinned ? I18n.tr("common.pin") : I18n.tr("common.unpin"), + "action": function () { + handlePin(appId); + } + }); + next.push({ + "icon": "close", + "text": I18n.tr("common.close") + " All", + "action": function () { + handleCloseAll(windows); + } + }); + } + } + + // Keep grouped list mode as a clean window switcher. + const canAddDesktopActions = !grouped || menuModeForGroup === "extended"; + + // Create a menu entry for each app-specific action defined in its .desktop file + if (canAddDesktopActions && typeof DesktopEntries !== 'undefined' && DesktopEntries.byId && appId) { + const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(appId) : DesktopEntries.byId(appId); + if (entry != null) { + entry.actions.forEach(function (action) { + next.push({ + "icon": "chevron-right", + "text": action.name, + "action": function () { + if (action.command && action.command.length > 0) { + Quickshell.execDetached(action.command); + } else if (action.execute) { + action.execute(); + } + if (Settings.data.dock.dockType === "attached") { + const panel = PanelService.getPanel("staticDockPanel", root.screen, false); + if (panel) + panel.close(); + } + } + }); + }); + } + } + + root.items = next; + // Force width recalculation when items change + calculateMenuWidth(); + } + + // Helper function to normalize app IDs for case-insensitive matching + function normalizeAppId(appId) { + if (!appId || typeof appId !== 'string') + return ""; + return appId.toLowerCase().trim(); + } + + // Helper function to get desktop entry ID from an app ID + function getDesktopEntryId(appId) { + if (!appId) + return appId; + + // Try to find the desktop entry using heuristic lookup + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) { + try { + const entry = DesktopEntries.heuristicLookup(appId); + if (entry && entry.id) { + return entry.id; + } + } catch (e) + // Fall through to return original appId + {} + } + + // Try direct lookup + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId) { + try { + const entry = DesktopEntries.byId(appId); + if (entry && entry.id) { + return entry.id; + } + } catch (e) + // Fall through to return original appId + {} + } + + // Return original appId if we can't find a desktop entry + return appId; + } + + // Helper functions for pin/unpin functionality + function isAppPinned(appId) { + if (!appId) + return false; + const pinnedApps = Settings.data.dock.pinnedApps || []; + const normalizedId = normalizeAppId(appId); + return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId); + } + + function toggleAppPin(appId) { + if (!appId) + return; + + // Get the desktop entry ID for consistent pinning + const desktopEntryId = getDesktopEntryId(appId); + const normalizedId = normalizeAppId(desktopEntryId); + + let pinnedApps = (Settings.data.dock.pinnedApps || []).slice(); // Create a copy + + // Find existing pinned app with case-insensitive matching + const existingIndex = pinnedApps.findIndex(pinnedId => normalizeAppId(pinnedId) === normalizedId); + const isPinned = existingIndex >= 0; + + if (isPinned) { + // Unpin: remove from array + pinnedApps.splice(existingIndex, 1); + } else { + // Pin: add desktop entry ID to array + pinnedApps.push(desktopEntryId); + } + + // Update the settings + Settings.data.dock.pinnedApps = pinnedApps; + } + + // Dock position for context menu placement + property string dockPosition: "bottom" + + anchor.item: anchorItem + // Position menu on opposite side of dock with comfortable spacing + anchor.rect.x: { + if (!anchorItem) + return 0; + switch (dockPosition) { + case "left": + return anchorItem.width + Style.marginL; // Open to right of dock + case "right": + return -implicitWidth - Style.marginL; // Open to left of dock + default: + return (anchorItem.width - implicitWidth) / 2; // Center horizontally + } + } + anchor.rect.y: { + if (!anchorItem) + return 0; + switch (dockPosition) { + case "top": + return anchorItem.height + Style.marginL; // Open below dock + case "bottom": + return -implicitHeight - Style.marginL; // Open above dock (default) + case "left": + case "right": + return (anchorItem.height - implicitHeight) / 2; // Center vertically + default: + return -implicitHeight - Style.marginL; + } + } + + function show(item, toplevelData, screen, groupModeOverride) { + if (!item) { + return; + } + + // First hide completely + visible = false; + + // Then set up new data + anchorItem = item; + if (toplevelData && typeof toplevelData === "object" && (toplevelData.appId !== undefined || toplevelData.toplevels !== undefined)) { + appData = toplevelData; + toplevel = toplevelData.toplevel || null; + } else { + appData = toplevelData ? { + "appId": toplevelData.appId, + "toplevel": toplevelData, + "toplevels": toplevelData ? [toplevelData] : [] + } : null; + toplevel = toplevelData; + } + targetScreen = screen || null; + forcedGroupMenuMode = groupModeOverride || ""; + initItems(); + + visible = true; + canAutoClose = false; + gracePeriodTimer.restart(); + } + + // Helper function to determine which menu item is under the mouse + function getHoveredItem(mouseY) { + const startY = Style.marginM; + const localY = mouseY - startY; + + if (localY < 0) + return -1; + + function findIndexInList(items, relativeY, baseIndex) { + let offset = 0; + for (let i = 0; i < items.length; i++) { + const h = rowHeightForItem(items[i]); + if (relativeY >= offset && relativeY < offset + h) + return baseIndex + i; + offset += h; + } + return -1; + } + + if (splitExtendedLayout) { + if (localY < scrollAreaHeight) { + return findIndexInList(scrollItems, localY + (menuFlick ? menuFlick.contentY : 0), 0); + } + if (localY < scrollAreaHeight + separatorBlockHeight) { + return -1; + } + return findIndexInList(fixedItems, localY - scrollAreaHeight - separatorBlockHeight, separatorIndex + 1); + } else { + return findIndexInList(scrollItems, localY + (menuFlick ? menuFlick.contentY : 0), 0); + } + } + + function fixedItemGlobalIndex(localIndex) { + if (!splitExtendedLayout) + return localIndex; + return separatorIndex + 1 + localIndex; + } + + function isScrollableHovered(mouseY) { + const localY = mouseY - Style.marginM; + return localY >= 0 && localY < scrollAreaHeight; + } + + function onWheelScroll(deltaY) { + if (!menuFlick || menuFlick.contentHeight <= menuFlick.height) + return; + const nextY = menuFlick.contentY - deltaY; + menuFlick.contentY = Math.max(0, Math.min(nextY, menuFlick.contentHeight - menuFlick.height)); + } + + function resetMenuState() { + root.items.length = 0; + root.appData = null; + root.toplevel = null; + root.forcedGroupMenuMode = ""; + menuContentWidth = menuMinWidth; + hoveredItem = -1; + if (menuFlick) + menuFlick.contentY = 0; + } + + function hide() { + visible = false; + resetMenuState(); + } + + function hideWithoutReset() { + visible = false; + } + + function closeAndReset() { + hide(); + root.requestClose(); + } + + function handleFocus(targetToplevel) { + if (targetToplevel?.activate) { + targetToplevel.activate(); + } + closeAndReset(); + } + + function handlePin(appId) { + if (appId) { + root.toggleAppPin(appId); + } + Qt.callLater(() => closeAndReset()); + } + + function handleClose(targetToplevel) { + const isValidToplevel = targetToplevel && ToplevelManager && ToplevelManager.toplevels.values.includes(targetToplevel); + + if (isValidToplevel && targetToplevel.close) { + targetToplevel.close(); + if (root.onAppClosed && typeof root.onAppClosed === "function") { + Qt.callLater(root.onAppClosed); + } + } + closeAndReset(); + } + + function handleCloseAll(windows) { + windows.forEach(window => { + if (window && ToplevelManager && ToplevelManager.toplevels.values.includes(window) && window.close) { + window.close(); + } + }); + if (root.onAppClosed && typeof root.onAppClosed === "function") { + Qt.callLater(root.onAppClosed); + } + closeAndReset(); + } + + function handleLauncherSettings() { + if (targetScreen) { + var panel = PanelService.getPanel("settingsPanel", targetScreen); + panel.requestedTab = SettingsPanel.Tab.Launcher; + panel.toggle(); + } + closeAndReset(); + } + + function handleDockSettings() { + if (targetScreen) { + var panel = PanelService.getPanel("settingsPanel", targetScreen); + panel.requestedTab = SettingsPanel.Tab.Dock; + panel.toggle(); + } + closeAndReset(); + } + + function handleLauncherWidgetSettings() { + if (targetScreen && launcherWidgetSection && launcherWidgetIndex >= 0) { + BarService.openWidgetSettings(targetScreen, launcherWidgetSection, launcherWidgetIndex, "Launcher", launcherWidgetSettings || {}); + } + closeAndReset(); + } + + // Short delay to ignore spurious events + Timer { + id: gracePeriodTimer + interval: 1500 + repeat: false + onTriggered: { + root.canAutoClose = true; + if (!menuHoverHandler.hovered) { + closeTimer.start(); + } + } + } + + Timer { + id: closeTimer + interval: 500 + repeat: false + running: false + onTriggered: { + root.hideWithoutReset(); + } + } + + Rectangle { + anchors.fill: parent + anchors.margins: border.width + color: Color.mSurfaceVariant + radius: Style.radiusS + border.color: Color.mOutline + border.width: Style.borderS + + HoverHandler { + id: menuHoverHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onHoveredChanged: { + if (hovered) { + closeTimer.stop(); + } else { + root.hoveredItem = -1; + if (root.canAutoClose) { + closeTimer.start(); + } + } + } + } + + WheelHandler { + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onWheel: event => { + if (!root.isScrollableHovered(event.y)) + return; + const delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y : event.angleDelta.y / 2; + root.onWheelScroll(delta); + event.accepted = true; + } + } + + Flickable { + id: menuFlick + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: Style.marginM + anchors.rightMargin: Style.marginM + anchors.topMargin: Style.marginM + height: root.scrollAreaHeight + clip: true + contentWidth: width + contentHeight: scrollColumn.height + flickableDirection: Flickable.VerticalFlick + boundsBehavior: Flickable.StopAtBounds + interactive: contentHeight > height + + ScrollBar.vertical: ScrollBar { + id: menuScrollBar + policy: ScrollBar.AsNeeded + visible: root.listOverflowing + interactive: true + hoverEnabled: true + } + + Column { + id: scrollColumn + width: menuFlick.width + spacing: 0 + + Repeater { + model: root.scrollItems + + Rectangle { + readonly property bool isSeparator: modelData && modelData.separator === true + width: scrollColumn.width + height: root.rowHeightForItem(modelData) + color: (!isSeparator && root.hoveredItem === index) ? Color.mHover : "transparent" + radius: Style.radiusXS + + Row { + id: rowLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Style.marginS + anchors.rightMargin: Style.marginS + spacing: Style.marginS + visible: !isSeparator + + NIcon { + icon: modelData.icon + pointSize: Style.fontSizeL + color: root.hoveredItem === index ? Color.mOnHover : Color.mOnSurfaceVariant + visible: icon !== "" + anchors.verticalCenter: parent.verticalCenter + } + + NText { + text: modelData.text + pointSize: Style.fontSizeS + color: root.hoveredItem === index ? Color.mOnHover : Color.mOnSurfaceVariant + anchors.verticalCenter: parent.verticalCenter + width: rowLayout.width - ((modelData.icon && modelData.icon !== "") ? (Style.fontSizeL + Style.marginS) : 0) + elide: Text.ElideRight + } + } + + MouseArea { + anchors.fill: parent + enabled: !parent.isSeparator && root.isItemActionable(index) + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + + onEntered: { + root.hoveredItem = index; + } + + onExited: { + if (root.hoveredItem === index) { + root.hoveredItem = -1; + } + } + + onClicked: { + if (root.isItemActionable(index)) { + root.items[index].action.call(); + } + } + } + } + } + } + } + + Rectangle { + id: separator + visible: root.splitExtendedLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: menuFlick.bottom + anchors.leftMargin: Style.marginS + anchors.rightMargin: Style.marginS + anchors.topMargin: Style.marginS + height: Style.borderS + color: Qt.alpha(Color.mOutline, 0.7) + radius: Style.radiusXS + } + + Column { + id: fixedColumn + visible: root.splitExtendedLayout && root.fixedItems.length > 0 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.leftMargin: Style.marginM + anchors.rightMargin: Style.marginM + anchors.topMargin: Style.marginS + anchors.bottomMargin: Style.marginM + anchors.top: separator.bottom + spacing: 0 + + Repeater { + model: root.fixedItems + + Rectangle { + id: fixedItemRect + readonly property int globalIndex: root.fixedItemGlobalIndex(index) + width: fixedColumn.width + height: root.rowHeightForItem(modelData) + color: root.hoveredItem === globalIndex ? Color.mHover : "transparent" + radius: Style.radiusXS + + Row { + id: fixedRowLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Style.marginS + anchors.rightMargin: Style.marginS + spacing: Style.marginS + + NIcon { + icon: modelData.icon + pointSize: Style.fontSizeL + color: root.hoveredItem === fixedItemRect.globalIndex ? Color.mOnHover : Color.mOnSurfaceVariant + visible: icon !== "" + anchors.verticalCenter: parent.verticalCenter + } + + NText { + text: modelData.text + pointSize: Style.fontSizeS + color: root.hoveredItem === fixedItemRect.globalIndex ? Color.mOnHover : Color.mOnSurfaceVariant + anchors.verticalCenter: parent.verticalCenter + width: fixedRowLayout.width - ((modelData.icon && modelData.icon !== "") ? (Style.fontSizeL + Style.marginS) : 0) + elide: Text.ElideRight + } + } + + MouseArea { + anchors.fill: parent + enabled: root.isItemActionable(parent.globalIndex) + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton + + onEntered: { + root.hoveredItem = parent.globalIndex; + } + + onExited: { + if (root.hoveredItem === parent.globalIndex) { + root.hoveredItem = -1; + } + } + + onClicked: { + if (root.isItemActionable(parent.globalIndex)) { + root.items[parent.globalIndex].action.call(); + } + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/LockScreen/LockContext.qml b/fedora/.config/quickshell/noctalia-shell/Modules/LockScreen/LockContext.qml new file mode 100644 index 0000000..40a6b35 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/LockScreen/LockContext.qml @@ -0,0 +1,170 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pam +import qs.Commons +import qs.Services.System + +Scope { + id: root + signal unlocked + signal failed + + property string currentText: "" + property bool waitingForPassword: false + property bool unlockInProgress: false + property bool showFailure: false + property bool showInfo: false + property string errorMessage: "" + property string infoMessage: "" + + readonly property string pamConfigDirectory: "/etc/pam.d" + property string pamConfig: Quickshell.env("NOCTALIA_PAM_SERVICE") || "login" + property bool pamReady: false + + Component.onCompleted: { + if (Quickshell.env("NOCTALIA_PAM_SERVICE")) { + Logger.i("LockContext", "NOCTALIA_PAM_SERVICE is set, using system PAM config: /etc/pam.d/" + pamConfig); + pamReady = true; + } else { + Logger.i("LockContext", "Probing for best PAM service..."); + detectPamServiceProc.running = true; + } + } + + Process { + id: detectPamServiceProc + command: ["sh", "-c", " + if [ -f /etc/pam.d/login ]; then echo 'login'; exit 0; fi; + if [ -f /etc/pam.d/system-auth ]; then echo 'system-auth'; exit 0; fi; + if [ -f /etc/pam.d/common-auth ]; then echo 'common-auth'; exit 0; fi; + echo 'login'; + "] + stdout: StdioCollector { + onStreamFinished: { + const service = String(text || "").trim(); + if (service.length > 0) { + root.pamConfig = service; + Logger.i("LockContext", "Detected PAM service: " + service); + } else { + Logger.w("LockContext", "Failed to detect PAM service, defaulting to login"); + } + root.pamReady = true; + } + } + stderr: StdioCollector {} + } + + onPamReadyChanged: { + if (pamReady) { + if (Settings.data.general.autoStartAuth && currentText === "") { + pam.start(); + } + } + } + + onShowInfoChanged: { + if (showInfo) { + showFailure = false; + } + } + + onShowFailureChanged: { + if (showFailure) { + showInfo = false; + } + } + + onCurrentTextChanged: { + if (currentText !== "") { + showInfo = false; + showFailure = false; + if (!waitingForPassword) { + pam.abort(); + } + if (Settings.data.general.allowPasswordWithFprintd) { + occupyFingerprintSensorProc.running = true; + } + } else { + occupyFingerprintSensorProc.running = false; + if (pamReady && Settings.data.general.autoStartAuth) { + pam.start(); + } + } + } + + function tryUnlock() { + if (!pamReady) { + Logger.w("LockContext", "PAM not ready yet, ignoring unlock attempt"); + return; + } + + if (waitingForPassword) { + pam.respond(currentText); + unlockInProgress = true; + waitingForPassword = false; + showInfo = false; + return; + } + + Logger.i("LockContext", "Starting PAM authentication for user:", pam.user); + pam.start(); + } + + Process { + id: occupyFingerprintSensorProc + command: ["fprintd-verify"] + } + + PamContext { + id: pam + configDirectory: root.pamConfigDirectory + config: root.pamConfig + user: HostService.username + + onPamMessage: { + Logger.i("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:", responseRequired); + + if (this.responseRequired) { + Logger.i("LockContext", "Responding to PAM with password"); + if (root.currentText !== "") { + this.respond(root.currentText); + unlockInProgress = true; + } else { + root.waitingForPassword = true; + infoMessage = I18n.tr("lock-screen.password"); + showInfo = true; + } + } else if (messageIsError) { + errorMessage = message; + showFailure = true; + } else { + infoMessage = message; + showInfo = true; + } + } + + onCompleted: result => { + Logger.i("LockContext", "PAM completed with result:", result); + if (result === PamResult.Success) { + Logger.i("LockContext", "Authentication successful"); + root.unlocked(); + } else { + Logger.i("LockContext", "Authentication failed"); + root.currentText = ""; + errorMessage = I18n.tr("authentication.failed"); + showFailure = true; + root.failed(); + } + root.unlockInProgress = false; + } + + onError: { + Logger.i("LockContext", "PAM error:", error, "message:", message); + errorMessage = message || "Authentication error"; + showFailure = true; + root.unlockInProgress = false; + root.failed(); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/LockScreen/LockScreen.qml b/fedora/.config/quickshell/noctalia-shell/Modules/LockScreen/LockScreen.qml new file mode 100644 index 0000000..88698b5 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/LockScreen/LockScreen.qml @@ -0,0 +1,387 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Pam +import Quickshell.Wayland +import qs.Commons +import qs.Services.Compositor +import qs.Services.Hardware +import qs.Services.Keyboard +import qs.Services.Media +import qs.Services.UI +import qs.Widgets + +Loader { + id: root + active: false + + // Track if the visualizer should be shown (lockscreen active + media playing + non-compact mode) + readonly property bool needsSpectrum: root.active && !Settings.data.general.compactLockScreen && Settings.data.audio.visualizerType !== "" && Settings.data.audio.visualizerType !== "none" + + onActiveChanged: { + if (root.active && root.needsSpectrum) { + SpectrumService.registerComponent("lockscreen"); + } else { + SpectrumService.unregisterComponent("lockscreen"); + } + + if (root.active) { + LockKeysService.registerComponent("lockscreen"); + } else { + LockKeysService.unregisterComponent("lockscreen"); + } + } + + onNeedsSpectrumChanged: { + if (root.needsSpectrum) { + SpectrumService.registerComponent("lockscreen"); + } else { + SpectrumService.unregisterComponent("lockscreen"); + } + } + + Component.onCompleted: { + // Register with panel service + PanelService.lockScreen = this; + } + + Component.onDestruction: { + SpectrumService.unregisterComponent("lockscreen"); + LockKeysService.unregisterComponent("lockscreen"); + } + + Timer { + id: unloadAfterUnlockTimer + interval: 250 + repeat: false + onTriggered: root.active = false + } + + function scheduleUnloadAfterUnlock() { + unloadAfterUnlockTimer.start(); + } + + sourceComponent: Component { + Item { + id: lockContainer + + LockContext { + id: lockContext + onUnlocked: { + lockSession.locked = false; + root.scheduleUnloadAfterUnlock(); + lockContext.currentText = ""; + } + onFailed: { + lockContext.currentText = ""; + } + } + + // Whether any monitor from the user's lockScreenMonitors list is currently connected. + readonly property bool anyConfiguredMonitorConnected: { + const configured = Settings.data.general.lockScreenMonitors; + if (!configured || configured.length === 0) + return false; + return (Quickshell.screens || []).some(s => configured.includes(s.name)); + } + + WlSessionLock { + id: lockSession + locked: root.active + + WlSessionLockSurface { + id: lockSurface + + Loader { + anchors.fill: parent + active: true + sourceComponent: (!lockContainer.anyConfiguredMonitorConnected || Settings.data.general.lockScreenMonitors.includes(lockSurface.screen?.name)) ? fullLockScreenComponent : blackScreenComponent + } + + Component { + id: fullLockScreenComponent + + Item { + Item { + id: batteryIndicator + + property bool isReady: BatteryService.batteryReady + property real percent: BatteryService.batteryPercentage + property bool charging: BatteryService.batteryCharging + property bool pluggedIn: BatteryService.batteryPluggedIn + property bool batteryVisible: isReady + property string icon: BatteryService.batteryIcon + } + + Item { + id: keyboardLayout + property string currentLayout: KeyboardLayoutService.currentLayout + } + + // Background with wallpaper, gradient, and screen corners + LockScreenBackground { + id: backgroundComponent + screen: lockSurface.screen + } + + Item { + anchors.fill: parent + + // Mouse area to trigger focus on cursor movement (workaround for Hyprland focus issues) + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + onEntered: { + // Avoid repeatedly forcing focus on every mouse move. + // This can churn text-input surface state during monitor/suspend transitions. + if (passwordInput && !passwordInput.activeFocus) { + passwordInput.forceActiveFocus(); + } + } + } + + // Header with avatar, welcome, time, date + LockScreenHeader { + id: headerComponent + } + + // Info notification + Rectangle { + width: infoRowLayout.implicitWidth + Style.marginXL * 1.5 + height: 50 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: (Settings.data.general.compactLockScreen ? 280 : 360) * Style.uiScaleRatio + radius: Style.radiusL + color: Color.mTertiary + visible: lockContext.showInfo && lockContext.infoMessage && !panelComponent.timerActive + opacity: visible ? 1.0 : 0.0 + + RowLayout { + id: infoRowLayout + anchors.centerIn: parent + spacing: Style.marginM + + NIcon { + icon: "circle-key" + pointSize: Style.fontSizeXL + color: Color.mOnTertiary + } + + NText { + text: lockContext.infoMessage + color: Color.mOnTertiary + pointSize: Style.fontSizeL + horizontalAlignment: Text.AlignHCenter + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + } + + // Error notification + Rectangle { + width: errorRowLayout.implicitWidth + Style.marginXL * 1.5 + height: 50 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: (Settings.data.general.compactLockScreen ? 280 : 360) * Style.uiScaleRatio + radius: Style.radiusL + color: Color.mError + visible: lockContext.showFailure && lockContext.errorMessage && !panelComponent.timerActive + opacity: visible ? 1.0 : 0.0 + + RowLayout { + id: errorRowLayout + anchors.centerIn: parent + spacing: Style.marginM + + NIcon { + icon: "alert-circle" + pointSize: Style.fontSizeXL + color: Color.mOnError + } + + NText { + text: lockContext.errorMessage || "Authentication failed" + color: Color.mOnError + pointSize: Style.fontSizeL + horizontalAlignment: Text.AlignHCenter + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + } + + // Countdown notification + Rectangle { + width: countdownRowLayout.implicitWidth + Style.marginXL * 1.5 + height: 50 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: (Settings.data.general.compactLockScreen ? 280 : 360) * Style.uiScaleRatio + radius: Style.radiusL + color: Color.mSurface + visible: panelComponent.timerActive + opacity: visible ? 1.0 : 0.0 + + RowLayout { + id: countdownRowLayout + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + NIcon { + icon: "clock" + pointSize: Style.fontSizeXL + color: Color.mPrimary + } + + NText { + text: I18n.tr("session-menu.action-in-seconds", { + "action": I18n.tr("common." + panelComponent.pendingAction), + "seconds": Math.ceil(panelComponent.timeRemaining / 1000) + }) + color: Color.mOnSurface + pointSize: Style.fontSizeL + horizontalAlignment: Text.AlignHCenter + font.weight: Style.fontWeightBold + } + + Item { + Layout.fillWidth: true + } + + NIconButton { + icon: "x" + tooltipText: I18n.tr("session-menu.cancel-timer") + baseSize: 32 + colorBg: Qt.alpha(Color.mPrimary, 0.1) + colorFg: Color.mPrimary + colorBgHover: Color.mPrimary + onClicked: panelComponent.cancelTimer() + } + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + } + + // Hidden input that receives actual text + TextInput { + id: passwordInput + width: 0 + height: 0 + visible: false + enabled: !lockContext.unlockInProgress + echoMode: TextInput.Password + passwordMaskDelay: 0 + + // Bidirectional sync — avoids a declarative binding which breaks on input + onTextChanged: { + if (lockContext.currentText !== text) + lockContext.currentText = text; + } + Connections { + target: lockContext + function onCurrentTextChanged() { + if (passwordInput.text !== lockContext.currentText) + passwordInput.text = lockContext.currentText; + } + } + + Keys.onPressed: function (event) { + if (Keybinds.checkKey(event, 'enter', Settings)) { + lockContext.tryUnlock(); + event.accepted = true; + } + if (Keybinds.checkKey(event, 'escape', Settings) && panelComponent.timerActive) { + panelComponent.cancelTimer(); + event.accepted = true; + } + } + + Component.onCompleted: forceActiveFocus() + } + + // Main panel with password, weather, media, session controls + LockScreenPanel { + id: panelComponent + lockControl: lockContext + batteryIndicator: batteryIndicator + keyboardLayout: keyboardLayout + passwordInput: passwordInput + } + } + } + } + + Component { + id: blackScreenComponent + + // Black surface for disabled monitors — still captures keyboard for password entry + Rectangle { + anchors.fill: parent + color: "black" + + TextInput { + id: blackScreenPasswordInput + width: 0 + height: 0 + visible: false + enabled: !lockContext.unlockInProgress + echoMode: TextInput.Password + passwordMaskDelay: 0 + + // Bidirectional sync — avoids a declarative binding which breaks on input + onTextChanged: { + if (lockContext.currentText !== text) + lockContext.currentText = text; + } + Connections { + target: lockContext + function onCurrentTextChanged() { + if (blackScreenPasswordInput.text !== lockContext.currentText) + blackScreenPasswordInput.text = lockContext.currentText; + } + } + + Keys.onPressed: function (event) { + if (Keybinds.checkKey(event, 'enter', Settings)) { + lockContext.tryUnlock(); + event.accepted = true; + } + } + + Component.onCompleted: forceActiveFocus() + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + onPositionChanged: blackScreenPasswordInput.forceActiveFocus() + } + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/LockScreen/LockScreenBackground.qml b/fedora/.config/quickshell/noctalia-shell/Modules/LockScreen/LockScreenBackground.qml new file mode 100644 index 0000000..dde0839 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/LockScreen/LockScreenBackground.qml @@ -0,0 +1,303 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import qs.Commons +import qs.Services.Compositor +import qs.Services.Power +import qs.Services.UI + +Item { + id: root + anchors.fill: parent + + // Cached wallpaper path - exposed for parent components + property string resolvedWallpaperPath: "" + property color tintColor: Settings.data.colorSchemes.darkMode ? Color.mSurface : Color.mOnSurface + + required property var screen + + // Request preprocessed wallpaper when lock screen becomes active or dimensions change + Component.onCompleted: { + if (screen) { + Qt.callLater(requestCachedWallpaper); + } + } + + onWidthChanged: { + if (screen && width > 0 && height > 0) { + Qt.callLater(requestCachedWallpaper); + } + } + + onHeightChanged: { + if (screen && width > 0 && height > 0) { + Qt.callLater(requestCachedWallpaper); + } + } + + // Listen for wallpaper changes + Connections { + target: WallpaperService + function onWallpaperChanged(screenName, path) { + if (screen && screenName === screen.name) { + Qt.callLater(requestCachedWallpaper); + } + } + } + + // Listen for display scale changes + Connections { + target: CompositorService + function onDisplayScalesChanged() { + if (screen && width > 0 && height > 0) { + Qt.callLater(requestCachedWallpaper); + } + } + } + + function requestCachedWallpaper() { + if (!screen || width <= 0 || height <= 0) { + return; + } + + // Check for solid color mode first + if (Settings.data.wallpaper.useSolidColor) { + resolvedWallpaperPath = ""; + return; + } + + const originalPath = WallpaperService.getWallpaper(screen.name) || ""; + if (originalPath === "") { + resolvedWallpaperPath = ""; + return; + } + + // Handle solid color paths + if (WallpaperService.isSolidColorPath(originalPath)) { + resolvedWallpaperPath = ""; + return; + } + + if (!ImageCacheService || !ImageCacheService.initialized) { + // Fallback to original if services not ready + resolvedWallpaperPath = originalPath; + return; + } + + const compositorScale = CompositorService.getDisplayScale(screen.name); + const targetWidth = Math.round(width * compositorScale); + const targetHeight = Math.round(height * compositorScale); + if (targetWidth <= 0 || targetHeight <= 0) { + return; + } + + // Don't set resolvedWallpaperPath until cache is ready + // This prevents loading the original huge image + ImageCacheService.getLarge(originalPath, targetWidth, targetHeight, function (cachedPath, success) { + if (success) { + resolvedWallpaperPath = cachedPath; + } else { + // Only fall back to original if caching failed + resolvedWallpaperPath = originalPath; + } + }); + } + + // Background - solid color or black fallback + Rectangle { + anchors.fill: parent + color: Settings.data.wallpaper.useSolidColor ? Settings.data.wallpaper.solidColor : "#000000" + } + + Image { + id: lockBgImage + visible: source !== "" && Settings.data.wallpaper.enabled && !Settings.data.wallpaper.useSolidColor && (!PowerProfileService.noctaliaPerformanceMode || !Settings.data.noctaliaPerformance.disableWallpaper) + anchors.fill: parent + fillMode: Image.PreserveAspectCrop + source: resolvedWallpaperPath + cache: false + smooth: true + mipmap: false + antialiasing: true + + layer.enabled: Settings.data.general.lockScreenBlur > 0 && !PowerProfileService.noctaliaPerformanceMode + layer.smooth: false + layer.effect: MultiEffect { + blurEnabled: true + blur: Settings.data.general.lockScreenBlur + blurMax: 48 + } + + // Tint overlay + Rectangle { + anchors.fill: parent + color: root.tintColor + opacity: Settings.data.general.lockScreenTint + } + } + + Rectangle { + visible: !Settings.data.wallpaper.useSolidColor + anchors.fill: parent + gradient: Gradient { + GradientStop { + position: 0.0 + color: Qt.alpha(Color.mShadow, 0.4) + } + GradientStop { + position: 0.3 + color: Qt.alpha(Color.mShadow, 0.2) + } + GradientStop { + position: 0.7 + color: Qt.alpha(Color.mShadow, 0.25) + } + GradientStop { + position: 1.0 + color: Qt.alpha(Color.mShadow, 0.5) + } + } + } + + // Screen corners for lock screen + Item { + anchors.fill: parent + visible: Settings.data.general.showScreenCorners + + property color cornerColor: Settings.data.general.forceBlackScreenCorners ? "black" : Color.mSurface + property real cornerRadius: Style.screenRadius + property real cornerSize: Style.screenRadius + + // Top-left concave corner + Canvas { + anchors.top: parent.top + anchors.left: parent.left + width: parent.cornerSize + height: parent.cornerSize + antialiasing: true + renderTarget: Canvas.FramebufferObject + smooth: false + + onPaint: { + const ctx = getContext("2d"); + if (!ctx) + return; + ctx.reset(); + ctx.clearRect(0, 0, width, height); + + ctx.fillStyle = parent.cornerColor; + ctx.fillRect(0, 0, width, height); + + ctx.globalCompositeOperation = "destination-out"; + ctx.fillStyle = "#ffffff"; + ctx.beginPath(); + ctx.arc(width, height, parent.cornerRadius, 0, 2 * Math.PI); + ctx.fill(); + } + + onWidthChanged: if (available) + requestPaint() + onHeightChanged: if (available) + requestPaint() + } + + // Top-right concave corner + Canvas { + anchors.top: parent.top + anchors.right: parent.right + width: parent.cornerSize + height: parent.cornerSize + antialiasing: true + renderTarget: Canvas.FramebufferObject + smooth: true + + onPaint: { + const ctx = getContext("2d"); + if (!ctx) + return; + ctx.reset(); + ctx.clearRect(0, 0, width, height); + + ctx.fillStyle = parent.cornerColor; + ctx.fillRect(0, 0, width, height); + + ctx.globalCompositeOperation = "destination-out"; + ctx.fillStyle = "#ffffff"; + ctx.beginPath(); + ctx.arc(0, height, parent.cornerRadius, 0, 2 * Math.PI); + ctx.fill(); + } + + onWidthChanged: if (available) + requestPaint() + onHeightChanged: if (available) + requestPaint() + } + + // Bottom-left concave corner + Canvas { + anchors.bottom: parent.bottom + anchors.left: parent.left + width: parent.cornerSize + height: parent.cornerSize + antialiasing: true + renderTarget: Canvas.FramebufferObject + smooth: true + + onPaint: { + const ctx = getContext("2d"); + if (!ctx) + return; + ctx.reset(); + ctx.clearRect(0, 0, width, height); + + ctx.fillStyle = parent.cornerColor; + ctx.fillRect(0, 0, width, height); + + ctx.globalCompositeOperation = "destination-out"; + ctx.fillStyle = "#ffffff"; + ctx.beginPath(); + ctx.arc(width, 0, parent.cornerRadius, 0, 2 * Math.PI); + ctx.fill(); + } + + onWidthChanged: if (available) + requestPaint() + onHeightChanged: if (available) + requestPaint() + } + + // Bottom-right concave corner + Canvas { + anchors.bottom: parent.bottom + anchors.right: parent.right + width: parent.cornerSize + height: parent.cornerSize + antialiasing: true + renderTarget: Canvas.FramebufferObject + smooth: true + + onPaint: { + const ctx = getContext("2d"); + if (!ctx) + return; + ctx.reset(); + ctx.clearRect(0, 0, width, height); + + ctx.fillStyle = parent.cornerColor; + ctx.fillRect(0, 0, width, height); + + ctx.globalCompositeOperation = "destination-out"; + ctx.fillStyle = "#ffffff"; + ctx.beginPath(); + ctx.arc(0, 0, parent.cornerRadius, 0, 2 * Math.PI); + ctx.fill(); + } + + onWidthChanged: if (available) + requestPaint() + onHeightChanged: if (available) + requestPaint() + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/LockScreen/LockScreenHeader.qml b/fedora/.config/quickshell/noctalia-shell/Modules/LockScreen/LockScreenHeader.qml new file mode 100644 index 0000000..8792cc0 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/LockScreen/LockScreenHeader.qml @@ -0,0 +1,196 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.System +import qs.Widgets + +// Time, Date, and User Profile Container +Rectangle { + id: root + + readonly property bool animationsEnabled: Settings.data.general.lockScreenAnimations || false + + // Use timer-driven properties instead of Time.now to avoid per-frame repaints. + // Time.now updates every frame (~60+ Hz); these update only when needed. + property date currentTime: new Date() + property date currentDate: new Date() + + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: root.currentTime = new Date() + } + + Timer { + interval: 60000 + running: true + repeat: true + onTriggered: root.currentDate = new Date() + } + + width: Math.max(500, contentRow.implicitWidth + 32) + height: Math.max(120, contentRow.implicitHeight + 32) + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 100 + radius: Style.radiusL + color: Color.mSurface + border.color: Qt.alpha(Color.mOutline, 0.2) + border.width: Style.borderS + + RowLayout { + id: contentRow + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.margin2XL + + // Left side: Avatar + Rectangle { + Layout.preferredWidth: 70 + Layout.preferredHeight: 70 + Layout.alignment: Qt.AlignVCenter + radius: width / 2 + color: "transparent" + + Rectangle { + anchors.fill: parent + radius: parent.radius + color: "transparent" + border.color: Qt.alpha(Color.mPrimary, 0.8) + border.width: Style.borderM + + SequentialAnimation on border.color { + loops: Animation.Infinite + running: root.animationsEnabled + ColorAnimation { + to: Qt.alpha(Color.mPrimary, 1.0) + duration: 2000 + easing.type: Easing.InOutQuad + } + ColorAnimation { + to: Qt.alpha(Color.mPrimary, 0.8) + duration: 2000 + easing.type: Easing.InOutQuad + } + } + } + + NImageRounded { + anchors.centerIn: parent + width: 66 + height: 66 + radius: width / 2 + imagePath: Settings.preprocessPath(Settings.data.general.avatarImage) + fallbackIcon: "person" + + SequentialAnimation on scale { + loops: Animation.Infinite + running: root.animationsEnabled + NumberAnimation { + to: 1.02 + duration: 4000 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 1.0 + duration: 4000 + easing.type: Easing.InOutQuad + } + } + } + } + + // Center: User Info Column (left-aligned text) + ColumnLayout { + Layout.alignment: Qt.AlignVCenter + spacing: Style.marginXXS + + // Welcome back + Username on one line + NText { + text: I18n.tr("system.welcome-back") + " " + HostService.displayName + "!" + pointSize: Style.fontSizeXXL + color: Color.mOnSurface + horizontalAlignment: Text.AlignLeft + } + + // Date below + NText { + text: { + var dateString = I18n.locale.toString(root.currentDate, I18n.dateFormat()); + return dateString.charAt(0).toUpperCase() + dateString.slice(1); + } + pointSize: Style.fontSizeXL + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignLeft + } + } + + // Spacer to push time to the right + Item { + Layout.fillWidth: true + } + + // Clock + Item { + Layout.preferredWidth: Settings.data.general.clockStyle === "analog" ? 70 : (Settings.data.general.clockStyle === "custom" ? 90 : 70) + Layout.preferredHeight: Settings.data.general.clockStyle === "analog" ? 70 : (Settings.data.general.clockStyle === "custom" ? 90 : 70) + Layout.alignment: Qt.AlignVCenter + + // Analog Clock + NClock { + anchors.centerIn: parent + width: 70 + height: 70 + visible: Settings.data.general.clockStyle === "analog" + now: root.currentTime + clockStyle: "analog" + backgroundColor: "transparent" + clockColor: Color.mOnSurface + secondHandColor: Color.mPrimary + } + + // Digital Clock (Standard) + NClock { + anchors.centerIn: parent + width: 70 + height: 70 + visible: Settings.data.general.clockStyle === "digital" + now: root.currentTime + clockStyle: "digital" + showProgress: true + progressColor: Color.mPrimary + backgroundColor: "transparent" + clockColor: Color.mOnSurface + hoursFontSize: Style.fontSizeL + minutesFontSize: Style.fontSizeL + hoursFontWeight: Style.fontWeightBold + minutesFontWeight: Style.fontWeightBold + } + + // Custom Clock (Stacked) + ColumnLayout { + anchors.centerIn: parent + width: parent.width + visible: Settings.data.general.clockStyle === "custom" + spacing: -3 + + Repeater { + model: I18n.locale.toString(root.currentTime, (Settings.data.general.clockFormat || "hh\\nmm").replace(/\\n/g, "\n")).split("\n") + NText { + text: modelData + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + Layout.alignment: Qt.AlignHCenter + Layout.maximumWidth: parent.width + Layout.fillWidth: true + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/LockScreen/LockScreenPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/LockScreen/LockScreenPanel.qml new file mode 100644 index 0000000..ddd0678 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/LockScreen/LockScreenPanel.qml @@ -0,0 +1,1216 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Compositor +import qs.Services.Hardware +import qs.Services.Keyboard +import qs.Services.Location +import qs.Services.Media +import qs.Widgets +import qs.Widgets.AudioSpectrum + +Item { + id: root + anchors.fill: parent + + required property var lockControl + required property var batteryIndicator + required property var keyboardLayout + required property TextInput passwordInput + + // Whether to enable lock screen animations (smooth cursor blink). + // Defaults to false to reduce GPU usage. Set Settings.data.general.lockScreenAnimations = true to restore. + readonly property bool animationsEnabled: Settings.data.general.lockScreenAnimations || false + + Component.onCompleted: { + if (Settings.data.general.autoStartAuth) { + doUnlock(); + } + } + + function doUnlock() { + if (lockControl) { + lockControl.tryUnlock(); + } + } + + // Timer properties + readonly property int timerDuration: Settings.data.general.lockScreenCountdownDuration + property string pendingAction: "" + property bool timerActive: false + property int timeRemaining: 0 + readonly property bool weatherReady: Settings.data.location.weatherEnabled && (LocationService.data.weather !== null) + + // Timer management functions + function startTimer(action) { + // Check if global countdown is disabled + if (!Settings.data.general.enableLockScreenCountdown) { + executeAction(action); + return; + } + + if (timerActive && pendingAction === action) { + // Second click - execute immediately + executeAction(action); + return; + } + + pendingAction = action; + timeRemaining = timerDuration; + timerActive = true; + countdownTimer.start(); + } + + function cancelTimer() { + timerActive = false; + pendingAction = ""; + timeRemaining = 0; + countdownTimer.stop(); + } + + function executeAction(action) { + // Stop timer but don't reset other properties yet + countdownTimer.stop(); + + // Execute the action + switch (action) { + case "logout": + CompositorService.logout(); + break; + case "suspend": + CompositorService.suspend(); + break; + case "hibernate": + CompositorService.hibernate(); + break; + case "reboot": + CompositorService.reboot(); + break; + case "userspaceReboot": + CompositorService.userspaceReboot(); + break; + case "shutdown": + CompositorService.shutdown(); + break; + } + + // Reset timer state + cancelTimer(); + } + + // Countdown timer + Timer { + id: countdownTimer + interval: 100 + repeat: true + onTriggered: { + timeRemaining -= interval; + if (timeRemaining <= 0) { + executeAction(pendingAction); + } + } + } + + // Compact status indicators container (compact mode only) + Rectangle { + width: { + var hasBattery = batteryIndicator.isReady; + var hasKeyboard = keyboardLayout.currentLayout !== "Unknown"; + var hasCaps = LockKeysService.capsLockOn; + var hasCapsSlot = hasBattery || hasKeyboard || hasCaps; + + var visibleCount = 0; + if (hasBattery) + visibleCount++; + if (hasKeyboard) + visibleCount++; + if (hasCapsSlot) + visibleCount++; + + if (visibleCount >= 3) { + return 280; + } else if (visibleCount === 2) { + return 200; + } else if (visibleCount === 1) { + return 120; + } else { + return 0; + } + } + height: 40 + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 96 + (Settings.data.general.compactLockScreen ? 116 : 220) + topLeftRadius: Style.radiusL + topRightRadius: Style.radiusL + color: Color.mSurface + visible: Settings.data.general.compactLockScreen && (batteryIndicator.isReady || keyboardLayout.currentLayout !== "Unknown" || LockKeysService.capsLockOn) + + RowLayout { + id: compactStatusRow + anchors.centerIn: parent + spacing: Style.marginL + + // Battery indicator + RowLayout { + spacing: Style.marginS + visible: batteryIndicator.isReady + + NIcon { + icon: batteryIndicator.icon + pointSize: Style.fontSizeM + color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurfaceVariant + } + + NText { + text: Math.round(batteryIndicator.percent) + "%" + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeM + } + } + + // Keyboard layout indicator + RowLayout { + spacing: 6 + visible: keyboardLayout.currentLayout !== "Unknown" + + NIcon { + icon: "keyboard" + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + + NText { + text: keyboardLayout.currentLayout + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeM + elide: Text.ElideRight + } + } + + // Caps Lock indicator + RowLayout { + spacing: 6 + visible: batteryIndicator.isReady || keyboardLayout.currentLayout !== "Unknown" || LockKeysService.capsLockOn + + NIcon { + icon: "lock" + pointSize: Style.fontSizeM + color: LockKeysService.capsLockOn ? Color.mPrimary : Qt.alpha(Color.mOnSurfaceVariant, 0.5) + } + + NText { + text: I18n.tr("bar.lock-keys.show-caps-lock-label") + color: LockKeysService.capsLockOn ? Color.mOnSurfaceVariant : Qt.alpha(Color.mOnSurfaceVariant, 0.65) + pointSize: Style.fontSizeM + elide: Text.ElideRight + } + } + } + } + + // Bottom container with weather, password input and controls + Rectangle { + id: bottomContainer + + // Support for removing the session/power buttons at the bottom. + readonly property int deltaY: Settings.data.general.showSessionButtonsOnLockScreen ? 0 : (Settings.data.general.compactLockScreen ? 36 : 48) + 14 + + height: { + let calcHeight = Settings.data.general.compactLockScreen ? 120 : 220; + if (!Settings.data.general.showSessionButtonsOnLockScreen) { + calcHeight -= bottomContainer.deltaY; + } + return calcHeight; + } + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: 100 + bottomContainer.deltaY + radius: Style.radiusL + color: Color.mSurface + + width: Settings.data.general.showHibernateOnLockScreen ? 860 : 810 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 14 + spacing: Style.marginL + + // Top info row + RowLayout { + Layout.fillWidth: true + Layout.preferredHeight: 65 + spacing: Style.marginXL + visible: !Settings.data.general.compactLockScreen + + // Media widget with visualizer + Item { + Layout.preferredWidth: Style.marginM + visible: MediaService.currentPlayer && MediaService.canPlay + } + + Rectangle { + Layout.preferredWidth: 220 + // Expand to take remaining space when weather is hidden + Layout.fillWidth: !(Settings.data.location.weatherEnabled && LocationService.data.weather !== null) + Layout.preferredHeight: 50 + radius: Style.radiusL + color: "transparent" + clip: true + visible: MediaService.currentPlayer && MediaService.canPlay + + Loader { + anchors.fill: parent + anchors.margins: 4 + active: Settings.data.audio.visualizerType === "linear" + z: 0 + sourceComponent: NLinearSpectrum { + anchors.fill: parent + values: SpectrumService.values + fillColor: Color.mPrimary + opacity: 0.4 + mirrored: Settings.data.audio.spectrumMirrored + } + } + + Loader { + anchors.fill: parent + anchors.margins: 4 + active: Settings.data.audio.visualizerType === "mirrored" + z: 0 + sourceComponent: NMirroredSpectrum { + anchors.fill: parent + values: SpectrumService.values + fillColor: Color.mPrimary + opacity: 0.4 + mirrored: Settings.data.audio.spectrumMirrored + } + } + + Loader { + anchors.fill: parent + anchors.margins: 4 + active: Settings.data.audio.visualizerType === "wave" + z: 0 + sourceComponent: NWaveSpectrum { + anchors.fill: parent + values: SpectrumService.values + fillColor: Color.mPrimary + opacity: 0.4 + mirrored: Settings.data.audio.spectrumMirrored + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: Style.marginM + z: 1 + + Rectangle { + Layout.preferredWidth: 34 + Layout.preferredHeight: 34 + radius: Math.min(Style.radiusL, width / 2) + color: "transparent" + clip: true + + NImageRounded { + anchors.fill: parent + anchors.margins: 2 + radius: Math.min(Style.radiusL, width / 2) + imagePath: MediaService.trackArtUrl + fallbackIcon: "disc" + fallbackIconSize: Style.fontSizeM + borderColor: Color.mOutline + borderWidth: Style.borderS + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXXS + + NText { + text: MediaService.trackTitle || "No media" + pointSize: Style.fontSizeM + color: Color.mOnSurface + Layout.fillWidth: true + elide: Text.ElideRight + } + + NText { + text: MediaService.trackArtist || "" + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + Layout.fillWidth: true + elide: Text.ElideRight + } + } + + // Media controls (when enabled) + RowLayout { + spacing: Style.marginXS + visible: Settings.data.general.enableLockScreenMediaControls + Layout.alignment: Qt.AlignHCenter + + Rectangle { + width: 28 + height: 28 + radius: Math.min(Style.radiusL, width / 2) + color: prevButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mOnSurface, 0.1) + visible: MediaService.canGoPrevious + + NIcon { + anchors.centerIn: parent + icon: "media-prev" + pointSize: Style.fontSizeM + color: prevButtonArea.containsMouse ? Color.mOnPrimary : Color.mOnSurface + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + id: prevButtonArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: MediaService.canGoPrevious ? MediaService.previous() : {} + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + Rectangle { + width: 32 + height: 32 + radius: Math.min(Style.radiusL, width / 2) + color: playPauseButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mOnSurface, 0.15) + visible: MediaService.canPlay || MediaService.canPause + + NIcon { + anchors.centerIn: parent + icon: MediaService.isPlaying ? "media-pause" : "media-play" + pointSize: Style.fontSizeL + color: playPauseButtonArea.containsMouse ? Color.mOnPrimary : Color.mOnSurface + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + id: playPauseButtonArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: (MediaService.canPlay || MediaService.canPause) ? MediaService.playPause() : {} + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + Rectangle { + width: 28 + height: 28 + radius: Math.min(Style.radiusL, width / 2) + color: nextButtonArea.containsMouse ? Color.mPrimary : Qt.alpha(Color.mOnSurface, 0.1) + visible: MediaService.canGoNext + + NIcon { + anchors.centerIn: parent + icon: "media-next" + pointSize: Style.fontSizeM + color: nextButtonArea.containsMouse ? Color.mOnPrimary : Color.mOnSurface + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + id: nextButtonArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: MediaService.canGoNext ? MediaService.next() : {} + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + } + } + } + + Rectangle { + Layout.preferredWidth: 1 + Layout.fillHeight: true + Layout.rightMargin: 4 + color: Qt.alpha(Color.mOutline, 0.3) + visible: MediaService.currentPlayer && MediaService.canPlay + } + + Item { + Layout.preferredWidth: Style.marginM + visible: !(MediaService.currentPlayer && MediaService.canPlay) + } + + // Current weather + RowLayout { + visible: Settings.data.location.weatherEnabled && LocationService.data.weather !== null + Layout.preferredWidth: 180 + spacing: Style.marginM + + Item { + Layout.preferredWidth: lockMainWeatherIconSide + Layout.preferredHeight: lockMainWeatherIconSide + Layout.alignment: Qt.AlignVCenter + readonly property int lockMainWeatherIconSide: Math.round(Style.fontSizeXXXL * Style.uiScaleRatio * 1.6) + + NIcon { + visible: !LocationService.taliaWeatherMascotActive || !weatherReady + anchors.centerIn: parent + icon: weatherReady ? LocationService.weatherSymbolFromCode(LocationService.data.weather.current_weather.weathercode) : "weather-cloud-off" + pointSize: Style.fontSizeXXXL + color: Color.mPrimary + } + Loader { + active: LocationService.taliaWeatherMascotActive && weatherReady + anchors.fill: parent + asynchronous: true + sourceComponent: Component { + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectFit + smooth: true + mipmap: true + asynchronous: true + source: Qt.resolvedUrl(LocationService.taliaWeatherImageFromCode(LocationService.data.weather.current_weather.weathercode)) + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXXS + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginL + + NText { + text: { + var temp = LocationService.data.weather.current_weather.temperature; + var suffix = "C"; + if (Settings.data.location.useFahrenheit) { + temp = LocationService.celsiusToFahrenheit(temp); + suffix = "F"; + } + temp = Math.round(temp); + return temp + "°" + suffix; + } + pointSize: Style.fontSizeXL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: { + var wind = LocationService.data.weather.current_weather.windspeed; + var unit = "km/h"; + if (Settings.data.location.useFahrenheit) { + wind = wind * 0.621371; // Convert km/h to mph + unit = "mph"; + } + wind = Math.round(wind); + return wind + " " + unit; + } + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NText { + text: Settings.data.location.name.split(",")[0] + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + visible: !Settings.data.location.hideWeatherCityName + } + + NText { + text: (LocationService.data.weather.current && LocationService.data.weather.current.relativehumidity_2m) ? LocationService.data.weather.current.relativehumidity_2m + "% humidity" : "" + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + } + } + } + + // Forecast + RowLayout { + visible: Settings.data.location.weatherEnabled && LocationService.data.weather !== null + Layout.preferredWidth: 260 + Layout.rightMargin: 8 + spacing: Style.marginXS + + Repeater { + model: MediaService.currentPlayer && MediaService.canPlay ? 2 : 4 + delegate: ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXXS + 1 + + NText { + text: { + var weatherDate = new Date(LocationService.data.weather.daily.time[index].replace(/-/g, "/")); + return I18n.locale.toString(weatherDate, "ddd"); + } + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + } + + Item { + Layout.preferredWidth: lockForecastWeatherIconSide + Layout.preferredHeight: lockForecastWeatherIconSide + Layout.alignment: Qt.AlignHCenter + readonly property int lockForecastWeatherIconSide: Math.round(Style.fontSizeXL * Style.uiScaleRatio * 1.6) + + NIcon { + visible: !LocationService.taliaWeatherMascotActive + anchors.centerIn: parent + icon: LocationService.weatherSymbolFromCode(LocationService.data.weather.daily.weathercode[index]) + pointSize: Style.fontSizeXL + color: Color.mOnSurfaceVariant + } + Loader { + active: LocationService.taliaWeatherMascotActive + anchors.fill: parent + asynchronous: true + sourceComponent: Component { + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectFit + smooth: true + mipmap: true + asynchronous: true + source: Qt.resolvedUrl(LocationService.taliaWeatherImageFromCode(LocationService.data.weather.daily.weathercode[index])) + } + } + } + } + + NText { + text: { + var max = LocationService.data.weather.daily.temperature_2m_max[index]; + var min = LocationService.data.weather.daily.temperature_2m_min[index]; + if (Settings.data.location.useFahrenheit) { + max = LocationService.celsiusToFahrenheit(max); + min = LocationService.celsiusToFahrenheit(min); + } + max = Math.round(max); + min = Math.round(min); + return max + "°/" + min + "°"; + } + pointSize: Style.fontSizeM + font.weight: Style.fontWeightMedium + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + } + } + } + } + + Item { + Layout.fillWidth: batteryIndicator.isReady + } + + // Battery and Keyboard Layout (full mode only) + ColumnLayout { + Layout.alignment: (batteryIndicator.isReady) ? (Qt.AlignRight | Qt.AlignVCenter) : Qt.AlignVCenter + spacing: Style.marginM + visible: batteryIndicator.isReady || keyboardLayout.currentLayout !== "Unknown" || LockKeysService.capsLockOn + + // Battery + RowLayout { + spacing: Style.marginXS + visible: batteryIndicator.isReady + + NIcon { + icon: batteryIndicator.icon + pointSize: Style.fontSizeM + color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurfaceVariant + } + + NText { + text: Math.round(batteryIndicator.percent) + "%" + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeM + } + } + + // Keyboard Layout + RowLayout { + spacing: Style.marginXS + visible: keyboardLayout.currentLayout !== "Unknown" + + NIcon { + icon: "keyboard" + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + + NText { + text: keyboardLayout.currentLayout + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeM + elide: Text.ElideRight + } + } + + // Caps Lock + RowLayout { + spacing: Style.marginXS + visible: batteryIndicator.isReady || keyboardLayout.currentLayout !== "Unknown" || LockKeysService.capsLockOn + + NIcon { + icon: "lock" + pointSize: Style.fontSizeM + color: LockKeysService.capsLockOn ? Color.mPrimary : Qt.alpha(Color.mOnSurfaceVariant, 0.5) + } + + NText { + text: I18n.tr("bar.lock-keys.show-caps-lock-label") + color: LockKeysService.capsLockOn ? Color.mOnSurfaceVariant : Qt.alpha(Color.mOnSurfaceVariant, 0.65) + pointSize: Style.fontSizeM + elide: Text.ElideRight + } + } + } + + Item { + Layout.preferredWidth: Style.marginM + } + } + + // Password input + RowLayout { + Layout.fillWidth: true + spacing: 0 + + Item { + Layout.preferredWidth: Style.marginM + } + + Rectangle { + id: passwordInputContainer + Layout.fillWidth: true + Layout.preferredHeight: 48 + radius: Style.iRadiusL + color: Color.mSurface + border.color: passwordInput.activeFocus ? Color.mPrimary : Qt.alpha(Color.mOutline, 0.3) + border.width: passwordInput.activeFocus ? 2 : 1 + + property bool passwordVisible: false + + // Ctrl + A to highlight the portion + Shortcut { + sequence: StandardKey.SelectAll + enabled: passwordInput.activeFocus + onActivated: passwordInput.selectAll() + } + + // Esc to clear selection + Shortcut { + sequences: [StandardKey.Cancel] + enabled: passwordInput.activeFocus && passwordInput.selectionStart !== passwordInput.selectionEnd + onActivated: passwordInput.deselect() + } + + Row { + anchors.left: parent.left + anchors.leftMargin: 18 + anchors.verticalCenter: parent.verticalCenter + spacing: Style.marginL + + NIcon { + icon: "login-2" + pointSize: Style.fontSizeL + color: passwordInput.activeFocus ? Color.mPrimary : Color.mOnSurfaceVariant + anchors.verticalCenter: parent.verticalCenter + } + + Row { + spacing: 0 + + Rectangle { + width: 2 + height: 20 + color: Color.mPrimary + visible: passwordInput.activeFocus && passwordInput.text.length === 0 + anchors.verticalCenter: parent.verticalCenter + + // Smooth fade animation (when animations enabled) + SequentialAnimation on opacity { + loops: Animation.Infinite + running: root.animationsEnabled && passwordInput.activeFocus && passwordInput.text.length === 0 + NumberAnimation { + to: 0 + duration: 530 + } + NumberAnimation { + to: 1 + duration: 530 + } + } + + // Simple toggle (when animations disabled) — no per-frame repaints + Timer { + interval: 530 + running: !root.animationsEnabled && passwordInput.activeFocus && passwordInput.text.length === 0 + repeat: true + onTriggered: parent.opacity = parent.opacity > 0.5 ? 0 : 1 + } + } + + // Host for dots / plain text and the caret (caret x follows passwordInput.cursorPosition) + Item { + id: passwordVisualHost + height: 20 + width: passwordInputContainer.passwordVisible ? Math.min(visiblePasswordPlainText.implicitWidth, 550) : Math.min(passwordDisplayContent.width, 550) + anchors.verticalCenter: parent.verticalCenter + + readonly property real caretVisualX: { + const len = passwordInput.text.length; + if (len <= 0) + return 0; + if (passwordInputContainer.passwordVisible) { + const adv = passwordCaretFontMetrics.advanceWidth(passwordInput.text.substring(0, passwordInput.cursorPosition)); + return Math.max(0, Math.min(adv, width)); + } + const w = passwordDisplayContent.width; + if (w <= 0) + return 0; + return Math.max(0, Math.min((passwordInput.cursorPosition / len) * w, width)); + } + + // Password dots display with selection support + Item { + width: Math.min(passwordDisplayContent.width, 550) + height: 20 + visible: passwordInput.text.length > 0 && !passwordInputContainer.passwordVisible + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + clip: true + + // Proportional selection highlight behind the dots + Rectangle { + id: selectionHighlight + visible: passwordInput.selectionStart !== passwordInput.selectionEnd && passwordInput.text.length > 0 + color: Qt.alpha(Color.mPrimary, 0.8) + height: parent.height + Style.marginS + anchors.verticalCenter: parent.verticalCenter + x: (passwordInput.selectionStart / passwordInput.text.length) * passwordDisplayContent.width + width: ((passwordInput.selectionEnd - passwordInput.selectionStart) / passwordInput.text.length) * passwordDisplayContent.width + } + + Row { + id: passwordDisplayContent + spacing: Style.marginXXXS + anchors.verticalCenter: parent.verticalCenter + + Repeater { + id: iconRepeater + model: ScriptModel { + values: Array(passwordInput.text.length) + } + + property list passwordChars: ["circle-filled", "pentagon-filled", "michelin-star-filled", "square-rounded-filled", "guitar-pick-filled", "blob-filled", "triangle-filled"] + + NIcon { + id: icon + + required property int index + // This will be called with index = -1 when the TextInput is deleted + // So we make sur index is positive to avoid warning on array accesses + property bool drawCustomChar: index >= 0 && Settings.data.general.passwordChars + // Flip color when this dot falls inside the active selection range + property bool isSelected: index >= 0 && passwordInput.selectionStart !== passwordInput.selectionEnd && index >= passwordInput.selectionStart && index < passwordInput.selectionEnd + + icon: drawCustomChar ? iconRepeater.passwordChars[index % iconRepeater.passwordChars.length] : "circle-filled" + pointSize: Style.fontSizeL + color: isSelected ? Color.mOnPrimary : Color.mPrimary + opacity: 1.0 + scale: animationsEnabled ? 0.5 : 1 + ParallelAnimation { + id: iconAnim + NumberAnimation { + target: icon + properties: "scale" + to: 1 + duration: Style.animationFast + easing.type: Easing.BezierSpline + easing.bezierCurve: Easing.OutInBounce + } + } + Component.onCompleted: { + if (animationsEnabled) { + iconAnim.start(); + } + } + } + } + } + + // Mouse area for click-to-position and drag-to-select + MouseArea { + anchors.fill: parent + cursorShape: Qt.IBeamCursor + + property int dragStartPos: 0 + property bool pendingSelectAll: false + + // Resets double-click state if user pauses too long between clicks + Timer { + id: doubleClickResetTimer + interval: 600 + onTriggered: parent.pendingSelectAll = false + } + + function charIndexFromX(mouseX) { + if (passwordInput.text.length === 0) + return 0; + var charWidth = passwordDisplayContent.width / passwordInput.text.length; + // floor so clicking anywhere on a dot selects that dot, not the next + return Math.max(0, Math.min(passwordInput.text.length - 1, Math.floor(mouseX / charWidth))); + } + + onPressed: function (mouse) { + doubleClickResetTimer.stop(); + passwordInput.forceActiveFocus(); + dragStartPos = charIndexFromX(mouse.x); + passwordInput.cursorPosition = dragStartPos; + } + + onPositionChanged: function (mouse) { + pendingSelectAll = false; + var curPos = charIndexFromX(mouse.x); + if (curPos <= dragStartPos) { + passwordInput.select(curPos, dragStartPos + 1); + } else { + passwordInput.select(dragStartPos, curPos + 1); + } + } + + onDoubleClicked: function (mouse) { + passwordInput.forceActiveFocus(); + if (pendingSelectAll) { + passwordInput.selectAll(); + pendingSelectAll = false; + } else { + var pos = charIndexFromX(mouse.x); + passwordInput.select(pos, Math.min(pos + 1, passwordInput.text.length)); + pendingSelectAll = true; + doubleClickResetTimer.restart(); + } + } + } + } + + NText { + id: visiblePasswordPlainText + text: passwordInput.text + color: Color.mPrimary + pointSize: Style.fontSizeM + visible: passwordInput.text.length > 0 && passwordInputContainer.passwordVisible + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight + width: Math.min(implicitWidth, 550) + } + + FontMetrics { + id: passwordCaretFontMetrics + font: visiblePasswordPlainText.font + } + + Rectangle { + width: 2 + height: 20 + x: passwordVisualHost.caretVisualX + color: Color.mPrimary + // Hide the cursor when text is selected + visible: passwordInput.activeFocus && passwordInput.text.length > 0 && passwordInput.selectionStart === passwordInput.selectionEnd + anchors.verticalCenter: parent.verticalCenter + + // Smooth fade animation (when animations enabled) + SequentialAnimation on opacity { + loops: Animation.Infinite + running: root.animationsEnabled && passwordInput.activeFocus && passwordInput.text.length > 0 && passwordInput.selectionStart === passwordInput.selectionEnd + NumberAnimation { + to: 0 + duration: 530 + } + NumberAnimation { + to: 1 + duration: 530 + } + } + + // Simple toggle (when animations disabled) — no per-frame repaints + Timer { + interval: 530 + running: !root.animationsEnabled && passwordInput.activeFocus && passwordInput.text.length > 0 && passwordInput.selectionStart === passwordInput.selectionEnd + repeat: true + onTriggered: parent.opacity = parent.opacity > 0.5 ? 0 : 1 + } + } + } + } + } + + // Eye button to toggle password visibility + Rectangle { + anchors.right: submitButton.left + anchors.rightMargin: 4 + anchors.verticalCenter: parent.verticalCenter + width: 36 + height: 36 + radius: Math.min(Style.iRadiusL, width / 2) + color: eyeButtonArea.containsMouse ? Color.mPrimary : "transparent" + visible: passwordInput.text.length > 0 + enabled: !lockContext || !lockContext.unlockInProgress + + NIcon { + anchors.centerIn: parent + icon: parent.parent.passwordVisible ? "eye-off" : "eye" + pointSize: Style.fontSizeM + color: eyeButtonArea.containsMouse ? Color.mOnPrimary : Color.mOnSurfaceVariant + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + id: eyeButtonArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: parent.parent.passwordVisible = !parent.parent.passwordVisible + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + // Submit button + Rectangle { + id: submitButton + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.verticalCenter: parent.verticalCenter + width: 36 + height: 36 + radius: Math.min(Style.iRadiusL, width / 2) + color: submitButtonArea.containsMouse ? Color.mPrimary : "transparent" + border.color: Color.mPrimary + border.width: Style.borderS + enabled: !lockContext || !lockContext.unlockInProgress + + NIcon { + anchors.centerIn: parent + icon: "arrow-forward" + pointSize: Style.fontSizeM + color: submitButtonArea.containsMouse ? Color.mOnPrimary : Color.mPrimary + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + id: submitButtonArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.doUnlock() + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + Item { + Layout.preferredWidth: Style.marginM + } + } + + // Session control buttons + RowLayout { + id: sessionButtonRow + Layout.fillWidth: true + Layout.preferredHeight: Settings.data.general.compactLockScreen ? 36 : 48 + Layout.alignment: Qt.AlignHCenter + spacing: Style.marginM + visible: Settings.data.general.showSessionButtonsOnLockScreen + + readonly property int buttonCount: Settings.data.general.showHibernateOnLockScreen ? 5 : 4 + readonly property real availableWidth: bottomContainer.width - 48 + readonly property real buttonWidth: (availableWidth - (buttonCount - 1) * spacing) / buttonCount + readonly property real buttonHeight: sessionButtonRow.height + + Item { + Layout.preferredWidth: sessionButtonRow.buttonWidth + Layout.preferredHeight: sessionButtonRow.buttonHeight + + NButton { + anchors.fill: parent + icon: "logout" + text: I18n.tr("common.logout") + outlined: true + backgroundColor: Color.mOnSurfaceVariant + textColor: Color.mOnPrimary + fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM + iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL + horizontalAlignment: Qt.AlignHCenter + buttonRadius: Style.radiusL + onClicked: startTimer("logout") + } + } + + Item { + Layout.preferredWidth: sessionButtonRow.buttonWidth + Layout.preferredHeight: sessionButtonRow.buttonHeight + + NButton { + anchors.fill: parent + icon: "suspend" + text: I18n.tr("common.suspend") + outlined: true + backgroundColor: Color.mOnSurfaceVariant + textColor: Color.mOnPrimary + fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM + iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL + horizontalAlignment: Qt.AlignHCenter + buttonRadius: Style.radiusL + onClicked: startTimer("suspend") + } + } + + Item { + Layout.preferredWidth: sessionButtonRow.buttonWidth + Layout.preferredHeight: sessionButtonRow.buttonHeight + visible: Settings.data.general.showHibernateOnLockScreen + + NButton { + anchors.fill: parent + icon: "hibernate" + text: I18n.tr("common.hibernate") + outlined: true + backgroundColor: Color.mOnSurfaceVariant + textColor: Color.mOnPrimary + fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM + iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL + horizontalAlignment: Qt.AlignHCenter + buttonRadius: Style.radiusL + onClicked: startTimer("hibernate") + } + } + + Item { + Layout.preferredWidth: sessionButtonRow.buttonWidth + Layout.preferredHeight: sessionButtonRow.buttonHeight + + NButton { + anchors.fill: parent + icon: "reboot" + text: I18n.tr("common.reboot") + outlined: true + backgroundColor: Color.mOnSurfaceVariant + textColor: Color.mOnPrimary + fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM + iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL + horizontalAlignment: Qt.AlignHCenter + buttonRadius: Style.radiusL + onClicked: startTimer("reboot") + } + } + + Item { + Layout.preferredWidth: sessionButtonRow.buttonWidth + Layout.preferredHeight: sessionButtonRow.buttonHeight + + NButton { + anchors.fill: parent + icon: "shutdown" + text: I18n.tr("common.shutdown") + outlined: true + backgroundColor: Color.mError + textColor: Color.mOnError + fontSize: Settings.data.general.compactLockScreen ? Style.fontSizeS : Style.fontSizeM + iconSize: Settings.data.general.compactLockScreen ? Style.fontSizeM : Style.fontSizeL + horizontalAlignment: Qt.AlignHCenter + buttonRadius: Style.radiusL + onClicked: startTimer("shutdown") + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/AllScreens.qml b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/AllScreens.qml new file mode 100644 index 0000000..2039dd0 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/AllScreens.qml @@ -0,0 +1,164 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland + +import qs.Commons +import qs.Modules.MainScreen +import qs.Services.UI + +// ------------------------------ +// MainScreen for each screen (manages bar + all panels) +// Wrapped in Loader to optimize memory - only loads when screen needs it +Variants { + model: Quickshell.screens + delegate: Item { + id: windowItem + required property ShellScreen modelData + + property bool shouldBeActive: { + if (!modelData || !modelData.name) { + return false; + } + + let shouldLoad = true; + if (!Settings.data.general.allowPanelsOnScreenWithoutBar) { + // Check if bar is configured for this screen + var monitors = Settings.data.bar.monitors || []; + shouldLoad = monitors.length === 0 || monitors.includes(modelData?.name); + } + + if (shouldLoad) { + Logger.d("AllScreens", "Screen activated: ", modelData?.name); + } + return shouldLoad; + } + + property bool windowLoaded: false + + // Main Screen loader - Bar and panels backgrounds + Loader { + id: windowLoader + active: parent.shouldBeActive + asynchronous: false + + property ShellScreen loaderScreen: modelData + + onLoaded: { + // Signal that window is loaded so exclusion zone can be created + parent.windowLoaded = true; + } + + sourceComponent: MainScreen { + screen: windowLoader.loaderScreen + } + } + + // Bar content in separate windows to prevent fullscreen redraws + // Note: Window stays alive when bar is hidden (visible=false) to avoid + // rapid Wayland surface destruction/creation that can crash compositors. + // Content is debounce-unloaded inside BarContentWindow. + Loader { + active: { + if (!parent.windowLoaded || !parent.shouldBeActive) + return false; + + // Check if bar is configured for this screen + var monitors = Settings.data.bar.monitors || []; + return monitors.length === 0 || monitors.includes(modelData?.name); + } + asynchronous: false + + sourceComponent: BarContentWindow { + screen: modelData + } + + onLoaded: { + Logger.d("AllScreens", "BarContentWindow created for", modelData?.name); + } + } + + // BarTriggerZone - thin invisible zone to reveal hidden bar + // Always loaded when auto-hide is enabled (it's just 1px, no performance impact) + Loader { + active: { + if (!parent.windowLoaded || !parent.shouldBeActive) + return false; + if (!BarService.effectivelyVisible) + return false; + if (Settings.getBarDisplayModeForScreen(modelData?.name) !== "auto_hide") + return false; + + // Check if bar is configured for this screen + var monitors = Settings.data.bar.monitors || []; + return monitors.length === 0 || monitors.includes(modelData?.name); + } + asynchronous: false + + sourceComponent: BarTriggerZone { + screen: modelData + } + + onLoaded: { + Logger.d("AllScreens", "BarTriggerZone created for", modelData?.name); + } + } + + // BarExclusionZone - created after MainScreen has fully loaded + // Note: Exclusion zone should NOT be affected by hideOnOverview setting. + // When bar is hidden during overview, the exclusion zone should remain to prevent + // windows from moving into the bar area. Auto-hide is handled by the component + // itself via ExclusionMode.Ignore/Auto. + Repeater { + model: Settings.data.bar.barType === "framed" ? ["top", "bottom", "left", "right"] : [Settings.getBarPositionForScreen(windowItem.modelData?.name)] + delegate: Loader { + active: { + if (!windowItem.windowLoaded || !windowItem.shouldBeActive) + return false; + + // Check if bar is configured for this screen + var monitors = Settings.data.bar.monitors || []; + return monitors.length === 0 || monitors.includes(windowItem.modelData?.name); + } + asynchronous: false + + sourceComponent: BarExclusionZone { + screen: windowItem.modelData + edge: modelData + } + + onLoaded: { + Logger.d("AllScreens", "BarExclusionZone (" + modelData + ") created for", windowItem.modelData?.name); + } + } + } + + // PopupMenuWindow - reusable popup window for both tray menus and context menus + // Stays alive when bar is hidden to avoid Wayland surface churn crashes. + // PopupMenuWindow manages its own visibility internally. + Loader { + active: { + // Desktop widgets edit mode needs popup window on ALL screens + if (DesktopWidgetRegistry.editMode && Settings.data.desktopWidgets.enabled) { + return true; + } + + // Normal bar-based condition + if (!parent.windowLoaded || !parent.shouldBeActive) + return false; + + // Check if bar is configured for this screen + var monitors = Settings.data.bar.monitors || []; + return monitors.length === 0 || monitors.includes(modelData?.name); + } + asynchronous: false + + sourceComponent: PopupMenuWindow { + screen: modelData + } + + onLoaded: { + Logger.d("AllScreens", "PopupMenuWindow created for", modelData?.name); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/Backgrounds/AllBackgrounds.qml b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/Backgrounds/AllBackgrounds.qml new file mode 100644 index 0000000..4041bf8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/Backgrounds/AllBackgrounds.qml @@ -0,0 +1,182 @@ +import QtQuick +import QtQuick.Shapes +import qs.Commons +import qs.Services.UI +import qs.Widgets + +/** +* AllBackgrounds - Unified Shape container for all bar and panel backgrounds +* +* Unified shadow system. This component contains a single Shape +* with multiple ShapePath children (one for bar, one for each panel type). +* +* Benefits: +* - Single GPU-accelerated rendering pass for all backgrounds +* - Unified shadow system (one MultiEffect for everything) +*/ +Item { + id: root + + // Reference Bar + required property var bar + + // Reference to MainScreen (for panel access) + required property var windowRoot + + readonly property color panelBackgroundColor: Color.mSurface + + anchors.fill: parent + + // Unified background container + Item { + anchors.fill: parent + + // When not using separate bar opacity, use unified approach (original behavior) + Item { + anchors.fill: parent + visible: !Settings.data.bar.useSeparateOpacity + + // Enable layer caching to prevent continuous re-rendering + layer.enabled: true + opacity: Color.adaptiveOpacity(Settings.data.ui.panelBackgroundOpacity) + + Shape { + id: unifiedBackgroundsShape + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + asynchronous: true + enabled: false + + Component.onCompleted: { + Logger.d("AllBackgrounds", "AllBackgrounds initialized"); + } + + /** + * Bar + */ + BarBackground { + bar: root.bar + shapeContainer: unifiedBackgroundsShape + windowRoot: root.windowRoot + backgroundColor: panelBackgroundColor + } + + /** + * Panel Background Slots + * Only 2 slots needed: one for currently open/opening panel, one for closing panel + */ + + // Slot 0: Currently open/opening panel + PanelBackground { + assignedPanel: { + var p = PanelService.backgroundSlotAssignments[0]; + // Only render if this panel belongs to this screen + return (p && p.screen === root.windowRoot.screen) ? p : null; + } + shapeContainer: unifiedBackgroundsShape + defaultBackgroundColor: panelBackgroundColor + } + + // Slot 1: Closing panel (during transitions) + PanelBackground { + assignedPanel: { + var p = PanelService.backgroundSlotAssignments[1]; + // Only render if this panel belongs to this screen + return (p && p.screen === root.windowRoot.screen) ? p : null; + } + shapeContainer: unifiedBackgroundsShape + defaultBackgroundColor: panelBackgroundColor + } + } + + // Apply shadow to the unified backgrounds + NDropShadow { + anchors.fill: parent + source: unifiedBackgroundsShape + } + } + + // When using separate bar opacity, separate the rendering + Item { + anchors.fill: parent + visible: Settings.data.bar.useSeparateOpacity + + // Panel backgrounds with panel opacity + Item { + anchors.fill: parent + + layer.enabled: true + opacity: Color.adaptiveOpacity(Settings.data.ui.panelBackgroundOpacity) + + Shape { + id: panelBackgroundsShape + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + asynchronous: true + enabled: false + + /** + * Panel Background Slots + * Only 2 slots needed: one for currently open/opening panel, one for closing panel + */ + + // Slot 0: Currently open/opening panel + PanelBackground { + assignedPanel: { + var p = PanelService.backgroundSlotAssignments[0]; + // Only render if this panel belongs to this screen + return (p && p.screen === root.windowRoot.screen) ? p : null; + } + shapeContainer: panelBackgroundsShape + defaultBackgroundColor: panelBackgroundColor + } + + // Slot 1: Closing panel (during transitions) + PanelBackground { + assignedPanel: { + var p = PanelService.backgroundSlotAssignments[1]; + // Only render if this panel belongs to this screen + return (p && p.screen === root.windowRoot.screen) ? p : null; + } + shapeContainer: panelBackgroundsShape + defaultBackgroundColor: panelBackgroundColor + } + } + + // Apply shadow to the panel backgrounds + NDropShadow { + anchors.fill: parent + source: panelBackgroundsShape + } + } + + // Bar background with separate opacity + Item { + anchors.fill: parent + + layer.enabled: true + opacity: Settings.data.bar.backgroundOpacity + + Shape { + id: barBackgroundShape + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + asynchronous: true + enabled: false + + BarBackground { + bar: root.bar + shapeContainer: barBackgroundShape + windowRoot: root.windowRoot + backgroundColor: panelBackgroundColor + } + } + + NDropShadow { + anchors.fill: parent + source: barBackgroundShape + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/Backgrounds/BarBackground.qml b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/Backgrounds/BarBackground.qml new file mode 100644 index 0000000..18f5e25 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/Backgrounds/BarBackground.qml @@ -0,0 +1,283 @@ +import QtQuick +import QtQuick.Shapes +import qs.Commons +import qs.Modules.MainScreen.Backgrounds +import qs.Services.UI + +/** +* BarBackground - ShapePath component for rendering the bar background +* +* Unified shadow system. This component is a ShapePath that will be +* a child of the unified AllBackgrounds Shape container. +* +* Uses 4-state per-corner system for flexible corner rendering: +* - State -1: No radius (flat/square corner) +* - State 0: Normal (inner curve) +* - State 1: Horizontal inversion (outer curve on X-axis) +* - State 2: Vertical inversion (outer curve on Y-axis) +*/ +ShapePath { + id: root + + // Required reference to the bar component + required property var bar + + // Required reference to AllBackgrounds shapeContainer + required property var shapeContainer + + // Required reference to windowRoot for screen access + required property var windowRoot + + required property color backgroundColor + + // Check if bar should be visible on this screen + readonly property bool shouldShow: { + // Check global bar visibility (includes overview state) + if (!BarService.effectivelyVisible) + return false; + + // Check screen-specific configuration + var monitors = Settings.data.bar.monitors || []; + var screenName = windowRoot?.screen?.name || ""; + + // If no monitors specified, show on all screens + // If monitors specified, only show if this screen is in the list + return monitors.length === 0 || monitors.includes(screenName); + } + + // Corner radius (from Style) + readonly property real radius: Style.radiusL + + // Framed bar properties + readonly property bool isFramed: Settings.data.bar.barType === "framed" + readonly property real frameThickness: Settings.data.bar.frameThickness ?? 12 + readonly property real frameRadius: Settings.data.bar.frameRadius ?? 20 + + // Bar position - since bar's parent fills the screen and Shape also fills the screen, + // we can use bar.x and bar.y directly (they're already in screen coordinates) + readonly property point barMappedPos: bar ? Qt.point(bar.x, bar.y) : Qt.point(0, 0) + + // Effective dimensions - 0 when bar shouldn't show (similar to panel behavior) + readonly property real barWidth: (bar && shouldShow) ? bar.width : 0 + readonly property real barHeight: (bar && shouldShow) ? bar.height : 0 + + // Screen dimensions for frame + readonly property real screenWidth: windowRoot?.screen?.width || 0 + readonly property real screenHeight: windowRoot?.screen?.height || 0 + + // Inner hole dimensions for framed mode - always relative to screen + readonly property string barPosition: Settings.getBarPositionForScreen(windowRoot?.screen?.name) + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property real holeX: (barPosition === "left") ? barWidth : frameThickness + readonly property real holeY: (barPosition === "top") ? barHeight : frameThickness + readonly property real holeWidth: screenWidth - (barPosition === "left" || barPosition === "right" ? (barWidth + frameThickness) : (frameThickness * 2)) + readonly property real holeHeight: screenHeight - (barPosition === "top" || barPosition === "bottom" ? (barHeight + frameThickness) : (frameThickness * 2)) + + // Flatten corners if bar is too small (handle null bar) + readonly property bool shouldFlatten: bar ? ShapeCornerHelper.shouldFlatten(barWidth, barHeight, radius) : false + readonly property real effectiveRadius: shouldFlatten ? (bar ? ShapeCornerHelper.getFlattenedRadius(Math.min(barWidth, barHeight), radius) : 0) : radius + + // Minimum safe arc radius — prevents zero-displacement zero-radius PathArcs + // that crash qTriangulate in CurveRenderer. 0.01px is sub-pixel and invisible. + readonly property real _minR: 0.01 + + // Helper function for getting corner radius based on state + function getCornerRadius(cornerState) { + // State -1 = flat corner — use minimum safe radius instead of 0 + // to prevent degenerate PathArc (zero displacement + zero radius) + if (cornerState === -1) + return _minR; + // All other states use effectiveRadius (clamped to safe minimum) + return Math.max(_minR, effectiveRadius); + } + + // Per-corner multipliers and radii based on bar's corner states (handle null bar) + readonly property real tlMultX: bar ? ShapeCornerHelper.getMultX(bar.topLeftCornerState) : 1 + readonly property real tlMultY: bar ? ShapeCornerHelper.getMultY(bar.topLeftCornerState) : 1 + readonly property real tlRadius: bar ? getCornerRadius(bar.topLeftCornerState) : 0 + + readonly property real trMultX: bar ? ShapeCornerHelper.getMultX(bar.topRightCornerState) : 1 + readonly property real trMultY: bar ? ShapeCornerHelper.getMultY(bar.topRightCornerState) : 1 + readonly property real trRadius: bar ? getCornerRadius(bar.topRightCornerState) : 0 + + readonly property real brMultX: bar ? ShapeCornerHelper.getMultX(bar.bottomRightCornerState) : 1 + readonly property real brMultY: bar ? ShapeCornerHelper.getMultY(bar.bottomRightCornerState) : 1 + readonly property real brRadius: bar ? getCornerRadius(bar.bottomRightCornerState) : 0 + + readonly property real blMultX: bar ? ShapeCornerHelper.getMultX(bar.bottomLeftCornerState) : 1 + readonly property real blMultY: bar ? ShapeCornerHelper.getMultY(bar.bottomLeftCornerState) : 1 + readonly property real blRadius: bar ? getCornerRadius(bar.bottomLeftCornerState) : 0 + + // True when the bar path has valid, non-degenerate geometry to render. + // Mirrors PanelBackground.isRenderable — prevents CurveRenderer crash on zero-area paths. + readonly property bool isRenderable: bar !== null && shouldShow && (isFramed ? (screenWidth > 0 && screenHeight > 0) : (barWidth > 0 && barHeight > 0)) + + // Edge overshoot: extend bar background beyond screen edges where both adjacent + // corners are flat (state -1) to prevent CurveRenderer antialiasing artifacts. + // Uses corner state checks instead of radius === 0 since flat corners now have _minR. + readonly property real screenEdgeOvershoot: 2 + readonly property real topEdgeOvs: (!isFramed && shouldShow && bar && bar.topLeftCornerState === -1 && bar.topRightCornerState === -1 && barMappedPos.y <= 0) ? -screenEdgeOvershoot : 0 + readonly property real bottomEdgeOvs: (!isFramed && shouldShow && bar && bar.bottomLeftCornerState === -1 && bar.bottomRightCornerState === -1 && (barMappedPos.y + barHeight) >= screenHeight) ? screenEdgeOvershoot : 0 + readonly property real leftEdgeOvs: (!isFramed && shouldShow && bar && bar.topLeftCornerState === -1 && bar.bottomLeftCornerState === -1 && barMappedPos.x <= 0) ? -screenEdgeOvershoot : 0 + readonly property real rightEdgeOvs: (!isFramed && shouldShow && bar && bar.topRightCornerState === -1 && bar.bottomRightCornerState === -1 && (barMappedPos.x + barWidth) >= screenWidth) ? screenEdgeOvershoot : 0 + + // Auto-hide opacity factor for background fade + property real opacityFactor: (bar && bar.isHidden) ? 0 : 1 + + Behavior on opacityFactor { + enabled: bar && bar.autoHide + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutQuad + } + } + + // ShapePath configuration + strokeWidth: -1 // No stroke, fill only + fillColor: isRenderable ? Qt.rgba(backgroundColor.r, backgroundColor.g, backgroundColor.b, backgroundColor.a * opacityFactor) : "transparent" + fillRule: isFramed ? ShapePath.OddEvenFill : ShapePath.WindingFill + + // Starting position — falls back to off-screen when not renderable so that + // all subsequent path elements form a valid non-degenerate off-screen square. + // Each edge is split between PathLine and PathArc so no arc has zero displacement, + // preventing CurveRenderer triangulation crashes on degenerate arcs. + // For framed mode the outer path is a full-screen rectangle; _minR offsets at each + // corner prevent zero-displacement zero-radius arcs that crash qTriangulate. + startX: isRenderable ? (isFramed ? _minR : (barMappedPos.x + leftEdgeOvs + tlRadius * tlMultX)) : -0.75 + startY: isRenderable ? (isFramed ? 0 : (barMappedPos.y + topEdgeOvs)) : -1 + + // ========== PATH DEFINITION ========== + + // 1. Main Bar / Outer Screen Rectangle + // When !isRenderable all elements use fallback coordinates forming a valid 1×1 + // off-screen square with non-degenerate arcs so CurveRenderer never receives + // a zero-area, bare-moveto, or zero-displacement arc path. + PathLine { + x: root.isRenderable ? (root.isFramed ? (root.screenWidth - root._minR) : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs - root.trRadius * root.trMultX)) : 0 + y: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.y + root.topEdgeOvs)) : -1 + } + + // Top-right corner + PathArc { + x: root.isRenderable ? (root.isFramed ? root.screenWidth : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs)) : 0 + y: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.y + root.topEdgeOvs + root.trRadius * root.trMultY)) : -0.75 + radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.trRadius) : 0 + radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.trRadius) : 0 + direction: ShapeCornerHelper.getArcDirection(root.trMultX, root.trMultY) + } + + PathLine { + x: root.isRenderable ? (root.isFramed ? root.screenWidth : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs)) : 0 + y: root.isRenderable ? (root.isFramed ? (root.screenHeight - root._minR) : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs - root.brRadius * root.brMultY)) : 0 + } + + // Bottom-right corner + PathArc { + x: root.isRenderable ? (root.isFramed ? (root.screenWidth - root._minR) : (root.barMappedPos.x + root.barWidth + root.rightEdgeOvs - root.brRadius * root.brMultX)) : -0.25 + y: root.isRenderable ? (root.isFramed ? root.screenHeight : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs)) : 0 + radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.brRadius) : 0 + radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.brRadius) : 0 + direction: ShapeCornerHelper.getArcDirection(root.brMultX, root.brMultY) + } + + PathLine { + x: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.x + root.leftEdgeOvs + root.blRadius * root.blMultX)) : -1 + y: root.isRenderable ? (root.isFramed ? root.screenHeight : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs)) : 0 + } + + // Bottom-left corner + PathArc { + x: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.x + root.leftEdgeOvs)) : -1 + y: root.isRenderable ? (root.isFramed ? (root.screenHeight - root._minR) : (root.barMappedPos.y + root.barHeight + root.bottomEdgeOvs - root.blRadius * root.blMultY)) : -0.25 + radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.blRadius) : 0 + radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.blRadius) : 0 + direction: ShapeCornerHelper.getArcDirection(root.blMultX, root.blMultY) + } + + PathLine { + x: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.x + root.leftEdgeOvs)) : -1 + y: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.y + root.topEdgeOvs + root.tlRadius * root.tlMultY)) : -1 + } + + // Top-left corner (back to start) + PathArc { + x: root.isRenderable ? (root.isFramed ? root._minR : (root.barMappedPos.x + root.leftEdgeOvs + root.tlRadius * root.tlMultX)) : -0.75 + y: root.isRenderable ? (root.isFramed ? 0 : (root.barMappedPos.y + root.topEdgeOvs)) : -1 + radiusX: root.isRenderable ? (root.isFramed ? root._minR : root.tlRadius) : 0 + radiusY: root.isRenderable ? (root.isFramed ? root._minR : root.tlRadius) : 0 + direction: ShapeCornerHelper.getArcDirection(root.tlMultX, root.tlMultY) + } + + // 2. Inner Hole for Framed Mode (Clockwise) + // When !isFramed, draws a tiny 1x1 rectangle inside the bar as a non-degenerate WindingFill + // no-op to prevent a zero-area degenerate subpath crashing qTriangulate. + // Note: an exact duplicate of the outer path cannot be used here because Qt's CurveRenderer + // has issues with exactly coincident subpaths, causing the fill to not render on some systems. + // When !isRenderable, falls back to a valid 1×1 off-screen square at (-3,-3)→(-2,-2). + readonly property real _nhX: barMappedPos.x + barWidth / 2 + readonly property real _nhY: barMappedPos.y + barHeight / 2 + PathMove { + x: root.isRenderable ? (root.isFramed ? (root.holeX + root.frameRadius) : (root._nhX + 0.25)) : -2.75 + y: root.isRenderable ? (root.isFramed ? root.holeY : root._nhY) : -3 + } + + // Top edge + PathLine { + x: root.isRenderable ? (root.isFramed ? (root.holeX + root.holeWidth - root.frameRadius) : (root._nhX + 1)) : -2 + y: root.isRenderable ? (root.isFramed ? root.holeY : root._nhY) : -3 + } + + // Top-right corner + PathArc { + x: root.isRenderable ? (root.isFramed ? (root.holeX + root.holeWidth) : (root._nhX + 1)) : -2 + y: root.isRenderable ? (root.isFramed ? (root.holeY + root.frameRadius) : (root._nhY + 0.25)) : -2.75 + radiusX: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0 + radiusY: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0 + direction: PathArc.Clockwise + } + + // Right edge + PathLine { + x: root.isRenderable ? (root.isFramed ? (root.holeX + root.holeWidth) : (root._nhX + 1)) : -2 + y: root.isRenderable ? (root.isFramed ? (root.holeY + root.holeHeight - root.frameRadius) : (root._nhY + 1)) : -2 + } + + // Bottom-right corner + PathArc { + x: root.isRenderable ? (root.isFramed ? (root.holeX + root.holeWidth - root.frameRadius) : (root._nhX + 0.75)) : -2.25 + y: root.isRenderable ? (root.isFramed ? (root.holeY + root.holeHeight) : (root._nhY + 1)) : -2 + radiusX: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0 + radiusY: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0 + direction: PathArc.Clockwise + } + + // Bottom edge + PathLine { + x: root.isRenderable ? (root.isFramed ? (root.holeX + root.frameRadius) : root._nhX) : -3 + y: root.isRenderable ? (root.isFramed ? (root.holeY + root.holeHeight) : (root._nhY + 1)) : -2 + } + + // Bottom-left corner + PathArc { + x: root.isRenderable ? (root.isFramed ? root.holeX : root._nhX) : -3 + y: root.isRenderable ? (root.isFramed ? (root.holeY + root.holeHeight - root.frameRadius) : (root._nhY + 0.75)) : -2.25 + radiusX: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0 + radiusY: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0 + direction: PathArc.Clockwise + } + + // Left edge + PathLine { + x: root.isRenderable ? (root.isFramed ? root.holeX : root._nhX) : -3 + y: root.isRenderable ? (root.isFramed ? (root.holeY + root.frameRadius) : root._nhY) : -3 + } + + // Top-left corner (back to start) + PathArc { + x: root.isRenderable ? (root.isFramed ? (root.holeX + root.frameRadius) : (root._nhX + 0.25)) : -2.75 + y: root.isRenderable ? (root.isFramed ? root.holeY : root._nhY) : -3 + radiusX: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0 + radiusY: root.isRenderable ? (root.isFramed ? root.frameRadius : 0) : 0 + direction: PathArc.Clockwise + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/Backgrounds/PanelBackground.qml b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/Backgrounds/PanelBackground.qml new file mode 100644 index 0000000..5842b10 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/Backgrounds/PanelBackground.qml @@ -0,0 +1,167 @@ +import QtQuick +import QtQuick.Shapes +import qs.Commons +import qs.Modules.MainScreen.Backgrounds + +/** +* PanelBackground - Dynamic ShapePath for rendering panel backgrounds +* +* Dynamically switches between panels based on which panel is currently +* assigned by PanelService. Only 2 instances are needed: one for the +* currently open panel and one for a closing panel during transitions. +* +* Uses 4-state per-corner system for flexible corner rendering: +* - State -1: No radius (flat/square corner) +* - State 0: Normal (inner curve) +* - State 1: Horizontal inversion (outer curve on X-axis) +* - State 2: Vertical inversion (outer curve on Y-axis) +*/ +ShapePath { + id: root + + // Dynamically assigned panel (null if slot is unused) + property var assignedPanel: null + + // Required reference to AllBackgrounds shapeContainer + required property var shapeContainer + + // Default background color (used if panel doesn't specify one) + property color defaultBackgroundColor: Color.mSurface + + // Corner radius (from Style) + readonly property real radius: Style.radiusL + + // Get panel's panelRegion (geometry placeholder) + readonly property var panelRegion: assignedPanel?.panelRegion ?? null + + // Get the actual panelBackground Item from panelRegion + // Only access panelItem if panelRegion exists and is visible + readonly property var panelBg: (panelRegion && panelRegion.visible) ? panelRegion.panelItem : null + + // Effective background color: use panel's if defined, else default + readonly property color effectiveBackgroundColor: { + if (!assignedPanel) + return "transparent"; + if (assignedPanel.panelBackgroundColor !== undefined) { + return assignedPanel.panelBackgroundColor; + } + return defaultBackgroundColor; + } + + // Panel position - panelBg is in screen coordinates already + readonly property real panelX: panelBg ? panelBg.x : 0 + readonly property real panelY: panelBg ? panelBg.y : 0 + readonly property real panelWidth: panelBg ? panelBg.width : 0 + readonly property real panelHeight: panelBg ? panelBg.height : 0 + readonly property bool isRenderable: assignedPanel && panelBg && panelWidth > 0 && panelHeight > 0 + + // Flatten corners if panel is too small + readonly property bool shouldFlatten: panelBg ? ShapeCornerHelper.shouldFlatten(panelWidth, panelHeight, radius) : false + readonly property real effectiveRadius: shouldFlatten ? ShapeCornerHelper.getFlattenedRadius(Math.min(panelWidth, panelHeight), radius) : radius + + // Minimum safe arc radius — prevents zero-displacement zero-radius PathArcs + // that crash qTriangulate in CurveRenderer. 0.01px is sub-pixel and invisible. + readonly property real _minR: 0.01 + + // Helper function for getting corner radius based on state + function getCornerRadius(cornerState) { + // State -1 = flat corner — use minimum safe radius instead of 0 + // to prevent degenerate PathArc (zero displacement + zero radius) + if (cornerState === -1) + return _minR; + // All other states use effectiveRadius (clamped to safe minimum) + return Math.max(_minR, effectiveRadius); + } + + // Per-corner multipliers and radii based on panelBg's corner states + readonly property real tlMultX: panelBg ? ShapeCornerHelper.getMultX(panelBg.topLeftCornerState) : 1 + readonly property real tlMultY: panelBg ? ShapeCornerHelper.getMultY(panelBg.topLeftCornerState) : 1 + readonly property real tlRadius: panelBg ? getCornerRadius(panelBg.topLeftCornerState) : 0 + + readonly property real trMultX: panelBg ? ShapeCornerHelper.getMultX(panelBg.topRightCornerState) : 1 + readonly property real trMultY: panelBg ? ShapeCornerHelper.getMultY(panelBg.topRightCornerState) : 1 + readonly property real trRadius: panelBg ? getCornerRadius(panelBg.topRightCornerState) : 0 + + readonly property real brMultX: panelBg ? ShapeCornerHelper.getMultX(panelBg.bottomRightCornerState) : 1 + readonly property real brMultY: panelBg ? ShapeCornerHelper.getMultY(panelBg.bottomRightCornerState) : 1 + readonly property real brRadius: panelBg ? getCornerRadius(panelBg.bottomRightCornerState) : 0 + + readonly property real blMultX: panelBg ? ShapeCornerHelper.getMultX(panelBg.bottomLeftCornerState) : 1 + readonly property real blMultY: panelBg ? ShapeCornerHelper.getMultY(panelBg.bottomLeftCornerState) : 1 + readonly property real blRadius: panelBg ? getCornerRadius(panelBg.bottomLeftCornerState) : 0 + + // ShapePath configuration + strokeWidth: -1 // No stroke, fill only + + // Start point - use tiny off-screen non-degenerate fallback when not renderable. + // Fallback forms a 1×1 off-screen square where each edge is split between a PathLine + // and a PathArc, ensuring no arc has zero displacement (which can crash qTriangulate). + startX: isRenderable ? (panelX + tlRadius * tlMultX) : -0.75 + startY: isRenderable ? panelY : -1 + + fillColor: isRenderable ? effectiveBackgroundColor : "transparent" + + // ========== PATH DEFINITION ========== + // Draws a rectangle with potentially inverted corners + // All coordinates are relative to startX/startY + + // Top edge (moving right) + PathLine { + relativeX: root.isRenderable ? (root.panelWidth - root.tlRadius * root.tlMultX - root.trRadius * root.trMultX) : 0.75 + relativeY: 0 + } + + // Top-right corner arc + PathArc { + relativeX: root.isRenderable ? (root.trRadius * root.trMultX) : 0 + relativeY: root.isRenderable ? (root.trRadius * root.trMultY) : 0.25 + radiusX: root.isRenderable ? root.trRadius : 0 + radiusY: root.isRenderable ? root.trRadius : 0 + direction: ShapeCornerHelper.getArcDirection(root.trMultX, root.trMultY) + } + + // Right edge (moving down) + PathLine { + relativeX: 0 + relativeY: root.isRenderable ? (root.panelHeight - root.trRadius * root.trMultY - root.brRadius * root.brMultY) : 0.75 + } + + // Bottom-right corner arc + PathArc { + relativeX: root.isRenderable ? (-root.brRadius * root.brMultX) : -0.25 + relativeY: root.isRenderable ? (root.brRadius * root.brMultY) : 0 + radiusX: root.isRenderable ? root.brRadius : 0 + radiusY: root.isRenderable ? root.brRadius : 0 + direction: ShapeCornerHelper.getArcDirection(root.brMultX, root.brMultY) + } + + // Bottom edge (moving left) + PathLine { + relativeX: root.isRenderable ? (-(root.panelWidth - root.brRadius * root.brMultX - root.blRadius * root.blMultX)) : -0.75 + relativeY: 0 + } + + // Bottom-left corner arc + PathArc { + relativeX: root.isRenderable ? (-root.blRadius * root.blMultX) : 0 + relativeY: root.isRenderable ? (-root.blRadius * root.blMultY) : -0.25 + radiusX: root.isRenderable ? root.blRadius : 0 + radiusY: root.isRenderable ? root.blRadius : 0 + direction: ShapeCornerHelper.getArcDirection(root.blMultX, root.blMultY) + } + + // Left edge (moving up) - closes the path back to start + PathLine { + relativeX: 0 + relativeY: root.isRenderable ? (-(root.panelHeight - root.blRadius * root.blMultY - root.tlRadius * root.tlMultY)) : -0.75 + } + + // Top-left corner arc (back to start) + PathArc { + relativeX: root.isRenderable ? (root.tlRadius * root.tlMultX) : 0.25 + relativeY: root.isRenderable ? (-root.tlRadius * root.tlMultY) : 0 + radiusX: root.isRenderable ? root.tlRadius : 0 + radiusY: root.isRenderable ? root.tlRadius : 0 + direction: ShapeCornerHelper.getArcDirection(root.tlMultX, root.tlMultY) + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/Backgrounds/ShapeCornerHelper.qml b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/Backgrounds/ShapeCornerHelper.qml new file mode 100644 index 0000000..f53cc67 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/Backgrounds/ShapeCornerHelper.qml @@ -0,0 +1,74 @@ +pragma Singleton + +import QtQuick +import QtQuick.Shapes +import Quickshell + +/** +* ShapeCornerHelper - Utility singleton for shape corner calculations +* +* Uses 4-state per-corner system for flexible corner rendering: +* - State -1: No radius (flat/square corner) +* - State 0: Normal (inner curve) +* - State 1: Horizontal inversion (outer curve on X-axis) +* - State 2: Vertical inversion (outer curve on Y-axis) +* +* The key technique: Using PathArc direction control (Clockwise vs Counterclockwise) +* combined with multipliers to create both inner and outer corner curves. +*/ +Singleton { + id: root + + /** + * Get X-axis multiplier for a corner state + * State 1 (horizontal invert) returns -1, others return 1 + */ + function getMultX(cornerState) { + return cornerState === 1 ? -1 : 1; + } + + /** + * Get Y-axis multiplier for a corner state + * State 2 (vertical invert) returns -1, others return 1 + */ + function getMultY(cornerState) { + return cornerState === 2 ? -1 : 1; + } + + /** + * Get PathArc direction for a corner based on its multipliers + * Uses XOR logic: if X inverted differs from Y inverted, use Counterclockwise + * This creates the outer curve effect for inverted corners + */ + function getArcDirection(multX, multY) { + return ((multX < 0) !== (multY < 0)) ? PathArc.Counterclockwise : PathArc.Clockwise; + } + + /** + * Convenience function to get arc direction directly from corner state + */ + function getArcDirectionFromState(cornerState) { + const multX = getMultX(cornerState); + const multY = getMultY(cornerState); + return getArcDirection(multX, multY); + } + + /** + * Get the "flattening" radius when shape dimensions are too small + * Prevents visual artifacts when radius exceeds dimensions + */ + function getFlattenedRadius(dimension, requestedRadius) { + if (dimension < requestedRadius * 2) { + return dimension / 2; + } + return requestedRadius; + } + + /** + * Check if a shape should use flattened corners + * Returns true if width or height is too small for the requested radius + */ + function shouldFlatten(width, height, radius) { + return width < radius * 2 || height < radius * 2; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/BarContentWindow.qml b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/BarContentWindow.qml new file mode 100644 index 0000000..32bcc95 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/BarContentWindow.qml @@ -0,0 +1,255 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Modules.Bar +import qs.Services.UI + +/** +* BarContentWindow - Separate transparent PanelWindow for bar content +* +* This window contains only the bar widgets (content), while the background +* is rendered in MainScreen's unified Shape system. This separation prevents +* fullscreen redraws when bar widgets redraw. +* +* This component should be instantiated once per screen by AllScreens.qml +*/ +PanelWindow { + id: barWindow + + // Note: screen property is inherited from PanelWindow and should be set by parent + color: "transparent" // Transparent - background is in MainScreen below + + // Window invisible when auto-hidden (blocks input) or toggled off via IPC. + // windowVisible stays true briefly after isHidden to allow fade-out animation. + property bool windowVisible: !isHidden + visible: contentLoaded && windowVisible && BarService.effectivelyVisible + + Component.onCompleted: { + Logger.d("BarContentWindow", "Bar content window created for screen:", barWindow.screen?.name); + if (!isHidden) + contentLoaded = true; + } + + // Wayland layer configuration + WlrLayershell.namespace: "noctalia-bar-content-" + (barWindow.screen?.name || "unknown") + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.exclusionMode: ExclusionMode.Ignore // Don't reserve space - BarExclusionZone in MainScreen handles that + + // Position and size to match bar location (per-screen) + readonly property string barPosition: Settings.getBarPositionForScreen(barWindow.screen?.name) + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property bool isFramed: Settings.data.bar.barType === "framed" + readonly property real frameThickness: Settings.data.bar.frameThickness ?? 12 + readonly property bool barFloating: Settings.data.bar.barType === "floating" + readonly property real barMarginH: Math.ceil(barFloating ? Settings.data.bar.marginHorizontal : 0) + readonly property real barMarginV: Math.ceil(barFloating ? Settings.data.bar.marginVertical : 0) + readonly property real barHeight: Style.getBarHeightForScreen(barWindow.screen?.name) + + // Auto-hide properties + readonly property bool autoHide: Settings.getBarDisplayModeForScreen(barWindow.screen?.name) === "auto_hide" + readonly property int hideDelay: Settings.data.bar.autoHideDelay || 500 + readonly property int showDelay: Settings.data.bar.autoShowDelay || 100 + property bool isHidden: autoHide + + // Hover tracking + property bool barHovered: false + + // Check if any panel is open on this screen + readonly property bool panelOpen: PanelService.openedPanel !== null + + // Timer for delayed hide + Timer { + id: hideTimer + interval: barWindow.hideDelay + onTriggered: { + if (barWindow.autoHide && !barWindow.barHovered && !barWindow.panelOpen && !BarService.popupOpen) { + BarService.setScreenHidden(barWindow.screen?.name, true); + } + } + } + + // Timer for delayed show + Timer { + id: showTimer + interval: barWindow.showDelay + onTriggered: { + // Only show if still hovered (via trigger zone or bar itself) + if (barWindow.autoHide && BarService.isBarHovered(barWindow.screen?.name)) { + BarService.setScreenHidden(barWindow.screen?.name, false); + } + } + } + + // React to auto-hide state changes from BarService + Connections { + target: BarService + function onBarAutoHideStateChanged(screenName, hidden) { + Logger.d("BarContentWindow", "onBarAutoHideStateChanged:", screenName, hidden, "my screen:", barWindow.screen?.name); + if (screenName === barWindow.screen?.name) { + barWindow.isHidden = hidden; + } + } + function onBarHoverStateChanged(screenName, hovered) { + if (screenName === barWindow.screen?.name && barWindow.autoHide) { + if (hovered) { + hideTimer.stop(); + // If bar is already visible, no need to delay + if (!barWindow.isHidden) { + showTimer.stop(); + } else { + // Bar is hidden, use show delay + showTimer.restart(); + } + } else if (!barWindow.barHovered && !barWindow.panelOpen) { + showTimer.stop(); + hideTimer.restart(); + } + } + } + } + + // Don't hide when panel is open + onPanelOpenChanged: { + if (panelOpen && autoHide) { + hideTimer.stop(); + BarService.setScreenHidden(barWindow.screen?.name, false); + } else if (!panelOpen && autoHide && !barHovered) { + hideTimer.restart(); + } + } + + // React to popup menu closing + Connections { + target: BarService + function onPopupOpenChanged() { + if (!BarService.popupOpen && barWindow.autoHide && !barWindow.barHovered && !barWindow.panelOpen) { + hideTimer.restart(); + } + } + } + + // React to displayMode changes + onAutoHideChanged: { + if (!autoHide) { + // Show bar when auto-hide is disabled + hideTimer.stop(); + showTimer.stop(); + barWindow.isHidden = false; + } + // When auto-hide is enabled, don't immediately hide - wait for mouse to leave + } + + // Anchor to the bar's edge + anchors { + top: barPosition === "top" || barIsVertical + bottom: barPosition === "bottom" || barIsVertical + left: barPosition === "left" || !barIsVertical + right: barPosition === "right" || !barIsVertical + } + + // Content stays loaded once initialized — never unloaded during auto-hide. + // Destroying and recreating widgets on every hide/show cycle caused nested + // QML incubation crashes (SIGSEGV in QV4::Object::insertMember) because + // async widget Loaders complete during incubateFor() and their onLoaded + // handlers trigger signal cascades mid-incubation. + // The bar is hidden via opacity + window visibility instead. + property bool contentLoaded: false + + // Delay window hide to allow fade-out animation to complete + Timer { + id: windowHideTimer + interval: Style.animationFast + onTriggered: { + if (barWindow.isHidden) + barWindow.windowVisible = false; + } + } + + onIsHiddenChanged: { + if (isHidden) { + // Delay window hide so fade-out is visible + windowHideTimer.restart(); + } else { + windowHideTimer.stop(); + windowVisible = true; + if (!contentLoaded) + contentLoaded = true; + } + } + + Connections { + target: BarService + function onEffectivelyVisibleChanged() { + if (BarService.effectivelyVisible && !barWindow.isHidden && !barWindow.contentLoaded) { + barWindow.contentLoaded = true; + } + } + } + + // Handle floating margins and framed mode offsets + margins { + top: (barPosition === "top") ? barMarginV : (isFramed ? frameThickness : barMarginV) + bottom: (barPosition === "bottom") ? barMarginV : (isFramed ? frameThickness : barMarginV) + left: (barPosition === "left") ? barMarginH : (isFramed ? frameThickness : barMarginH) + right: (barPosition === "right") ? barMarginH : (isFramed ? frameThickness : barMarginH) + } + + // Set a tight window size + implicitWidth: barIsVertical ? barHeight : barWindow.screen.width + implicitHeight: barIsVertical ? barWindow.screen.height : barHeight + + // Bar content loader - loaded once, stays active for lifetime + Loader { + id: barLoader + anchors.fill: parent + active: barWindow.contentLoaded + + sourceComponent: Item { + anchors.fill: parent + + // Fade animation + opacity: barWindow.isHidden ? 0 : 1 + + Behavior on opacity { + enabled: barWindow.autoHide + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutQuad + } + } + + Bar { + id: barContent + anchors.fill: parent + screen: barWindow.screen + + // Hover detection using HoverHandler (doesn't block child hover events) + HoverHandler { + id: hoverHandler + + onHoveredChanged: { + if (hovered) { + barWindow.barHovered = true; + BarService.setScreenHovered(barWindow.screen?.name, true); + if (barWindow.autoHide) { + hideTimer.stop(); + showTimer.restart(); + } + } else { + // Skip if already hidden (being destroyed) + if (barWindow.isHidden) + return; + barWindow.barHovered = false; + BarService.setScreenHovered(barWindow.screen?.name, false); + if (barWindow.autoHide && !barWindow.panelOpen) { + showTimer.stop(); + hideTimer.restart(); + } + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/BarExclusionZone.qml b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/BarExclusionZone.qml new file mode 100644 index 0000000..cad17da --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/BarExclusionZone.qml @@ -0,0 +1,74 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services.Compositor +import qs.Services.UI + +/** +* BarExclusionZone - Invisible PanelWindow that reserves exclusive space for the bar +* +* This is a minimal window that works with the compositor to reserve space, +* while the actual bar UI is rendered in MainScreen. +*/ +PanelWindow { + id: root + + // Edge to anchor to and thickness to reserve + property string edge: Settings.getBarPositionForScreen(screen?.name) + property real thickness: (edge === Settings.getBarPositionForScreen(screen?.name)) ? Style.getBarHeightForScreen(screen?.name) : (Settings.data.bar.frameThickness ?? 12) + + readonly property bool autoHide: Settings.getBarDisplayModeForScreen(screen?.name) === "auto_hide" + readonly property bool nonExclusive: Settings.getBarDisplayModeForScreen(screen?.name) === "non_exclusive" + readonly property bool barFloating: Settings.data.bar.barType === "floating" + readonly property real barMarginH: (barFloating && edge === Settings.getBarPositionForScreen(screen?.name)) ? Math.ceil(Settings.data.bar.marginHorizontal) : 0 + readonly property real barMarginV: (barFloating && edge === Settings.getBarPositionForScreen(screen?.name)) ? Math.ceil(Settings.data.bar.marginVertical) : 0 + // Allow users to enable a 1-physical-pixel inset for the exclusion zone so window borders can bleed under the bar + readonly property real bleedOffset: Settings.data.bar.enableExclusionZoneInset ? 1.0 : 0.0 + readonly property real bleedInset: { + const info = CompositorService.displayScales[screen?.name]; + const scale = (info && info.scale) ? info.scale : 1.0; + return bleedOffset / scale; + } + + // Invisible - just reserves space + color: "transparent" + + mask: Region {} + + // Wayland layer shell configuration + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.namespace: "noctalia-bar-exclusion-" + edge + "-" + (screen?.name || "unknown") + // When auto-hide, non-exclusive mode is enabled, OR bar is explicitly hidden via IPC, don't reserve space + // Note: We check BarService.isVisible directly, NOT effectivelyVisible, because we want + // the exclusion zone to stay during overview (effectivelyVisible is false during overview + // when hideOnOverview is enabled, but isVisible remains true) + WlrLayershell.exclusionMode: (autoHide || nonExclusive || !BarService.isVisible) ? ExclusionMode.Ignore : ExclusionMode.Auto + + // Anchor based on specified edge + anchors { + top: edge === "top" + bottom: edge === "bottom" + left: edge === "left" || edge === "top" || edge === "bottom" + right: edge === "right" || edge === "top" || edge === "bottom" + } + + // Size based on orientation + implicitWidth: { + if (edge === "left" || edge === "right") { + return thickness + barMarginH - bleedInset; + } + return 0; // Auto-width when left/right anchors are true + } + + implicitHeight: { + if (edge === "top" || edge === "bottom") { + return thickness + barMarginV - bleedInset; + } + return 0; // Auto-height when top/bottom anchors are true + } + + Component.onCompleted: { + Logger.d("BarExclusionZone", "Created for screen:", screen?.name); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/BarTriggerZone.qml b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/BarTriggerZone.qml new file mode 100644 index 0000000..b7a9cd5 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/BarTriggerZone.qml @@ -0,0 +1,66 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services.UI + +/** +* BarTriggerZone - Thin invisible window at screen edge to reveal hidden bar +* +* This window is only active when the bar is in auto-hide mode and hidden. +* When the mouse enters this zone, it triggers the bar to show. +*/ +PanelWindow { + id: root + + readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name) + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property int triggerSize: 1 + + // Track if component is being destroyed to prevent signals during cleanup + property bool isBeingDestroyed: false + Component.onDestruction: isBeingDestroyed = true + + // Invisible trigger zone + color: "transparent" + focusable: false + + WlrLayershell.namespace: "noctalia-bar-trigger-" + (screen?.name || "unknown") + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.exclusionMode: ExclusionMode.Ignore + + // Anchor to bar's edge + anchors { + top: barPosition === "top" || barIsVertical + bottom: barPosition === "bottom" || barIsVertical + left: barPosition === "left" || !barIsVertical + right: barPosition === "right" || !barIsVertical + } + + // Size based on orientation - thin strip at edge + implicitWidth: barIsVertical ? triggerSize : 0 + implicitHeight: !barIsVertical ? triggerSize : 0 + + MouseArea { + id: triggerArea + anchors.fill: parent + hoverEnabled: true + + onEntered: { + if (root.isBeingDestroyed) + return; + // Signal hover - BarContentWindow will handle the show delay + BarService.setScreenHovered(root.screen?.name, true); + } + + onExited: { + if (root.isBeingDestroyed) + return; + BarService.setScreenHovered(root.screen?.name, false); + } + } + + Component.onCompleted: { + Logger.d("BarTriggerZone", "Created for screen:", screen?.name); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/MainScreen.qml b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/MainScreen.qml new file mode 100644 index 0000000..4526a29 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/MainScreen.qml @@ -0,0 +1,690 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import "Backgrounds" as Backgrounds + +import qs.Commons + +// All panels +import qs.Modules.Bar +import qs.Modules.Bar.Extras +import qs.Modules.Panels.Audio +import qs.Modules.Panels.Battery +import qs.Modules.Panels.Bluetooth +import qs.Modules.Panels.Brightness +import qs.Modules.Panels.Changelog +import qs.Modules.Panels.Clock +import qs.Modules.Panels.ControlCenter +import qs.Modules.Panels.Dock +import qs.Modules.Panels.Launcher +import qs.Modules.Panels.Media +import qs.Modules.Panels.Network +import qs.Modules.Panels.NotificationHistory +import qs.Modules.Panels.Plugins +import qs.Modules.Panels.SessionMenu +import qs.Modules.Panels.Settings +import qs.Modules.Panels.SetupWizard +import qs.Modules.Panels.SystemStats +import qs.Modules.Panels.Tray +import qs.Modules.Panels.Wallpaper +import qs.Services.Compositor +import qs.Services.Power +import qs.Services.UI + +/** +* MainScreen - Single PanelWindow per screen that manages all panels and the bar +*/ +PanelWindow { + id: root + + Component.onCompleted: { + Logger.d("MainScreen", "Initialized for screen:", screen?.name, "- Dimensions:", screen?.width, "x", screen?.height, "- Position:", screen?.x, ",", screen?.y); + } + + WlrLayershell.layer: WlrLayer.Top + WlrLayershell.namespace: "noctalia-background-" + (screen?.name || "unknown") + WlrLayershell.exclusionMode: ExclusionMode.Ignore // Don't reserve space - BarExclusionZone handles that + WlrLayershell.keyboardFocus: { + // No panel open anywhere: no keyboard focus needed + if (!root.isAnyPanelOpen) { + return WlrKeyboardFocus.None; + } + // Panel open on THIS screen: use panel's preferred focus mode + if (root.isPanelOpen) { + // Hyprland's Exclusive captures ALL input globally (including pointer), + // preventing click-to-close from working on other monitors. + // Workaround: briefly use Exclusive when panel opens (for text input focus), + // then switch to OnDemand (for click-to-close on other screens). + if (CompositorService.isHyprland) { + return PanelService.isInitializingKeyboard ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.OnDemand; + } + return PanelService.openedPanel.exclusiveKeyboard ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.OnDemand; + } + // Panel open on ANOTHER screen: OnDemand allows receiving pointer events for click-to-close + return WlrKeyboardFocus.OnDemand; + } + + anchors { + top: true + bottom: true + left: true + right: true + } + + // Desktop dimming when panels are open + property real dimmerOpacity: Settings.data.general.dimmerOpacity ?? 0.8 + property bool isPanelOpen: (PanelService.openedPanel !== null) && (PanelService.openedPanel.screen === screen) + property bool isPanelClosing: (PanelService.openedPanel !== null) && PanelService.openedPanel.isClosing + property bool isAnyPanelOpen: PanelService.openedPanel !== null + + color: { + if (dimmerOpacity > 0 && isPanelOpen && !isPanelClosing) { + return Qt.alpha(Color.mShadow, dimmerOpacity); + } + return "transparent"; + } + + Behavior on color { + enabled: !PanelService.closedImmediately + ColorAnimation { + duration: isPanelClosing ? Style.animationFaster : Style.animationNormal + easing.type: Easing.OutQuad + } + } + + // Reset closedImmediately flag after color change is applied + onColorChanged: { + if (PanelService.closedImmediately) { + PanelService.closedImmediately = false; + } + } + + // Check if bar should be visible on this screen + readonly property bool barShouldShow: { + // Check global bar visibility (includes overview state) + if (!BarService.effectivelyVisible) + return false; + + // Check screen-specific configuration + var monitors = Settings.data.bar.monitors || []; + var screenName = screen?.name || ""; + + // If no monitors specified, show on all screens + // If monitors specified, only show if this screen is in the list + return monitors.length === 0 || monitors.includes(screenName); + } + + // Make everything click-through except bar + mask: Region { + id: clickableMask + + // Cover entire window (everything is masked/click-through) + x: 0 + y: 0 + width: root.width + height: root.height + intersection: Intersection.Xor + + // Only include regions that are actually needed + // panelRegions is handled by PanelService, bar is local to this screen + regions: [barMaskRegion, backgroundMaskRegion] + + // Bar region - subtract bar area from mask (only if bar should be shown on this screen) + Region { + id: barMaskRegion + + readonly property bool isFramed: Settings.data.bar.barType === "framed" + readonly property real barThickness: Style.barHeight + readonly property real frameThickness: Settings.data.bar.frameThickness ?? 12 + readonly property string barPos: Settings.data.bar.position || "top" + + // Bar / Frame Mask + Region { + // Mode: Simple or Floating + x: barPlaceholder.x + y: barPlaceholder.y + width: (!barMaskRegion.isFramed && root.barShouldShow) ? barPlaceholder.width : 0 + height: (!barMaskRegion.isFramed && root.barShouldShow) ? barPlaceholder.height : 0 + intersection: Intersection.Subtract + } + + // Mode: Framed - 4 sides + Region { + // Top side + Region { + x: 0 + y: 0 + width: (barMaskRegion.isFramed && root.barShouldShow) ? root.width : 0 + height: (barMaskRegion.isFramed && root.barShouldShow) ? (barMaskRegion.barPos === "top" ? barMaskRegion.barThickness : barMaskRegion.frameThickness) : 0 + intersection: Intersection.Subtract + } + + // Bottom side + Region { + x: 0 + y: (barMaskRegion.isFramed && root.barShouldShow) ? (root.height - (barMaskRegion.barPos === "bottom" ? barMaskRegion.barThickness : barMaskRegion.frameThickness)) : 0 + width: (barMaskRegion.isFramed && root.barShouldShow) ? root.width : 0 + height: (barMaskRegion.isFramed && root.barShouldShow) ? (barMaskRegion.barPos === "bottom" ? barMaskRegion.barThickness : barMaskRegion.frameThickness) : 0 + intersection: Intersection.Subtract + } + + // Left side + Region { + x: 0 + y: 0 + width: (barMaskRegion.isFramed && root.barShouldShow) ? (barMaskRegion.barPos === "left" ? barMaskRegion.barThickness : barMaskRegion.frameThickness) : 0 + height: (barMaskRegion.isFramed && root.barShouldShow) ? root.height : 0 + intersection: Intersection.Subtract + } + + // Right side + Region { + x: (barMaskRegion.isFramed && root.barShouldShow) ? (root.width - (barMaskRegion.barPos === "right" ? barMaskRegion.barThickness : barMaskRegion.frameThickness)) : 0 + width: (barMaskRegion.isFramed && root.barShouldShow) ? (barMaskRegion.barPos === "right" ? barMaskRegion.barThickness : barMaskRegion.frameThickness) : 0 + height: (barMaskRegion.isFramed && root.barShouldShow) ? root.height : 0 + intersection: Intersection.Subtract + } + } + } + + // Background region for click-to-close - reactive sizing + // Uses isAnyPanelOpen so clicking on any screen's background closes the panel + Region { + id: backgroundMaskRegion + x: 0 + y: 0 + width: root.isAnyPanelOpen ? root.width : 0 + height: root.isAnyPanelOpen ? root.height : 0 + intersection: Intersection.Subtract + } + } + + // Blur behind the bar and open panels — attached to PanelWindow (required by BackgroundEffect API) + BackgroundEffect.blurRegion: Settings.data.general.enableBlurBehind ? blurRegion : null + Region { + id: blurRegion + // ── Non-framed bar (simple/floating): single rectangle with bar corner states ── + Region { + x: (!barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? barPlaceholder.x : 0 + y: (!barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? barPlaceholder.y : 0 + width: (!barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? barPlaceholder.width : 0 + height: (!barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? barPlaceholder.height : 0 + radius: Style.radiusL + topLeftCorner: barPlaceholder.topLeftCornerState + topRightCorner: barPlaceholder.topRightCornerState + bottomLeftCorner: barPlaceholder.bottomLeftCornerState + bottomRightCorner: barPlaceholder.bottomRightCornerState + } + + // ── Framed bar: full screen minus rounded hole ── + Region { + x: 0 + y: 0 + width: (barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? root.width : 0 + height: (barPlaceholder.isFramed && root.barShouldShow && !barPlaceholder.isHidden) ? root.height : 0 + + Region { + intersection: Intersection.Subtract + x: backgroundBlur.frameHoleX + y: backgroundBlur.frameHoleY + width: backgroundBlur.frameHoleX2 - backgroundBlur.frameHoleX + height: backgroundBlur.frameHoleY2 - backgroundBlur.frameHoleY + radius: backgroundBlur.frameR + } + } + + // ── Panel blur regions ── + // Opening panel + Region { + x: backgroundBlur.panelBg ? Math.round(backgroundBlur.panelBg.x) : 0 + y: backgroundBlur.panelBg ? Math.round(backgroundBlur.panelBg.y) : 0 + width: backgroundBlur.panelBg ? Math.round(backgroundBlur.panelBg.width) : 0 + height: backgroundBlur.panelBg ? Math.round(backgroundBlur.panelBg.height) : 0 + radius: Style.radiusL + topLeftCorner: backgroundBlur.panelBg ? backgroundBlur.panelBg.topLeftCornerState : CornerState.Normal + topRightCorner: backgroundBlur.panelBg ? backgroundBlur.panelBg.topRightCornerState : CornerState.Normal + bottomLeftCorner: backgroundBlur.panelBg ? backgroundBlur.panelBg.bottomLeftCornerState : CornerState.Normal + bottomRightCorner: backgroundBlur.panelBg ? backgroundBlur.panelBg.bottomRightCornerState : CornerState.Normal + } + + // Closing panel (coexists with opening panel during transition) + Region { + x: backgroundBlur.closingPanelBg ? Math.round(backgroundBlur.closingPanelBg.x) : 0 + y: backgroundBlur.closingPanelBg ? Math.round(backgroundBlur.closingPanelBg.y) : 0 + width: backgroundBlur.closingPanelBg ? Math.round(backgroundBlur.closingPanelBg.width) : 0 + height: backgroundBlur.closingPanelBg ? Math.round(backgroundBlur.closingPanelBg.height) : 0 + radius: Style.radiusL + topLeftCorner: backgroundBlur.closingPanelBg ? backgroundBlur.closingPanelBg.topLeftCornerState : CornerState.Normal + topRightCorner: backgroundBlur.closingPanelBg ? backgroundBlur.closingPanelBg.topRightCornerState : CornerState.Normal + bottomLeftCorner: backgroundBlur.closingPanelBg ? backgroundBlur.closingPanelBg.bottomLeftCornerState : CornerState.Normal + bottomRightCorner: backgroundBlur.closingPanelBg ? backgroundBlur.closingPanelBg.bottomRightCornerState : CornerState.Normal + } + } + + // -------------------------------------- + // Container for all UI elements + Item { + id: container + width: root.width + height: root.height + + // Unified backgrounds container / unified shadow system + // Renders all bar and panel backgrounds as ShapePaths within a single Shape + // This allows the shadow effect to apply to all backgrounds in one render pass + Backgrounds.AllBackgrounds { + id: unifiedBackgrounds + anchors.fill: parent + bar: barPlaceholder.barItem || null + windowRoot: root + z: 0 // Behind all content + } + + // Background MouseArea for closing panels when clicking outside + // Uses isAnyPanelOpen so clicking on any screen's background closes the panel + MouseArea { + anchors.fill: parent + enabled: root.isAnyPanelOpen + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: mouse => { + if (PanelService.openedPanel) { + PanelService.openedPanel.close(); + } + } + z: 0 // Behind panels and bar + } + + // --------------------------------------- + // All panels always exist + // --------------------------------------- + AudioPanel { + id: audioPanel + objectName: "audioPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + MediaPlayerPanel { + id: mediaPlayerPanel + objectName: "mediaPlayerPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + BatteryPanel { + id: batteryPanel + objectName: "batteryPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + BluetoothPanel { + id: bluetoothPanel + objectName: "bluetoothPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + BrightnessPanel { + id: brightnessPanel + objectName: "brightnessPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + ControlCenterPanel { + id: controlCenterPanel + objectName: "controlCenterPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + ChangelogPanel { + id: changelogPanel + objectName: "changelogPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + ClockPanel { + id: clockPanel + objectName: "clockPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + Launcher { + id: launcherPanel + objectName: "launcherPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + NotificationHistoryPanel { + id: notificationHistoryPanel + objectName: "notificationHistoryPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + SessionMenu { + id: sessionMenuPanel + objectName: "sessionMenuPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + SettingsPanel { + id: settingsPanel + objectName: "settingsPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + SetupWizard { + id: setupWizardPanel + objectName: "setupWizardPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + TrayDrawerPanel { + id: trayDrawerPanel + objectName: "trayDrawerPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + WallpaperPanel { + id: wallpaperPanel + objectName: "wallpaperPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + NetworkPanel { + id: networkPanel + objectName: "networkPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + SystemStatsPanel { + id: systemStatsPanel + objectName: "systemStatsPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + StaticDockPanel { + id: staticDockPanel + objectName: "staticDockPanel-" + (root.screen?.name || "unknown") + screen: root.screen + } + + // ---------------------------------------------- + // Plugin panel slots + // ---------------------------------------------- + PluginPanelSlot { + id: pluginPanel1 + objectName: "pluginPanel1-" + (root.screen?.name || "unknown") + screen: root.screen + slotNumber: 1 + } + + PluginPanelSlot { + id: pluginPanel2 + objectName: "pluginPanel2-" + (root.screen?.name || "unknown") + screen: root.screen + slotNumber: 2 + } + + // ---------------------------------------------- + // Bar background placeholder - just for background positioning (actual bar content is in BarContentWindow) + Item { + id: barPlaceholder + + // Expose self as barItem for AllBackgrounds compatibility + readonly property var barItem: barPlaceholder + + // Screen reference + property ShellScreen screen: root.screen + + // Bar background positioning properties (per-screen) + readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name) + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property bool isFramed: Settings.data.bar.barType === "framed" + readonly property real frameThickness: Settings.data.bar.frameThickness ?? 12 + readonly property bool barFloating: Settings.data.bar.barType === "floating" + readonly property real barMarginH: barFloating ? Math.floor(Settings.data.bar.marginHorizontal) : 0 + readonly property real barMarginV: barFloating ? Math.floor(Settings.data.bar.marginVertical) : 0 + readonly property real barHeight: Style.getBarHeightForScreen(screen?.name) + + // Auto-hide properties (read by AllBackgrounds for background fade) + readonly property bool autoHide: Settings.getBarDisplayModeForScreen(screen?.name) === "auto_hide" + property bool isHidden: autoHide + + Connections { + target: BarService + function onBarAutoHideStateChanged(screenName, hidden) { + if (screenName === barPlaceholder.screen?.name) { + barPlaceholder.isHidden = hidden; + } + } + } + + // Expose bar dimensions directly on this Item for BarBackground + // Use screen dimensions directly + x: { + if (barPosition === "right") + return (screen?.width ?? 0) - barHeight - barMarginH; + if (isFramed && !barIsVertical) + return frameThickness; + return barMarginH; + } + y: { + if (barPosition === "bottom") + return (screen?.height ?? 0) - barHeight - barMarginV; + if (isFramed && barIsVertical) + return frameThickness; + return barMarginV; + } + width: { + if (barIsVertical) { + return barHeight; + } + if (isFramed) + return (screen?.width ?? 0) - frameThickness * 2; + return (screen?.width ?? 0) - barMarginH * 2; + } + height: { + if (!barIsVertical) { + return barHeight; + } + if (isFramed) + return (screen?.height ?? 0) - frameThickness * 2; + return (screen?.height ?? 0) - barMarginV * 2; + } + + // Corner states (same as Bar.qml) + readonly property int topLeftCornerState: { + if (barFloating) + return 0; + if (barPosition === "top") + return -1; + if (barPosition === "left") + return -1; + if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "right")) { + return barIsVertical ? 1 : 2; + } + return -1; + } + + readonly property int topRightCornerState: { + if (barFloating) + return 0; + if (barPosition === "top") + return -1; + if (barPosition === "right") + return -1; + if (Settings.data.bar.outerCorners && (barPosition === "bottom" || barPosition === "left")) { + return barIsVertical ? 1 : 2; + } + return -1; + } + + readonly property int bottomLeftCornerState: { + if (barFloating) + return 0; + if (barPosition === "bottom") + return -1; + if (barPosition === "left") + return -1; + if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "right")) { + return barIsVertical ? 1 : 2; + } + return -1; + } + + readonly property int bottomRightCornerState: { + if (barFloating) + return 0; + if (barPosition === "bottom") + return -1; + if (barPosition === "right") + return -1; + if (Settings.data.bar.outerCorners && (barPosition === "top" || barPosition === "left")) { + return barIsVertical ? 1 : 2; + } + return -1; + } + } + + // Screen Corners + ScreenCorners {} + + // Blur behind the bar and open panels + // Helper object holding computed properties for blur regions + QtObject { + id: backgroundBlur + + // Panel background geometry (from the currently open panel on this screen) + readonly property var panelBg: { + var op = PanelService.openedPanel; + if (!op || op.screen !== root.screen || op.blurEnabled === false) + return null; + var region = op.panelRegion; + return (region && region.visible) ? region.panelItem : null; + } + + // Panel background geometry for the closing panel (may coexist with panelBg) + readonly property var closingPanelBg: { + var cp = PanelService.closingPanel; + if (!cp || cp.screen !== root.screen || cp.blurEnabled === false) + return null; + var region = cp.panelRegion; + return (region && region.visible) ? region.panelItem : null; + } + + // Framed bar: inner hole boundary (where the hole begins on each axis) + // These are the x/y coordinates of the 4 inner hole corners + readonly property real frameHoleX: barPlaceholder.barPosition === "left" ? barPlaceholder.barHeight : barPlaceholder.frameThickness + readonly property real frameHoleY: barPlaceholder.barPosition === "top" ? barPlaceholder.barHeight : barPlaceholder.frameThickness + readonly property real frameHoleX2: root.width - (barPlaceholder.barPosition === "right" ? barPlaceholder.barHeight : barPlaceholder.frameThickness) + readonly property real frameHoleY2: root.height - (barPlaceholder.barPosition === "bottom" ? barPlaceholder.barHeight : barPlaceholder.frameThickness) + readonly property real frameR: Settings.data.bar.frameRadius ?? 20 + } + + // Native idle inhibitor — one per active MainScreen window. + // Multiple inhibitors bound to the same enabled state are harmless; + // having one per screen is more robust than picking a "primary" screen. + IdleInhibitor { + window: root + enabled: IdleInhibitorService.isInhibited + + Component.onCompleted: { + IdleInhibitorService.nativeInhibitorAvailable = true; + Logger.d("IdleInhibitor", "Native IdleInhibitor active on screen:", root.screen?.name); + } + } + } + + // Centralized Keyboard Shortcuts + + // These shortcuts delegate to the opened panel's handler functions + // Panels can implement: onEscapePressed, onTabPressed, onBackTabPressed, + // onUpPressed, onDownPressed, onReturnPressed, etc... + Instantiator { + model: Settings.data.general.keybinds.keyEscape || [] + Shortcut { + sequence: modelData + enabled: root.isPanelOpen && (PanelService.openedPanel.onEscapePressed !== undefined) && !PanelService.isKeybindRecording + onActivated: PanelService.openedPanel.onEscapePressed() + } + } + + Shortcut { + sequence: "Tab" + enabled: root.isPanelOpen && (PanelService.openedPanel.onTabPressed !== undefined) + onActivated: PanelService.openedPanel.onTabPressed() + } + + Shortcut { + sequence: "Backtab" + enabled: root.isPanelOpen && (PanelService.openedPanel.onBackTabPressed !== undefined) + onActivated: PanelService.openedPanel.onBackTabPressed() + } + + Instantiator { + model: Settings.data.general.keybinds.keyUp || [] + Shortcut { + sequence: modelData + enabled: root.isPanelOpen && (PanelService.openedPanel.onUpPressed !== undefined) && !PanelService.isKeybindRecording + onActivated: PanelService.openedPanel.onUpPressed() + } + } + + Instantiator { + model: Settings.data.general.keybinds.keyDown || [] + Shortcut { + sequence: modelData + enabled: root.isPanelOpen && (PanelService.openedPanel.onDownPressed !== undefined) && !PanelService.isKeybindRecording + onActivated: PanelService.openedPanel.onDownPressed() + } + } + + Instantiator { + model: Settings.data.general.keybinds.keyEnter || [] + Shortcut { + sequence: modelData + enabled: root.isPanelOpen && (PanelService.openedPanel.onEnterPressed !== undefined) && !PanelService.isKeybindRecording + onActivated: PanelService.openedPanel.onEnterPressed() + } + } + + Instantiator { + model: Settings.data.general.keybinds.keyLeft || [] + Shortcut { + sequence: modelData + enabled: root.isPanelOpen && (PanelService.openedPanel.onLeftPressed !== undefined) && !PanelService.isKeybindRecording + onActivated: PanelService.openedPanel.onLeftPressed() + } + } + + Instantiator { + model: Settings.data.general.keybinds.keyRight || [] + Shortcut { + sequence: modelData + enabled: root.isPanelOpen && (PanelService.openedPanel.onRightPressed !== undefined) && !PanelService.isKeybindRecording + onActivated: PanelService.openedPanel.onRightPressed() + } + } + + Shortcut { + sequence: "Home" + enabled: root.isPanelOpen && (PanelService.openedPanel.onHomePressed !== undefined) + onActivated: PanelService.openedPanel.onHomePressed() + } + + Shortcut { + sequence: "End" + enabled: root.isPanelOpen && (PanelService.openedPanel.onEndPressed !== undefined) + onActivated: PanelService.openedPanel.onEndPressed() + } + + Shortcut { + sequence: "PgUp" + enabled: root.isPanelOpen && (PanelService.openedPanel.onPageUpPressed !== undefined) + onActivated: PanelService.openedPanel.onPageUpPressed() + } + + Shortcut { + sequence: "PgDown" + enabled: root.isPanelOpen && (PanelService.openedPanel.onPageDownPressed !== undefined) + onActivated: PanelService.openedPanel.onPageDownPressed() + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/PopupMenuWindow.qml b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/PopupMenuWindow.qml new file mode 100644 index 0000000..eb034a4 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/PopupMenuWindow.qml @@ -0,0 +1,167 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services.Compositor +import qs.Services.UI +import qs.Widgets + +// Generic full-screen popup window for menus and context menus +// This is a top-level PanelWindow (sibling to MainScreen, not nested inside it) +// Provides click-outside-to-close functionality for any popup content +// Loads TrayMenu by default but can show context menus via showContextMenu() +PanelWindow { + id: root + + required property ShellScreen screen + property string windowType: "popupmenu" // Used for namespace and registration + + // Content item to display (set by the popup that uses this window) + property var contentItem: null + + // Expose the trayMenu Loader directly (for backward compatibility) + readonly property alias trayMenuLoader: trayMenuLoader + + // Dynamic context menu callback for items in other windows (e.g., desktop widgets) + property var dynamicMenuCallback: null + + anchors.top: true + anchors.left: true + anchors.right: true + anchors.bottom: true + visible: false + color: "transparent" + + // Use Top layer for proper event handling, but on labwc use Bottom + // to avoid stealing input from popups while still catching outside clicks. + // However, when a dialog is open, always use Top so dialogs appear above apps. + WlrLayershell.layer: (CompositorService.isLabwc && !hasDialog) ? WlrLayer.Bottom : WlrLayer.Top + WlrLayershell.keyboardFocus: hasDialog ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + WlrLayershell.namespace: "noctalia-" + windowType + "-" + (screen?.name || "unknown") + WlrLayershell.exclusionMode: ExclusionMode.Ignore + + // Track if a dialog is currently open (needed for keyboard focus) + property bool hasDialog: false + + // Register with PanelService so widgets can find this window + Component.onCompleted: { + objectName = "popupMenuWindow-" + (screen?.name || "unknown"); + PanelService.registerPopupMenuWindow(screen, root); + } + + Component.onDestruction: { + PanelService.unregisterPopupMenuWindow(screen); + } + + // Load TrayMenu as the default content + Loader { + id: trayMenuLoader + source: Quickshell.shellDir + "/Modules/Bar/Extras/TrayMenu.qml" + onLoaded: { + if (item) { + item.screen = root.screen; + // Set the loaded item as default content + root.contentItem = item; + } + } + } + + // Dynamic context menu - created as child of this window (Top layer) so input works correctly + // Used for items in other windows like desktop widgets (bottom layer) + NPopupContextMenu { + id: dynamicMenu + visible: false + screen: root.screen + minWidth: 180 + + onTriggered: (action, item) => { + if (root.dynamicMenuCallback) { + // Callback returns true if it will handle closing (e.g., opening a dialog) + var handled = root.dynamicMenuCallback(action); + if (!handled) { + root.close(); + } + } else { + root.close(); + } + } + } + + function open() { + visible = true; + BarService.popupOpen = true; + } + + // Show a context menu (temporarily replaces TrayMenu as content) + function showContextMenu(menu) { + if (menu) { + contentItem = menu; + open(); + } + } + + // Show a dynamic context menu with model and callback at screen coordinates + // Used for items in other window layers (e.g., desktop widgets in bottom layer) + function showDynamicContextMenu(model, screenX, screenY, callback) { + dynamicMenu.model = model; + dynamicMenuCallback = callback; + + // Use the anchor point item for positioning at absolute coordinates + dynamicMenu.anchorItem = anchorPoint; + anchorPoint.x = screenX; + anchorPoint.y = screenY; + + contentItem = dynamicMenu; + dynamicMenu.visible = true; + open(); + } + + // Invisible anchor point for dynamic menu positioning + Item { + id: anchorPoint + width: 1 + height: 1 + } + + // Hide just the dynamic menu without closing the popup window + // Used when transitioning from context menu to a dialog + function hideDynamicMenu() { + dynamicMenu.visible = false; + } + + function close() { + visible = false; + BarService.popupOpen = false; + // Call close/hide method on current content + if (contentItem) { + if (typeof contentItem.hideMenu === "function") { + contentItem.hideMenu(); + } else if (typeof contentItem.close === "function") { + contentItem.close(); + } + } + // Hide dynamic menu + dynamicMenu.visible = false; + dynamicMenuCallback = null; + // Restore TrayMenu as default content + if (trayMenuLoader.item) { + contentItem = trayMenuLoader.item; + } + } + + // Full-screen click catcher - click anywhere outside content closes the window + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: root.close() + } + + // Container for dialogs that need a full-screen Item parent (e.g., Qt Popup) + Item { + id: dialogContainer + anchors.fill: parent + } + + // Expose the dialog container for external use + readonly property alias dialogParent: dialogContainer +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/ScreenCorners.qml b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/ScreenCorners.qml new file mode 100644 index 0000000..4387ecf --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/ScreenCorners.qml @@ -0,0 +1,231 @@ +import QtQuick +import QtQuick.Shapes +import qs.Commons + +/** +* ScreenCorners - Shape component for rendering screen corners +* +* Renders concave corners at the screen edges to create a rounded screen effect. +* Self-contained Shape component (no shadows). +*/ +Item { + id: root + + anchors.fill: parent + + // Wrapper with layer caching to reduce GPU tessellation overhead + Item { + anchors.fill: parent + + // Cache the Shape to a texture to prevent continuous re-tessellation + layer.enabled: true + + Shape { + id: cornersShape + + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + asynchronous: true + enabled: false // Disable mouse input + visible: cornersPath.cornerRadius > 0 && width > 0 && height > 0 + + ShapePath { + id: cornersPath + + // Corner configuration + readonly property color cornerColor: Settings.data.general.forceBlackScreenCorners ? "black" : Color.mSurface + readonly property real cornerRadius: Style.screenRadius + readonly property real cornerSize: Style.screenRadius + + // Determine margins based on bar position + readonly property real topMargin: 0 + readonly property real bottomMargin: 0 + readonly property real leftMargin: 0 + readonly property real rightMargin: 0 + + // Screen dimensions + readonly property real screenWidth: cornersShape.width + readonly property real screenHeight: cornersShape.height + + // Only show screen corners if enabled and appropriate conditions are met + readonly property bool shouldShow: Settings.data.general.showScreenCorners + + // ShapePath configuration + strokeWidth: -1 // No stroke, fill only + fillColor: shouldShow ? cornerColor : "transparent" + + // Smooth color animation (disabled during theme transitions to sync with Color.qml) + Behavior on fillColor { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + } + } + + // ========== PATH DEFINITION ========== + // Draws 4 separate corner squares at screen edges + // Each corner square has a concave arc on the inner diagonal + + // ========== TOP-LEFT CORNER ========== + // Arc is at the bottom-right of this square (inner diagonal) + // Start at top-left screen corner + startX: leftMargin + startY: topMargin + + // Top edge (moving right) + PathLine { + relativeX: cornersPath.cornerSize + relativeY: 0 + } + + // Right edge (moving down toward arc) + PathLine { + relativeX: 0 + relativeY: cornersPath.cornerSize - cornersPath.cornerRadius + } + + // Concave arc (bottom-right corner of square, curving inward toward screen center) + PathArc { + relativeX: -cornersPath.cornerRadius + relativeY: cornersPath.cornerRadius + radiusX: cornersPath.cornerRadius + radiusY: cornersPath.cornerRadius + direction: PathArc.Counterclockwise + } + + // Bottom edge (moving left) + PathLine { + relativeX: -(cornersPath.cornerSize - cornersPath.cornerRadius) + relativeY: 0 + } + + // Left edge (moving up) - closes back to start + PathLine { + relativeX: 0 + relativeY: -cornersPath.cornerSize + } + + // ========== TOP-RIGHT CORNER ========== + // Arc is at the bottom-left of this square (inner diagonal) + PathMove { + x: cornersPath.screenWidth - cornersPath.rightMargin - cornersPath.cornerSize + y: cornersPath.topMargin + } + + // Top edge (moving right) + PathLine { + relativeX: cornersPath.cornerSize + relativeY: 0 + } + + // Right edge (moving down) + PathLine { + relativeX: 0 + relativeY: cornersPath.cornerSize + } + + // Bottom edge (moving left toward arc) + PathLine { + relativeX: -(cornersPath.cornerSize - cornersPath.cornerRadius) + relativeY: 0 + } + + // Concave arc (bottom-left corner of square, curving inward toward screen center) + PathArc { + relativeX: -cornersPath.cornerRadius + relativeY: -cornersPath.cornerRadius + radiusX: cornersPath.cornerRadius + radiusY: cornersPath.cornerRadius + direction: PathArc.Counterclockwise + } + + // Left edge (moving up) - closes back to start + PathLine { + relativeX: 0 + relativeY: -(cornersPath.cornerSize - cornersPath.cornerRadius) + } + + // ========== BOTTOM-LEFT CORNER ========== + // Arc is at the top-right of this square (inner diagonal) + PathMove { + x: cornersPath.leftMargin + y: cornersPath.screenHeight - cornersPath.bottomMargin - cornersPath.cornerSize + } + + // Top edge (moving right toward arc) + PathLine { + relativeX: cornersPath.cornerSize - cornersPath.cornerRadius + relativeY: 0 + } + + // Concave arc (top-right corner of square, curving inward toward screen center) + PathArc { + relativeX: cornersPath.cornerRadius + relativeY: cornersPath.cornerRadius + radiusX: cornersPath.cornerRadius + radiusY: cornersPath.cornerRadius + direction: PathArc.Counterclockwise + } + + // Right edge (moving down) + PathLine { + relativeX: 0 + relativeY: cornersPath.cornerSize - cornersPath.cornerRadius + } + + // Bottom edge (moving left) + PathLine { + relativeX: -cornersPath.cornerSize + relativeY: 0 + } + + // Left edge (moving up) - closes back to start + PathLine { + relativeX: 0 + relativeY: -cornersPath.cornerSize + } + + // ========== BOTTOM-RIGHT CORNER ========== + // Arc is at the top-left of this square (inner diagonal) + // Start at bottom-right of square (different from other corners!) + PathMove { + x: cornersPath.screenWidth - cornersPath.rightMargin + y: cornersPath.screenHeight - cornersPath.bottomMargin + } + + // Bottom edge (moving left) + PathLine { + relativeX: -cornersPath.cornerSize + relativeY: 0 + } + + // Left edge (moving up toward arc) + PathLine { + relativeX: 0 + relativeY: -(cornersPath.cornerSize - cornersPath.cornerRadius) + } + + // Concave arc (top-left corner of square, curving inward toward screen center) + PathArc { + relativeX: cornersPath.cornerRadius + relativeY: -cornersPath.cornerRadius + radiusX: cornersPath.cornerRadius + radiusY: cornersPath.cornerRadius + direction: PathArc.Counterclockwise + } + + // Top edge (moving right) + PathLine { + relativeX: cornersPath.cornerSize - cornersPath.cornerRadius + relativeY: 0 + } + + // Right edge (moving down) - closes back to start + PathLine { + relativeX: 0 + relativeY: cornersPath.cornerSize + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/SmartPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/SmartPanel.qml new file mode 100644 index 0000000..e3839d0 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/MainScreen/SmartPanel.qml @@ -0,0 +1,1363 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.UI + +/** +* SmartPanel for use within MainScreen +*/ +Item { + id: root + + // Screen property provided by MainScreen + property ShellScreen screen: null + + // Panel content: Text, icons, etc... + property Component panelContent: null + + // PanelID for binding panels to widgets of the same type + property var panelID: null + + // Panel size properties + property real preferredWidth: 700 + property real preferredHeight: 900 + property real preferredWidthRatio + property real preferredHeightRatio + property color panelBackgroundColor: Color.mSurface + property color panelBorderColor: Color.mOutline + property var buttonItem: null + property bool forceAttachToBar: false + + // Anchoring properties + property bool panelAnchorHorizontalCenter: false + property bool panelAnchorVerticalCenter: false + property bool panelAnchorTop: false + property bool panelAnchorBottom: false + property bool panelAnchorLeft: false + property bool panelAnchorRight: false + + // Button position properties + property bool useButtonPosition: false + property point buttonPosition: Qt.point(0, 0) + property int buttonWidth: 0 + property int buttonHeight: 0 + + // Edge snapping: if panel is within this distance (in pixels) from a screen edge, snap + property real edgeSnapDistance: 50 + + // Track whether panel is open + property bool isPanelOpen: false + + // Track actual visibility (delayed until content is loaded and sized) + property bool isPanelVisible: false + + // Track size animation completion for sequential opacity animation + property bool sizeAnimationComplete: false + + // Derived state: track opening transition + readonly property bool isOpening: isPanelVisible && !isClosing && !sizeAnimationComplete + + // Track close animation state: fade opacity first, then shrink size + property bool isClosing: false + property bool opacityFadeComplete: false + property bool closeFinalized: false // Prevent double-finalization + + // Safety: Watchdog timers to prevent stuck states + property bool closeWatchdogActive: false + property bool openWatchdogActive: false + + // Cached animation direction - set when panel opens, doesn't change during animation + // These are computed once when opening and used for the entire open/close cycle + property bool cachedAnimateFromTop: false + property bool cachedAnimateFromBottom: false + property bool cachedAnimateFromLeft: false + property bool cachedAnimateFromRight: false + + readonly property bool animationsDisabled: Settings.data.general.animationDisabled + property bool cachedShouldAnimateWidth: false + property bool cachedShouldAnimateHeight: false + + // Whether blur should be applied behind this panel + property bool blurEnabled: true + + // Close with escape key + property bool closeWithEscape: true + + property bool exclusiveKeyboard: true + + // Keyboard event handler + // These are called from MainScreen's centralized shortcuts + // override these in specific panels to handle shortcuts + function onEscapePressed() { + if (closeWithEscape) + close(); + } + + // Expose panel region for background rendering + readonly property var panelRegion: panelContent.geometryPlaceholder + + readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name) + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property real barHeight: barShouldShow ? Style.getBarHeightForScreen(screen?.name) : 0 + readonly property bool hasBar: modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false + readonly property bool isFramed: Settings.data.bar.barType === "framed" && hasBar + readonly property real frameThickness: Settings.data.bar.frameThickness ?? 12 + readonly property bool barFloating: Settings.data.bar.barType === "floating" + readonly property real barMarginH: (barFloating && barShouldShow) ? Math.ceil(Settings.data.bar.marginHorizontal) : 0 + readonly property real barMarginV: (barFloating && barShouldShow) ? Math.ceil(Settings.data.bar.marginVertical) : 0 + readonly property real attachmentOverlap: 1 // Panel extends into bar area to fix hairline gap with fractional scaling + + // Check if bar should be visible on this screen + readonly property bool barShouldShow: { + if (!BarService.effectivelyVisible) + return false; + var monitors = Settings.data.bar.monitors || []; + var screenName = screen?.name || ""; + return monitors.length === 0 || monitors.includes(screenName); + } + + // Helper to detect if any anchor is explicitly set + readonly property bool hasExplicitHorizontalAnchor: panelAnchorHorizontalCenter || panelAnchorLeft || panelAnchorRight + readonly property bool hasExplicitVerticalAnchor: panelAnchorVerticalCenter || panelAnchorTop || panelAnchorBottom + + // Effective anchor properties (depend on allowAttach) + // These are true when allowAttach is enabled AND: + // 1. Explicitly anchored to that edge, OR + // 2. Using button position and bar is on that edge, OR + // 3. No explicit anchors and bar is on that edge (default centering behavior) + readonly property bool effectivePanelAnchorTop: panelContent.allowAttach && (panelAnchorTop || (useButtonPosition && barPosition === "top") || (!hasExplicitVerticalAnchor && barPosition === "top" && !barIsVertical)) + readonly property bool effectivePanelAnchorBottom: panelContent.allowAttach && (panelAnchorBottom || (useButtonPosition && barPosition === "bottom") || (!hasExplicitVerticalAnchor && barPosition === "bottom" && !barIsVertical)) + readonly property bool effectivePanelAnchorLeft: panelContent.allowAttach && (panelAnchorLeft || (useButtonPosition && barPosition === "left") || (!hasExplicitHorizontalAnchor && barPosition === "left" && barIsVertical)) + readonly property bool effectivePanelAnchorRight: panelContent.allowAttach && (panelAnchorRight || (useButtonPosition && barPosition === "right") || (!hasExplicitHorizontalAnchor && barPosition === "right" && barIsVertical)) + + signal opened + signal closed + + Connections { + target: Style + + function onUiScaleRatioChanged() { + if (root.isPanelOpen && root.isPanelVisible) { + root.setPosition(); + } + } + } + + // Panel visibility and sizing + visible: isPanelVisible + width: parent ? parent.width : 0 + height: parent ? parent.height : 0 + + // Panel control functions + function toggle(buttonItem, buttonName) { + if (!isPanelOpen) { + open(buttonItem, buttonName); + } else { + close(); + } + } + + function open(buttonItem, buttonName) { + // Reset immediate close flag to ensure animations work properly + PanelService.closedImmediately = false; + // Reset to default - fixes panel being stuck in one position + root.useButtonPosition = false; + + // Calculate the bar window's position on screen based on bar settings + // The BarContentWindow uses anchors + margins, so we need to compute its origin + var barWindowX = 0; + var barWindowY = 0; + var screenWidth = root.screen?.width || 0; + var screenHeight = root.screen?.height || 0; + + if (root.barPosition === "right") { + barWindowX = screenWidth - root.barMarginH - root.barHeight; + } else if (root.barPosition === "left") { + barWindowX = root.barMarginH; + } else if (root.isFramed) { + barWindowX = root.frameThickness; + } else { + // Horizontal floating bars: BarContentWindow has margins.left = barMarginH + barWindowX = root.barMarginH; + } + + if (root.barPosition === "bottom") { + barWindowY = screenHeight - root.barMarginV - root.barHeight; + } else if (root.barPosition === "top") { + barWindowY = root.barMarginV; + } else if (root.isFramed) { + barWindowY = root.frameThickness; + } else { + // Vertical floating bars: BarContentWindow has margins.top = barMarginV + barWindowY = root.barMarginV; + } + + if (!buttonItem && buttonName) { + // Check if buttonName is actually a point object (click coordinates) + if (typeof buttonName === "object" && buttonName.x !== undefined && buttonName.y !== undefined) { + root.buttonItem = null; + // Click coordinates are in BarContentWindow-local space, offset to screen space + root.buttonPosition = Qt.point(barWindowX + buttonName.x, barWindowY + buttonName.y); + root.buttonWidth = 0; + root.buttonHeight = 0; + root.useButtonPosition = true; + } else { + // buttonName is a widget name, look it up + buttonItem = BarService.lookupWidget(buttonName, screen.name); + } + } + + // Validate buttonItem is a valid QML Item with mapToItem function + if (buttonItem && typeof buttonItem.mapToItem === "function") { + try { + root.buttonItem = buttonItem; + // Map button position within its window (BarContentWindow-local coordinates) + var buttonLocal = buttonItem.mapToItem(null, 0, 0); + + root.buttonPosition = Qt.point(barWindowX + buttonLocal.x, barWindowY + buttonLocal.y); + root.buttonWidth = buttonItem.width; + root.buttonHeight = buttonItem.height; + root.useButtonPosition = true; + } catch (e) { + Logger.w("SmartPanel", "Failed to get button position, using default positioning:", e); + root.buttonItem = null; + root.useButtonPosition = false; + } + } else if (!root.useButtonPosition) { + // No valid button provided and no click position: reset button position mode + root.buttonItem = null; + } + + // Set isPanelOpen to trigger content loading, but don't show yet + isPanelOpen = true; + + // Notify PanelService + PanelService.willOpenPanel(root); + + // Position and visibility will be set by Loader.onLoaded + // This ensures no flicker from default size to content size + } + + function close() { + // Reset immediate close flag to ensure animations work properly + PanelService.closedImmediately = false; + + // Start close sequence: fade opacity first + isClosing = true; + sizeAnimationComplete = false; + closeFinalized = false; + + // Stop the open animation timer if it's still running + opacityTrigger.stop(); + openWatchdogActive = false; + openWatchdogTimer.stop(); + + // Start close watchdog timer + closeWatchdogActive = true; + closeWatchdogTimer.restart(); + + // If opacity is already 0 (closed during open animation before fade-in), + // skip directly to size animation + if (root.opacity === 0.0) { + opacityFadeComplete = true; + } else { + opacityFadeComplete = false; + } + + // Opacity will fade out, then size will shrink, then finalizeClose() will complete + Logger.d("SmartPanel", "Closing panel", objectName); + } + + function closeImmediately() { + // Close without any animation, useful for app launches to avoid focus issues + opacityTrigger.stop(); + openWatchdogActive = false; + openWatchdogTimer.stop(); + closeWatchdogActive = false; + closeWatchdogTimer.stop(); + + // Don't set opacity directly as it breaks the binding + root.isPanelVisible = false; + root.sizeAnimationComplete = false; + root.isClosing = false; + root.opacityFadeComplete = false; + root.closeFinalized = true; + root.isPanelOpen = false; + panelBackground.dimensionsInitialized = false; + + // Signal immediate close so MainScreen can skip dimmer animation + PanelService.closedImmediately = true; + PanelService.closedPanel(root); + closed(); + + // Flush pending double-buffered Wayland state (blur regions) that won't + // be committed otherwise — after an app launch the compositor may stop + // sending frame callbacks, leaving the render loop idle. + Window.window?.flushWaylandState(); + + Logger.d("SmartPanel", "Panel closed immediately", objectName); + } + + function finalizeClose() { + // Prevent double-finalization + if (root.closeFinalized) { + Logger.w("SmartPanel", "finalizeClose called but already finalized - ignoring", objectName); + return; + } + + // Complete the close sequence after animations finish + root.closeFinalized = true; + root.closeWatchdogActive = false; + closeWatchdogTimer.stop(); + + root.isPanelVisible = false; + root.isPanelOpen = false; + root.isClosing = false; + root.opacityFadeComplete = false; + + // Reset dimensionsInitialized for next opening + panelBackground.dimensionsInitialized = false; + + PanelService.closedPanel(root); + closed(); + + // Flush pending double-buffered Wayland state (blur regions). + Window.window?.flushWaylandState(); + + Logger.d("SmartPanel", "Panel close finalized", objectName); + } + + function setPosition() { + // Don't calculate position if parent dimensions aren't available yet + // This prevents centering around (0,0) when width/height are still 0 + if (!root.width || !root.height) { + Logger.d("SmartPanel", "Skipping setPosition - dimensions not ready:", root.width, "x", root.height); + // Retry on next frame when dimensions should be available + Qt.callLater(setPosition); + return; + } + + // Effective screen margins (account for frame thickness) + var effMarginL = Style.marginL + (root.isFramed ? root.frameThickness : 0); + var effMarginR = Style.marginL + (root.isFramed ? root.frameThickness : 0); + var effMarginT = Style.marginL + (root.isFramed ? root.frameThickness : 0); + var effMarginB = Style.marginL + (root.isFramed ? root.frameThickness : 0); + + // Calculate panel dimensions first (needed for positioning) + var w; + // Priority 1: Content-driven size (dynamic) + if (contentLoader.item && contentLoader.item.contentPreferredWidth !== undefined) { + w = contentLoader.item.contentPreferredWidth; + } // Priority 2: Ratio-based size + else if (root.preferredWidthRatio !== undefined) { + w = Math.round(Math.max(root.width * root.preferredWidthRatio, root.preferredWidth)); + } // Priority 3: Static preferred width + else { + w = root.preferredWidth; + } + var panelWidth = Math.min(w, root.width - effMarginL - effMarginR); + // For floating bars, additionally clamp to available space accounting for corner insets + if (root.barFloating && !root.barIsVertical) { + var floatCornerInset = Style.radiusL * 2; + panelWidth = Math.min(panelWidth, root.width - 2 * (root.barMarginH + floatCornerInset)); + } + + var h; + // Priority 1: Content-driven size (dynamic) + if (contentLoader.item && contentLoader.item.contentPreferredHeight !== undefined) { + h = contentLoader.item.contentPreferredHeight; + } // Priority 2: Ratio-based size + else if (root.preferredHeightRatio !== undefined) { + h = Math.round(Math.max(root.height * root.preferredHeightRatio, root.preferredHeight)); + } // Priority 3: Static preferred height + else { + h = root.preferredHeight; + } + var panelHeight = Math.min(h, root.height - root.barHeight - effMarginT - effMarginB); + // For vertical floating bars, clamp panelHeight to available space accounting for corner insets + if (root.barFloating && root.barIsVertical) { + var floatCornerInset = Style.radiusL * 2; + panelHeight = Math.min(panelHeight, root.height - 2 * (root.barMarginV + floatCornerInset)); + } + + // Update panelBackground target size (will be animated) + panelBackground.targetWidth = panelWidth; + panelBackground.targetHeight = panelHeight; + + // Pre-compute bar edge positions with overlap (used multiple times below) + // For attached panels, we extend slightly into the bar area to prevent hairline gaps + var leftBarEdgeWithOverlap = root.barMarginH + root.barHeight - root.attachmentOverlap; + var rightBarEdgeWithOverlap = root.width - root.barMarginH - root.barHeight + root.attachmentOverlap; + var topBarEdgeWithOverlap = root.barMarginV + root.barHeight - root.attachmentOverlap; + var bottomBarEdgeWithOverlap = root.height - root.barMarginV - root.barHeight + root.attachmentOverlap; + + if (root.isFramed) { + if (root.barPosition === "left") + leftBarEdgeWithOverlap = root.barHeight - root.attachmentOverlap; + if (root.barPosition === "right") + rightBarEdgeWithOverlap = root.width - root.barHeight + root.attachmentOverlap; + if (root.barPosition === "top") + topBarEdgeWithOverlap = root.barHeight - root.attachmentOverlap; + if (root.barPosition === "bottom") + bottomBarEdgeWithOverlap = root.height - root.barHeight + root.attachmentOverlap; + } + + // Calculate position + var calculatedX; + var calculatedY; + + // ===== X POSITIONING ===== + if (root.useButtonPosition && root.width > 0 && panelWidth > 0) { + if (root.barIsVertical) { + // For vertical bars + if (panelContent.allowAttach) { + // Attached panels: align with bar edge (left or right side) + if (root.barPosition === "left") { + calculatedX = leftBarEdgeWithOverlap; + } else { + calculatedX = rightBarEdgeWithOverlap - panelWidth; + } + } else { + // Detached panels: center on button X position + var panelX = root.buttonPosition.x + root.buttonWidth / 2 - panelWidth / 2; + var minX = effMarginL; + var maxX = root.width - panelWidth - effMarginR; + + // Account for vertical bar taking up space + if (root.barPosition === "left") { + minX = (root.isFramed ? 0 : root.barMarginH) + root.barHeight + Style.marginL; + } else if (root.barPosition === "right") { + maxX = root.width - (root.isFramed ? 0 : root.barMarginH) - root.barHeight - panelWidth - Style.marginL; + } + + panelX = Math.max(minX, Math.min(panelX, maxX)); + calculatedX = panelX; + } + } else { + // For horizontal bars, center panel on button X position + var panelX = root.buttonPosition.x + root.buttonWidth / 2 - panelWidth / 2; + if (panelContent.allowAttach) { + var cornerInset = root.barFloating ? Style.radiusL * 2 : 0; + var barLeftEdge = (root.isFramed ? root.frameThickness - root.attachmentOverlap : root.barMarginH) + cornerInset; + var barRightEdge = root.width - (root.isFramed ? root.frameThickness - root.attachmentOverlap : root.barMarginH) - cornerInset; + panelX = Math.max(barLeftEdge, Math.min(panelX, barRightEdge - panelWidth)); + } else { + panelX = Math.max(effMarginL, Math.min(panelX, root.width - panelWidth - effMarginR)); + } + calculatedX = panelX; + } + } else { + // Standard anchor positioning + if (root.panelAnchorHorizontalCenter) { + if (root.barIsVertical) { + if (root.barPosition === "left") { + var availableStart = (root.isFramed ? 0 : root.barMarginH) + root.barHeight; + var availableWidth = root.width - availableStart - (root.isFramed ? root.frameThickness : 0); + calculatedX = availableStart + (availableWidth - panelWidth) / 2; + } else if (root.barPosition === "right") { + var availableWidth = root.width - (root.isFramed ? 0 : root.barMarginH) - root.barHeight - (root.isFramed ? root.frameThickness : 0); + calculatedX = (root.isFramed ? root.frameThickness : 0) + (availableWidth - panelWidth) / 2; + } else { + calculatedX = (root.width - panelWidth) / 2; + } + } else { + calculatedX = (root.width - panelWidth) / 2; + } + } else if (root.panelAnchorRight) { + // Use raw panelAnchorRight for positioning decision + if (root.effectivePanelAnchorRight) { + // Attached: snap to edge/bar + if (root.barIsVertical && root.barPosition === "right") { + calculatedX = rightBarEdgeWithOverlap - panelWidth; + } else { + var panelOnSameEdgeAsBar = (root.barPosition === "top" && root.effectivePanelAnchorTop) || (root.barPosition === "bottom" && root.effectivePanelAnchorBottom); + if (!root.barIsVertical && root.barFloating && panelOnSameEdgeAsBar) { + var rightCornerInset = Style.radiusL * 2; + calculatedX = root.width - root.barMarginH - rightCornerInset - panelWidth; + } else { + calculatedX = root.width - panelWidth - (root.isFramed ? root.frameThickness - root.attachmentOverlap : 0); + } + } + } else { + // Not attached: position at right with margin + calculatedX = root.width - panelWidth - effMarginR; + } + } else if (root.panelAnchorLeft) { + // Use raw panelAnchorLeft for positioning decision + if (root.effectivePanelAnchorLeft) { + // Attached: snap to edge/bar + if (root.barIsVertical && root.barPosition === "left") { + calculatedX = leftBarEdgeWithOverlap; + } else { + var panelOnSameEdgeAsBar = (root.barPosition === "top" && root.effectivePanelAnchorTop) || (root.barPosition === "bottom" && root.effectivePanelAnchorBottom); + if (!root.barIsVertical && root.barFloating && panelOnSameEdgeAsBar) { + var leftCornerInset = Style.radiusL * 2; + calculatedX = root.barMarginH + leftCornerInset; + } else { + calculatedX = (root.isFramed ? root.frameThickness - root.attachmentOverlap : 0); + } + } + } else { + // Not attached: position at left with margin + calculatedX = effMarginL; + } + } else { + // No explicit anchor: attach to bar if allowAttach, otherwise center + if (root.barIsVertical) { + if (panelContent.allowAttach) { + // Attach to the bar edge (with overlap into bar area) + if (root.barPosition === "left") { + calculatedX = leftBarEdgeWithOverlap; + } else { + calculatedX = rightBarEdgeWithOverlap - panelWidth; + } + } else { + // Not attached: center in available space + if (root.barPosition === "left") { + var availableStart = (root.isFramed ? 0 : root.barMarginH) + root.barHeight; + var availableWidth = root.width - availableStart - effMarginR; + calculatedX = availableStart + (availableWidth - panelWidth) / 2; + } else { + var availableWidth = root.width - (root.isFramed ? 0 : root.barMarginH) - root.barHeight - effMarginL; + calculatedX = effMarginL + (availableWidth - panelWidth) / 2; + } + } + } else { + if (panelContent.allowAttach) { + var cornerInset = Style.radiusL + (root.barFloating ? Style.radiusL : 0); + var barLeftEdge = (root.isFramed ? root.frameThickness - root.attachmentOverlap : root.barMarginH) + cornerInset; + var barRightEdge = root.width - (root.isFramed ? root.frameThickness - root.attachmentOverlap : root.barMarginH) - cornerInset; + var centeredX = (root.width - panelWidth) / 2; + calculatedX = Math.max(barLeftEdge, Math.min(centeredX, barRightEdge - panelWidth)); + } else { + calculatedX = (root.width - panelWidth) / 2; + } + } + } + } + + // Edge snapping for X + if (panelContent.allowAttach && !root.barFloating && root.width > 0 && panelWidth > 0) { + var leftEdgePos = root.barPosition === "left" ? leftBarEdgeWithOverlap : (root.isFramed ? root.frameThickness - root.attachmentOverlap : root.barMarginH); + var rightEdgePos = root.barPosition === "right" ? rightBarEdgeWithOverlap - panelWidth : root.width - (root.isFramed ? root.frameThickness - root.attachmentOverlap : root.barMarginH) - panelWidth; + + // Only snap to left edge if panel is actually meant to be at left (or no explicit anchor) + var shouldSnapToLeft = root.effectivePanelAnchorLeft || (!root.hasExplicitHorizontalAnchor && root.barPosition === "left"); + // Only snap to right edge if panel is actually meant to be at right (or no explicit anchor) + var shouldSnapToRight = root.effectivePanelAnchorRight || (!root.hasExplicitHorizontalAnchor && root.barPosition === "right"); + + if (shouldSnapToLeft && Math.abs(calculatedX - leftEdgePos) <= root.edgeSnapDistance) { + calculatedX = leftEdgePos; + } else if (shouldSnapToRight && Math.abs(calculatedX - rightEdgePos) <= root.edgeSnapDistance) { + calculatedX = rightEdgePos; + } + } + + // ===== Y POSITIONING ===== + if (root.useButtonPosition && root.height > 0 && panelHeight > 0) { + if (root.barPosition === "top") { + if (panelContent.allowAttach) { + calculatedY = topBarEdgeWithOverlap; + } else { + calculatedY = (root.isFramed ? 0 : root.barMarginV) + root.barHeight + Style.marginM; + } + } else if (root.barPosition === "bottom") { + if (panelContent.allowAttach) { + calculatedY = bottomBarEdgeWithOverlap - panelHeight; + } else { + calculatedY = root.height - (root.isFramed ? 0 : root.barMarginV) - root.barHeight - panelHeight - Style.marginM; + } + } else if (root.barIsVertical) { + var panelY = root.buttonPosition.y + root.buttonHeight / 2 - panelHeight / 2; + var extraPadding = (panelContent.allowAttach && root.barFloating) ? Style.radiusL : 0; + if (panelContent.allowAttach) { + var cornerInset = extraPadding + (root.barFloating ? Style.radiusL : 0); + var barTopEdge = (root.isFramed ? root.frameThickness - root.attachmentOverlap : root.barMarginV) + cornerInset; + var barBottomEdge = root.height - (root.isFramed ? root.frameThickness - root.attachmentOverlap : root.barMarginV) - cornerInset; + panelY = Math.max(barTopEdge, Math.min(panelY, barBottomEdge - panelHeight)); + } else { + panelY = Math.max(effMarginT + extraPadding, Math.min(panelY, root.height - panelHeight - effMarginB - extraPadding)); + } + calculatedY = panelY; + } + } else { + // Standard anchor positioning + var barOffset = !panelContent.allowAttach && (root.barPosition === "top" || root.barPosition === "bottom") ? (root.isFramed ? 0 : root.barMarginV) + root.barHeight + Style.marginM : 0; + + if (panelContent.allowAttach && !root.barIsVertical) { + // Attached to horizontal bar: position with overlap + if ((root.effectivePanelAnchorTop && root.barPosition === "top") || (!root.hasExplicitVerticalAnchor && root.barPosition === "top")) { + calculatedY = topBarEdgeWithOverlap; + } else if ((root.effectivePanelAnchorBottom && root.barPosition === "bottom") || (!root.hasExplicitVerticalAnchor && root.barPosition === "bottom")) { + calculatedY = bottomBarEdgeWithOverlap - panelHeight; + } + } + + if (calculatedY === undefined) { + if (root.panelAnchorVerticalCenter) { + if (!root.barIsVertical) { + if (root.barPosition === "top") { + var availableStart = (root.isFramed ? 0 : root.barMarginV) + root.barHeight; + var availableHeight = root.height - availableStart - (root.isFramed ? root.frameThickness : 0); + calculatedY = availableStart + (availableHeight - panelHeight) / 2; + } else if (root.barPosition === "bottom") { + var availableHeight = root.height - (root.isFramed ? 0 : root.barMarginV) - root.barHeight - (root.isFramed ? root.frameThickness : 0); + calculatedY = (root.isFramed ? root.frameThickness : 0) + (availableHeight - panelHeight) / 2; + } else { + calculatedY = (root.height - panelHeight) / 2; + } + } else { + calculatedY = (root.height - panelHeight) / 2; + } + } else if (root.panelAnchorTop) { + if (root.effectivePanelAnchorTop) { + calculatedY = root.barPosition === "top" ? topBarEdgeWithOverlap : (root.isFramed ? root.frameThickness - root.attachmentOverlap : 0); + } else { + var topBarOffset = (root.barPosition === "top") ? barOffset : 0; + calculatedY = topBarOffset + effMarginT; + } + } else if (root.panelAnchorBottom) { + if (root.effectivePanelAnchorBottom) { + calculatedY = root.barPosition === "bottom" ? bottomBarEdgeWithOverlap - panelHeight : root.height - panelHeight - (root.isFramed ? root.frameThickness - root.attachmentOverlap : 0); + } else { + var bottomBarOffset = (root.barPosition === "bottom") ? barOffset : 0; + calculatedY = root.height - panelHeight - bottomBarOffset - effMarginB; + } + } else { + // No explicit vertical anchor + if (root.barIsVertical) { + if (panelContent.allowAttach) { + var cornerInset = root.barFloating ? Style.radiusL * 2 : 0; + var barTopEdge = (root.isFramed ? root.frameThickness - root.attachmentOverlap : root.barMarginV) + cornerInset; + var barBottomEdge = root.height - (root.isFramed ? root.frameThickness - root.attachmentOverlap : root.barMarginV) - cornerInset; + var centeredY = (root.height - panelHeight) / 2; + calculatedY = Math.max(barTopEdge, Math.min(centeredY, barBottomEdge - panelHeight)); + } else { + calculatedY = (root.height - panelHeight) / 2; + } + } else { + // Horizontal bar, not attached + if (root.barPosition === "top") { + calculatedY = barOffset + effMarginT; + } else if (root.barPosition === "bottom") { + calculatedY = effMarginT; + } else { + calculatedY = effMarginT; + } + } + } + } + } + + // Edge snapping for Y + if (panelContent.allowAttach && !root.barFloating && root.height > 0 && panelHeight > 0) { + var topEdgePos = root.barPosition === "top" ? topBarEdgeWithOverlap : (root.isFramed ? root.frameThickness - root.attachmentOverlap : root.barMarginV); + var bottomEdgePos = root.barPosition === "bottom" ? bottomBarEdgeWithOverlap - panelHeight : root.height - (root.isFramed ? root.frameThickness - root.attachmentOverlap : root.barMarginV) - panelHeight; + + // Only snap to top edge if panel is actually meant to be at top (or no explicit anchor) + var shouldSnapToTop = root.effectivePanelAnchorTop || (!root.hasExplicitVerticalAnchor && root.barPosition === "top"); + // Only snap to bottom edge if panel is actually meant to be at bottom (or no explicit anchor) + var shouldSnapToBottom = root.effectivePanelAnchorBottom || (!root.hasExplicitVerticalAnchor && root.barPosition === "bottom"); + + if (shouldSnapToTop && Math.abs(calculatedY - topEdgePos) <= root.edgeSnapDistance) { + calculatedY = topEdgePos; + } else if (shouldSnapToBottom && Math.abs(calculatedY - bottomEdgePos) <= root.edgeSnapDistance) { + calculatedY = bottomEdgePos; + } + } + + // Apply calculated positions (set targets for animation) + panelBackground.targetX = calculatedX; + panelBackground.targetY = calculatedY; + + // Logger.d("SmartPanel", "Position calculated:", calculatedX, calculatedY); + // Logger.d("SmartPanel", " Panel size:", panelWidth, "x", panelHeight); + // Logger.d("SmartPanel", " Parent size:", root.width, "x", root.height); + } + + // Watch for changes in content-driven sizes and update position + Connections { + target: contentLoader.item + ignoreUnknownSignals: true + + function onContentPreferredWidthChanged() { + if (root.isPanelOpen && root.isPanelVisible) { + root.setPosition(); + } + } + + function onContentPreferredHeightChanged() { + if (root.isPanelOpen && root.isPanelVisible) { + root.setPosition(); + } + } + } + + // Opacity animation + // Opening: fade in after size animation reaches 75% + // Closing: fade out immediately + opacity: { + if (isClosing) + return 0.0; // Fade out when closing + if (isPanelVisible && sizeAnimationComplete) + return 1.0; // Fade in when opening + return 0.0; + } + + Behavior on opacity { + enabled: !PanelService.closedImmediately + NumberAnimation { + id: opacityAnimation + duration: root.isClosing ? Style.animationFaster : Style.animationFast + easing.type: Easing.OutQuad + + onRunningChanged: { + // Safety: If animation didn't run (zero duration), handle immediately + if (!running && duration === 0) { + if (root.isClosing && root.opacity === 0.0) { + root.opacityFadeComplete = true; + var shouldFinalizeNow = panelContent.geometryPlaceholder && !panelContent.geometryPlaceholder.shouldAnimateWidth && !panelContent.geometryPlaceholder.shouldAnimateHeight; + if (shouldFinalizeNow) { + // Logger.d("SmartPanel", "Zero-duration opacity + no size animation - finalizing", root.objectName); + Qt.callLater(root.finalizeClose); + } + } else if (root.isPanelVisible && root.opacity === 1.0) { + // Open completed with zero duration + root.openWatchdogActive = false; + openWatchdogTimer.stop(); + } + return; + } + + // When opacity fade completes during close, trigger size animation + if (!running && root.isClosing && root.opacity === 0.0) { + root.opacityFadeComplete = true; + // If no size animation will run (centered attached panels only), finalize immediately + // Detached panels (allowAttach === false) should always animate from top + var shouldFinalizeNow = panelContent.geometryPlaceholder && !panelContent.geometryPlaceholder.shouldAnimateWidth && !panelContent.geometryPlaceholder.shouldAnimateHeight; + if (shouldFinalizeNow) { + //Logger.d("SmartPanel", "No animation - finalizing immediately", root.objectName); + Qt.callLater(root.finalizeClose); + } else { + //Logger.d("SmartPanel", "Animation will run - waiting for size animation", root.objectName, "shouldAnimateHeight:", panelContent.geometryPlaceholder.shouldAnimateHeight, "shouldAnimateWidth:", panelContent.geometryPlaceholder.shouldAnimateWidth); + } + } // When opacity fade completes during open, stop watchdog + else if (!running && root.isPanelVisible && root.opacity === 1.0) { + root.openWatchdogActive = false; + openWatchdogTimer.stop(); + } + } + } + } + + // Timer to trigger opacity fade at 50% of size animation + Timer { + id: opacityTrigger + interval: Style.animationNormal * 0.5 + repeat: false + onTriggered: { + if (root.isPanelVisible) { + root.sizeAnimationComplete = true; + } + } + } + + // Watchdog timer for open sequence (safety mechanism) + Timer { + id: openWatchdogTimer + interval: Style.animationNormal * 3 // 3x normal animation time + repeat: false + onTriggered: { + if (root.openWatchdogActive) { + Logger.w("SmartPanel", "Open watchdog timeout - forcing panel visible state", root.objectName); + root.openWatchdogActive = false; + // Force completion of open sequence + if (root.isPanelOpen && !root.isPanelVisible) { + root.isPanelVisible = true; + root.sizeAnimationComplete = true; + } + } + } + } + + // Watchdog timer for close sequence (safety mechanism) + Timer { + id: closeWatchdogTimer + interval: Style.animationFast * 3 // 3x fast animation time + repeat: false + onTriggered: { + if (root.closeWatchdogActive && !root.closeFinalized) { + Logger.w("SmartPanel", "Close watchdog timeout - forcing panel close", root.objectName); + // Force finalization + Qt.callLater(root.finalizeClose); + } + } + } + + // ------------------------------------------------ + // Panel Content + Item { + id: panelContent + anchors.fill: parent + + // Screen-dependent attachment properties + // Allow panel content to override allowAttach (e.g., plugin panels) + readonly property bool allowAttach: { + if (contentLoader.item && contentLoader.item.allowAttach !== undefined) { + return contentLoader.item.allowAttach; + } + return Settings.data.ui.panelsAttachedToBar || root.forceAttachToBar; + } + readonly property bool allowAttachToBar: { + if (!(Settings.data.ui.panelsAttachedToBar || root.forceAttachToBar)) { + return false; + } + + // A panel can only be attached to a bar if there is a bar on that screen + var monitors = Settings.data.bar.monitors || []; + var result = monitors.length === 0 || monitors.includes(root.screen?.name || ""); + return result; + } + + // Edge detection - detect if panel is touching screen edges + readonly property bool touchingLeftEdge: allowAttach && panelBackground.x <= (isFramed ? frameThickness + 1 : 1) + readonly property bool touchingRightEdge: allowAttach && (panelBackground.x + panelBackground.width) >= (root.width - (isFramed ? frameThickness + 1 : 1)) + readonly property bool touchingTopEdge: allowAttach && panelBackground.y <= (isFramed ? frameThickness + 1 : 1) + readonly property bool touchingBottomEdge: allowAttach && (panelBackground.y + panelBackground.height) >= (root.height - (isFramed ? frameThickness + 1 : 1)) + + // Bar edge detection - detect if panel is touching bar edges (for cases where centered panels snap to bar due to height constraints) + readonly property bool touchingTopBar: allowAttachToBar && root.barPosition === "top" && !root.barIsVertical && Math.abs(panelBackground.y - ((isFramed ? 0 : root.barMarginV) + root.barHeight)) <= 1 + readonly property bool touchingBottomBar: allowAttachToBar && root.barPosition === "bottom" && !root.barIsVertical && Math.abs((panelBackground.y + panelBackground.height) - (root.height - (isFramed ? 0 : root.barMarginV) - root.barHeight)) <= 1 + readonly property bool touchingLeftBar: allowAttachToBar && root.barPosition === "left" && root.barIsVertical && Math.abs(panelBackground.x - ((isFramed ? 0 : root.barMarginH) + root.barHeight)) <= 1 + readonly property bool touchingRightBar: allowAttachToBar && root.barPosition === "right" && root.barIsVertical && Math.abs((panelBackground.x + panelBackground.width) - (root.width - (isFramed ? 0 : root.barMarginH) - root.barHeight)) <= 1 + + // Expose panelBackground for geometry placeholder + property alias geometryPlaceholder: panelBackground + + // The actual panel background - provides geometry for PanelBackground rendering + Item { + id: panelBackground + + // Expose self as panelItem for PanelBackground compatibility + readonly property var panelItem: panelBackground + + // Store target dimensions (Initialize to 0, set by setPosition()) + property real targetWidth: 0 + property real targetHeight: 0 + property real targetX: root.x + property real targetY: root.y + + // Track whether dimensions have been initialized (to prevent initial changes from animating) + property bool dimensionsInitialized: false + + property var bezierCurve: [0.05, 0, 0.133, 0.06, 0.166, 0.4, 0.208, 0.82, 0.25, 1, 1, 1] + + // Determine which edges the panel is closest to for animation direction + // Use target position (not animated position) to avoid binding loops + readonly property bool willTouchTopBar: { + if (!panelContent.allowAttachToBar || root.barPosition !== "top" || root.barIsVertical) + return false; + var targetTopBarY = (isFramed ? 0 : root.barMarginV) + root.barHeight; + return Math.abs(panelBackground.targetY - targetTopBarY) <= 1; + } + readonly property bool willTouchBottomBar: { + if (!panelContent.allowAttachToBar || root.barPosition !== "bottom" || root.barIsVertical) + return false; + var targetBottomBarY = root.height - (isFramed ? 0 : root.barMarginV) - root.barHeight - panelBackground.targetHeight; + return Math.abs(panelBackground.targetY - targetBottomBarY) <= 1; + } + readonly property bool willTouchLeftBar: { + if (!panelContent.allowAttachToBar || root.barPosition !== "left" || !root.barIsVertical) + return false; + var targetLeftBarX = (isFramed ? 0 : root.barMarginH) + root.barHeight; + return Math.abs(panelBackground.targetX - targetLeftBarX) <= 1; + } + readonly property bool willTouchRightBar: { + if (!panelContent.allowAttachToBar || root.barPosition !== "right" || !root.barIsVertical) + return false; + var targetRightBarX = root.width - (isFramed ? 0 : root.barMarginH) - root.barHeight - panelBackground.targetWidth; + return Math.abs(panelBackground.targetX - targetRightBarX) <= 1; + } + readonly property bool willTouchTopEdge: panelContent.allowAttach && panelBackground.targetY <= (isFramed ? frameThickness + 1 : 1) + readonly property bool willTouchBottomEdge: panelContent.allowAttach && (panelBackground.targetY + panelBackground.targetHeight) >= (root.height - (isFramed ? frameThickness + 1 : 1)) + readonly property bool willTouchLeftEdge: panelContent.allowAttach && panelBackground.targetX <= (isFramed ? frameThickness + 1 : 1) + readonly property bool willTouchRightEdge: panelContent.allowAttach && (panelBackground.targetX + panelBackground.targetWidth) >= (root.width - (isFramed ? frameThickness + 1 : 1)) + + readonly property bool isActuallyAttachedToAnyEdge: { + return willTouchTopBar || willTouchBottomBar || willTouchLeftBar || willTouchRightBar || willTouchTopEdge || willTouchBottomEdge || willTouchLeftEdge || willTouchRightEdge; + } + + readonly property bool animateFromTop: { + // Non-attached panels always roll down from top + // Check both the setting and whether panel actually touches any edge + if (!panelContent.allowAttach || !isActuallyAttachedToAnyEdge) { + return true; + } + // When panel is opening, use effective anchors and calculated positions + if (!root.isPanelVisible) { + // Attached to horizontal bar at top + if (panelContent.allowAttachToBar && root.effectivePanelAnchorTop && !root.barIsVertical) { + return true; + } + // Attached to vertical bar (left/right) - don't animate from top + // Panels attach to bar if they have allowAttach (with or without explicit anchor) + var attachedToVerticalBar = panelContent.allowAttachToBar && root.barIsVertical && ((root.effectivePanelAnchorLeft && root.barPosition === "left") || (root.effectivePanelAnchorRight && root.barPosition === "right")); + if (attachedToVerticalBar) { + return false; + } + // Panel touching left/right/bottom screen edge - animate from that edge instead + var touchingNonTopEdge = (willTouchLeftEdge || willTouchRightEdge || willTouchBottomEdge) && !willTouchTopBar && !willTouchBottomBar && !willTouchLeftBar && !willTouchRightBar; + if (touchingNonTopEdge) { + return false; + } + // Panel touching top screen edge (not bar) + if (willTouchTopEdge && !willTouchTopBar && !willTouchBottomBar && !willTouchLeftBar && !willTouchRightBar) { + return true; + } + // Panel anchored to left/right/bottom edge - animate from that edge instead + if (root.panelAnchorLeft || root.panelAnchorRight || root.panelAnchorBottom) { + return false; + } + // Attached to top edge + if (panelContent.allowAttach && root.panelAnchorTop) { + return true; + } + // Default: animate from top (for floating panels with no explicit anchors) + return true; + } + // Panel is visible - use calculated positions + if (willTouchTopBar) { + return true; + } + if (willTouchTopEdge && !willTouchTopBar && !willTouchBottomBar && !willTouchLeftBar && !willTouchRightBar) { + return true; + } + if (!isActuallyAttachedToAnyEdge) { + return true; + } + return false; + } + readonly property bool animateFromBottom: { + // Non-attached panels always roll down from top, not bottom + if (!panelContent.allowAttach || !isActuallyAttachedToAnyEdge) { + return false; + } + if (!root.isPanelVisible) { + // Attached to horizontal bar at bottom + if (panelContent.allowAttachToBar && root.effectivePanelAnchorBottom && !root.barIsVertical) { + return true; + } + // Attached to vertical bar (left/right) - don't animate from bottom + var attachedToVerticalBar = panelContent.allowAttachToBar && root.barIsVertical && ((root.effectivePanelAnchorLeft && root.barPosition === "left") || (root.effectivePanelAnchorRight && root.barPosition === "right")); + if (attachedToVerticalBar) { + return false; + } + // Panel touching bottom screen edge (not bar) + if (willTouchBottomEdge && !willTouchTopBar && !willTouchBottomBar && !willTouchLeftBar && !willTouchRightBar) { + return true; + } + // Panel anchored to top/left/right edge - don't animate from bottom + if (root.panelAnchorTop || root.panelAnchorLeft || root.panelAnchorRight) { + return false; + } + // Attached to bottom edge (when bar is vertical, panel can still be anchored to bottom) + if (panelContent.allowAttach && root.panelAnchorBottom) { + return true; + } + return false; + } + if (willTouchBottomBar) { + return true; + } + if (willTouchBottomEdge && !willTouchTopBar && !willTouchBottomBar && !willTouchLeftBar && !willTouchRightBar) { + return true; + } + return false; + } + readonly property bool animateFromLeft: { + // Non-attached panels always roll down from top, not left + if (!panelContent.allowAttach || !isActuallyAttachedToAnyEdge) { + return false; + } + if (!root.isPanelVisible) { + // Attached to vertical bar on left - must verify bar is actually on left + if (panelContent.allowAttachToBar && root.effectivePanelAnchorLeft && root.barIsVertical && root.barPosition === "left") { + return true; + } + // Panel touching left screen edge (not bar) + if (willTouchLeftEdge && !willTouchTopBar && !willTouchBottomBar && !willTouchLeftBar && !willTouchRightBar) { + return true; + } + // Panel anchored to left edge - animate from left + // Takes precedence over top/bottom when bar is vertical + if (root.panelAnchorLeft) { + return true; + } + return false; + } + if (willTouchTopBar || willTouchBottomBar) { + return false; + } + if (willTouchLeftBar) { + return true; + } + if (willTouchTopEdge || willTouchBottomEdge) { + return false; + } + if (willTouchLeftEdge && !willTouchLeftBar && !willTouchTopBar && !willTouchBottomBar && !willTouchRightBar) { + return true; + } + return false; + } + readonly property bool animateFromRight: { + // Non-attached panels always roll down from top, not right + if (!panelContent.allowAttach || !isActuallyAttachedToAnyEdge) { + return false; + } + if (!root.isPanelVisible) { + // Attached to vertical bar on right - must verify bar is actually on right + if (panelContent.allowAttachToBar && root.effectivePanelAnchorRight && root.barIsVertical && root.barPosition === "right") { + return true; + } + // Panel touching right screen edge (not bar) + if (willTouchRightEdge && !willTouchTopBar && !willTouchBottomBar && !willTouchLeftBar && !willTouchRightBar) { + return true; + } + // Panel anchored to right edge - animate from right + // Takes precedence over top/bottom when bar is vertical + if (root.panelAnchorRight) { + return true; + } + return false; + } + if (willTouchTopBar || willTouchBottomBar) { + return false; + } + if (willTouchRightBar) { + return true; + } + if (willTouchTopEdge || willTouchBottomEdge) { + return false; + } + if (willTouchRightEdge && !willTouchLeftBar && !willTouchTopBar && !willTouchBottomBar && !willTouchRightBar) { + return true; + } + return false; + } + + // Determine animation axis based on which edge is closest + // Priority: horizontal edges (top/bottom) take precedence over vertical edges (left/right) + // This prevents diagonal animations when panel is attached to a corner + // Use reactive values here - they're evaluated BEFORE isPanelVisible becomes true + readonly property bool shouldAnimateWidth: !shouldAnimateHeight && (animateFromLeft || animateFromRight) + readonly property bool shouldAnimateHeight: animateFromTop || animateFromBottom + + // Current animated width/height (referenced by x/y for right/bottom positioning) + readonly property real currentWidth: { + if (isClosing && opacityFadeComplete && shouldAnimateWidth) + return 0; + if (isClosing || isPanelVisible) + return targetWidth; + // If not animating width, start at target (no visual change) + // If animating width, start at 0 (will animate to target) + return shouldAnimateWidth ? 0 : targetWidth; + } + readonly property real currentHeight: { + if (isClosing && opacityFadeComplete && shouldAnimateHeight) + return 0; + if (isClosing || isPanelVisible) + return targetHeight; + // If not animating height, start at target (no visual change) + // If animating height, start at 0 (will animate to target) + return shouldAnimateHeight ? 0 : targetHeight; + } + + width: currentWidth + height: currentHeight + + x: { + // Offset x to make panel grow/shrink from the appropriate edge + // Use CACHED values to prevent recalculation during animation + if (root.cachedAnimateFromRight && root.cachedShouldAnimateWidth) { + // Keep the RIGHT edge fixed at its target position + var targetRightEdge = targetX + targetWidth; + return targetRightEdge - width; + } + return targetX; + } + y: { + // Offset y to make panel grow/shrink from the appropriate edge + // Use CACHED values to prevent recalculation during animation + if (root.cachedAnimateFromBottom && root.cachedShouldAnimateHeight) { + // Keep the BOTTOM edge fixed at its target position + var targetBottomEdge = targetY + targetHeight; + return targetBottomEdge - height; + } + return targetY; + } + + Behavior on width { + enabled: !PanelService.closedImmediately + NumberAnimation { + id: widthAnimation + // Use 0ms if dimensions not initialized to prevent initial changes from animating + // During opening: use 0ms if not animating width, otherwise use normal duration + // During closing: use 0ms if not animating width, otherwise use fast duration + // During normal content resizing: always use normal duration + duration: !panelBackground.dimensionsInitialized ? 0 : (root.isOpening && !panelBackground.shouldAnimateWidth) ? 0 : root.isOpening ? Style.animationNormal : (root.isClosing && !panelBackground.shouldAnimateWidth) ? 0 : root.isClosing ? Style.animationFast : Style.animationNormal + easing.type: Easing.BezierSpline + easing.bezierCurve: panelBackground.bezierCurve + + onRunningChanged: { + // Safety: Zero-duration animation handling + if (!running && duration === 0) { + if (root.isClosing && panelBackground.width === 0 && panelBackground.shouldAnimateWidth) { + Logger.d("SmartPanel", "Zero-duration width animation - finalizing", root.objectName); + Qt.callLater(root.finalizeClose); + } + return; + } + + // When width shrink completes during close, finalize + if (!running && root.isClosing && panelBackground.width === 0 && panelBackground.shouldAnimateWidth) { + Qt.callLater(root.finalizeClose); + } + } + } + } + + Behavior on height { + enabled: !PanelService.closedImmediately + NumberAnimation { + id: heightAnimation + // Use 0ms if dimensions not initialized to prevent initial changes from animating + // During opening: use 0ms if not animating height, otherwise use normal duration + // During closing: use 0ms if not animating height, otherwise use fast duration + // During normal content resizing: always use normal duration + duration: !panelBackground.dimensionsInitialized ? 0 : (root.isOpening && !panelBackground.shouldAnimateHeight) ? 0 : root.isOpening ? Style.animationNormal : (root.isClosing && !panelBackground.shouldAnimateHeight) ? 0 : root.isClosing ? Style.animationFast : Style.animationNormal + easing.type: Easing.BezierSpline + easing.bezierCurve: panelBackground.bezierCurve + + onRunningChanged: { + // Safety: Zero-duration animation handling + if (!running && duration === 0) { + if (root.isClosing && panelBackground.height === 0 && panelBackground.shouldAnimateHeight) { + Logger.d("SmartPanel", "Zero-duration height animation - finalizing", root.objectName); + Qt.callLater(root.finalizeClose); + } + return; + } + + // When height shrink completes during close, finalize + if (!running && root.isClosing && panelBackground.height === 0 && panelBackground.shouldAnimateHeight) { + Qt.callLater(root.finalizeClose); + } + } + } + } + + // Corner states for PanelBackground to read + // State -1: No radius (flat/square corner) + // State 0: Normal (inner curve) + // State 1: Horizontal inversion (outer curve on X-axis) + // State 2: Vertical inversion (outer curve on Y-axis) + + // Smart corner state calculation based on bar attachment and edge touching + property int topLeftCornerState: { + // If bar is not visible, don't show outer corners based on bar attachment + if (!root.barShouldShow) { + // Only check edge touching, not bar touching + var edgeInverted = panelContent.allowAttach && (panelContent.touchingLeftEdge || panelContent.touchingTopEdge); + if (edgeInverted) { + if (panelContent.touchingLeftEdge && panelContent.touchingTopEdge) + return 0; // Both edges: no inversion (normal rounded corner) + if (panelContent.touchingLeftEdge) + return 2; // Left edge: vertical inversion + if (panelContent.touchingTopEdge) + return 1; // Top edge: horizontal inversion + } + return 0; + } + + var barTouchInverted = panelContent.touchingTopBar || panelContent.touchingLeftBar; + // Invert if touching either edge that forms this corner (left OR top), regardless of bar position + var edgeInverted = panelContent.allowAttach && (panelContent.touchingLeftEdge || panelContent.touchingTopEdge); + + if (barTouchInverted || edgeInverted) { + // Determine inversion direction based on which edge is touched + if (panelContent.touchingLeftEdge && panelContent.touchingTopEdge) + return 0; // Both edges: no inversion (normal rounded corner) + if (panelContent.touchingLeftEdge) + return 2; // Left edge: vertical inversion + if (panelContent.touchingTopEdge) + return 1; // Top edge: horizontal inversion + return root.barIsVertical ? 2 : 1; + } + return 0; + } + + property int topRightCornerState: { + // If bar is not visible, don't show outer corners based on bar attachment + if (!root.barShouldShow) { + // Only check edge touching, not bar touching + var edgeInverted = panelContent.allowAttach && (panelContent.touchingRightEdge || panelContent.touchingTopEdge); + if (edgeInverted) { + if (panelContent.touchingRightEdge && panelContent.touchingTopEdge) + return 0; // Both edges: no inversion (normal rounded corner) + if (panelContent.touchingRightEdge) + return 2; // Right edge: vertical inversion + if (panelContent.touchingTopEdge) + return 1; // Top edge: horizontal inversion + } + return 0; + } + + var barTouchInverted = panelContent.touchingTopBar || panelContent.touchingRightBar; + // Invert if touching either edge that forms this corner (right OR top), regardless of bar position + var edgeInverted = panelContent.allowAttach && (panelContent.touchingRightEdge || panelContent.touchingTopEdge); + + if (barTouchInverted || edgeInverted) { + // Determine inversion direction based on which edge is touched + if (panelContent.touchingRightEdge && panelContent.touchingTopEdge) + return 0; // Both edges: no inversion (normal rounded corner) + if (panelContent.touchingRightEdge) + return 2; // Right edge: vertical inversion + if (panelContent.touchingTopEdge) + return 1; // Top edge: horizontal inversion + return root.barIsVertical ? 2 : 1; + } + return 0; + } + + property int bottomLeftCornerState: { + // If bar is not visible, don't show outer corners based on bar attachment + if (!root.barShouldShow) { + // Only check edge touching, not bar touching + var edgeInverted = panelContent.allowAttach && (panelContent.touchingLeftEdge || panelContent.touchingBottomEdge); + if (edgeInverted) { + if (panelContent.touchingLeftEdge && panelContent.touchingBottomEdge) + return 0; // Both edges: no inversion (normal rounded corner) + if (panelContent.touchingLeftEdge) + return 2; // Left edge: vertical inversion + if (panelContent.touchingBottomEdge) + return 1; // Bottom edge: horizontal inversion + } + return 0; + } + + var barTouchInverted = panelContent.touchingBottomBar || panelContent.touchingLeftBar; + // Invert if touching either edge that forms this corner (left OR bottom), regardless of bar position + var edgeInverted = panelContent.allowAttach && (panelContent.touchingLeftEdge || panelContent.touchingBottomEdge); + + if (barTouchInverted || edgeInverted) { + // Determine inversion direction based on which edge is touched + if (panelContent.touchingLeftEdge && panelContent.touchingBottomEdge) + return 0; // Both edges: no inversion (normal rounded corner) + if (panelContent.touchingLeftEdge) + return 2; // Left edge: vertical inversion + if (panelContent.touchingBottomEdge) + return 1; // Bottom edge: horizontal inversion + return root.barIsVertical ? 2 : 1; + } + return 0; + } + + property int bottomRightCornerState: { + // If bar is not visible, don't show outer corners based on bar attachment + if (!root.barShouldShow) { + // Only check edge touching, not bar touching + var edgeInverted = panelContent.allowAttach && (panelContent.touchingRightEdge || panelContent.touchingBottomEdge); + if (edgeInverted) { + if (panelContent.touchingRightEdge && panelContent.touchingBottomEdge) + return 0; // Both edges: no inversion (normal rounded corner) + if (panelContent.touchingRightEdge) + return 2; // Right edge: vertical inversion + if (panelContent.touchingBottomEdge) + return 1; // Bottom edge: horizontal inversion + } + return 0; + } + + var barTouchInverted = panelContent.touchingBottomBar || panelContent.touchingRightBar; + // Invert if touching either edge that forms this corner (right OR bottom), regardless of bar position + var edgeInverted = panelContent.allowAttach && (panelContent.touchingRightEdge || panelContent.touchingBottomEdge); + + if (barTouchInverted || edgeInverted) { + // Determine inversion direction based on which edge is touched + if (panelContent.touchingRightEdge && panelContent.touchingBottomEdge) + return 0; // Both edges: no inversion (normal rounded corner) + if (panelContent.touchingRightEdge) + return 2; // Right edge: vertical inversion + if (panelContent.touchingBottomEdge) + return 1; // Bottom edge: horizontal inversion + return root.barIsVertical ? 2 : 1; + } + return 0; + } + + // MouseArea to catch clicks on the panel and prevent them from reaching the background + // This prevents closing the panel when clicking inside it + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + z: -1 // Behind content, but on the panel background + onClicked: mouse => { + mouse.accepted = true; // Accept and ignore - prevents propagation to background + } + } + } + + // Panel top content: Text, icons, etc... + Loader { + id: contentLoader + active: isPanelOpen + x: panelBackground.x + y: panelBackground.y + width: panelBackground.width + height: panelBackground.height + sourceComponent: root.panelContent + + onLoaded: { + // Wait for contentPreferredWidth/Height to be available before making visible + Qt.callLater(function () { + // Calculate position with stable contentPreferredWidth/Height values + setPosition(); + + // Mark dimensions as initialized to enable animations + panelBackground.dimensionsInitialized = true; + + // Cache animation direction BEFORE isPanelVisible becomes true + // This locks in the direction for the entire open/close cycle + root.cachedAnimateFromTop = panelBackground.animateFromTop; + root.cachedAnimateFromBottom = panelBackground.animateFromBottom; + root.cachedAnimateFromLeft = panelBackground.animateFromLeft; + root.cachedAnimateFromRight = panelBackground.animateFromRight; + root.cachedShouldAnimateWidth = panelBackground.shouldAnimateWidth; + root.cachedShouldAnimateHeight = panelBackground.shouldAnimateHeight; + + // Make panel visible, now only the intended dimension will animate + root.isPanelVisible = true; + + if (root.animationsDisabled) { + // Skip delay when animations are disabled + root.sizeAnimationComplete = true; + } else { + opacityTrigger.start(); + } + + // Start open watchdog timer (skip when animations disabled - everything completes synchronously) + if (!root.animationsDisabled) { + root.openWatchdogActive = true; + openWatchdogTimer.start(); + } + + opened(); + }); + } + } + } + + Component.onCompleted: { + PanelService.registerPanel(root); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Notification/Notification.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Notification/Notification.qml new file mode 100644 index 0000000..a88d14b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Notification/Notification.qml @@ -0,0 +1,812 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Notifications +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Commons +import qs.Services.System +import qs.Widgets + +// Simple notification popup - displays multiple notifications +Variants { + + model: { + const screens = Quickshell.screens.filter(screen => Settings.data.notifications.monitors.includes(screen.name)); + // Empty list can mean two things : + // - No (visible) notification display activated in settings + // - One or more (not visible) displays are activated but unplugged + // In both cases we fallback to show notification on all screens + return screens.length === 0 ? Quickshell.screens : screens; + } + + delegate: Loader { + id: root + + required property ShellScreen modelData + + property ListModel notificationModel: NotificationService.popupModel + + // Deferred activation via Qt.callLater to avoid activating the Loader + // synchronously during ListModel.insert() (which would cause nested + // incubation with the inner Repeater). + property bool shouldBeActive: false + active: shouldBeActive || delayTimer.running + + // Keep loader active briefly after last notification to allow animations to complete + Timer { + id: delayTimer + interval: Style.animationSlow + 200 + repeat: false + } + + function activate() { + shouldBeActive = true; + } + + Connections { + target: notificationModel + function onCountChanged() { + if (notificationModel.count > 0) { + if (!root.shouldBeActive) { + Qt.callLater(root.activate); + } + } else if (root.shouldBeActive) { + root.shouldBeActive = false; + delayTimer.restart(); + } + } + } + + sourceComponent: PanelWindow { + id: notifWindow + screen: modelData + + WlrLayershell.namespace: "noctalia-notifications-" + (screen?.name || "unknown") + WlrLayershell.layer: (Settings.data.notifications?.overlayLayer) ? WlrLayer.Overlay : WlrLayer.Top + WlrLayershell.exclusionMode: ExclusionMode.Ignore + + color: "transparent" + + // Make shadow area click-through, only notification content is clickable + mask: Region { + x: 0 + y: 0 + width: notifWindow.width + height: notifWindow.height + intersection: Intersection.Xor + + Region { + // The clickable content area is inset by shadowPadding from all edges + x: notifWindow.shadowPadding + y: notifWindow.shadowPadding + width: notifWindow.notifWidth + height: Math.max(0, notifWindow.height - notifWindow.shadowPadding * 2) + intersection: Intersection.Subtract + } + } + + // Parse location setting + readonly property string location: Settings.data.notifications.location || "top_right" + readonly property bool isTop: location.startsWith("top") + readonly property bool isBottom: location.startsWith("bottom") + readonly property bool isLeft: location.endsWith("_left") + readonly property bool isRight: location.endsWith("_right") + readonly property bool isCentered: location === "top" || location === "bottom" + + readonly property string barPos: Settings.getBarPositionForScreen(notifWindow.screen?.name) + readonly property bool isFloating: Settings.data.bar.barType === "floating" + readonly property real barHeight: Style.getBarHeightForScreen(notifWindow.screen?.name) + + readonly property bool isFramed: Settings.data.bar.barType === "framed" + readonly property real frameThickness: Settings.data.bar.frameThickness ?? 8 + + readonly property bool isCompact: Settings.data.notifications.density === "compact" + readonly property int notifWidth: Math.round((isCompact ? 320 : 440) * Style.uiScaleRatio) + readonly property int shadowPadding: Style.shadowBlurMax + Style.marginL + + // Calculate bar and frame offsets for each edge separately + readonly property int barOffsetTop: { + if (barPos !== "top") + return isFramed ? frameThickness : 0; + const floatMarginV = isFloating ? Math.ceil(Settings.data.bar.marginVertical) : 0; + return barHeight + floatMarginV; + } + + readonly property int barOffsetBottom: { + if (barPos !== "bottom") + return isFramed ? frameThickness : 0; + const floatMarginV = isFloating ? Math.ceil(Settings.data.bar.marginVertical) : 0; + return barHeight + floatMarginV; + } + + readonly property int barOffsetLeft: { + if (barPos !== "left") + return isFramed ? frameThickness : 0; + const floatMarginH = isFloating ? Math.ceil(Settings.data.bar.marginHorizontal) : 0; + return barHeight + floatMarginH; + } + + readonly property int barOffsetRight: { + if (barPos !== "right") + return isFramed ? frameThickness : 0; + const floatMarginH = isFloating ? Math.ceil(Settings.data.bar.marginHorizontal) : 0; + return barHeight + floatMarginH; + } + + // Anchoring + anchors.top: isTop + anchors.bottom: isBottom + anchors.left: isLeft + anchors.right: isRight + + // Margins for PanelWindow - only apply bar offset for the specific edge where the bar is + margins.top: isTop ? barOffsetTop - shadowPadding + Style.marginM : 0 + margins.bottom: isBottom ? barOffsetBottom - shadowPadding : 0 + margins.left: isLeft ? barOffsetLeft - shadowPadding + Style.marginM : 0 + margins.right: isRight ? barOffsetRight - shadowPadding + Style.marginM : 0 + + implicitWidth: notifWidth + shadowPadding * 2 + implicitHeight: notificationStack.implicitHeight + Style.marginL + + property var animateConnection: null + + Component.onCompleted: { + animateConnection = function (notificationId) { + var delegate = null; + if (notificationRepeater) { + for (var i = 0; i < notificationRepeater.count; i++) { + var item = notificationRepeater.itemAt(i); + if (item?.notificationId === notificationId) { + delegate = item; + break; + } + } + } + + try { + if (delegate && typeof delegate.animateOut === "function" && !delegate.isRemoving) { + delegate.animateOut(); + } + } catch (e) { + // Service fallback if delegate is already invalid + NotificationService.dismissPopup(notificationId); + } + }; + + NotificationService.animateAndRemove.connect(animateConnection); + } + + Component.onDestruction: { + if (animateConnection) { + NotificationService.animateAndRemove.disconnect(animateConnection); + animateConnection = null; + } + } + + ColumnLayout { + id: notificationStack + + anchors { + top: parent.isTop ? parent.top : undefined + bottom: parent.isBottom ? parent.bottom : undefined + left: parent.isLeft ? parent.left : undefined + right: parent.isRight ? parent.right : undefined + horizontalCenter: parent.isCentered ? parent.horizontalCenter : undefined + } + + spacing: -notifWindow.shadowPadding * 2 + Style.marginM + + Behavior on implicitHeight { + enabled: !Settings.data.general.animationDisabled + SpringAnimation { + spring: 2.0 + damping: 0.4 + epsilon: 0.01 + mass: 0.8 + } + } + + Repeater { + id: notificationRepeater + model: notificationModel + + delegate: Item { + id: card + + property string notificationId: model.id + property var notificationData: model + property bool isHovered: false + property bool isRemoving: false + + readonly property int animationDelay: index * 100 + readonly property int slideDistance: 300 + + Layout.preferredWidth: notifWidth + notifWindow.shadowPadding * 2 + Layout.preferredHeight: (notifWindow.isCompact ? compactContent.implicitHeight : notificationContent.implicitHeight) + Style.margin2M + notifWindow.shadowPadding * 2 + Layout.maximumHeight: Layout.preferredHeight + + // Animation properties + property real scaleValue: 0.8 + property real opacityValue: 0.0 + property real slideOffset: 0 + property real swipeOffset: 0 + property real swipeOffsetY: 0 + property real pressGlobalX: 0 + property real pressGlobalY: 0 + property bool isSwiping: false + property bool suppressClick: false + readonly property bool useVerticalSwipe: notifWindow.location === "bottom" || notifWindow.location === "top" + readonly property real swipeStartThreshold: Math.round(18 * Style.uiScaleRatio) + readonly property real swipeDismissThreshold: Math.max(110, cardBackground.width * 0.32) + readonly property real verticalSwipeDismissThreshold: Math.max(70, cardBackground.height * 0.35) + + scale: scaleValue + opacity: opacityValue + transform: Translate { + x: card.swipeOffset + y: card.slideOffset + card.swipeOffsetY + } + + readonly property real slideInOffset: notifWindow.isTop ? -slideDistance : slideDistance + readonly property real slideOutOffset: slideInOffset + + function clampSwipeDelta(deltaX) { + if (notifWindow.isRight) + return Math.max(0, deltaX); + if (notifWindow.isLeft) + return Math.min(0, deltaX); + return deltaX; + } + + function clampVerticalSwipeDelta(deltaY) { + if (notifWindow.isBottom) + return Math.max(0, deltaY); + if (notifWindow.isTop) + return Math.min(0, deltaY); + return deltaY; + } + + // Animation setup + function triggerEntryAnimation() { + animInDelayTimer.stop(); + removalTimer.stop(); + resumeTimer.stop(); + isRemoving = false; + isHovered = false; + isSwiping = false; + swipeOffset = 0; + swipeOffsetY = 0; + if (Settings.data.general.animationDisabled) { + slideOffset = 0; + scaleValue = 1.0; + opacityValue = 1.0; + return; + } + + slideOffset = slideInOffset; + scaleValue = 0.8; + opacityValue = 0.0; + animInDelayTimer.interval = animationDelay; + animInDelayTimer.start(); + } + + Component.onCompleted: triggerEntryAnimation() + + onNotificationIdChanged: triggerEntryAnimation() + + Timer { + id: animInDelayTimer + interval: 0 + repeat: false + onTriggered: { + if (card.isRemoving) + return; + slideOffset = 0; + scaleValue = 1.0; + opacityValue = 1.0; + } + } + + function animateOut() { + if (isRemoving) + return; + animInDelayTimer.stop(); + resumeTimer.stop(); + isRemoving = true; + isSwiping = false; + swipeOffset = 0; + swipeOffsetY = 0; + if (!Settings.data.general.animationDisabled) { + slideOffset = slideOutOffset; + scaleValue = 0.8; + opacityValue = 0.0; + } + } + + function dismissBySwipe() { + if (isRemoving) + return; + animInDelayTimer.stop(); + resumeTimer.stop(); + isRemoving = true; + isSwiping = false; + if (!Settings.data.general.animationDisabled) { + if (useVerticalSwipe) { + swipeOffset = 0; + swipeOffsetY = swipeOffsetY >= 0 ? cardBackground.height + Style.marginXL : -cardBackground.height - Style.marginXL; + } else { + swipeOffset = swipeOffset >= 0 ? cardBackground.width + Style.marginXL : -cardBackground.width - Style.marginXL; + swipeOffsetY = 0; + } + scaleValue = 0.8; + opacityValue = 0.0; + } else { + swipeOffset = 0; + swipeOffsetY = 0; + } + } + + function runAction(actionId, isDismissed) { + if (!isDismissed) { + if (NotificationService.invokeActionAndSuppressClose(notificationId, actionId)) + card.animateOut(); + } else { + if (Settings.data.notifications.clearDismissed) + NotificationService.removeFromHistory(notificationId); + card.animateOut(); + } + } + + Timer { + id: removalTimer + interval: Style.animationSlow + repeat: false + onTriggered: { + NotificationService.dismissPopup(notificationId); + } + } + + onIsRemovingChanged: { + if (isRemoving) { + removalTimer.start(); + } + } + + Behavior on scale { + enabled: !Settings.data.general.animationDisabled + SpringAnimation { + spring: 3 + damping: 0.4 + epsilon: 0.01 + mass: 0.8 + } + } + + Behavior on opacity { + enabled: !Settings.data.general.animationDisabled + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + Behavior on slideOffset { + enabled: !Settings.data.general.animationDisabled + SpringAnimation { + spring: 2.5 + damping: 0.3 + epsilon: 0.01 + mass: 0.6 + } + } + + Behavior on swipeOffset { + enabled: !Settings.data.general.animationDisabled && !card.isSwiping + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + Behavior on swipeOffsetY { + enabled: !Settings.data.general.animationDisabled && !card.isSwiping + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + // Sub item with the right dimensions, really usefull for the + // HoverHandler: card items are overlapping because of the + // negative spacing of notificationStack. + Item { + id: displayedCard + + anchors.fill: parent + anchors.margins: notifWindow.shadowPadding + + HoverHandler { + onHoveredChanged: { + isHovered = hovered; + if (isHovered) { + resumeTimer.stop(); + NotificationService.pauseTimeout(notificationId); + } else { + resumeTimer.start(); + } + } + } + + Timer { + id: resumeTimer + interval: 50 + repeat: false + onTriggered: { + if (!isHovered) { + NotificationService.resumeTimeout(notificationId); + } + } + } + + // Right-click to dismiss + MouseArea { + id: cardDragArea + anchors.fill: cardBackground + acceptedButtons: Qt.LeftButton | Qt.RightButton + hoverEnabled: true + onPressed: mouse => { + if (mouse.button === Qt.LeftButton) { + const globalPoint = cardDragArea.mapToGlobal(mouse.x, mouse.y); + card.pressGlobalX = globalPoint.x; + card.pressGlobalY = globalPoint.y; + card.isSwiping = false; + card.suppressClick = false; + } + } + onPositionChanged: mouse => { + if (!(mouse.buttons & Qt.LeftButton) || card.isRemoving) + return; + const globalPoint = cardDragArea.mapToGlobal(mouse.x, mouse.y); + const rawDeltaX = globalPoint.x - card.pressGlobalX; + const rawDeltaY = globalPoint.y - card.pressGlobalY; + const deltaX = card.clampSwipeDelta(rawDeltaX); + const deltaY = card.clampVerticalSwipeDelta(rawDeltaY); + if (!card.isSwiping) { + if (card.useVerticalSwipe) { + if (Math.abs(deltaY) < card.swipeStartThreshold) + return; + card.isSwiping = true; + } else { + if (Math.abs(deltaX) < card.swipeStartThreshold) + return; + card.isSwiping = true; + } + } + if (card.useVerticalSwipe) { + card.swipeOffset = 0; + card.swipeOffsetY = deltaY; + } else { + card.swipeOffset = deltaX; + card.swipeOffsetY = 0; + } + } + onReleased: mouse => { + if (mouse.button === Qt.RightButton) { + card.animateOut(); + if (Settings.data.notifications.clearDismissed) { + NotificationService.removeFromHistory(notificationId); + } + return; + } + + if (mouse.button !== Qt.LeftButton) + return; + + if (card.isSwiping) { + const dismissDistance = card.useVerticalSwipe ? Math.abs(card.swipeOffsetY) : Math.abs(card.swipeOffset); + const threshold = card.useVerticalSwipe ? card.verticalSwipeDismissThreshold : card.swipeDismissThreshold; + if (dismissDistance >= threshold) { + card.dismissBySwipe(); + if (Settings.data.notifications.clearDismissed) { + NotificationService.removeFromHistory(notificationId); + } + } else { + card.swipeOffset = 0; + card.swipeOffsetY = 0; + } + card.suppressClick = true; + card.isSwiping = false; + return; + } + + if (card.suppressClick) + return; + + var actions = model.actionsJson ? JSON.parse(model.actionsJson) : []; + var hasDefault = actions.some(function (a) { + return a.identifier === "default"; + }); + if (hasDefault && NotificationService.invokeActionAndSuppressClose(notificationId, "default")) { + card.animateOut(); + } else { + // Without a default action, or if invoking it fails, + // the best fallback is focusing the sender window by app identity. + NotificationService.focusSenderWindow(model.appName); + card.animateOut(); + } + } + onCanceled: { + card.isSwiping = false; + card.swipeOffset = 0; + card.swipeOffsetY = 0; + } + } + + // Background with border + Rectangle { + id: cardBackground + anchors.fill: parent + radius: Style.radiusL + border.color: Qt.alpha(Color.mOutline, Color.adaptiveOpacity(Settings.data.notifications.backgroundOpacity) || 1.0) + border.width: Style.borderS + color: Qt.alpha(Color.mSurface, Color.adaptiveOpacity(Settings.data.notifications.backgroundOpacity) || 1.0) + + // Progress bar + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: 2 + color: "transparent" + + Rectangle { + id: progressBar + readonly property real progressWidth: cardBackground.width - (2 * cardBackground.radius) + height: parent.height + x: cardBackground.radius + (progressWidth * (1 - model.progress)) / 2 + width: progressWidth * model.progress + + color: { + var baseColor = model.urgency === 2 ? Color.mError : model.urgency === 0 ? Color.mOnSurface : Color.mPrimary; + return Qt.alpha(baseColor, Color.adaptiveOpacity(Settings.data.notifications.backgroundOpacity) || 1.0); + } + + antialiasing: true + + Behavior on width { + enabled: !card.isRemoving + NumberAnimation { + duration: 100 + easing.type: Easing.Linear + } + } + + Behavior on x { + enabled: !card.isRemoving + NumberAnimation { + duration: 100 + easing.type: Easing.Linear + } + } + } + } + } + + NDropShadow { + anchors.fill: cardBackground + source: cardBackground + autoPaddingEnabled: true + } + + // Content + ColumnLayout { + id: notificationContent + visible: !notifWindow.isCompact + anchors.fill: cardBackground + anchors.margins: Style.marginM + spacing: Style.marginM + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginL + Layout.leftMargin: Style.marginM + Layout.rightMargin: Style.marginM + Layout.topMargin: Style.marginM + Layout.bottomMargin: Style.marginM + + NImageRounded { + Layout.preferredWidth: Math.round(40 * Style.uiScaleRatio) + Layout.preferredHeight: Math.round(40 * Style.uiScaleRatio) + Layout.alignment: Qt.AlignVCenter + radius: Math.min(Style.radiusL, Layout.preferredWidth / 2) + imagePath: model.originalImage || "" + borderColor: "transparent" + borderWidth: 0 + fallbackIcon: "bell" + fallbackIconSize: 24 + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + + // Header with urgency indicator + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + Rectangle { + Layout.preferredWidth: 6 + Layout.preferredHeight: 6 + Layout.alignment: Qt.AlignVCenter + radius: Style.radiusXS + color: model.urgency === 2 ? Color.mError : model.urgency === 0 ? Color.mOnSurface : Color.mPrimary + } + + NText { + text: model.appName || "Unknown App" + pointSize: Style.fontSizeXS + font.weight: Style.fontWeightBold + color: Color.mSecondary + } + + NText { + textFormat: Text.PlainText + text: " " + Time.formatRelativeTime(model.timestamp) + pointSize: Style.fontSizeXXS + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignBottom + } + + Item { + Layout.fillWidth: true + } + } + + NText { + text: model.summary || I18n.tr("common.no-summary") + pointSize: Style.fontSizeM + font.weight: Style.fontWeightMedium + color: Color.mOnSurface + textFormat: Text.StyledText + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + maximumLineCount: 3 + elide: Text.ElideRight + visible: text.length > 0 + Layout.fillWidth: true + Layout.rightMargin: Style.marginM + } + + NText { + text: model.body || "" + pointSize: Style.fontSizeM + color: Color.mOnSurface + textFormat: Text.StyledText + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + + maximumLineCount: 5 + elide: Text.ElideRight + visible: text.length > 0 + Layout.fillWidth: true + Layout.rightMargin: Style.marginXL + } + + // Actions + Flow { + Layout.fillWidth: true + spacing: Style.marginS + Layout.topMargin: Style.marginM + flow: Flow.LeftToRight + + property string parentNotificationId: notificationId + property var parsedActions: { + try { + return model.actionsJson ? JSON.parse(model.actionsJson) : []; + } catch (e) { + return []; + } + } + visible: parsedActions.length > 0 + + Repeater { + model: parent.parsedActions + + delegate: NButton { + property var actionData: modelData + + text: { + var actionText = actionData.text || "Open"; + if (actionText.includes(",")) { + return actionText.split(",")[1] || actionText; + } + return actionText; + } + fontSize: Style.fontSizeS + backgroundColor: Color.mPrimary + textColor: hovered ? Color.mOnHover : Color.mOnPrimary + hoverColor: Color.mHover + outlined: false + implicitHeight: 24 + onClicked: { + card.runAction(actionData.identifier, false); + } + } + } + } + } + } + } + + // Close button + NIconButton { + visible: !notifWindow.isCompact + icon: "close" + tooltipText: I18n.tr("tooltips.dismiss-notification") + baseSize: Style.baseWidgetSize * 0.6 + anchors.top: cardBackground.top + anchors.topMargin: Style.marginXL + anchors.right: cardBackground.right + anchors.rightMargin: Style.marginXL + + onClicked: { + card.runAction("", true); + } + } + + // Compact content + RowLayout { + id: compactContent + visible: notifWindow.isCompact + anchors.fill: cardBackground + anchors.margins: Style.marginM + spacing: Style.marginS + + NImageRounded { + Layout.preferredWidth: Math.round(24 * Style.uiScaleRatio) + Layout.preferredHeight: Math.round(24 * Style.uiScaleRatio) + Layout.alignment: Qt.AlignVCenter + radius: Style.radiusXS + imagePath: model.originalImage || "" + borderColor: "transparent" + borderWidth: 0 + fallbackIcon: "bell" + fallbackIconSize: 16 + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NText { + text: model.summary || I18n.tr("common.no-summary") + pointSize: Style.fontSizeM + font.weight: Style.fontWeightMedium + color: Color.mOnSurface + textFormat: Text.StyledText + maximumLineCount: 1 + elide: Text.ElideRight + Layout.fillWidth: true + } + + NText { + visible: model.body && model.body.length > 0 + Layout.fillWidth: true + text: model.body || "" + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + textFormat: Text.StyledText + wrapMode: Text.Wrap + maximumLineCount: 2 + elide: Text.ElideRight + } + } + } + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/OSD/OSD.qml b/fedora/.config/quickshell/noctalia-shell/Modules/OSD/OSD.qml new file mode 100644 index 0000000..a64eee6 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/OSD/OSD.qml @@ -0,0 +1,868 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services.Hardware +import qs.Services.Keyboard +import qs.Services.Media +import qs.Services.UI +import qs.Widgets + +// Unified OSD component that displays volume, input volume, and brightness changes +Variants { + id: osd + + // Do not change the order or it will break settings. + enum Type { + Volume, + InputVolume, + Brightness, + LockKey + } + + model: Quickshell.screens.filter(screen => (Settings.data.osd.monitors.includes(screen.name) || Settings.data.osd.monitors.length === 0) && Settings.data.osd.enabled) + + delegate: Loader { + id: root + + required property ShellScreen modelData + + active: false + + // OSD State + property int currentOSDType: -1 // OSD.Type enum value, -1 means none + property bool startupComplete: false + property real currentBrightness: 0 + property bool suppressInputOSD: false + + // Lock Key States + property string lastLockKeyChanged: "" // "caps", "num", "scroll", or "" + + // Current values (computed properties) + readonly property real currentVolume: AudioService.volume + readonly property bool isMuted: AudioService.muted + readonly property real currentInputVolume: AudioService.inputVolume + readonly property bool isInputMuted: AudioService.inputMuted + readonly property real epsilon: 0.005 + + // LockKey OSD enabled state (reactive to settings) + readonly property bool lockKeyOSDEnabled: { + const enabledTypes = Settings.data.osd.enabledTypes || []; + if (enabledTypes.length === 0) + return false; + return enabledTypes.includes(OSD.Type.LockKey); + } + + readonly property var validIcons: { + const iconKeys = Object.keys(Icons.icons); + const aliasKeys = Object.keys(Icons.aliases); + return new Set([...iconKeys, ...aliasKeys]); + } + + function getIcon() { + switch (currentOSDType) { + case OSD.Type.Volume: + if (isMuted) + return "volume-mute"; + // Show volume-x icon when volume is effectively 0% (within rounding threshold) + if (currentVolume < root.epsilon) + return "volume-x"; + return currentVolume <= 0.5 ? "volume-low" : "volume-high"; + case OSD.Type.InputVolume: + return isInputMuted ? "microphone-off" : "microphone"; + case OSD.Type.Brightness: + // Show sun-off icon when brightness is effectively 0% (within rounding threshold) + if (currentBrightness < root.epsilon) + return "sun-off"; + return currentBrightness <= 0.5 ? "brightness-low" : "brightness-high"; + case OSD.Type.LockKey: + return "keyboard"; + default: + return ""; + } + } + + function getCurrentValue() { + switch (currentOSDType) { + case OSD.Type.Volume: + return isMuted ? 0 : currentVolume; + case OSD.Type.InputVolume: + return isInputMuted ? 0 : currentInputVolume; + case OSD.Type.Brightness: + return currentBrightness; + case OSD.Type.LockKey: + return 1.0; // Always show 100% when showing lock key status + + default: + return 0; + } + } + + function getMaxValue() { + if (currentOSDType === OSD.Type.Volume || currentOSDType === OSD.Type.InputVolume) { + return Settings.data.audio.volumeOverdrive ? 1.5 : 1.0; + } + return 1.0; + } + + function getDisplayPercentage() { + if (currentOSDType === OSD.Type.LockKey) { + // For lock keys, return the pre-determined status text + return lastLockKeyChanged; + } + + const value = getCurrentValue(); + const max = getMaxValue(); + if ((currentOSDType === OSD.Type.Volume || currentOSDType === OSD.Type.InputVolume) && Settings.data.audio.volumeOverdrive) { + const pct = Math.round(value * 100); + return pct + "%"; + } + const pct = Math.round(Math.min(max, value) * 100); + return pct + "%"; + } + + function getProgressColor() { + const isMutedState = (currentOSDType === OSD.Type.Volume && isMuted) || (currentOSDType === OSD.Type.InputVolume && isInputMuted); + if (isMutedState) { + return Color.mError; + } + // When volumeOverdrive is enabled, show error color if volume is above 100% + if ((currentOSDType === OSD.Type.Volume || currentOSDType === OSD.Type.InputVolume) && Settings.data.audio.volumeOverdrive) { + const value = getCurrentValue(); + if (value > 1.0) { + return Color.mError; + } + } + // For lock keys, use a different color to indicate the lock state + if (currentOSDType === OSD.Type.LockKey) { + // Check the specific lock key that was changed + if (lastLockKeyChanged.startsWith("CAPS")) { + return LockKeysService.capsLockOn ? Color.mPrimary : Color.mOnSurfaceVariant; + } else if (lastLockKeyChanged.startsWith("NUM")) { + return LockKeysService.numLockOn ? Color.mPrimary : Color.mOnSurfaceVariant; + } else if (lastLockKeyChanged.startsWith("SCROLL")) { + return LockKeysService.scrollLockOn ? Color.mPrimary : Color.mOnSurfaceVariant; + } + } + return Color.mPrimary; + } + + function getIconColor() { + const isMutedState = (currentOSDType === OSD.Type.Volume && isMuted) || (currentOSDType === OSD.Type.InputVolume && isInputMuted); + if (isMutedState) + return Color.mError; + + if (currentOSDType === OSD.Type.LockKey) { + // Check the specific lock key that was changed + if (lastLockKeyChanged.startsWith("CAPS")) { + return LockKeysService.capsLockOn ? Color.mPrimary : Color.mOnSurfaceVariant; + } else if (lastLockKeyChanged.startsWith("NUM")) { + return LockKeysService.numLockOn ? Color.mPrimary : Color.mOnSurfaceVariant; + } else if (lastLockKeyChanged.startsWith("SCROLL")) { + return LockKeysService.scrollLockOn ? Color.mPrimary : Color.mOnSurfaceVariant; + } + } + + return Color.mOnSurface; + } + + // Brightness Handling + function connectBrightnessMonitors() { + for (var i = 0; i < BrightnessService.monitors.length; i++) { + const monitor = BrightnessService.monitors[i]; + monitor.brightnessUpdated.disconnect(onBrightnessChanged); + monitor.brightnessUpdated.connect(onBrightnessChanged); + } + } + + function onBrightnessChanged(newBrightness) { + if (!root) + return; + + root.currentBrightness = newBrightness; + // Don't show OSD if brightness panel is open + var brightnessPanel = PanelService.getPanel("brightnessPanel", root.modelData); + var controlCenterPanel = PanelService.getPanel("controlCenterPanel", root.modelData); + + if (brightnessPanel && brightnessPanel.isPanelOpen) + return; + if (controlCenterPanel && controlCenterPanel.isPanelOpen) { + var cards = Settings.data.controlCenter.cards || []; + if (cards.some(c => c.enabled && c.id === "brightness-card")) + return; + } + showOSD(OSD.Type.Brightness); + } + + // Check if a specific OSD type is enabled + function isTypeEnabled(type) { + const enabledTypes = Settings.data.osd.enabledTypes || []; + // If enabledTypes is empty, no types are enabled (no OSD will be shown) + if (enabledTypes.length === 0) + return false; + return enabledTypes.includes(type); + } + + // OSD Display Control + function showOSD(type) { + // Ignore all OSD requests during startup period + if (!startupComplete) + return; + + // Check if this OSD type is enabled + if (!isTypeEnabled(type)) + return; + + // Suppress Audio OSD if Audio Panel or Control Center (with audio card) is open + if (type === OSD.Type.Volume || type === OSD.Type.InputVolume) { + var audioPanel = PanelService.getPanel("audioPanel", root.modelData); + if (audioPanel && audioPanel.isPanelOpen) + return; + var controlCenterPanel = PanelService.getPanel("controlCenterPanel", root.modelData); + if (controlCenterPanel && controlCenterPanel.isPanelOpen) { + var cards = Settings.data.controlCenter.cards || []; + if (cards.some(c => c.enabled && c.id === "audio-card")) + return; + } + } + + currentOSDType = type; + + if (!root.active) { + root.active = true; + } + + if (root.item) { + root.item.showOSD(); + } else { + Qt.callLater(() => { + if (root.item) + root.item.showOSD(); + }); + } + } + + function hideOSD() { + if (root.item?.osdItem) { + root.item.osdItem.hideImmediately(); + } else if (root.active) { + root.active = false; + } + } + + // AudioService monitoring + Connections { + target: AudioService + + function onVolumeChanged() { + showOSD(OSD.Type.Volume); + } + + function onVolumeAtMaximum() { + showOSD(OSD.Type.Volume); + } + + function onVolumeAtMinimum() { + showOSD(OSD.Type.Volume); + } + + function onMutedChanged() { + if (AudioService.consumeOutputOSDSuppression()) + return; + showOSD(OSD.Type.Volume); + } + + function onInputVolumeChanged() { + if (suppressInputOSD) + return; + if (AudioService.hasInput) + showOSD(OSD.Type.InputVolume); + } + + function onInputMutedChanged() { + if (suppressInputOSD) + return; + if (!AudioService.hasInput) + return; + if (AudioService.consumeInputOSDSuppression()) + return; + showOSD(OSD.Type.InputVolume); + } + + // Refresh OSD when device changes to ensure correct volume is displayed + function onSinkChanged() { + suppressInputOSD = true; + inputSuppressionTimer.restart(); + // If volume OSD is currently showing, refresh it to show new device's volume + if (root.currentOSDType === OSD.Type.Volume) { + Qt.callLater(() => { + showOSD(OSD.Type.Volume); + }); + } + } + + function onSourceChanged() { + // If input volume OSD is currently showing, refresh it to show new device's volume + if (root.currentOSDType === OSD.Type.InputVolume) { + Qt.callLater(() => { + showOSD(OSD.Type.InputVolume); + }); + } + } + } + + // Brightness monitoring + Connections { + target: BrightnessService + function onMonitorsChanged() { + connectBrightnessMonitors(); + } + } + + // Register/unregister LockKeysService polling based on whether LockKey OSD is enabled + onLockKeyOSDEnabledChanged: { + if (lockKeyOSDEnabled) { + LockKeysService.registerComponent("osd:" + (modelData?.name || "unknown")); + } else { + LockKeysService.unregisterComponent("osd:" + (modelData?.name || "unknown")); + } + } + Component.onCompleted: { + if (lockKeyOSDEnabled) { + LockKeysService.registerComponent("osd:" + (modelData?.name || "unknown")); + } + } + + // LockKeys monitoring with a cleaner approach + // Only connect when LockKey OSD is enabled to avoid starting the service unnecessarily + Connections { + target: root.lockKeyOSDEnabled ? LockKeysService : null + + function onCapsLockChanged(active) { + root.lastLockKeyChanged = active ? "CAPS ON" : "CAPS OFF"; + root.showOSD(OSD.Type.LockKey); + } + + function onNumLockChanged(active) { + root.lastLockKeyChanged = active ? "NUM ON" : "NUM OFF"; + root.showOSD(OSD.Type.LockKey); + } + + function onScrollLockChanged(active) { + root.lastLockKeyChanged = active ? "SCROLL ON" : "SCROLL OFF"; + root.showOSD(OSD.Type.LockKey); + } + } + + // Startup timer - connect brightness monitors and enable OSD after 2 seconds + Timer { + id: startupTimer + interval: 2000 + running: true + onTriggered: { + connectBrightnessMonitors(); + root.startupComplete = true; + } + } + + // Timer to reset the input volume OSD suppression + Timer { + id: inputSuppressionTimer + interval: 300 + repeat: false + onTriggered: root.suppressInputOSD = false + } + + Component.onDestruction: { + LockKeysService.unregisterComponent("osd:" + (modelData?.name || "unknown")); + if (typeof BrightnessService !== "undefined" && BrightnessService.monitors) { + for (var i = 0; i < BrightnessService.monitors.length; i++) { + try { + BrightnessService.monitors[i].brightnessUpdated.disconnect(onBrightnessChanged); + } catch (e) { + // Ignore errors if already disconnected or not connected + } + } + } + } + + // Visual Component + sourceComponent: PanelWindow { + id: panel + screen: modelData + + // Position configuration + readonly property string location: Settings.data.osd?.location || "top_right" + readonly property bool isTop: location === "top" || location.startsWith("top") + readonly property bool isBottom: location === "bottom" || location.startsWith("bottom") + readonly property bool isLeft: location.includes("_left") || location === "left" + readonly property bool isRight: location.includes("_right") || location === "right" + readonly property bool verticalMode: location === "left" || location === "right" + + // Hidden text element for measuring lock key text width + NText { + id: lockKeyTextMetrics + visible: false + text: root.getDisplayPercentage() + pointSize: Style.fontSizeS + family: Settings.data.ui.fontFixed + elide: Text.ElideNone + wrapMode: Text.NoWrap + } + + // Dimensions + readonly property bool isShortMode: root.currentOSDType === OSD.Type.LockKey + readonly property int longHWidth: Math.round(320 * Style.uiScaleRatio) + readonly property int longHHeight: Math.round(72 * Style.uiScaleRatio) + readonly property int shortHWidth: Math.round(180 * Style.uiScaleRatio) + readonly property int longVWidth: Math.round(80 * Style.uiScaleRatio) + readonly property int longVHeight: Math.round(280 * Style.uiScaleRatio) + readonly property int shortVHeight: Math.round(180 * Style.uiScaleRatio) + + // Dynamic width for horizontal lock keys based on text length + // Explicitly bind to contentWidth to ensure reactivity + readonly property int lockKeyHWidth: { + if (root.currentOSDType !== OSD.Type.LockKey || verticalMode) { + return shortHWidth; + } + const text = root.getDisplayPercentage(); + if (!text) { + return shortHWidth; + } + // Access contentWidth to create binding dependency + const textWidth = Math.ceil(lockKeyTextMetrics.contentWidth || 0); + if (textWidth === 0) { + // Fallback: estimate based on text length if measurement not ready + const fontSize = Style.fontSizeS * Settings.data.ui.fontFixedScale * Style.uiScaleRatio; + const estimatedWidth = text.length * fontSize * 0.6; + const iconWidth = Style.fontSizeXL * Style.uiScaleRatio; + const margins = Style.margin2L; + const spacing = Style.marginM; + const bgMargins = Style.margin2M * 1.5; + return Math.max(shortHWidth, Math.round((estimatedWidth + iconWidth + margins + spacing + bgMargins) * 1.1)); + } + const iconWidth = Style.fontSizeXL * Style.uiScaleRatio; + const margins = Style.margin2L; // Left and right content margins + const spacing = Style.marginM; // Spacing between icon and text + const bgMargins = Style.margin2M * 1.5; // Background margins + const totalWidth = textWidth + iconWidth + margins + spacing + bgMargins; + // Ensure minimum width and add some buffer + return Math.max(shortHWidth, Math.round(totalWidth * 1.1)); + } + + // Dynamic height for vertical lock keys based on text length + readonly property int lockKeyVHeight: { + if (root.currentOSDType !== OSD.Type.LockKey || !verticalMode) { + return shortVHeight; + } + const text = root.getDisplayPercentage(); + const charCount = text ? text.length : 0; + if (charCount === 0) { + return shortVHeight; + } + // Calculate height: font size * char count + margins + icon space + // Font size M (11pt) scaled, plus some spacing between chars + const fontSize = Style.fontSizeS * Settings.data.ui.fontFixedScale * Style.uiScaleRatio; + const charHeight = fontSize * 1.3; // Add 30% for line height (matches Layout.preferredHeight) + const textHeight = charCount * charHeight; + // Background margins (Style.marginM * 1.5 * 2 for top and bottom) + const bgMargins = Style.marginM * 1.5 * 2; + // Content margins (Style.margin2L for top and bottom) + const contentMargins = Style.margin2L; + // Icon size: fontSizeXL scaled, with extra space for icon rendering and padding + const iconSize = Style.fontSizeXL * Style.uiScaleRatio * 1.8; // Add 80% for icon rendering and padding + // Spacing between text and icon (Style.marginM for lock keys) + const textIconSpacing = Style.marginM; + // Add extra buffer to ensure everything fits comfortably + const buffer = Style.marginL; + const totalHeight = textHeight + bgMargins + contentMargins + iconSize + textIconSpacing + buffer; + // Ensure minimum height and add extra padding for safety + return Math.max(shortVHeight, Math.round(totalHeight * 1.1)); + } + + readonly property int barThickness: { + const base = Math.max(8, Math.round(8 * Style.uiScaleRatio)); + return base % 2 === 0 ? base : base + 1; + } + + anchors.top: isTop + anchors.bottom: isBottom + anchors.left: isLeft + anchors.right: isRight + + readonly property string screenBarPosition: Settings.getBarPositionForScreen(root.modelData?.name) + readonly property real barHeight: Style.getBarHeightForScreen(root.modelData?.name) + readonly property bool isFramed: Settings.data.bar.barType === "framed" + readonly property real frameThickness: Settings.data.bar.frameThickness ?? 8 + + function calculateMargin(isAnchored, position) { + if (!isAnchored) + return 0; + + let base = Style.marginM; + if (screenBarPosition === position) { + const isVertical = position === "top" || position === "bottom"; + const floatExtra = Math.ceil(Settings.data.bar.barType === "floating" ? (isVertical ? Settings.data.bar.marginVertical : Settings.data.bar.marginHorizontal) : 0); + return barHeight + base + floatExtra; + } + + if (isFramed) { + return base + frameThickness; + } + + return base; + } + + margins.top: calculateMargin(anchors.top, "top") + margins.bottom: calculateMargin(anchors.bottom, "bottom") + margins.left: calculateMargin(anchors.left, "left") + margins.right: calculateMargin(anchors.right, "right") + + implicitWidth: verticalMode ? longVWidth : (isShortMode ? lockKeyHWidth : longHWidth) + implicitHeight: verticalMode ? (isShortMode ? lockKeyVHeight : longVHeight) : longHHeight + color: "transparent" + + WlrLayershell.namespace: "noctalia-osd-" + (screen?.name || "unknown") + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + WlrLayershell.layer: Settings.data.osd?.overlayLayer ? WlrLayer.Overlay : WlrLayer.Top + WlrLayershell.exclusionMode: ExclusionMode.Ignore + + // Click-through — OSD is display-only, no input needed + mask: Region {} + + Item { + id: osdItem + anchors.fill: parent + visible: false + opacity: 0 + scale: 0.85 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + + Timer { + id: hideTimer + interval: Settings.data.osd.autoHideMs + onTriggered: osdItem.hide() + } + + Timer { + id: visibilityTimer + interval: Style.animationNormal + 50 + onTriggered: { + osdItem.visible = false; + root.currentOSDType = -1; + root.lastLockKeyChanged = ""; + root.active = false; + } + } + + Rectangle { + id: background + anchors.fill: parent + anchors.margins: Style.marginM * 1.5 + radius: Style.radiusL + color: Qt.alpha(Color.mSurface, Color.adaptiveOpacity(Settings.data.osd.backgroundOpacity) || 1.0) + border.color: Qt.alpha(Color.mOutline, Color.adaptiveOpacity(Settings.data.osd.backgroundOpacity) || 1.0) + border.width: { + const bw = Math.max(2, Style.borderM); + return bw % 2 === 0 ? bw : bw + 1; + } + } + + NDropShadow { + anchors.fill: background + source: background + autoPaddingEnabled: true + } + + Loader { + id: contentLoader + anchors.fill: background + anchors.margins: Style.marginM + active: true + sourceComponent: panel.verticalMode ? verticalContent : horizontalContent + } + + Component { + id: horizontalContent + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginL + anchors.rightMargin: Style.marginL + spacing: Style.marginM + + TextMetrics { + id: percentageMetrics + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS * (Settings.data.ui.fontFixedScale * Style.uiScaleRatio) + text: "150%" + } + + // Common Icon for all types + NIcon { + icon: root.getIcon() + color: root.getIconColor() + pointSize: Style.fontSizeXL + Layout.alignment: Qt.AlignVCenter + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + } + + // Lock Key Status Text (replaces progress bar) + NText { + visible: root.currentOSDType === OSD.Type.LockKey + text: root.getDisplayPercentage() + color: root.getProgressColor() + pointSize: Style.fontSizeS + elide: Text.ElideNone + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + Layout.alignment: Qt.AlignVCenter + } + + // Progress Bar for Volume/Brightness + Rectangle { + visible: root.currentOSDType !== OSD.Type.LockKey + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + height: panel.barThickness + radius: Math.min(Style.iRadiusL, panel.barThickness / 2) + color: Color.mSurfaceVariant + + Rectangle { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: parent.width * Math.min(1.0, root.getCurrentValue() / root.getMaxValue()) + radius: parent.radius + color: root.getProgressColor() + + Behavior on width { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + } + } + + // Percentage Text for Volume/Brightness + NText { + visible: root.currentOSDType !== OSD.Type.LockKey + text: root.getDisplayPercentage() + color: Color.mOnSurface + pointSize: Style.fontSizeS + family: Settings.data.ui.fontFixed + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + Layout.fillWidth: false + Layout.preferredWidth: Math.ceil(percentageMetrics.width) + Math.round(8 * Style.uiScaleRatio) + Layout.maximumWidth: Math.ceil(percentageMetrics.width) + Math.round(8 * Style.uiScaleRatio) + Layout.minimumWidth: Math.ceil(percentageMetrics.width) + } + } + } + + Component { + id: verticalContent + ColumnLayout { + anchors.fill: parent + anchors.topMargin: Style.marginL + anchors.bottomMargin: Style.marginL + spacing: root.currentOSDType === OSD.Type.LockKey ? Style.marginM : Style.marginS + clip: root.currentOSDType !== OSD.Type.LockKey + + ColumnLayout { + id: textVerticalLayout + visible: root.currentOSDType === OSD.Type.LockKey + Layout.fillWidth: true + Layout.fillHeight: false + Layout.alignment: Qt.AlignHCenter + spacing: 0 + + property var verticalTextChars: [] + + function updateVerticalTextChars() { + const text = root.getDisplayPercentage(); + const chars = []; + for (let i = 0; i < text.length; i++) { + chars.push(text[i]); + } + verticalTextChars = chars; + } + + Component.onCompleted: updateVerticalTextChars() + + Connections { + target: root + function onLastLockKeyChangedChanged() { + if (root.currentOSDType === OSD.Type.LockKey) { + textVerticalLayout.updateVerticalTextChars(); + } + } + function onCurrentOSDTypeChanged() { + if (root.currentOSDType === OSD.Type.LockKey) { + textVerticalLayout.updateVerticalTextChars(); + } + } + } + + Repeater { + model: textVerticalLayout.verticalTextChars + + NText { + text: modelData || "" + color: root.getProgressColor() + pointSize: Style.fontSizeS + family: Settings.data.ui.fontFixed + Layout.fillWidth: true + Layout.preferredHeight: { + const fontSize = Style.fontSizeS * Settings.data.ui.fontFixedScale * Style.uiScaleRatio; + return Math.round(fontSize * 1.3); + } + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + } + + NText { + visible: root.currentOSDType !== OSD.Type.LockKey + text: root.getDisplayPercentage() + color: Color.mOnSurface + pointSize: Style.fontSizeS + family: Settings.data.ui.fontFixed + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + Layout.preferredHeight: Math.round(20 * Style.uiScaleRatio) + } + + Item { + visible: root.currentOSDType !== OSD.Type.LockKey + Layout.fillWidth: true + Layout.fillHeight: root.currentOSDType !== OSD.Type.LockKey + + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.bottom: parent.bottom + width: panel.barThickness + radius: Math.min(Style.iRadiusL, panel.barThickness / 2) + color: Color.mSurfaceVariant + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: parent.height * Math.min(1.0, root.getCurrentValue() / root.getMaxValue()) + radius: parent.radius + color: root.getProgressColor() + + Behavior on height { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + } + } + } + + NIcon { + icon: root.getIcon() + color: root.getIconColor() + pointSize: root.currentOSDType === OSD.Type.LockKey ? Style.fontSizeXL : Style.fontSizeL + Layout.alignment: root.currentOSDType === OSD.Type.LockKey ? Qt.AlignHCenter : (Qt.AlignHCenter | Qt.AlignBottom) + Layout.preferredHeight: root.currentOSDType === OSD.Type.LockKey ? (Style.fontSizeXL * Style.uiScaleRatio * 1.5) : -1 + Layout.minimumHeight: root.currentOSDType === OSD.Type.LockKey ? (Style.fontSizeXL * Style.uiScaleRatio) : 0 + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + } + } + } + + // Delay showing the OSD to allow the layout to settle after activation. + // Without this, the percentage text renders outside the box on first + // show. + Timer { + id: showDelayTimer + interval: 30 + onTriggered: { + osdItem.visible = true; + osdItem.opacity = 1; + osdItem.scale = 1.0; + hideTimer.start(); + } + } + + function show() { + hideTimer.stop(); + visibilityTimer.stop(); + showDelayTimer.start(); + } + + function hide() { + hideTimer.stop(); + visibilityTimer.stop(); + osdItem.opacity = 0; + osdItem.scale = 0.85; + visibilityTimer.start(); + } + + function hideImmediately() { + hideTimer.stop(); + visibilityTimer.stop(); + osdItem.opacity = 0; + osdItem.scale = 0.85; + osdItem.visible = false; + root.currentOSDType = -1; + root.active = false; + } + } + + function showOSD() { + osdItem.show(); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Audio/AudioPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Audio/AudioPanel.qml new file mode 100644 index 0000000..31e2280 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Audio/AudioPanel.qml @@ -0,0 +1,791 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Pipewire +import Quickshell.Widgets +import qs.Commons +import qs.Modules.MainScreen +import qs.Services.Media +import qs.Widgets + +SmartPanel { + id: root + + preferredWidth: Math.round(440 * Style.uiScaleRatio) + preferredHeight: Math.round(420 * Style.uiScaleRatio) + + panelContent: Item { + id: panelContent + + // Volume state (lazy-loaded with panelContent) + property real localOutputVolume: AudioService.volume || 0 + property bool localOutputVolumeChanging: false + property int lastSinkId: -1 + + property real localInputVolume: AudioService.inputVolume || 0 + property bool localInputVolumeChanging: false + property int lastSourceId: -1 + + readonly property bool outputVolumeGuard: outputVolumeSlider.sliderActive || localOutputVolumeChanging + readonly property bool inputVolumeGuard: inputVolumeSlider.sliderActive || localInputVolumeChanging + + // UI state (lazy-loaded with panelContent) + property int currentTabIndex: 0 + + Component.onCompleted: { + var vol = AudioService.volume; + localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; + var inputVol = AudioService.inputVolume; + localInputVolume = (inputVol !== undefined && !isNaN(inputVol)) ? inputVol : 0; + if (AudioService.sink) { + lastSinkId = AudioService.sink.id; + } + if (AudioService.source) { + lastSourceId = AudioService.source.id; + } + } + + // Reset local volume when device changes - use current device's volume + Connections { + target: AudioService + function onSinkChanged() { + if (AudioService.sink) { + const newSinkId = AudioService.sink.id; + if (newSinkId !== panelContent.lastSinkId) { + panelContent.lastSinkId = newSinkId; + // Immediately set local volume to current device's volume + var vol = AudioService.volume; + panelContent.localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; + } + } else { + panelContent.lastSinkId = -1; + panelContent.localOutputVolume = 0; + } + } + } + + Connections { + target: AudioService + function onSourceChanged() { + if (AudioService.source) { + const newSourceId = AudioService.source.id; + if (newSourceId !== panelContent.lastSourceId) { + panelContent.lastSourceId = newSourceId; + // Immediately set local volume to current device's volume + var vol = AudioService.inputVolume; + panelContent.localInputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; + } + } else { + panelContent.lastSourceId = -1; + panelContent.localInputVolume = 0; + } + } + } + + // Connections to update local volumes when AudioService changes + Connections { + target: AudioService + function onVolumeChanged() { + if (!panelContent.outputVolumeGuard && !AudioService.isSettingOutputVolume && AudioService.sink && AudioService.sink.id === panelContent.lastSinkId) { + var vol = AudioService.volume; + panelContent.localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; + } + } + } + + Connections { + target: AudioService + function onInputVolumeChanged() { + if (!panelContent.inputVolumeGuard && !AudioService.isSettingInputVolume && AudioService.source && AudioService.source.id === panelContent.lastSourceId) { + var vol = AudioService.inputVolume; + panelContent.localInputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; + } + } + } + + Connections { + target: outputVolumeSlider + function onSliderActiveChanged() { + if (!outputVolumeSlider.sliderActive && AudioService.sink && AudioService.sink.id === panelContent.lastSinkId) { + var vol = AudioService.volume; + panelContent.localOutputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; + } + } + } + + Connections { + target: inputVolumeSlider + function onSliderActiveChanged() { + if (!inputVolumeSlider.sliderActive && AudioService.source && AudioService.source.id === panelContent.lastSourceId) { + var vol = AudioService.inputVolume; + panelContent.localInputVolume = (vol !== undefined && !isNaN(vol)) ? vol : 0; + } + } + } + + // Timer to debounce volume changes + // Only sync if the device hasn't changed (check by comparing IDs) + Timer { + interval: 100 + running: true + repeat: true + onTriggered: { + // Only sync if sink hasn't changed + if (AudioService.sink && AudioService.sink.id === panelContent.lastSinkId) { + if (Math.abs(panelContent.localOutputVolume - AudioService.volume) >= 0.01) { + AudioService.setVolume(panelContent.localOutputVolume); + } + } + // Only sync if source hasn't changed + if (AudioService.source && AudioService.source.id === panelContent.lastSourceId) { + if (Math.abs(panelContent.localInputVolume - AudioService.inputVolume) >= 0.01) { + AudioService.setInputVolume(panelContent.localInputVolume); + } + } + } + } + + // Find application streams that are actually playing audio (connected to default sink) + // Use linkGroups to find nodes connected to the default audio sink + // Note: We need to use link IDs since source/target properties require binding + readonly property var appStreams: AudioService.appStreams + + // Use implicitHeight from content + margins to avoid binding loops + property real contentPreferredHeight: mainColumn.implicitHeight + Style.margin2L + + ColumnLayout { + id: mainColumn + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + // HEADER + NBox { + Layout.fillWidth: true + implicitHeight: header.implicitHeight + Style.margin2M + + ColumnLayout { + id: header + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + RowLayout { + NIcon { + icon: "settings-audio" + pointSize: Style.fontSizeXXL + color: Color.mPrimary + } + + NText { + text: I18n.tr("panels.audio.title") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + root.close(); + } + } + } + + NTabBar { + id: tabBar + Layout.fillWidth: true + margins: Style.marginS + currentIndex: panelContent.currentTabIndex + distributeEvenly: true + onCurrentIndexChanged: panelContent.currentTabIndex = currentIndex + + NTabButton { + text: I18n.tr("common.volumes") + tabIndex: 0 + checked: tabBar.currentIndex === 0 + } + + NTabButton { + text: I18n.tr("common.devices") + tabIndex: 1 + checked: tabBar.currentIndex === 1 + } + } + } + } + + // Content Stack + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: panelContent.currentTabIndex + + // Applications Tab (Volume) + NScrollView { + id: volumeScrollView + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + contentWidth: availableWidth + reserveScrollbarSpace: false + gradientColor: Color.mSurface + + ColumnLayout { + spacing: Style.marginM + width: volumeScrollView.availableWidth + + // Output Volume + NBox { + Layout.fillWidth: true + Layout.preferredHeight: outputVolumeColumn.implicitHeight + Style.margin2M + + ColumnLayout { + id: outputVolumeColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginM + spacing: Style.marginM + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NText { + text: I18n.tr("common.output") + pointSize: Style.fontSizeM + color: Color.mPrimary + } + + NText { + text: AudioService.sink ? (" - " + (AudioService.sink.description || AudioService.sink.name || "")) : "" + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + elide: Text.ElideRight + Layout.fillWidth: true + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NValueSlider { + id: outputVolumeSlider + Layout.fillWidth: true + from: 0 + to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0 + value: localOutputVolume + stepSize: 0.01 + heightRatio: 0.5 + onMoved: function (value) { + localOutputVolume = value; + } + onPressedChanged: function (pressed) { + localOutputVolumeChanging = pressed; + } + } + + NText { + text: Math.round((panelContent.outputVolumeGuard ? localOutputVolume : AudioService.volume) * 100) + "%" + pointSize: Style.fontSizeM + family: Settings.data.ui.fontFixed + color: Color.mOnSurface + opacity: enabled ? 1.0 : 0.6 + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: 45 * Style.uiScaleRatio + horizontalAlignment: Text.AlignRight + } + + NIconButton { + icon: AudioService.getOutputIcon() + tooltipText: I18n.tr("tooltips.output-muted") + baseSize: Style.baseWidgetSize * 0.7 + onClicked: { + AudioService.suppressOutputOSD(); + AudioService.setOutputMuted(!AudioService.muted); + } + } + } + } + } + + // Input Volume + NBox { + Layout.fillWidth: true + Layout.preferredHeight: inputVolumeColumn.implicitHeight + Style.margin2M + + ColumnLayout { + id: inputVolumeColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginM + spacing: Style.marginM + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NText { + text: I18n.tr("common.input") + pointSize: Style.fontSizeM + color: Color.mPrimary + } + + NText { + text: AudioService.source ? (" - " + (AudioService.source.description || AudioService.source.name || "")) : "" + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + elide: Text.ElideRight + Layout.fillWidth: true + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NValueSlider { + id: inputVolumeSlider + Layout.fillWidth: true + from: 0 + to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0 + value: localInputVolume + stepSize: 0.01 + heightRatio: 0.5 + onMoved: function (value) { + localInputVolume = value; + } + onPressedChanged: function (pressed) { + localInputVolumeChanging = pressed; + } + } + + NText { + text: Math.round((panelContent.inputVolumeGuard ? localInputVolume : AudioService.inputVolume) * 100) + "%" + pointSize: Style.fontSizeM + family: Settings.data.ui.fontFixed + color: Color.mOnSurface + opacity: enabled ? 1.0 : 0.6 + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: 45 * Style.uiScaleRatio + horizontalAlignment: Text.AlignRight + } + + NIconButton { + icon: AudioService.getInputIcon() + tooltipText: I18n.tr("tooltips.input-muted") + baseSize: Style.baseWidgetSize * 0.7 + onClicked: { + AudioService.suppressInputOSD(); + AudioService.setInputMuted(!AudioService.inputMuted); + } + } + } + } + } + + // Bind all app stream nodes to access their audio properties + PwObjectTracker { + id: appStreamsTracker + objects: panelContent.appStreams + } + + Repeater { + model: panelContent.appStreams + + NBox { + id: appBox + required property PwNode modelData + Layout.fillWidth: true + Layout.preferredHeight: appRow.implicitHeight + Style.margin2M + visible: !isCaptureStream + + // Track individual node to ensure properties are bound + PwObjectTracker { + objects: modelData ? [modelData] : [] + } + + property PwNodeAudio nodeAudio: (modelData && modelData.audio) ? modelData.audio : null + property real appVolume: (nodeAudio && nodeAudio.volume !== undefined) ? nodeAudio.volume : 0.0 + property bool appMuted: (nodeAudio && nodeAudio.muted !== undefined) ? nodeAudio.muted : false + + // Check if this is a capture stream (after node is bound) + readonly property bool isCaptureStream: { + if (!modelData || !modelData.properties) + return false; + const props = modelData.properties; + // Exclude capture streams - check for stream.capture.sink property + if (props["stream.capture.sink"] !== undefined) { + return true; + } + const mediaClass = props["media.class"] || ""; + // Exclude Stream/Input (capture) but allow Stream/Output (playback) + if (mediaClass.includes("Capture") || mediaClass === "Stream/Input" || mediaClass === "Stream/Input/Audio") { + return true; + } + const mediaRole = props["media.role"] || ""; + if (mediaRole === "Capture") { + return true; + } + return false; + } + + // Helper function to validate if a fuzzy match is actually related + function isValidMatch(searchTerm, entry) { + if (!entry) + return false; + var search = searchTerm.toLowerCase(); + var id = (entry.id || "").toLowerCase(); + var name = (entry.name || "").toLowerCase(); + var icon = (entry.icon || "").toLowerCase(); + // Match is valid if search term appears in entry or entry appears in search + return id.includes(search) || name.includes(search) || icon.includes(search) || search.includes(id.split('.').pop()) || search.includes(name.replace(/\s+/g, '')); + } + + readonly property string appName: { + if (!modelData) + return "Unknown App"; + + var props = modelData.properties; + var desc = modelData.description || ""; + var name = modelData.name || ""; + + if (!props) { + if (desc) + return desc; + if (name) { + var nameParts = name.split(/[-_]/); + if (nameParts.length > 0 && nameParts[0]) + return nameParts[0].charAt(0).toUpperCase() + nameParts[0].slice(1); + return name; + } + return "Unknown App"; + } + + var binaryName = props["application.process.binary"] || ""; + + // Try binary name first (fixes Electron apps like vesktop) + if (binaryName) { + var binParts = binaryName.split("/"); + if (binParts.length > 0) { + var binName = binParts[binParts.length - 1].toLowerCase(); + var entry = ThemeIcons.findAppEntry(binName); + // Only use entry if it's actually related to binary name + if (entry && entry.name && isValidMatch(binName, entry)) + return entry.name; + } + } + + var computedAppName = props["application.name"] || ""; + var mediaName = props["media.name"] || ""; + var mediaTitle = props["media.title"] || ""; + var appId = props["application.id"] || ""; + + if (appId) { + var entry = ThemeIcons.findAppEntry(appId); + if (entry && entry.name && isValidMatch(appId, entry)) + return entry.name; + if (!computedAppName) { + var parts = appId.split("."); + if (parts.length > 0 && parts[0]) + computedAppName = parts[0].charAt(0).toUpperCase() + parts[0].slice(1); + } + } + + if (!computedAppName && binaryName) { + var binParts = binaryName.split("/"); + if (binParts.length > 0 && binParts[binParts.length - 1]) + computedAppName = binParts[binParts.length - 1].charAt(0).toUpperCase() + binParts[binParts.length - 1].slice(1); + } + + var result = computedAppName || mediaTitle || mediaName || binaryName || desc || name; + + if (!result || result === "" || result === "Unknown App") { + if (name) { + var nameParts = name.split(/[-_]/); + if (nameParts.length > 0 && nameParts[0]) + result = nameParts[0].charAt(0).toUpperCase() + nameParts[0].slice(1); + } + } + + return result || "Unknown App"; + } + + readonly property string appIcon: { + if (!modelData) + return ThemeIcons.iconFromName("application-x-executable", "application-x-executable"); + + var props = modelData.properties; + + if (!props) { + var name = modelData.name || ""; + if (name) { + var nameParts = name.split(/[-_]/); + if (nameParts.length > 0) { + var entry = ThemeIcons.findAppEntry(nameParts[0].toLowerCase()); + if (entry && entry.icon && isValidMatch(nameParts[0].toLowerCase(), entry)) + return ThemeIcons.iconFromName(entry.icon, "application-x-executable"); + } + } + return ThemeIcons.iconFromName("application-x-executable", "application-x-executable"); + } + + var binaryName = props["application.process.binary"] || ""; + if (binaryName) { + var binParts = binaryName.split("/"); + if (binParts.length > 0) { + var binName = binParts[binParts.length - 1].toLowerCase(); + var entry = ThemeIcons.findAppEntry(binName); + if (entry && entry.icon && isValidMatch(binName, entry)) + return ThemeIcons.iconFromName(entry.icon, ""); + } + } + + var iconName = props["application.icon-name"] || ""; + if (iconName && ThemeIcons.iconExists(iconName)) { + var iconPath = ThemeIcons.iconFromName(iconName, ""); + if (iconPath && iconPath !== "") + return iconPath; + } + + var appId = props["application.id"] || ""; + if (appId) { + var entry = ThemeIcons.findAppEntry(appId); + if (entry && entry.icon && isValidMatch(appId, entry)) + return ThemeIcons.iconFromName(entry.icon, ""); + } + + var appName = props["application.name"] || ""; + if (appName) { + var entry = ThemeIcons.findAppEntry(appName.toLowerCase()); + if (entry && entry.icon && isValidMatch(appName.toLowerCase(), entry)) + return ThemeIcons.iconFromName(entry.icon, ""); + } + + var name = modelData.name || ""; + if (name) { + var nameParts = name.split(/[-_]/); + if (nameParts.length > 0) { + var entry = ThemeIcons.findAppEntry(nameParts[0].toLowerCase()); + if (entry && entry.icon && isValidMatch(nameParts[0].toLowerCase(), entry)) + return ThemeIcons.iconFromName(entry.icon, ""); + } + } + + return ThemeIcons.iconFromName("application-x-executable", "application-x-executable"); + } + + RowLayout { + id: appRow + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + // App Icon + IconImage { + id: appIconImage + Layout.preferredWidth: Style.baseWidgetSize + Layout.preferredHeight: Style.baseWidgetSize + source: appBox.appIcon + smooth: true + asynchronous: true + + // Fallback icon if image fails to load + NIcon { + anchors.fill: parent + icon: "apps" + pointSize: Style.fontSizeXL + color: Color.mPrimary + visible: appIconImage.status === Image.Error || appIconImage.status === Image.Null || appBox.appIcon === "" + } + } + + // App Name and Volume Slider + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NText { + text: appBox.appName || "Unknown App" + pointSize: Style.fontSizeM + color: Color.mOnSurface + elide: Text.ElideRight + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NValueSlider { + Layout.fillWidth: true + from: 0 + to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0 + value: (appBox.appVolume !== undefined) ? appBox.appVolume : 0.0 + stepSize: 0.01 + heightRatio: 0.5 + enabled: !!(appBox.nodeAudio && appBox.modelData && appBox.modelData.ready === true) + onMoved: function (value) { + if (appBox.nodeAudio && appBox.modelData && appBox.modelData.ready === true) { + appBox.nodeAudio.volume = value; + var key = AudioService.getAppKey(appBox.modelData); + if (key) + AudioService.setAppStreamVolume(key, value); + } + } + } + + NText { + text: Math.round((appBox.appVolume !== undefined ? appBox.appVolume : 0.0) * 100) + "%" + pointSize: Style.fontSizeM + family: Settings.data.ui.fontFixed + color: Color.mOnSurface + opacity: enabled ? 1.0 : 0.6 + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: 45 * Style.uiScaleRatio + horizontalAlignment: Text.AlignRight + enabled: !!(appBox.nodeAudio && appBox.modelData && appBox.modelData.ready === true) + } + + // Mute Button + NIconButton { + icon: (appBox.appMuted === true) ? "volume-mute" : "volume-high" + tooltipText: (appBox.appMuted === true) ? I18n.tr("tooltips.unmute") : I18n.tr("tooltips.mute") + baseSize: Style.baseWidgetSize * 0.7 + enabled: !!(appBox.nodeAudio && appBox.modelData && appBox.modelData.ready === true) + onClicked: { + if (appBox.nodeAudio && appBox.modelData && appBox.modelData.ready === true) { + var newMuted = !appBox.appMuted; + appBox.nodeAudio.muted = newMuted; + var key = AudioService.getAppKey(appBox.modelData); + if (key) + AudioService.setAppStreamMuted(key, newMuted); + } + } + } + } + } + } + } + } + + // Empty state + NText { + visible: panelContent.appStreams.length === 0 + text: I18n.tr("panels.audio.panel-applications-empty") + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + Layout.topMargin: Style.marginXL + } + } + } + + // Devices Tab + NScrollView { + id: devicesScrollView + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + contentWidth: availableWidth + reserveScrollbarSpace: false + gradientColor: Color.mSurface + + // AudioService Devices + ColumnLayout { + spacing: Style.marginM + width: devicesScrollView.availableWidth + + // ------------------------------- + // Output Devices + ButtonGroup { + id: sinks + } + + NBox { + Layout.fillWidth: true + Layout.preferredHeight: outputColumn.implicitHeight + Style.margin2M + + ColumnLayout { + id: outputColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginM + spacing: Style.marginS + + NText { + text: I18n.tr("panels.audio.devices-output-device-label") + pointSize: Style.fontSizeL + color: Color.mPrimary + } + + Repeater { + model: AudioService.sinks + NRadioButton { + ButtonGroup.group: sinks + required property PwNode modelData + pointSize: Style.fontSizeS + text: modelData.description + checked: AudioService.sink?.id === modelData.id + onClicked: { + AudioService.setAudioSink(modelData); + localOutputVolume = AudioService.volume; + } + Layout.fillWidth: true + } + } + } + } + + // ------------------------------- + // Input Devices + ButtonGroup { + id: sources + } + + NBox { + Layout.fillWidth: true + Layout.preferredHeight: inputColumn.implicitHeight + Style.margin2M + + ColumnLayout { + id: inputColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.margins: Style.marginM + spacing: Style.marginS + + NText { + text: I18n.tr("panels.audio.devices-input-device-label") + pointSize: Style.fontSizeL + color: Color.mPrimary + } + + Repeater { + model: AudioService.sources + NRadioButton { + ButtonGroup.group: sources + required property PwNode modelData + pointSize: Style.fontSizeS + text: modelData.description + checked: AudioService.source?.id === modelData.id + onClicked: AudioService.setAudioSource(modelData) + Layout.fillWidth: true + } + } + } + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Battery/BatteryPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Battery/BatteryPanel.qml new file mode 100644 index 0000000..5bb03a5 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Battery/BatteryPanel.qml @@ -0,0 +1,401 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.UPower +import qs.Commons +import qs.Modules.MainScreen +import qs.Services.Hardware +import qs.Services.Networking +import qs.Services.Power +import qs.Services.UI +import qs.Widgets + +SmartPanel { + id: root + + preferredWidth: Math.round(440 * Style.uiScaleRatio) + preferredHeight: Math.round(460 * Style.uiScaleRatio) + + panelContent: Item { + id: panelContent + + property real contentPreferredHeight: mainLayout.implicitHeight + Style.margin2L + + property var batteryWidgetInstance: BarService.lookupWidget("Battery", screen ? screen.name : null) + readonly property var batteryWidgetSettings: batteryWidgetInstance ? batteryWidgetInstance.widgetSettings : null + readonly property var batteryWidgetMetadata: BarWidgetRegistry.widgetMetadata["Battery"] + readonly property bool powerProfileAvailable: PowerProfileService.available + readonly property var powerProfiles: [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance] + readonly property bool profilesAvailable: PowerProfileService.available + property int profileIndex: profileToIndex(PowerProfileService.profile) + readonly property bool showPowerProfiles: panelID ? panelID.showPowerProfiles : resolveWidgetSetting("showPowerProfiles", false) + readonly property bool showNoctaliaPerformance: panelID ? panelID.showNoctaliaPerformance : resolveWidgetSetting("showNoctaliaPerformance", false) + readonly property bool isLowBattery: BatteryService.isLowBattery + readonly property bool isCriticalBattery: BatteryService.isCriticalBattery + readonly property var primaryDevice: BatteryService.primaryDevice + + function profileToIndex(p) { + return powerProfiles.indexOf(p) ?? 1; + } + + function indexToProfile(idx) { + return powerProfiles[idx] ?? PowerProfile.Balanced; + } + + function setProfileByIndex(idx) { + var prof = indexToProfile(idx); + profileIndex = idx; + PowerProfileService.setProfile(prof); + } + + function resolveWidgetSetting(key, defaultValue) { + if (batteryWidgetSettings && batteryWidgetSettings[key] !== undefined) + return batteryWidgetSettings[key]; + if (batteryWidgetMetadata && batteryWidgetMetadata[key] !== undefined) + return batteryWidgetMetadata[key]; + return defaultValue; + } + + Connections { + target: PowerProfileService + function onProfileChanged() { + panelContent.profileIndex = panelContent.profileToIndex(PowerProfileService.profile); + } + } + + Connections { + target: BarService + function onActiveWidgetsChanged() { + panelContent.batteryWidgetInstance = BarService.lookupWidget("Battery", screen ? screen.name : null); + } + } + + ColumnLayout { + id: mainLayout + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + // HEADER + NBox { + Layout.fillWidth: true + implicitHeight: headerRow.implicitHeight + Style.margin2M + + RowLayout { + id: headerRow + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + NIcon { + pointSize: Style.fontSizeXXL + color: (BatteryService.isCharging(primaryDevice) || BatteryService.isPluggedIn(primaryDevice)) ? Color.mPrimary : (BatteryService.isCriticalBattery(primaryDevice) || BatteryService.isLowBattery(primaryDevice)) ? Color.mError : Color.mOnSurface + icon: BatteryService.getIcon(BatteryService.getPercentage(primaryDevice), BatteryService.isCharging(primaryDevice), BatteryService.isPluggedIn(primaryDevice), BatteryService.isDeviceReady(primaryDevice)) + } + + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + + NText { + text: I18n.tr("common.battery") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + elide: Text.ElideRight + } + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: root.close() + } + } + } + + // Charge level + health/time + NBox { + Layout.fillWidth: true + implicitHeight: chargeLayout.implicitHeight + Style.margin2L + visible: BatteryService.laptopBatteries.length > 0 || BatteryService.bluetoothBatteries.length > 0 + + ColumnLayout { + id: chargeLayout + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginL + + // Laptop batteries section + Repeater { + model: BatteryService.laptopBatteries + delegate: ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + + RowLayout { + Item { + id: batteryInfoItem + implicitWidth: batteryInfoRow.implicitWidth + implicitHeight: batteryInfoRow.implicitHeight + + RowLayout { + id: batteryInfoRow + anchors.fill: parent + + NIcon { + icon: BatteryService.getIcon(BatteryService.getPercentage(modelData), BatteryService.isCharging(modelData), BatteryService.isPluggedIn(modelData), BatteryService.isDeviceReady(modelData)) + color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface + } + + NText { + readonly property string dName: BatteryService.getDeviceName(modelData) + text: dName ? dName : I18n.tr("common.battery") + color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface + pointSize: Style.fontSizeS + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + if (modelData.healthSupported) { + TooltipService.show(batteryInfoItem, `${I18n.tr("battery.battery-health")}: ${Math.round(modelData.healthPercentage)}%`); + } + } + onExited: TooltipService.hide(batteryInfoItem) + } + } + + Item { + Layout.fillWidth: true + } + + NText { + text: BatteryService.getTimeRemainingText(modelData) + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + Rectangle { + Layout.fillWidth: true + height: Math.round(8 * Style.uiScaleRatio) + radius: Math.min(Style.radiusL, height / 2) + color: Color.mSurface + + Rectangle { + anchors.verticalCenter: parent.verticalCenter + height: parent.height + radius: parent.radius + width: { + var p = BatteryService.getPercentage(modelData); + var ratio = Math.max(0, Math.min(1, p / 100)); + return parent.width * ratio; + } + color: Color.mPrimary + } + } + + NText { + Layout.preferredWidth: 40 * Style.uiScaleRatio + horizontalAlignment: Text.AlignRight + text: `${BatteryService.getPercentage(modelData)}%` + color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface + pointSize: Style.fontSizeS + font.weight: Style.fontWeightBold + } + } + } + } + } + } + + NDivider { + Layout.fillWidth: true + visible: BatteryService.laptopBatteries.length > 0 && BatteryService.bluetoothBatteries.length > 0 + } + + // Other devices (Bluetooth) section + Repeater { + model: BatteryService.bluetoothBatteries + delegate: ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NIcon { + icon: BluetoothService.getDeviceIcon(modelData) + color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface + } + + NText { + readonly property string dName: BatteryService.getDeviceName(modelData) + text: dName ? dName : I18n.tr("common.bluetooth") + color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface + pointSize: Style.fontSizeS + } + } + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + Rectangle { + Layout.fillWidth: true + height: Math.round(8 * Style.uiScaleRatio) + radius: Math.min(Style.radiusL, height / 2) + color: Color.mSurface + + Rectangle { + anchors.verticalCenter: parent.verticalCenter + height: parent.height + radius: parent.radius + width: { + var p = BatteryService.getPercentage(modelData); + var ratio = Math.max(0, Math.min(1, p / 100)); + return parent.width * ratio; + } + color: Color.mPrimary + } + } + + NText { + Layout.preferredWidth: 40 * Style.uiScaleRatio + horizontalAlignment: Text.AlignRight + text: `${BatteryService.getPercentage(modelData)}%` + color: (BatteryService.isCharging(modelData) || BatteryService.isPluggedIn(modelData)) ? Color.mPrimary : (BatteryService.isCriticalBattery(modelData) || BatteryService.isLowBattery(modelData)) ? Color.mError : Color.mOnSurface + pointSize: Style.fontSizeS + font.weight: Style.fontWeightBold + } + } + } + } + } + } + + NBox { + Layout.fillWidth: true + height: controlsLayout.implicitHeight + Style.margin2L + visible: showPowerProfiles || showNoctaliaPerformance + + ColumnLayout { + id: controlsLayout + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + ColumnLayout { + visible: powerProfileAvailable && showPowerProfiles + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NText { + text: I18n.tr("battery.power-profile") + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NText { + text: PowerProfileService.getName(profileIndex) + color: Color.mOnSurfaceVariant + } + } + + NValueSlider { + Layout.fillWidth: true + from: 0 + to: 2 + stepSize: 1 + snapAlways: true + heightRatio: 0.5 + value: profileIndex + enabled: profilesAvailable + onPressedChanged: (pressed, v) => { + if (!pressed) { + setProfileByIndex(v); + } + } + onMoved: v => { + profileIndex = v; + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NIcon { + icon: "powersaver" + pointSize: Style.fontSizeS + color: PowerProfileService.getIcon() === "powersaver" ? Color.mPrimary : Color.mOnSurfaceVariant + } + + NIcon { + icon: "balanced" + pointSize: Style.fontSizeS + color: PowerProfileService.getIcon() === "balanced" ? Color.mPrimary : Color.mOnSurfaceVariant + Layout.fillWidth: true + } + + NIcon { + icon: "performance" + pointSize: Style.fontSizeS + color: PowerProfileService.getIcon() === "performance" ? Color.mPrimary : Color.mOnSurfaceVariant + } + } + } + + NDivider { + Layout.fillWidth: true + visible: showPowerProfiles && PowerProfileService.available && showNoctaliaPerformance + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + visible: showNoctaliaPerformance + + NText { + text: I18n.tr("toast.noctalia-performance.label") + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NIcon { + icon: PowerProfileService.noctaliaPerformanceMode ? "rocket" : "rocket-off" + pointSize: Style.fontSizeL + color: PowerProfileService.noctaliaPerformanceMode ? Color.mPrimary : Color.mOnSurfaceVariant + } + + NToggle { + checked: PowerProfileService.noctaliaPerformanceMode + onToggled: checked => PowerProfileService.noctaliaPerformanceMode = checked + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Bluetooth/BluetoothPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Bluetooth/BluetoothPanel.qml new file mode 100644 index 0000000..90a59ee --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Bluetooth/BluetoothPanel.qml @@ -0,0 +1,208 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Bluetooth +import "../Settings/Tabs/Connections" as BluetoothPrefs +import qs.Commons +import qs.Modules.MainScreen +import qs.Modules.Panels.Settings +import qs.Services.Hardware +import qs.Services.Networking +import qs.Services.UI +import qs.Widgets + +SmartPanel { + id: root + + preferredWidth: Math.round(440 * Style.uiScaleRatio) + preferredHeight: Math.round(500 * Style.uiScaleRatio) + + panelContent: Rectangle { + id: panelContent + color: "transparent" + + property real contentPreferredHeight: Math.min(root.preferredHeight, mainColumn.implicitHeight + Style.margin2L) + + ColumnLayout { + id: mainColumn + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + // Header + NBox { + Layout.fillWidth: true + Layout.preferredHeight: headerRow.implicitHeight + Style.margin2M + + RowLayout { + id: headerRow + anchors.fill: parent + anchors.margins: Style.marginM + + NIcon { + icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off" + pointSize: Style.fontSizeXXL + color: BluetoothService.enabled ? Color.mPrimary : Color.mOnSurfaceVariant + } + + NLabel { + label: I18n.tr("common.bluetooth") + Layout.fillWidth: true + } + + NToggle { + id: bluetoothSwitch + checked: BluetoothService.enabled + enabled: !NetworkService.airplaneModeEnabled && BluetoothService.bluetoothAvailable + onToggled: checked => BluetoothService.setBluetoothEnabled(checked) + baseSize: Style.baseWidgetSize * 0.65 + } + + NIconButton { + icon: Settings.data.network.bluetoothAutoConnect ? "bluetooth-connected" : "bluetooth" + tooltipText: Settings.data.network.bluetoothAutoConnect ? I18n.tr("tooltips.bluetooth-auto-connect-on") : I18n.tr("tooltips.bluetooth-auto-connect-off") + colorFg: Settings.data.network.bluetoothAutoConnect ? Color.mPrimary : Color.mOnSurfaceVariant + baseSize: Style.baseWidgetSize * 0.8 + onClicked: Settings.data.network.bluetoothAutoConnect = !Settings.data.network.bluetoothAutoConnect + } + + NIconButton { + icon: "settings" + tooltipText: I18n.tr("tooltips.open-settings") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 1, screen) + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + root.close(); + } + } + } + } + + NScrollView { + id: bluetoothScrollView + Layout.fillWidth: true + Layout.fillHeight: true + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + reserveScrollbarSpace: false + gradientColor: Color.mSurface + + ColumnLayout { + id: devicesList + width: bluetoothScrollView.availableWidth + spacing: Style.marginM + + // Adapter not available of disabled + NBox { + id: disabledBox + visible: !BluetoothService.enabled + Layout.fillWidth: true + Layout.preferredHeight: disabledColumn.implicitHeight + Style.margin2M + + ColumnLayout { + id: disabledColumn + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginL + + Item { + Layout.fillHeight: true + } + + NIcon { + icon: "bluetooth-off" + pointSize: 48 + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: I18n.tr("bluetooth.panel.disabled") + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: I18n.tr("bluetooth.panel.enable-message") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + Item { + Layout.fillHeight: true + } + } + } + + // Empty state when no paired devices + NBox { + id: emptyBox + visible: { + if (!BluetoothService.enabled || !BluetoothService.devices) + return false; + // Pulling pairedDevices count from the source component + return (btSource.pairedDevices.length === 0 && btSource.connectedDevices.length === 0); + } + Layout.fillWidth: true + Layout.preferredHeight: emptyColumn.implicitHeight + Style.margin2M + + ColumnLayout { + id: emptyColumn + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginL + + Item { + Layout.fillHeight: true + } + + NIcon { + icon: "bluetooth" + pointSize: 48 + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: I18n.tr("bluetooth.panel.no-devices") + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NButton { + text: I18n.tr("common.settings") + icon: "settings" + Layout.alignment: Qt.AlignHCenter + onClicked: SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 1, screen) + } + + Item { + Layout.fillHeight: true + } + } + } + + // Pull connected/paired lists from BluetoothSubTab + BluetoothPrefs.BluetoothSubTab { + id: btSource + Layout.fillWidth: true + showOnlyLists: true + visible: !disabledBox.visible && !emptyBox.visible + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Brightness/BrightnessPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Brightness/BrightnessPanel.qml new file mode 100644 index 0000000..4093345 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Brightness/BrightnessPanel.qml @@ -0,0 +1,285 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Modules.MainScreen +import qs.Services.Compositor +import qs.Services.Hardware +import qs.Services.UI +import qs.Widgets + +SmartPanel { + id: root + + preferredWidth: Math.round(440 * Style.uiScaleRatio) + preferredHeight: Math.round(420 * Style.uiScaleRatio) + + panelContent: Item { + id: panelContent + property real contentPreferredHeight: mainColumn.implicitHeight + Style.margin2L + + property var brightnessWidgetInstance: BarService.lookupWidget("Brightness", screen ? screen.name : null) + readonly property var brightnessWidgetSettings: brightnessWidgetInstance ? brightnessWidgetInstance.widgetSettings : null + readonly property var brightnessWidgetMetadata: BarWidgetRegistry.widgetMetadata["Brightness"] + + function resolveWidgetSetting(key, defaultValue) { + if (brightnessWidgetSettings && brightnessWidgetSettings[key] !== undefined) + return brightnessWidgetSettings[key]; + if (brightnessWidgetMetadata && brightnessWidgetMetadata[key] !== undefined) + return brightnessWidgetMetadata[key]; + return defaultValue; + } + + Connections { + target: BarService + function onActiveWidgetsChanged() { + panelContent.brightnessWidgetInstance = BarService.lookupWidget("Brightness", screen ? screen.name : null); + } + } + + property real globalBrightness: 0 + property bool globalBrightnessChanging: false + property int globalBrightnessCapableMonitors: 0 + + function getIcon(brightness) { + return brightness <= 0.5 ? "brightness-low" : "brightness-high"; + } + + function getControllableMonitors() { + var monitors = BrightnessService.monitors || []; + return monitors.filter(m => m && m.brightnessControlAvailable); + } + + function updateGlobalBrightness() { + var monitors = getControllableMonitors(); + panelContent.globalBrightnessCapableMonitors = monitors.length; + + if (panelContent.globalBrightnessChanging) + return; + + if (monitors.length === 0) { + panelContent.globalBrightness = 0; + return; + } + + var total = 0; + monitors.forEach(m => { + var brightnessValue = isNaN(m.brightness) ? 0 : m.brightness; + total += brightnessValue; + }); + panelContent.globalBrightness = total / monitors.length; + } + + function applyGlobalBrightness(value) { + var monitors = BrightnessService.monitors || []; + monitors.forEach(m => { + if (m && m.brightnessControlAvailable) { + m.setBrightness(value); + } + }); + } + + Component.onCompleted: updateGlobalBrightness() + + Connections { + target: BrightnessService + function onMonitorBrightnessChanged(monitor, newBrightness) { + panelContent.updateGlobalBrightness(); + } + function onMonitorsChanged() { + panelContent.updateGlobalBrightness(); + } + function onDdcMonitorsChanged() { + panelContent.updateGlobalBrightness(); + } + } + + ColumnLayout { + id: mainColumn + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + // HEADER + NBox { + Layout.fillWidth: true + implicitHeight: headerRow.implicitHeight + Style.margin2M + + RowLayout { + id: headerRow + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + NIcon { + icon: "settings-display" + pointSize: Style.fontSizeXXL + color: Color.mPrimary + } + + NText { + text: I18n.tr("panels.display.title") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + root.close(); + } + } + } + } + + NScrollView { + id: brightnessScrollView + Layout.fillWidth: true + Layout.fillHeight: true + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + contentWidth: availableWidth + reserveScrollbarSpace: false + gradientColor: Color.mSurface + + // AudioService Devices + ColumnLayout { + spacing: Style.marginM + width: brightnessScrollView.availableWidth + + NBox { + Layout.fillWidth: true + visible: panelContent.globalBrightnessCapableMonitors > 1 && panelContent.resolveWidgetSetting("applyToAllMonitors", false) + implicitHeight: globalBrightnessContent.implicitHeight + (Style.marginXL) + + ColumnLayout { + id: globalBrightnessContent + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginM + spacing: Style.marginS + + NLabel { + label: I18n.tr("panels.display.monitors-global-brightness-label") + description: I18n.tr("panels.display.monitors-global-brightness-description") + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NIcon { + icon: panelContent.getIcon(panelContent.globalBrightness) + pointSize: Style.fontSizeXL + color: Color.mOnSurface + } + + NValueSlider { + id: globalBrightnessSlider + from: 0 + to: 1 + value: panelContent.globalBrightness + stepSize: 0.01 + enabled: panelContent.globalBrightnessCapableMonitors > 0 + onMoved: value => { + panelContent.globalBrightness = value; + panelContent.applyGlobalBrightness(value); + } + onPressedChanged: (pressed, value) => { + panelContent.globalBrightnessChanging = pressed; + panelContent.globalBrightness = value; + panelContent.applyGlobalBrightness(value); + } + Layout.fillWidth: true + text: "" + } + + NText { + text: panelContent.globalBrightnessCapableMonitors > 0 ? Math.round(panelContent.globalBrightness * 100) + "%" : "N/A" + Layout.preferredWidth: 55 + horizontalAlignment: Text.AlignRight + Layout.alignment: Qt.AlignVCenter + } + } + } + } + + Repeater { + model: Quickshell.screens || [] + delegate: NBox { + Layout.fillWidth: true + Layout.preferredHeight: outputColumn.implicitHeight + Style.margin2M + + property var brightnessMonitor: BrightnessService.getMonitorForScreen(modelData) + readonly property real compositorScale: { + const info = CompositorService.displayScales[modelData.name]; + return (info && info.scale) ? info.scale : 1.0; + } + + ColumnLayout { + id: outputColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginM + spacing: Style.marginS + + NLabel { + label: modelData.name || "Unknown" + labelColor: Color.mPrimary + description: { + I18n.tr("system.monitor-description", { + "model": modelData.model, + "width": modelData.width * compositorScale, + "height": modelData.height * compositorScale, + "scale": compositorScale + }); + } + } + + RowLayout { + + Layout.fillWidth: true + spacing: Style.marginS + NIcon { + icon: getIcon(brightnessMonitor ? brightnessMonitor.brightness : 0) + pointSize: Style.fontSizeXL + color: Color.mOnSurface + } + + NValueSlider { + id: brightnessSlider + from: 0 + to: 1 + value: brightnessMonitor ? brightnessMonitor.brightness : 0.5 + stepSize: 0.01 + enabled: brightnessMonitor ? brightnessMonitor.brightnessControlAvailable : false + onMoved: value => { + if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable) { + brightnessMonitor.setBrightness(value); + } + } + onPressedChanged: (pressed, value) => { + if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable) { + brightnessMonitor.setBrightness(value); + } + } + Layout.fillWidth: true + text: brightnessMonitor ? Math.round(brightnessSlider.value * 100) + "%" : "N/A" + } + } + } + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Changelog/ChangelogPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Changelog/ChangelogPanel.qml new file mode 100644 index 0000000..48337d1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Changelog/ChangelogPanel.qml @@ -0,0 +1,202 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.MainScreen +import qs.Services.Noctalia +import qs.Services.UI +import qs.Widgets + +SmartPanel { + id: root + + preferredWidth: Math.round(820 * Style.uiScaleRatio) + preferredHeight: Math.round(620 * Style.uiScaleRatio) + panelAnchorHorizontalCenter: true + panelAnchorVerticalCenter: true + + panelContent: Rectangle { + id: panelContent + color: Color.mSurfaceVariant + radius: Style.radiusM + border.color: Color.mOutline + border.width: Style.borderS + + readonly property string currentVersion: UpdateService.currentVersion + readonly property string previousVersion: UpdateService.previousVersion + readonly property bool hasPreviousVersion: previousVersion && previousVersion.length > 0 + readonly property string releaseContent: UpdateService.releaseContent || "" + + function colorizeHeaders(text) { + if (!text) + return ""; + const color = Color.mPrimary; + return text.replace(/^# (.*)$/gm, `

$1


`).replace(/^## (.*)$/gm, `

$1


`); + } + readonly property string subtitleText: hasPreviousVersion ? I18n.tr("changelog.panel.subtitle-updated", { + "previousVersion": previousVersion + }) : I18n.tr("changelog.panel.subtitle-fresh") + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NIcon { + icon: "sparkles" + color: Color.mPrimary + pointSize: Style.fontSizeXXL + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NText { + text: I18n.tr("changelog.panel.title", { + "version": currentVersion || UpdateService.currentVersion + }) + pointSize: Style.fontSizeXL + font.weight: Style.fontWeightBold + color: Color.mPrimary + wrapMode: Text.WordWrap + } + + NText { + text: subtitleText + color: Color.mOnSurface + opacity: Style.opacityMedium + wrapMode: Text.WordWrap + } + } + + Item { + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + onClicked: root.close() + Layout.alignment: Qt.AlignTop | Qt.AlignRight + Layout.preferredHeight: Style.baseWidgetSize + Layout.preferredWidth: Style.baseWidgetSize + } + } + + Rectangle { + clip: true + Layout.fillWidth: true + color: Qt.alpha(Color.mPrimary, 0.08) + radius: Style.radiusS + border.color: Color.mPrimary + border.width: Style.borderS + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + NText { + text: hasPreviousVersion ? previousVersion : I18n.tr("changelog.panel.version-new-user") + font.weight: Style.fontWeightSemiBold + color: Color.mPrimary + } + + NIcon { + icon: "arrow-right" + color: Color.mPrimary + } + + NText { + text: currentVersion || UpdateService.currentVersion + font.weight: Style.fontWeightSemiBold + color: Color.mPrimary + } + } + } + + NDivider { + Layout.fillWidth: true + } + + NScrollView { + id: changelogScrollView + Layout.fillWidth: true + Layout.fillHeight: true + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + padding: 0 + + ColumnLayout { + width: changelogScrollView.availableWidth + spacing: Style.marginM + + NText { + visible: UpdateService.fetchError !== "" + text: UpdateService.fetchError + color: Color.mError + wrapMode: Text.WordWrap + } + + NText { + Layout.fillWidth: true + visible: releaseContent.length > 0 + text: panelContent.colorizeHeaders(releaseContent) + wrapMode: Text.WordWrap + elide: Text.ElideNone + textFormat: Text.MarkdownText + color: Color.mOnSurface + } + + NText { + visible: releaseContent.length === 0 + text: I18n.tr("changelog.panel.empty") + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NButton { + Layout.fillWidth: true + icon: "brand-discord" + text: I18n.tr("changelog.panel.buttons-discord") + outlined: true + onClicked: UpdateService.openDiscord() + } + + NButton { + Layout.fillWidth: true + visible: UpdateService.feedbackUrl !== "" + icon: "forms" + text: I18n.tr("changelog.panel.buttons-feedback") + outlined: true + onClicked: UpdateService.openFeedbackForm() + } + + NButton { + Layout.fillWidth: true + icon: "check" + text: I18n.tr("changelog.panel.buttons-dismiss") + onClicked: root.close() + } + } + } + } + + onClosed: { + if (UpdateService && UpdateService.changelogCurrentVersion) { + UpdateService.markChangelogSeen(UpdateService.changelogCurrentVersion); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Clock/ClockPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Clock/ClockPanel.qml new file mode 100644 index 0000000..5ab12db --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Clock/ClockPanel.qml @@ -0,0 +1,76 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Modules.Cards +import qs.Modules.MainScreen +import qs.Services.Location +import qs.Services.UI +import qs.Widgets + +SmartPanel { + id: root + + panelContent: Item { + id: panelContent + anchors.fill: parent + + readonly property real contentPreferredWidth: Math.round((Settings.data.location.showWeekNumberInCalendar ? 440 : 420) * Style.uiScaleRatio) + readonly property real contentPreferredHeight: content.implicitHeight + Style.margin2L + + ColumnLayout { + id: content + x: Style.marginL + y: Style.marginL + width: parent.width - Style.margin2L + spacing: Style.marginL + + // All clock panel cards + Repeater { + model: Settings.data.calendar.cards + Loader { + active: modelData.enabled && (modelData.id !== "weather-card" || Settings.data.location.weatherEnabled) + visible: active + Layout.fillWidth: true + sourceComponent: { + switch (modelData.id) { + case "calendar-header-card": + return calendarHeaderCard; + case "calendar-month-card": + return calendarMonthCard; + case "weather-card": + return weatherCard; + default: + return null; + } + } + } + } + } + } + + Component { + id: calendarHeaderCard + CalendarHeaderCard { + Layout.fillWidth: true + } + } + + Component { + id: calendarMonthCard + CalendarMonthCard { + Layout.fillWidth: true + } + } + + Component { + id: weatherCard + WeatherCard { + Layout.fillWidth: true + forecastDays: 5 + showLocation: false + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/ControlCenterPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/ControlCenterPanel.qml new file mode 100644 index 0000000..e652e67 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/ControlCenterPanel.qml @@ -0,0 +1,191 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.Cards +import qs.Modules.MainScreen +import qs.Services.Media +import qs.Services.UI +import qs.Widgets + +SmartPanel { + id: root + + // Positioning + readonly property string controlCenterPosition: Settings.data.controlCenter.position + + // Check if there's a bar on this screen + readonly property bool hasBarOnScreen: { + var monitors = Settings.data.bar.monitors || []; + return monitors.length === 0 || monitors.includes(screen?.name); + } + + // When position is "close_to_bar_button" but there's no bar, fall back to center + readonly property bool shouldCenter: controlCenterPosition === "close_to_bar_button" && !hasBarOnScreen + + panelAnchorHorizontalCenter: shouldCenter || (controlCenterPosition !== "close_to_bar_button" && (controlCenterPosition.endsWith("_center") || controlCenterPosition === "center")) + panelAnchorVerticalCenter: shouldCenter || controlCenterPosition === "center" + panelAnchorLeft: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_left") + panelAnchorRight: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.endsWith("_right") + panelAnchorBottom: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("bottom_") + panelAnchorTop: !shouldCenter && controlCenterPosition !== "close_to_bar_button" && controlCenterPosition.startsWith("top_") + + preferredWidth: Math.round(440 * Style.uiScaleRatio) + preferredHeight: { + var height = 0; + var count = 0; + for (var i = 0; i < Settings.data.controlCenter.cards.length; i++) { + const card = Settings.data.controlCenter.cards[i]; + if (!card.enabled) + continue; + const contributes = (card.id !== "weather-card" || Settings.data.location.weatherEnabled); + if (!contributes) + continue; + count++; + switch (card.id) { + case "profile-card": + height += profileHeight; + break; + case "shortcuts-card": + height += shortcutsHeight; + break; + case "audio-card": + height += audioHeight; + break; + case "brightness-card": + height += brightnessHeight; + break; + case "weather-card": + height += weatherHeight; + break; + case "media-sysmon-card": + height += mediaSysMonHeight; + break; + default: + break; + } + } + return height + (count + 1) * Style.marginL; + } + + readonly property int profileHeight: Math.round(64 * Style.uiScaleRatio) + readonly property int shortcutsHeight: Math.round(52 * Style.uiScaleRatio) + readonly property int audioHeight: Math.round(60 * Style.uiScaleRatio) + readonly property int brightnessHeight: Math.round(60 * Style.uiScaleRatio) + readonly property int mediaSysMonHeight: Math.round(260 * Style.uiScaleRatio) + + // We keep a dynamic weather height due to a more complex layout and font scaling + property int weatherHeight: Math.round(210 * Style.uiScaleRatio) + + onOpened: { + MediaService.autoSwitchingPaused = true; + } + + onClosed: { + MediaService.autoSwitchingPaused = false; + } + + panelContent: Item { + id: panelContent + + ColumnLayout { + id: layout + x: Style.marginL + y: Style.marginL + width: parent.width - Style.margin2L + spacing: Style.marginL + + Repeater { + model: Settings.data.controlCenter.cards + Loader { + active: modelData.enabled && (modelData.id !== "weather-card" || Settings.data.location.weatherEnabled) + visible: active + Layout.fillWidth: true + Layout.preferredHeight: { + switch (modelData.id) { + case "profile-card": + return profileHeight; + case "shortcuts-card": + return shortcutsHeight; + case "audio-card": + return audioHeight; + case "brightness-card": + return brightnessHeight; + case "weather-card": + return weatherHeight; + case "media-sysmon-card": + return mediaSysMonHeight; + default: + return 0; + } + } + sourceComponent: { + switch (modelData.id) { + case "profile-card": + return profileCard; + case "shortcuts-card": + return shortcutsCard; + case "audio-card": + return audioCard; + case "brightness-card": + return brightnessCard; + case "weather-card": + return weatherCard; + case "media-sysmon-card": + return mediaSysMonCard; + } + } + } + } + } + + Component { + id: profileCard + ProfileCard {} + } + + Component { + id: shortcutsCard + ShortcutsCard {} + } + + Component { + id: audioCard + AudioCard {} + } + + Component { + id: brightnessCard + BrightnessCard {} + } + + Component { + id: weatherCard + WeatherCard { + Component.onCompleted: { + root.weatherHeight = this.height; + } + } + } + + Component { + id: mediaSysMonCard + RowLayout { + spacing: Style.marginL + + // Media card + MediaCard { + Layout.fillWidth: true + Layout.fillHeight: true + } + + // System monitors combined in one card + SystemMonitorCard { + Layout.preferredWidth: Math.round(Style.baseWidgetSize * 2.625) + Layout.fillHeight: true + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/ControlCenterWidgetLoader.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/ControlCenterWidgetLoader.qml new file mode 100644 index 0000000..28f77d8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/ControlCenterWidgetLoader.qml @@ -0,0 +1,114 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.Noctalia +import qs.Services.UI + +Item { + id: root + + required property string widgetId + required property var widgetScreen + required property var widgetProps + + property string section: widgetProps && widgetProps.section || "" + property int sectionIndex: widgetProps && widgetProps.sectionWidgetIndex || 0 + + // Don't reserve space unless the loaded widget is really visible + implicitWidth: getImplicitSize(loader.item, "implicitWidth") + implicitHeight: getImplicitSize(loader.item, "implicitHeight") + + function getImplicitSize(item, prop) { + return (item && item.visible) ? item[prop] : 0; + } + + readonly property bool _isPlugin: ControlCenterWidgetRegistry.isPluginWidget(widgetId) + + function _loadPluginWidget() { + var comp = ControlCenterWidgetRegistry.getWidget(widgetId); + if (!comp) + return; + var pluginId = widgetId.substring(7); // Remove "plugin:" prefix + var api = PluginService.getPluginAPI(pluginId); + loader.setSource(comp.url, api ? { + "pluginApi": api + } : {}); + } + + Loader { + id: loader + anchors.fill: parent + asynchronous: false + + // Core widgets use sourceComponent; plugin widgets use setSource() + // so pluginApi is available from the first binding evaluation. + Component.onCompleted: { + if (root._isPlugin) { + root._loadPluginWidget(); + } else { + sourceComponent = Qt.binding(function () { + return ControlCenterWidgetRegistry.getWidget(widgetId); + }); + } + } + + onLoaded: { + if (!item) + return; + + // Apply properties to loaded widget + for (var prop in widgetProps) { + if (item.hasOwnProperty(prop)) { + item[prop] = widgetProps[prop]; + } + } + + // Set screen property + if (item.hasOwnProperty("screen")) { + item.screen = widgetScreen; + } + + // Call custom onLoaded if it exists + if (item.hasOwnProperty("onLoaded")) { + item.onLoaded(); + } + } + + Component.onDestruction: { + // Explicitly clear references + widgetProps = null; + } + } + + // Error handling + Component.onCompleted: { + if (!ControlCenterWidgetRegistry.hasWidget(widgetId)) { + Logger.w("ControlCenterWidgetLoader", "Widget not found in registry:", widgetId); + // Retry briefly in case the registry initializes after this component + retryTimer.start(); + } + } + + // Retry mechanism to cope with early evaluation before registry is ready + Timer { + id: retryTimer + interval: 150 + repeat: true + running: false + property int attempts: 0 + onTriggered: { + attempts += 1; + if (ControlCenterWidgetRegistry.hasWidget(widgetId)) { + loader.sourceComponent = ControlCenterWidgetRegistry.getWidget(widgetId); + stop(); + attempts = 0; + return; + } + if (attempts >= 20) { // ~3s max + stop(); + attempts = 0; + Logger.w("ControlCenterWidgetLoader", "Giving up waiting for widget:", widgetId); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/AirplaneMode.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/AirplaneMode.qml new file mode 100644 index 0000000..d22963b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/AirplaneMode.qml @@ -0,0 +1,18 @@ +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Networking +import qs.Services.UI +import qs.Widgets + +NIconButtonHot { + property ShellScreen screen + + icon: !NetworkService.airplaneModeEnabled ? "plane-off" : "plane" + hot: NetworkService.airplaneModeEnabled + tooltipText: I18n.tr("toast.airplane-mode.title") + onClicked: { + NetworkService.setAirplaneMode(!NetworkService.airplaneModeEnabled); + } + enabled: NetworkService.wifiAvailable && BluetoothService.bluetoothAvailable +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/Bluetooth.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/Bluetooth.qml new file mode 100644 index 0000000..b6ede84 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/Bluetooth.qml @@ -0,0 +1,23 @@ +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Networking +import qs.Services.UI +import qs.Widgets + +NIconButtonHot { + property ShellScreen screen + + icon: !BluetoothService.enabled ? "bluetooth-off" : ((BluetoothService.connectedDevices && BluetoothService.connectedDevices.length > 0) ? "bluetooth-connected" : "bluetooth") + tooltipText: I18n.tr("common.bluetooth") + onClicked: { + var p = PanelService.getPanel("bluetoothPanel", screen); + if (p) + p.toggle(this); + } + onRightClicked: { + if (!NetworkService.airplaneModeEnabled) { + BluetoothService.setBluetoothEnabled(!BluetoothService.enabled); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/CustomButton.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/CustomButton.qml new file mode 100644 index 0000000..f719ae4 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/CustomButton.qml @@ -0,0 +1,178 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Widgets + +Item { + id: root + + property string widgetId: "CustomButton" + property var widgetSettings: null + + property string onClickedCommand: "" + property string onRightClickedCommand: "" + property string onMiddleClickedCommand: "" + property string stateChecksJson: "[]" // Store state checks as JSON string + + property string generalTooltipText: "" + property bool enableOnStateLogic: false + property bool showExecTooltip: true + property bool _hasCustomTooltip: false + + property string _currentIcon: "heart" // Default icon + property bool _isHot: false + property var _parsedStateChecks: [] // Local array for parsed state checks + + property int _currentStateCheckIndex: -1 + property string _activeStateIcon: "" + + implicitWidth: button.implicitWidth + implicitHeight: button.implicitHeight + + Connections { + target: root + + function _updatePropertiesFromSettings() { + if (!widgetSettings) { + return; + } + + onClickedCommand = widgetSettings.onClicked || ""; + onRightClickedCommand = widgetSettings.onRightClicked || ""; + onMiddleClickedCommand = widgetSettings.onMiddleClicked || ""; + stateChecksJson = widgetSettings.stateChecksJson || "[]"; // Populate from widgetSettings + try { + _parsedStateChecks = JSON.parse(stateChecksJson); + } catch (e) { + Logger.e("CustomButton", "Failed to parse stateChecksJson:", e.message); + _parsedStateChecks = []; + } + generalTooltipText = widgetSettings.generalTooltipText || ""; + _hasCustomTooltip = !!(widgetSettings.generalTooltipText && widgetSettings.generalTooltipText.trim() !== ""); + enableOnStateLogic = widgetSettings.enableOnStateLogic || false; + showExecTooltip = widgetSettings.showExecTooltip !== undefined ? widgetSettings.showExecTooltip : true; + + updateState(); + } + + function onWidgetSettingsChanged() { + if (widgetSettings) { + _updatePropertiesFromSettings(); + } + } + } + + Process { + id: stateCheckProcessExecutor + running: false + command: _currentStateCheckIndex !== -1 && _parsedStateChecks.length > _currentStateCheckIndex ? ["sh", "-lc", _parsedStateChecks[_currentStateCheckIndex].command] : [] + onExited: function (exitCode, stdout, stderr) { + var currentCheckItem = _parsedStateChecks[_currentStateCheckIndex]; + var currentCommand = currentCheckItem.command; + if (exitCode === 0) { + // Command succeeded, this is the active state + _isHot = true; + _activeStateIcon = currentCheckItem.icon || widgetSettings.icon || "heart"; + } else { + // Command failed, try next one + _currentStateCheckIndex++; + _checkNextState(); + } + } + } + + Timer { + id: stateUpdateTimer + interval: 200 + running: false + repeat: false + onTriggered: { + if (enableOnStateLogic && _parsedStateChecks.length > 0) { + _currentStateCheckIndex = 0; + _checkNextState(); + } else { + _isHot = false; + _activeStateIcon = widgetSettings.icon || "heart"; + } + } + } + + function _checkNextState() { + if (_currentStateCheckIndex < _parsedStateChecks.length) { + var currentCheckItem = _parsedStateChecks[_currentStateCheckIndex]; + if (currentCheckItem && currentCheckItem.command) { + stateCheckProcessExecutor.running = true; + } else { + _currentStateCheckIndex++; + _checkNextState(); + } + } else { + // All checks failed + _isHot = false; + _activeStateIcon = widgetSettings.icon || "heart"; + } + } + + function updateState() { + if (!enableOnStateLogic || _parsedStateChecks.length === 0) { + _isHot = false; + _activeStateIcon = widgetSettings.icon || "heart"; + return; + } + stateUpdateTimer.restart(); + } + + function _buildTooltipText() { + // Build tooltip based on settings + let tooltip = ""; + + // If user set custom text, always show it + // If no custom text and showExecTooltip is off, show i18n default + if (_hasCustomTooltip) { + tooltip = generalTooltipText; + } else if (!showExecTooltip) { + tooltip = I18n.tr("bar.custom-button.default-tooltip"); + } + + // Add command details if enabled + if (showExecTooltip) { + if (onClickedCommand) { + tooltip += (tooltip ? "\n" : "") + I18n.tr("bar.custom-button.left-click-label") + `: ${onClickedCommand}`; + } + if (onRightClickedCommand) { + tooltip += (tooltip ? "\n" : "") + I18n.tr("bar.custom-button.right-click-label") + `: ${onRightClickedCommand}`; + } + if (onMiddleClickedCommand) { + tooltip += (tooltip ? "\n" : "") + I18n.tr("bar.custom-button.middle-click-label") + `: ${onMiddleClickedCommand}`; + } + } + + return tooltip; + } + + NIconButtonHot { + id: button + icon: _activeStateIcon + hot: _isHot + tooltipText: _buildTooltipText() + onClicked: { + if (onClickedCommand) { + Quickshell.execDetached(["sh", "-lc", onClickedCommand]); + updateState(); + } + } + onRightClicked: { + if (onRightClickedCommand) { + Quickshell.execDetached(["sh", "-lc", onRightClickedCommand]); + updateState(); + } + } + onMiddleClicked: { + if (onMiddleClickedCommand) { + Quickshell.execDetached(["sh", "-lc", onMiddleClickedCommand]); + updateState(); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/DarkMode.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/DarkMode.qml new file mode 100644 index 0000000..8ae4f41 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/DarkMode.qml @@ -0,0 +1,13 @@ +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Power +import qs.Widgets + +NIconButtonHot { + property ShellScreen screen + + icon: "dark-mode" + tooltipText: Settings.data.colorSchemes.darkMode ? I18n.tr("tooltips.switch-to-light-mode") : I18n.tr("tooltips.switch-to-dark-mode") + onClicked: Settings.data.colorSchemes.darkMode = !Settings.data.colorSchemes.darkMode +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/KeepAwake.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/KeepAwake.qml new file mode 100644 index 0000000..9778dcd --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/KeepAwake.qml @@ -0,0 +1,14 @@ +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Power +import qs.Widgets + +NIconButtonHot { + property ShellScreen screen + + icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off" + hot: IdleInhibitorService.isInhibited + tooltipText: I18n.tr("tooltips.keep-awake") + onClicked: IdleInhibitorService.manualToggle() +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/Network.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/Network.qml new file mode 100644 index 0000000..9fe91ee --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/Network.qml @@ -0,0 +1,21 @@ +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Networking +import qs.Services.UI +import qs.Widgets + +NIconButtonHot { + property ShellScreen screen + icon: NetworkService.getIcon() + tooltipText: NetworkService.getStatusText(true) + onClicked: { + var panel = PanelService.getPanel("networkPanel", screen); + panel?.toggle(this); + } + onRightClicked: { + if (!NetworkService.airplaneModeEnabled) { + NetworkService.setWifiEnabled(!NetworkService.wifiEnabled); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/NightLight.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/NightLight.qml new file mode 100644 index 0000000..fc0dc53 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/NightLight.qml @@ -0,0 +1,34 @@ +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.Panels.Settings +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +NIconButtonHot { + property ShellScreen screen + + enabled: ProgramCheckerService.wlsunsetAvailable + icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off" + hot: Settings.data.nightLight.enabled + tooltipText: I18n.tr("common.night-light") + + onClicked: { + if (!Settings.data.nightLight.enabled) { + Settings.data.nightLight.enabled = true; + Settings.data.nightLight.forced = false; + } else if (Settings.data.nightLight.enabled && !Settings.data.nightLight.forced) { + Settings.data.nightLight.forced = true; + } else { + Settings.data.nightLight.enabled = false; + Settings.data.nightLight.forced = false; + } + } + + onRightClicked: { + var settingsPanel = PanelService.getPanel("settingsPanel", screen); + settingsPanel.requestedTab = SettingsPanel.Tab.Display; + settingsPanel.open(); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/NoctaliaPerformance.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/NoctaliaPerformance.qml new file mode 100644 index 0000000..f47dae2 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/NoctaliaPerformance.qml @@ -0,0 +1,14 @@ +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Power +import qs.Widgets + +NIconButtonHot { + property ShellScreen screen + + icon: PowerProfileService.noctaliaPerformanceMode ? "rocket" : "rocket-off" + tooltipText: I18n.tr("tooltips.noctalia-performance-enabled") + hot: PowerProfileService.noctaliaPerformanceMode + onClicked: PowerProfileService.toggleNoctaliaPerformance() +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/Notifications.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/Notifications.qml new file mode 100644 index 0000000..ecefebd --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/Notifications.qml @@ -0,0 +1,16 @@ +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +NIconButtonHot { + property ShellScreen screen + + icon: NotificationService.doNotDisturb ? "bell-off" : "bell" + hot: NotificationService.doNotDisturb + tooltipText: I18n.tr("common.notifications") + onClicked: PanelService.getPanel("notificationHistoryPanel", screen)?.toggle(this) + onRightClicked: NotificationService.doNotDisturb = !NotificationService.doNotDisturb +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/PowerProfile.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/PowerProfile.qml new file mode 100644 index 0000000..42184f0 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/PowerProfile.qml @@ -0,0 +1,19 @@ +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.UPower +import qs.Commons +import qs.Services.Power +import qs.Widgets + +// Performance +NIconButtonHot { + property ShellScreen screen + + readonly property bool hasPP: PowerProfileService.available + + enabled: hasPP + icon: PowerProfileService.getIcon() + hot: !PowerProfileService.isDefault() + tooltipText: I18n.tr("control-center.power-profile.tooltip-action") + onClicked: PowerProfileService.cycleProfile() +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/WallpaperSelector.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/WallpaperSelector.qml new file mode 100644 index 0000000..184d024 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/ControlCenter/Widgets/WallpaperSelector.qml @@ -0,0 +1,15 @@ +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +NIconButtonHot { + property ShellScreen screen + + enabled: Settings.data.wallpaper.enabled + icon: "wallpaper-selector" + tooltipText: I18n.tr("wallpaper.panel.title") + onClicked: PanelService.getPanel("wallpaperPanel", screen)?.toggle() + onRightClicked: WallpaperService.setRandomWallpaper() +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Dock/StaticDockPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Dock/StaticDockPanel.qml new file mode 100644 index 0000000..7e221d2 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Dock/StaticDockPanel.qml @@ -0,0 +1,615 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Modules.Dock +import qs.Modules.MainScreen + +SmartPanel { + id: root + panelBackgroundColor: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity) + + readonly property string dockPosition: Settings.data.dock.position + readonly property bool isVertical: dockPosition === "left" || dockPosition === "right" + readonly property bool hasBar: modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false + readonly property bool barAtSameEdge: hasBar && Settings.getBarPositionForScreen(modelData?.name) === dockPosition + readonly property bool isFramed: Settings.data.bar.barType === "framed" && hasBar + property bool isDockHovered: false + property bool panelHovered: false + readonly property int iconSize: Math.round(12 + 24 * (Settings.data.dock.size ?? 1)) + readonly property int maxWidth: screen ? screen.width * 0.8 : 1000 + readonly property int maxHeight: screen ? screen.height * 0.8 : 1000 + readonly property bool autoHide: false + readonly property int hideDelay: 1000 + readonly property int showDelay: 100 + + // Shared state with dock content + property bool dockHovered: false + property bool anyAppHovered: false + property bool menuHovered: false + property bool hidden: false + property bool peekHovered: false + + // Track the currently open context menu + property var currentContextMenu: null + + // Combined model of running apps and pinned apps + property var dockApps: [] + property var groupCycleIndices: ({}) + + // Track the session order of apps (transient reordering) + property var sessionAppOrder: [] + + // Drag and Drop state for visual feedback + property int dragSourceIndex: -1 + property int dragTargetIndex: -1 + + // Revision counter to force icon re-evaluation + property int iconRevision: 0 + + property alias hideTimer: hideTimer + property alias showTimer: showTimer + + onMenuHoveredChanged: { + if (!menuHovered && !panelHovered) { + hoverCloseTimer.restart(); + } + } + + onAnyAppHoveredChanged: { + if (anyAppHovered) { + hoverCloseTimer.stop(); + } else if (!panelHovered && !menuHovered && !dockHovered && !isDockHovered) { + hoverCloseTimer.restart(); + } + } + + onClosed: { + hoverCloseTimer.stop(); + isDockHovered = false; + } + + onOpened: { + // Don't auto-start close timer here — the peek zone's onExited in Dock.qml + // starts hideTimer which checks panel.isDockHovered, giving the user time + // to move into the panel without it closing prematurely. + } + + panelAnchorTop: dockPosition === "top" + panelAnchorBottom: dockPosition === "bottom" + panelAnchorLeft: dockPosition === "left" + panelAnchorRight: dockPosition === "right" + panelAnchorHorizontalCenter: !isVertical + panelAnchorVerticalCenter: isVertical + + forceAttachToBar: hasBar + exclusiveKeyboard: false + + // when dragging ended but the cursor is outside the dock area, restart the timer + onDragSourceIndexChanged: { + if (dragSourceIndex === -1) { + if (autoHide && !dockHovered && !anyAppHovered && !peekHovered && !menuHovered) { + hideTimer.restart(); + } + } + } + + // Function to close any open context menu + function closeAllContextMenus() { + if (currentContextMenu && currentContextMenu.visible) { + currentContextMenu.hide(); + } + } + + function getAppKey(appData) { + if (!appData) + return null; + + if (Settings.data.dock.groupApps) { + return appData.appId; + } + + // Use stable appId for pinned apps to maintain their slot regardless of running state + if (appData.type === "pinned" || appData.type === "pinned-running") { + return appData.appId; + } + + // prefer toplevel object identity for unpinned running apps to distinguish instances + if (appData.toplevel) + return appData.toplevel; + + // fallback to appId + return appData.appId; + } + + function sortDockApps(apps) { + if (!sessionAppOrder || sessionAppOrder.length === 0) { + return apps; + } + + const sorted = []; + const remaining = [...apps]; + + // Pick apps that are in the session order + for (let i = 0; i < sessionAppOrder.length; i++) { + const key = sessionAppOrder[i]; + + // Pick ALL matching apps (e.g. all instances of a pinned app) + while (true) { + const idx = remaining.findIndex(app => getAppKey(app) === key); + if (idx !== -1) { + sorted.push(remaining[idx]); + remaining.splice(idx, 1); + } else { + break; + } + } + } + + // Append any new/remaining apps + remaining.forEach(app => sorted.push(app)); + + return sorted; + } + + function reorderApps(fromIndex, toIndex) { + if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 || fromIndex >= dockApps.length || toIndex >= dockApps.length) + return; + + const list = [...dockApps]; + const item = list.splice(fromIndex, 1)[0]; + list.splice(toIndex, 0, item); + + dockApps = list; + sessionAppOrder = dockApps.map(getAppKey); + savePinnedOrder(); + } + + function savePinnedOrder() { + const currentPinned = Settings.data.dock.pinnedApps || []; + const newPinned = []; + const seen = new Set(); + + // Extract pinned apps in their current visual order + dockApps.forEach(app => { + if (app.appId && !seen.has(app.appId)) { + const isPinned = currentPinned.some(p => normalizeAppId(p) === normalizeAppId(app.appId)); + + if (isPinned) { + newPinned.push(app.appId); + seen.add(app.appId); + } + } + }); + + // Check if any pinned apps were missed (unlikely if dockApps is correct) + currentPinned.forEach(p => { + if (!seen.has(p)) { + newPinned.push(p); + seen.add(p); + } + }); + + if (JSON.stringify(currentPinned) !== JSON.stringify(newPinned)) { + Settings.data.dock.pinnedApps = newPinned; + } + } + + // Helper function to normalize app IDs for case-insensitive matching + function normalizeAppId(appId) { + if (!appId || typeof appId !== 'string') + return ""; + let id = appId.toLowerCase().trim(); + if (id.endsWith(".desktop")) + id = id.substring(0, id.length - 8); + return id; + } + + // Helper function to check if an app ID matches a pinned app (case-insensitive) + function isAppIdPinned(appId, pinnedApps) { + if (!appId || !pinnedApps || pinnedApps.length === 0) + return false; + const normalizedId = normalizeAppId(appId); + // Direct match + if (pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId)) + return true; + // Resolve via desktop entry lookup (handles StartupWMClass != .desktop filename) + const resolved = resolveToDesktopEntryId(appId); + if (resolved !== appId) { + const normalizedResolved = normalizeAppId(resolved); + return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedResolved); + } + return false; + } + + // Desktop entry ID resolution cache (cleared when DesktopEntries change) + property var _desktopEntryIdCache: ({}) + + // Resolve a toplevel appId to its canonical .desktop entry ID via heuristic lookup. + function resolveToDesktopEntryId(appId) { + if (!appId) + return appId; + if (_desktopEntryIdCache.hasOwnProperty(appId)) + return _desktopEntryIdCache[appId]; + try { + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) { + const entry = DesktopEntries.heuristicLookup(appId); + if (entry && entry.id) { + _desktopEntryIdCache[appId] = entry.id; + return entry.id; + } + } + } catch (e) {} + _desktopEntryIdCache[appId] = appId; + return appId; + } + + // Helper function to get app name from desktop entry + function getAppNameFromDesktopEntry(appId) { + if (!appId) + return appId; + + try { + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) { + const entry = DesktopEntries.heuristicLookup(appId); + if (entry && entry.name) { + return entry.name; + } + } + + if (typeof DesktopEntries !== 'undefined' && DesktopEntries.byId) { + const entry = DesktopEntries.byId(appId); + if (entry && entry.name) { + return entry.name; + } + } + } catch (e) + // Fall through to return original appId + {} + + // Return original appId if we can't find a desktop entry + return appId; + } + + function getToplevelsForEntry(appData) { + if (!appData) + return []; + + if (appData.toplevels && appData.toplevels.length > 0) { + return appData.toplevels.filter(toplevel => toplevel && (!Settings.data.dock.onlySameOutput || !toplevel.screens || toplevel.screens.includes(screen))); + } + + if (!appData.toplevel) + return []; + + if (Settings.data.dock.onlySameOutput && appData.toplevel.screens && !appData.toplevel.screens.includes(screen)) + return []; + + return [appData.toplevel]; + } + + function getPrimaryToplevelForEntry(appData) { + const toplevels = getToplevelsForEntry(appData); + if (toplevels.length === 0) + return null; + + if (ToplevelManager && ToplevelManager.activeToplevel && toplevels.includes(ToplevelManager.activeToplevel)) + return ToplevelManager.activeToplevel; + + return toplevels[0]; + } + + // Build grouped render model without mutating the raw toplevel list. + function buildGroupedDockApps(apps) { + if (!Settings.data.dock.groupApps) { + return apps.map(app => { + const entry = Object.assign({}, app); + entry.toplevels = getToplevelsForEntry(app); + return entry; + }); + } + + const grouped = []; + const groupedById = new Map(); + + apps.forEach(app => { + const appId = app.appId; + const toplevels = getToplevelsForEntry(app); + const existing = groupedById.get(appId); + + if (existing) { + toplevels.forEach(toplevel => { + if (!existing.toplevels.includes(toplevel)) { + existing.toplevels.push(toplevel); + } + }); + if (app.type === "pinned" || app.type === "pinned-running") { + existing.isPinned = true; + } + } else { + const entry = { + "type": app.type, + "appId": appId, + "title": app.title, + "toplevels": toplevels.slice(), + "isPinned": app.type === "pinned" || app.type === "pinned-running" + }; + grouped.push(entry); + groupedById.set(appId, entry); + } + }); + + grouped.forEach(entry => { + entry.toplevel = getPrimaryToplevelForEntry(entry); + if (entry.toplevels.length > 0 && entry.isPinned) { + entry.type = "pinned-running"; + } else if (entry.toplevels.length > 0) { + entry.type = "running"; + } else { + entry.type = "pinned"; + } + if (entry.toplevel && entry.toplevel.title && entry.toplevel.title.trim() !== "") { + entry.title = entry.toplevel.title; + } + }); + + return grouped; + } + + // Function to update the combined dock apps model + function updateDockApps() { + const runningApps = ToplevelManager ? (ToplevelManager.toplevels.values || []) : []; + const pinnedApps = Settings.data.dock.pinnedApps || []; + const combined = []; + const processedToplevels = new Set(); + const processedPinnedAppIds = new Set(); + + // push an app onto combined with the given appType + function pushApp(appType, toplevel, appId, title) { + // Use canonical ID for pinned apps to ensure key stability + const canonicalId = isAppIdPinned(appId, pinnedApps) ? (pinnedApps.find(p => normalizeAppId(p) === normalizeAppId(appId)) || appId) : appId; + + // For running apps, track by toplevel object to allow multiple instances + if (toplevel) { + if (processedToplevels.has(toplevel)) { + return; + } + if (Settings.data.dock.onlySameOutput && toplevel.screens && !toplevel.screens.includes(screen)) { + return; + } + combined.push({ + "type": appType, + "toplevel": toplevel, + "toplevels": toplevel ? [toplevel] : [], + "appId": canonicalId, + "title": title + }); + processedToplevels.add(toplevel); + } else { + // For pinned apps that aren't running, track by appId to avoid duplicates + if (processedPinnedAppIds.has(canonicalId)) { + return; + } + combined.push({ + "type": appType, + "toplevel": toplevel, + "toplevels": [], + "appId": canonicalId, + "title": title + }); + processedPinnedAppIds.add(canonicalId); + } + } + + function pushRunning(first) { + runningApps.forEach(toplevel => { + if (toplevel) { + // Use robust matching to check if pinned + const isPinned = isAppIdPinned(toplevel.appId, pinnedApps); + if (!first && isPinned && processedToplevels.has(toplevel)) { + return; // Already added by pushPinned() + } + pushApp((first && isPinned) ? "pinned-running" : "running", toplevel, toplevel.appId, toplevel.title); + } + }); + } + + function pushPinned() { + pinnedApps.forEach(pinnedAppId => { + // Find all running instances of this pinned app using robust matching + // Also resolve toplevel appId via desktop entry lookup to handle + // StartupWMClass != .desktop filename (e.g. zen -> zen-browser-bin) + const normalizedPinned = normalizeAppId(pinnedAppId); + const matchingToplevels = runningApps.filter(app => { + if (!app) + return false; + if (normalizeAppId(app.appId) === normalizedPinned) + return true; + const resolved = resolveToDesktopEntryId(app.appId); + return resolved !== app.appId && normalizeAppId(resolved) === normalizedPinned; + }); + + if (matchingToplevels.length > 0) { + // Add all running instances as pinned-running + matchingToplevels.forEach(toplevel => { + pushApp("pinned-running", toplevel, pinnedAppId, toplevel.title); + }); + } else { + // App is pinned but not running - add once + pushApp("pinned", null, pinnedAppId, getAppNameFromDesktopEntry(pinnedAppId) || pinnedAppId); + } + }); + } + + // if pinnedStatic then push all pinned and then all remaining running apps + if (Settings.data.dock.pinnedStatic) { + pushPinned(); + pushRunning(false); + + // else add all running apps and then remaining pinned apps + } else { + pushRunning(true); + pushPinned(); + } + + const sortedApps = sortDockApps(combined); + dockApps = buildGroupedDockApps(sortedApps); + const cycleState = root.groupCycleIndices || {}; + const nextCycleState = {}; + dockApps.forEach(app => { + if (app && app.appId && cycleState[app.appId] !== undefined) { + nextCycleState[app.appId] = cycleState[app.appId]; + } + }); + root.groupCycleIndices = nextCycleState; + + // Sync session order if needed + if (!sessionAppOrder || sessionAppOrder.length === 0) { + sessionAppOrder = dockApps.map(getAppKey); + } else { + const currentKeys = new Set(dockApps.map(getAppKey)); + const existingKeys = new Set(); + const newOrder = []; + + // Keep existing keys that are still present + sessionAppOrder.forEach(key => { + if (currentKeys.has(key)) { + newOrder.push(key); + existingKeys.add(key); + } + }); + + // Add new keys at the end + dockApps.forEach(app => { + const key = getAppKey(app); + if (!existingKeys.has(key)) { + newOrder.push(key); + existingKeys.add(key); + } + }); + + if (JSON.stringify(newOrder) !== JSON.stringify(sessionAppOrder)) { + sessionAppOrder = newOrder; + } + } + } + + // Update dock apps when toplevels change + Connections { + target: ToplevelManager ? ToplevelManager.toplevels : null + function onValuesChanged() { + updateDockApps(); + } + } + + // Update dock apps when pinned apps change + Connections { + target: Settings.data.dock + function onPinnedAppsChanged() { + updateDockApps(); + } + function onOnlySameOutputChanged() { + updateDockApps(); + } + function onGroupAppsChanged() { + updateDockApps(); + } + } + + // Initial update when component is ready + Component.onCompleted: { + if (ToplevelManager) { + updateDockApps(); + } + } + + // Refresh icons when DesktopEntries becomes available + Connections { + target: DesktopEntries.applications + function onValuesChanged() { + root.iconRevision++; + root._desktopEntryIdCache = {}; + } + } + + Timer { + id: hideTimer + interval: hideDelay + onTriggered: {} + } + + Timer { + id: showTimer + interval: showDelay + onTriggered: {} + } + + Timer { + id: hoverCloseTimer + interval: hideDelay + onTriggered: { + if (root.dockHovered || root.isDockHovered || root.anyAppHovered || root.menuHovered || (root.currentContextMenu && root.currentContextMenu.visible)) { + restart(); + return; + } + root.isDockHovered = false; + root.close(); + } + } + + panelContent: Item { + id: panelContent + + property bool allowAttach: true + property real frameThickness: isFramed && !barAtSameEdge && !Settings.data.dock.sitOnFrame ? Settings.data.bar.frameThickness : 0 + property real contentPreferredWidth: Math.round(dockContainerWrapper.width) - (isVertical ? frameThickness : 0) + property real contentPreferredHeight: Math.round(dockContainerWrapper.height) - (!isVertical ? frameThickness : 0) + + Item { + id: hoverArea + anchors.fill: dockContainerWrapper + anchors.margins: -frameThickness + + // Detect hover over dock area including frame thickness + HoverHandler { + id: dockHoverArea + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onHoveredChanged: { + root.panelHovered = hovered; + if (hovered) { + root.isDockHovered = true; + hoverCloseTimer.stop(); + } else { + root.isDockHovered = false; + if (root.menuHovered || (root.currentContextMenu && root.currentContextMenu.visible)) { + hoverCloseTimer.stop(); + } else { + hoverCloseTimer.restart(); + } + } + } + } + } + + Item { + id: dockContainerWrapper + readonly property real frameThickness: isFramed ? Settings.data.bar.frameThickness : 0 + width: dockContent.dockContainer.width + height: dockContent.dockContainer.height + anchors.top: root.dockPosition === "bottom" ? parent.top : undefined + anchors.bottom: root.dockPosition === "top" ? parent.bottom : undefined + anchors.left: root.dockPosition === "right" ? parent.left : undefined + anchors.right: root.dockPosition === "left" ? parent.right : undefined + + DockContent { + id: dockContent + anchors.fill: parent + dockRoot: root + extraTop: 0 + extraBottom: 0 + extraLeft: 0 + extraRight: 0 + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/ClipboardPreview.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/ClipboardPreview.qml new file mode 100644 index 0000000..9b7ebcb --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/ClipboardPreview.qml @@ -0,0 +1,195 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.Keyboard +import qs.Widgets + +Item { + id: previewPanel + + property var currentItem: null + property string fullContent: "" + property string imageDataUrl: "" + property bool loadingFullContent: false + property bool isImageContent: false + + implicitHeight: contentArea.implicitHeight + Style.margin2L + + function loadContent() { + if (!currentItem || !currentItem.clipboardId) + return; + + if (isImageContent) { + // For images, check cache first then decode + imageDataUrl = ClipboardService.getImageData(currentItem.clipboardId) || ""; + loadingFullContent = !imageDataUrl; + if (!imageDataUrl && currentItem.mime) { + ClipboardService.decodeToDataUrl(currentItem.clipboardId, currentItem.mime, null); + } + } else { + // For text, check sync cache first + const cached = ClipboardService.getContent(currentItem.clipboardId); + if (cached) { + fullContent = cached; + loadingFullContent = false; + } else { + // Show preview while loading full content + fullContent = currentItem.preview || ""; + loadingFullContent = true; + // Async decode as fallback + var requestedId = currentItem.clipboardId; + ClipboardService.decode(requestedId, function (content) { + if (!previewPanel || !previewPanel.currentItem) { + return; + } + if (previewPanel.currentItem.clipboardId === requestedId) { + var trimmed = content ? content.trim() : ""; + if (trimmed !== "") { + previewPanel.fullContent = trimmed; + } + previewPanel.loadingFullContent = false; + } + }); + } + } + } + + Connections { + target: previewPanel + function onCurrentItemChanged() { + fullContent = ""; + imageDataUrl = ""; + loadingFullContent = false; + isImageContent = currentItem && currentItem.isImage; + + if (currentItem && currentItem.clipboardId) { + loadContent(); + } + } + } + + readonly property int _rev: ClipboardService.revision + on_RevChanged: { + // When cache updates, try to load content if we're still showing loading or preview + if (currentItem && currentItem.clipboardId && !isImageContent && loadingFullContent) { + const cached = ClipboardService.getContent(currentItem.clipboardId); + if (cached) { + fullContent = cached; + loadingFullContent = false; + } + } + } + + Timer { + id: imageUpdateTimer + interval: 200 + running: currentItem && currentItem.isImage && imageDataUrl === "" + repeat: currentItem && currentItem.isImage && imageDataUrl === "" + + onTriggered: { + if (currentItem && currentItem.clipboardId) { + const newData = ClipboardService.getImageData(currentItem.clipboardId) || ""; + if (newData !== imageDataUrl) { + imageDataUrl = newData; + if (newData) { + loadingFullContent = false; + } + } + } + } + } + + Item { + id: contentArea + anchors.fill: parent + anchors.margins: Style.marginS + + BusyIndicator { + anchors.centerIn: parent + running: loadingFullContent + visible: loadingFullContent + width: Style.baseWidgetSize + height: width + } + + // Preview for image entry + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginS + visible: isImageContent && !loadingFullContent && imageDataUrl !== "" + + NImageRounded { + id: previewImage + Layout.fillWidth: true + Layout.fillHeight: true + radius: Style.marginS + imagePath: imageDataUrl + imageFillMode: Image.PreserveAspectFit + } + + NDivider { + Layout.fillWidth: true + Layout.bottomMargin: Style.marginS + } + + NText { + Layout.fillWidth: true + text: { + const meta = ClipboardService.parseImageMeta(currentItem?.preview); + if (meta) + return `${meta.fmt} • ${meta.w}×${meta.h} • ${meta.size}`; + // Fallback to basic info + const format = (currentItem?.mime || "image").split("/")[1]?.toUpperCase() || "Image"; + return `${format} • ${previewImage.implicitWidth}×${previewImage.implicitHeight}`; + } + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + + // Preview for text entry + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginS + visible: !isImageContent && !loadingFullContent + + NScrollView { + id: clipboardScrollView + Layout.fillWidth: true + Layout.fillHeight: true + horizontalPolicy: Settings.data.appLauncher.clipboardWrapText ? ScrollBar.AlwaysOff : ScrollBar.AsNeeded + + NText { + text: fullContent + width: Settings.data.appLauncher.clipboardWrapText ? clipboardScrollView.availableWidth : implicitWidth + wrapMode: Settings.data.appLauncher.clipboardWrapText ? Text.Wrap : Text.NoWrap + textFormat: Text.PlainText + font.pointSize: Style.fontSizeM + font.family: Settings.data.ui.fontFixed + color: Color.mOnSurface + } + } + + NDivider { + Layout.fillWidth: true + Layout.bottomMargin: Style.marginS + } + + NText { + Layout.fillWidth: true + visible: fullContent.length > 0 + text: { + const chars = fullContent.length; + const words = fullContent.split(/\s+/).filter(w => w.length > 0).length; + const lines = fullContent.split('\n').length; + return `${chars} chars, ${words} words, ${lines} lines`; + } + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Helpers/LauncherNavigation.js b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Helpers/LauncherNavigation.js new file mode 100644 index 0000000..00a1c5e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Helpers/LauncherNavigation.js @@ -0,0 +1,134 @@ +function selectNext(selectedIndex, resultsLength) { + if (resultsLength > 0 && selectedIndex < resultsLength - 1) + return selectedIndex + 1; + return selectedIndex; +} + +function selectPrevious(selectedIndex, resultsLength) { + if (resultsLength > 0 && selectedIndex > 0) + return selectedIndex - 1; + return selectedIndex; +} + +function selectNextWrapped(selectedIndex, resultsLength, allowWrap) { + if (resultsLength > 0) { + if (allowWrap) + return (selectedIndex + 1) % resultsLength; + return selectNext(selectedIndex, resultsLength); + } + return selectedIndex; +} + +function selectPreviousWrapped(selectedIndex, resultsLength, allowWrap) { + if (resultsLength > 0) { + if (allowWrap) + return (((selectedIndex - 1) % resultsLength) + resultsLength) % resultsLength; + return selectPrevious(selectedIndex, resultsLength); + } + return selectedIndex; +} + +function selectFirst() { + return 0; +} + +function selectLast(resultsLength) { + return resultsLength > 0 ? resultsLength - 1 : 0; +} + +function selectNextPage(selectedIndex, resultsLength, entryHeight) { + if (resultsLength > 0) { + var page = Math.max(1, Math.floor(600 / entryHeight)); + return Math.min(selectedIndex + page, resultsLength - 1); + } + return selectedIndex; +} + +function selectPreviousPage(selectedIndex, resultsLength, entryHeight) { + if (resultsLength > 0) { + var page = Math.max(1, Math.floor(600 / entryHeight)); + return Math.max(selectedIndex - page, 0); + } + return selectedIndex; +} + +function selectPreviousRow(selectedIndex, resultsLength, gridColumns) { + if (resultsLength <= 0 || gridColumns <= 0) + return selectedIndex; + + var currentRow = Math.floor(selectedIndex / gridColumns); + var currentCol = selectedIndex % gridColumns; + + if (currentRow > 0) { + var targetRow = currentRow - 1; + var itemsInTargetRow = Math.min(gridColumns, resultsLength - targetRow * gridColumns); + if (currentCol < itemsInTargetRow) + return targetRow * gridColumns + currentCol; + return targetRow * gridColumns + itemsInTargetRow - 1; + } + + // Wrap to last row, same column + var totalRows = Math.ceil(resultsLength / gridColumns); + var lastRow = totalRows - 1; + var itemsInLastRow = Math.min(gridColumns, resultsLength - lastRow * gridColumns); + if (currentCol < itemsInLastRow) + return lastRow * gridColumns + currentCol; + return resultsLength - 1; +} + +function selectNextRow(selectedIndex, resultsLength, gridColumns) { + if (resultsLength <= 0 || gridColumns <= 0) + return selectedIndex; + + var currentRow = Math.floor(selectedIndex / gridColumns); + var currentCol = selectedIndex % gridColumns; + var totalRows = Math.ceil(resultsLength / gridColumns); + + if (currentRow < totalRows - 1) { + var targetRow = currentRow + 1; + var targetIndex = targetRow * gridColumns + currentCol; + if (targetIndex < resultsLength) + return targetIndex; + var itemsInTargetRow = resultsLength - targetRow * gridColumns; + if (itemsInTargetRow > 0) + return targetRow * gridColumns + itemsInTargetRow - 1; + return Math.min(currentCol, resultsLength - 1); + } + + // Wrap to first row, same column + return Math.min(currentCol, resultsLength - 1); +} + +function selectPreviousColumn(selectedIndex, resultsLength, gridColumns) { + if (resultsLength <= 0) + return selectedIndex; + + var currentRow = Math.floor(selectedIndex / gridColumns); + var currentCol = selectedIndex % gridColumns; + + if (currentCol > 0) + return currentRow * gridColumns + (currentCol - 1); + if (currentRow > 0) + return (currentRow - 1) * gridColumns + (gridColumns - 1); + + var totalRows = Math.ceil(resultsLength / gridColumns); + var lastRowIndex = (totalRows - 1) * gridColumns + (gridColumns - 1); + return Math.min(lastRowIndex, resultsLength - 1); +} + +function selectNextColumn(selectedIndex, resultsLength, gridColumns) { + if (resultsLength <= 0) + return selectedIndex; + + var currentRow = Math.floor(selectedIndex / gridColumns); + var currentCol = selectedIndex % gridColumns; + var itemsInCurrentRow = Math.min(gridColumns, resultsLength - currentRow * gridColumns); + + if (currentCol < itemsInCurrentRow - 1) + return currentRow * gridColumns + (currentCol + 1); + + var totalRows = Math.ceil(resultsLength / gridColumns); + if (currentRow < totalRows - 1) + return (currentRow + 1) * gridColumns; + return 0; +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Launcher.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Launcher.qml new file mode 100644 index 0000000..6623ce3 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Launcher.qml @@ -0,0 +1,182 @@ +import QtQuick +import QtQuick.Controls +import Quickshell + +import qs.Commons +import qs.Modules.MainScreen +import qs.Services.UI +import qs.Widgets + +SmartPanel { + id: root + + // Disable when overlay mode is enabled (LauncherOverlayWindow handles it) + enabled: !Settings.data.appLauncher.overviewLayer + visible: !Settings.data.appLauncher.overviewLayer + + // Reference to core (set after panelContent loads) + property var launcherCoreRef: null + + // Expose core launcher for external access (e.g., IPC) + readonly property string searchText: launcherCoreRef ? launcherCoreRef.searchText : "" + readonly property int selectedIndex: launcherCoreRef ? launcherCoreRef.selectedIndex : 0 + readonly property var results: launcherCoreRef ? launcherCoreRef.results : [] + readonly property var activeProvider: launcherCoreRef ? launcherCoreRef.activeProvider : null + readonly property var currentProvider: launcherCoreRef ? launcherCoreRef.currentProvider : null + readonly property bool isGridView: launcherCoreRef ? launcherCoreRef.isGridView : false + readonly property int gridColumns: launcherCoreRef ? launcherCoreRef.gridColumns : 5 + + function setSearchText(text) { + if (launcherCoreRef) + launcherCoreRef.setSearchText(text); + } + + // Preview panel support + readonly property bool previewActive: { + if (!launcherCoreRef) + return false; + var provider = launcherCoreRef.activeProvider; + if (!provider || !provider.hasPreview) + return false; + if (!Settings.data.appLauncher.enableClipPreview) + return false; + return selectedIndex >= 0 && results && !!results[selectedIndex]; + } + readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio) + + // Panel sizing + readonly property int listPanelWidth: Math.round(500 * Style.uiScaleRatio) + readonly property int totalBaseWidth: listPanelWidth + Style.margin2L + + preferredWidth: totalBaseWidth + preferredHeight: Math.round(600 * Style.uiScaleRatio) + preferredWidthRatio: 0.25 + preferredHeightRatio: 0.5 + + // Positioning + readonly property string screenBarPosition: Settings.getBarPositionForScreen(screen?.name) + readonly property string panelPosition: { + if (Settings.data.appLauncher.position === "follow_bar") { + if (screenBarPosition === "left" || screenBarPosition === "right") { + return `center_${screenBarPosition}`; + } else { + return `${screenBarPosition}_center`; + } + } else { + return Settings.data.appLauncher.position; + } + } + panelAnchorHorizontalCenter: !root.useButtonPosition && (panelPosition === "center" || panelPosition.endsWith("_center")) + panelAnchorVerticalCenter: !root.useButtonPosition && panelPosition === "center" + panelAnchorLeft: !root.useButtonPosition && panelPosition !== "center" && panelPosition.endsWith("_left") + panelAnchorRight: !root.useButtonPosition && panelPosition !== "center" && panelPosition.endsWith("_right") + panelAnchorBottom: !root.useButtonPosition && panelPosition.startsWith("bottom_") + panelAnchorTop: !root.useButtonPosition && panelPosition.startsWith("top_") + + panelContent: Rectangle { + id: ui + color: "transparent" + opacity: launcherCore.resultsReady ? 1.0 : 0.0 + + Component.onCompleted: root.launcherCoreRef = launcherCore + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + + // Preview Panel (external) - uses provider's preview component + NDropShadow { + source: previewBox + anchors.fill: previewBox + autoPaddingEnabled: true + visible: previewBox.visible + z: previewBox.z - 1 + } + + NBox { + id: previewBox + visible: root.previewActive + width: root.previewPanelWidth + height: Math.round(400 * Style.uiScaleRatio) + forceOpaque: true // no blur for now + x: root.panelAnchorRight ? -(root.previewPanelWidth + Style.marginM) : ui.width + Style.marginM + y: { + var view = launcherCore.resultsView; + if (!view) + return Style.marginL; + var row = launcherCore.isGridView ? Math.floor(launcherCore.selectedIndex / launcherCore.gridColumns) : launcherCore.selectedIndex; + var gridCellSize = Math.floor((root.listPanelWidth - (2 * Style.marginXS) - ((launcherCore.targetGridColumns - 1) * Style.marginS)) / launcherCore.targetGridColumns); + var itemHeight = launcherCore.isGridView ? (gridCellSize + Style.marginXXS) : (launcherCore.entryHeight + (view.spacing || 0)); + var yPos = row * itemHeight - (view.contentY || 0); + var mapped = view.mapToItem(ui, 0, yPos); + return Math.max(Style.marginL, Math.min(mapped.y, ui.height - previewBox.height - Style.marginL)); + } + z: -1 + + opacity: visible ? 1.0 : 0.0 + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + Behavior on y { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + Loader { + id: previewLoader + anchors.fill: parent + active: root.previewActive + source: { + if (!active) + return ""; + var provider = launcherCore.activeProvider; + if (provider && provider.previewComponentPath) + return provider.previewComponentPath; + return ""; + } + + onLoaded: updatePreviewItem() + onItemChanged: updatePreviewItem() + + function updatePreviewItem() { + if (!item || launcherCore.selectedIndex < 0 || !launcherCore.results[launcherCore.selectedIndex]) + return; + var provider = launcherCore.activeProvider; + if (provider && provider.getPreviewData) { + item.currentItem = provider.getPreviewData(launcherCore.results[launcherCore.selectedIndex]); + } else { + item.currentItem = launcherCore.results[launcherCore.selectedIndex]; + } + } + } + } + + // Core launcher (state, providers, UI) + LauncherCore { + id: launcherCore + anchors.fill: parent + screen: root.screen + isOpen: root.isPanelOpen + onRequestClose: root.close() + // Defer so the signal emission completes before SmartPanel + // sets isPanelOpen=false and the contentLoader destroys us. + onRequestCloseImmediately: Qt.callLater(root.closeImmediately) + } + + // Update preview when selection changes + Connections { + target: launcherCore + function onSelectedIndexChanged() { + if (previewLoader.item) + previewLoader.updatePreviewItem(); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/LauncherCore.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/LauncherCore.qml new file mode 100644 index 0000000..fb8f7fc --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/LauncherCore.qml @@ -0,0 +1,909 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import "Helpers/LauncherNavigation.js" as LauncherNav + +import "Providers" +import qs.Commons +import qs.Services.Keyboard +import qs.Services.UI +import qs.Widgets + +// Core launcher logic and UI - shared between SmartPanel (Launcher.qml) and overlay (LauncherOverlayWindow.qml) +Rectangle { + id: root + color: "transparent" + + // External interface - set by parent + property var screen: null + property bool isOpen: false + signal requestClose + signal requestCloseImmediately + + function closeImmediately() { + requestCloseImmediately(); + } + + // Expose for preview panel positioning + readonly property var resultsView: resultsSwapView.item + + // State + property string searchText: "" + property int selectedIndex: 0 + property var results: [] + property var providers: [] + property var activeProvider: null + property bool resultsReady: false + property var pluginProviderInstances: ({}) + property bool ignoreMouseHover: true // Transient flag, should always be true on init + + // Global mouse tracking for movement detection across delegates + property real globalLastMouseX: 0 + property real globalLastMouseY: 0 + property bool globalMouseInitialized: false + property bool mouseTrackingReady: false // Delay tracking until panel is settled + + readonly property bool animationsDisabled: Settings.data.general.animationDisabled + + Timer { + id: mouseTrackingDelayTimer + interval: root.animationsDisabled ? 0 : (Style.animationNormal + 50) // Wait for panel animation to complete + safety margin + repeat: false + onTriggered: { + root.mouseTrackingReady = true; + root.globalMouseInitialized = false; // Reset so we get fresh initial position + } + } + + readonly property var defaultProvider: appsProvider + readonly property var currentProvider: activeProvider || defaultProvider + + readonly property string launcherDensity: (currentProvider && currentProvider.ignoreDensity === false) ? (Settings.data.appLauncher.density || "default") : "comfortable" + readonly property int effectiveIconSize: launcherDensity === "comfortable" ? 48 : (launcherDensity === "default" ? 36 : 24) + readonly property int badgeSize: Math.round(effectiveIconSize * Style.uiScaleRatio) + readonly property int entryHeight: Math.round(badgeSize + (launcherDensity === "compact" ? (Style.marginL + Style.marginXXS) : (Style.marginXL + Style.marginS))) + + readonly property bool providerShowsCategories: (currentProvider.showsCategories !== undefined ? currentProvider.showsCategories : true) && providerCategories.length > 0 + + readonly property var providerCategories: { + if (currentProvider.availableCategories && currentProvider.availableCategories.length > 0) { + return currentProvider.availableCategories; + } + return currentProvider.categories || []; + } + + readonly property bool showProviderCategories: { + if (!providerShowsCategories || providerCategories.length === 0) + return false; + if (currentProvider === defaultProvider) + return Settings.data.appLauncher.showCategories; + return true; + } + + readonly property bool providerHasDisplayString: results.length > 0 && !!results[0].displayString + + readonly property string providerSupportedLayouts: { + if (activeProvider && activeProvider.supportedLayouts) + return activeProvider.supportedLayouts; + if (results.length > 0 && results[0].provider && results[0].provider.supportedLayouts) + return results[0].provider.supportedLayouts; + if (defaultProvider && defaultProvider.supportedLayouts) + return defaultProvider.supportedLayouts; + return "both"; + } + + readonly property bool showLayoutToggle: !providerHasDisplayString && providerSupportedLayouts === "both" + + readonly property string layoutMode: { + if (searchText === ">") + return "list"; + if (providerSupportedLayouts === "grid") + return "grid"; + if (providerSupportedLayouts === "list") + return "list"; + if (providerSupportedLayouts === "single") + return "single"; + if (providerHasDisplayString) + return "grid"; + return Settings.data.appLauncher.viewMode; + } + + readonly property bool isGridView: layoutMode === "grid" + readonly property bool isSingleView: layoutMode === "single" + readonly property bool isCompactDensity: launcherDensity === "compact" + + readonly property int targetGridColumns: { + let base = 5; + if (launcherDensity === "comfortable") + base = 4; + else if (launcherDensity === "compact") + base = 6; + + if (!activeProvider || activeProvider === defaultProvider) + return base; + + if (activeProvider.preferredGridColumns) { + let multiplier = base / 5.0; + return Math.max(1, Math.round(activeProvider.preferredGridColumns * multiplier)); + } + + return base; + } + readonly property int listPanelWidth: Math.round(500 * Style.uiScaleRatio) + readonly property int gridContentWidth: listPanelWidth - (2 * Style.marginXS) + readonly property int gridCellSize: Math.floor((gridContentWidth - ((targetGridColumns - 1) * Style.marginS)) / targetGridColumns) + + readonly property int gridColumns: targetGridColumns + + // Check if current provider allows wrap navigation (default true) + readonly property bool allowWrapNavigation: { + var provider = activeProvider || currentProvider; + return provider && provider.wrapNavigation !== undefined ? provider.wrapNavigation : true; + } + + // Listen for plugin provider registry changes + Connections { + target: LauncherProviderRegistry + function onPluginProviderRegistryUpdated() { + root.syncPluginProviders(); + } + } + + // Lifecycle + onIsOpenChanged: { + if (isOpen) { + onOpened(); + } else { + onClosed(); + } + } + + onSearchTextChanged: { + if (isOpen) { + updateResults(); + } + } + + function onOpened() { + ignoreMouseHover = true; + globalMouseInitialized = false; + mouseTrackingReady = false; + mouseTrackingDelayTimer.restart(); + + // Show launcher immediately, results will populate asynchronously + resultsReady = true; + focusSearchInput(); + + Qt.callLater(() => { + syncPluginProviders(); + for (let provider of providers) { + if (provider.onOpened) + provider.onOpened(); + } + updateResults(); + }); + } + + function onClosed() { + searchText = ""; + ignoreMouseHover = true; + if (resultsSwapView) + resultsSwapView.resetVisuals(); + for (let provider of providers) { + if (provider.onClosed) + provider.onClosed(); + } + } + + function close() { + requestClose(); + } + + function applyCategorySelection(tabIndex, categories) { + const categoryList = categories || providerCategories; + if (!categoryList || tabIndex < 0 || tabIndex >= categoryList.length) + return false; + + currentProvider.selectCategory(categoryList[tabIndex]); + categoryTabs.currentIndex = tabIndex; + return true; + } + + function selectCategoryWithSlide(tabIndex) { + if (!showProviderCategories || !currentProvider || !currentProvider.selectCategory) + return; + + const cats = providerCategories; + if (!cats || tabIndex < 0 || tabIndex >= cats.length) + return; + + const currentIdx = cats.indexOf(currentProvider.selectedCategory); + if (tabIndex === currentIdx) + return; + + const canAnimate = !animationsDisabled && resultsSwapView.width > 0 && resultsSwapView.height > 0; + if (!canAnimate) { + applyCategorySelection(tabIndex, cats); + return; + } + + const direction = tabIndex > currentIdx ? 1 : -1; + resultsSwapView.swap(direction, () => applyCategorySelection(tabIndex, providerCategories)); + } + + // Public API + function setSearchText(text) { + searchText = text; + } + + function focusSearchInput() { + if (searchInput.inputItem) { + searchInput.inputItem.forceActiveFocus(); + } + } + + // Provider registration + function registerProvider(provider) { + providers.push(provider); + provider.launcher = root; + if (provider.init) + provider.init(); + } + + function syncPluginProviders() { + var registeredIds = LauncherProviderRegistry.getPluginProviders(); + var changed = false; + + // Remove providers that are no longer registered + for (var existingId in pluginProviderInstances) { + if (registeredIds.indexOf(existingId) === -1) { + var idx = providers.indexOf(pluginProviderInstances[existingId]); + if (idx >= 0) + providers.splice(idx, 1); + delete pluginProviderInstances[existingId]; + Logger.d("Launcher", "Removed plugin provider:", existingId); + changed = true; + } + } + + // Adopt persistent instances from the registry + for (var i = 0; i < registeredIds.length; i++) { + var providerId = registeredIds[i]; + if (!pluginProviderInstances[providerId]) { + var instance = LauncherProviderRegistry.getProviderInstance(providerId); + if (instance) { + pluginProviderInstances[providerId] = instance; + providers.push(instance); + instance.launcher = root; + Logger.d("Launcher", "Adopted plugin provider:", providerId); + changed = true; + } + } + } + + // Update results only if providers changed + if (changed && root.isOpen) { + updateResults(); + } + } + + // Search handling + function updateResults() { + results = []; + var newActiveProvider = null; + + // Check for command mode + if (searchText.startsWith(">")) { + for (let provider of providers) { + if (provider.handleCommand && provider.handleCommand(searchText)) { + newActiveProvider = provider; + results = provider.getResults(searchText); + break; + } + } + + // Show available commands if just ">" or filter commands if partial match + if (!newActiveProvider) { + let allCommands = []; + for (let provider of providers) { + if (provider.commands) + allCommands = allCommands.concat(provider.commands()); + } + if (searchText === ">") { + results = allCommands; + } else if (searchText.length > 1) { + const query = searchText.substring(1); + if (typeof FuzzySort !== 'undefined') { + const fuzzyResults = FuzzySort.go(query, allCommands, { + "keys": ["name"], + "limit": 50 + }); + results = fuzzyResults.map(result => result.obj); + } else { + const queryLower = query.toLowerCase(); + results = allCommands.filter(cmd => (cmd.name || "").toLowerCase().includes(queryLower)); + } + } + } + } else { + // Regular search - let providers contribute results + let allResults = []; + for (let provider of providers) { + if (provider.handleSearch) { + const providerResults = provider.getResults(searchText); + allResults = allResults.concat(providerResults); + } + } + + // Sort by _score (higher = better match), items without _score go first + if (searchText.trim() !== "") { + const boostByUsage = Settings.data.appLauncher.sortByMostUsed; + + allResults.sort((a, b) => { + let sa = a._score !== undefined ? a._score : 0; + let sb = b._score !== undefined ? b._score : 0; + + // Boost scores for frequently used items from tracked providers + // _score is normalized 0–1, so boost is scaled to nudge, not overwhelm + if (boostByUsage) { + if (a.provider && a.provider.trackUsage && a.usageKey) { + sa += 0.1 * Math.log2(1 + ShellState.getLauncherUsageCount(a.usageKey)); + } + if (b.provider && b.provider.trackUsage && b.usageKey) { + sb += 0.1 * Math.log2(1 + ShellState.getLauncherUsageCount(b.usageKey)); + } + } + + return sb - sa; + }); + } + results = allResults; + } + + // Update activeProvider only after computing new state to avoid UI flicker + activeProvider = newActiveProvider; + selectedIndex = 0; + } + + // Navigation functions (delegated to LauncherNavigation.js) + function selectNext() { + selectedIndex = LauncherNav.selectNext(selectedIndex, results.length); + } + function selectPrevious() { + selectedIndex = LauncherNav.selectPrevious(selectedIndex, results.length); + } + function selectNextWrapped() { + selectedIndex = LauncherNav.selectNextWrapped(selectedIndex, results.length, allowWrapNavigation); + } + function selectPreviousWrapped() { + selectedIndex = LauncherNav.selectPreviousWrapped(selectedIndex, results.length, allowWrapNavigation); + } + function selectFirst() { + selectedIndex = LauncherNav.selectFirst(); + } + function selectLast() { + selectedIndex = LauncherNav.selectLast(results.length); + } + function selectNextPage() { + selectedIndex = LauncherNav.selectNextPage(selectedIndex, results.length, entryHeight); + } + function selectPreviousPage() { + selectedIndex = LauncherNav.selectPreviousPage(selectedIndex, results.length, entryHeight); + } + function selectPreviousRow() { + selectedIndex = LauncherNav.selectPreviousRow(selectedIndex, results.length, gridColumns); + } + function selectNextRow() { + selectedIndex = LauncherNav.selectNextRow(selectedIndex, results.length, gridColumns); + } + function selectPreviousColumn() { + selectedIndex = LauncherNav.selectPreviousColumn(selectedIndex, results.length, gridColumns); + } + function selectNextColumn() { + selectedIndex = LauncherNav.selectNextColumn(selectedIndex, results.length, gridColumns); + } + + function activate() { + if (results.length > 0 && results[selectedIndex]) { + const item = results[selectedIndex]; + const provider = item.provider || currentProvider; + + // Track usage for providers that opt in (cross-provider "most used" tracking) + if (Settings.data.appLauncher.sortByMostUsed && provider && provider.trackUsage && item.usageKey) { + ShellState.recordLauncherUsage(item.usageKey); + } + + // Check if auto-paste is enabled and provider/item supports it + if (Settings.data.appLauncher.autoPasteClipboard && provider && provider.supportsAutoPaste && item.autoPasteText) { + if (item.onAutoPaste) + item.onAutoPaste(); + closeImmediately(); + Qt.callLater(() => { + ClipboardService.pasteText(item.autoPasteText); + }); + return; + } + + if (item.onActivate) + item.onActivate(); + } + } + + function checkKey(event, settingName) { + return Keybinds.checkKey(event, settingName, Settings); + } + + // Keyboard handler + function handleKeyPress(event) { + if (checkKey(event, 'escape')) { + close(); + event.accepted = true; + return; + } + + if (checkKey(event, 'enter')) { + activate(); + event.accepted = true; + return; + } + + if (checkKey(event, 'up')) { + if (!isSingleView) { + isGridView ? selectPreviousRow() : selectPreviousWrapped(); + } + event.accepted = true; + return; + } + + if (checkKey(event, 'down')) { + if (!isSingleView) { + isGridView ? selectNextRow() : selectNextWrapped(); + } + event.accepted = true; + return; + } + + if (checkKey(event, 'left')) { + if (isGridView) { + selectPreviousColumn(); + event.accepted = true; + return; + } + } + + if (checkKey(event, 'right')) { + if (isGridView) { + selectNextColumn(); + event.accepted = true; + return; + } + } + + // Static bindings + switch (event.key) { + case Qt.Key_Tab: + if (showProviderCategories) { + var cats = providerCategories; + var idx = cats.indexOf(currentProvider.selectedCategory); + var nextIdx = (idx + 1) % cats.length; + selectCategoryWithSlide(nextIdx); + } else { + selectNextWrapped(); + } + event.accepted = true; + break; + case Qt.Key_Backtab: + if (showProviderCategories) { + var cats2 = providerCategories; + var idx2 = cats2.indexOf(currentProvider.selectedCategory); + var prevIdx = ((idx2 - 1) % cats2.length + cats2.length) % cats2.length; + selectCategoryWithSlide(prevIdx); + } else { + selectPreviousWrapped(); + } + event.accepted = true; + break; + case Qt.Key_Home: + selectFirst(); + event.accepted = true; + break; + case Qt.Key_End: + selectLast(); + event.accepted = true; + break; + case Qt.Key_PageUp: + selectPreviousPage(); + event.accepted = true; + break; + case Qt.Key_PageDown: + selectNextPage(); + event.accepted = true; + break; + case Qt.Key_Delete: + if (selectedIndex >= 0 && results && results[selectedIndex]) { + var item = results[selectedIndex]; + var provider = item.provider || currentProvider; + if (provider && provider.canDeleteItem && provider.canDeleteItem(item)) + provider.deleteItem(item); + } + event.accepted = true; + break; + } + } + + // ----------------------- + // Provider components + // ----------------------- + ApplicationsProvider { + id: appsProvider + Component.onCompleted: { + registerProvider(this); + Logger.d("Launcher", "Registered: ApplicationsProvider"); + } + } + + ClipboardProvider { + id: clipProvider + Component.onCompleted: { + if (Settings.data.appLauncher.enableClipboardHistory) { + registerProvider(this); + Logger.d("Launcher", "Registered: ClipboardProvider"); + } + } + } + + CommandProvider { + id: cmdProvider + Component.onCompleted: { + registerProvider(this); + Logger.d("Launcher", "Registered: CommandProvider"); + } + } + + EmojiProvider { + id: emojiProvider + Component.onCompleted: { + registerProvider(this); + Logger.d("Launcher", "Registered: EmojiProvider"); + } + } + + CalculatorProvider { + id: calcProvider + Component.onCompleted: { + registerProvider(this); + Logger.d("Launcher", "Registered: CalculatorProvider"); + } + } + + SettingsProvider { + id: settingsProvider + Component.onCompleted: { + registerProvider(this); + Logger.d("Launcher", "Registered: SettingsProvider"); + } + } + + SessionProvider { + id: sessionProvider + Component.onCompleted: { + registerProvider(this); + Logger.d("Launcher", "Registered: SessionProvider"); + } + } + + WindowsProvider { + id: windowsProvider + Component.onCompleted: { + registerProvider(this); + Logger.d("Launcher", "Registered: WindowsProvider"); + } + } + + // ==================== UI Content ==================== + + opacity: resultsReady ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + + HoverHandler { + id: globalHoverHandler + enabled: !Settings.data.appLauncher.ignoreMouseInput + + onPointChanged: { + if (!root.mouseTrackingReady) { + return; + } + + if (!root.globalMouseInitialized) { + root.globalLastMouseX = point.position.x; + root.globalLastMouseY = point.position.y; + root.globalMouseInitialized = true; + return; + } + + const deltaX = Math.abs(point.position.x - root.globalLastMouseX); + const deltaY = Math.abs(point.position.y - root.globalLastMouseY); + if (deltaX + deltaY >= 5) { + root.ignoreMouseHover = false; + root.globalLastMouseX = point.position.x; + root.globalLastMouseY = point.position.y; + } + } + } + + ColumnLayout { + anchors.fill: parent + anchors.topMargin: Style.marginL + anchors.bottomMargin: Style.marginL + spacing: Style.marginL + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: Style.marginL + Layout.rightMargin: Style.marginL + spacing: Style.marginS + + NTextInput { + id: searchInput + Layout.fillWidth: true + radius: Style.iRadiusM + text: root.searchText + placeholderText: I18n.tr("placeholders.search-launcher") + fontSize: Style.fontSizeM + onTextChanged: root.searchText = text + + Component.onCompleted: { + if (searchInput.inputItem) { + searchInput.inputItem.forceActiveFocus(); + searchInput.inputItem.Keys.onPressed.connect(function (event) { + root.handleKeyPress(event); + }); + } + } + } + + NIconButton { + visible: root.showLayoutToggle + icon: Settings.data.appLauncher.viewMode === "grid" ? "layout-list" : "layout-grid" + tooltipText: Settings.data.appLauncher.viewMode === "grid" ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view") + customRadius: Style.iRadiusM + Layout.preferredWidth: searchInput.height + Layout.preferredHeight: searchInput.height + onClicked: Settings.data.appLauncher.viewMode = Settings.data.appLauncher.viewMode === "grid" ? "list" : "grid" + } + } + + // Unified category tabs (works with any provider that has categories) + NTabBar { + id: categoryTabs + visible: root.showProviderCategories + Layout.fillWidth: true + Layout.leftMargin: Style.marginL + Layout.rightMargin: Style.marginL + margins: 0 + border.color: Style.boxBorderColor + border.width: Style.borderS + + property int computedCurrentIndex: visible && root.providerCategories.length > 0 ? root.providerCategories.indexOf(root.currentProvider.selectedCategory) : 0 + currentIndex: computedCurrentIndex + + Repeater { + model: root.providerCategories + NTabButton { + required property string modelData + required property int index + icon: root.currentProvider.categoryIcons ? (root.currentProvider.categoryIcons[modelData] || "star") : "star" + tooltipText: root.currentProvider.getCategoryName ? root.currentProvider.getCategoryName(modelData) : modelData + tabIndex: index + checked: categoryTabs.currentIndex === index + onClicked: root.selectCategoryWithSlide(index) + } + } + } + + // Results view + NSlideSwapView { + id: resultsSwapView + Layout.fillWidth: true + Layout.leftMargin: Style.marginL + Layout.rightMargin: Style.marginL + Layout.fillHeight: true + animationsEnabled: !root.animationsDisabled + sourceComponent: root.isSingleView ? singleViewComponent : (root.isGridView ? gridViewComponent : listViewComponent) + } + + // -------------------------- + // LIST VIEW + Component { + id: listViewComponent + NListView { + id: resultsList + + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AlwaysOff + reserveScrollbarSpace: false + gradientColor: Settings.data.ui.panelBackgroundOpacity < 1 ? "transparent" : Color.mSurface + wheelScrollMultiplier: 4.0 + + width: parent.width + height: parent.height + spacing: Style.marginS + model: root.results + currentIndex: root.selectedIndex + cacheBuffer: resultsList.height * 2 + interactive: !Settings.data.appLauncher.ignoreMouseInput + onCurrentIndexChanged: { + cancelFlick(); + if (currentIndex >= 0) { + positionViewAtIndex(currentIndex, ListView.Contain); + } + } + onModelChanged: {} + + delegate: LauncherListDelegate { + launcher: root + } + } + } + + // -------------------------- + // SINGLE ITEM VIEW + Component { + id: singleViewComponent + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + NBox { + anchors.fill: parent + color: Color.mSurfaceVariant + forceOpaque: true + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL + Layout.fillWidth: true + Layout.fillHeight: true + + Item { + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + NText { + text: root.results.length > 0 ? root.results[0].name : "" + pointSize: Style.fontSizeL + font.weight: Font.Bold + color: Color.mPrimary + } + } + + NScrollView { + id: descriptionScrollView + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + Layout.topMargin: Style.fontSizeL + Style.marginXL + Layout.fillWidth: true + Layout.fillHeight: true + horizontalPolicy: ScrollBar.AlwaysOff + reserveScrollbarSpace: false + + NText { + width: descriptionScrollView.availableWidth + text: root.results.length > 0 ? root.results[0].description : "" + pointSize: Style.fontSizeM + font.weight: Font.Bold + color: Color.mOnSurface + horizontalAlignment: Text.AlignHLeft + verticalAlignment: Text.AlignTop + wrapMode: Text.Wrap + markdownTextEnabled: true + } + } + } + } + } + } + + // // -------------------------- + // GRID VIEW + Component { + id: gridViewComponent + NGridView { + id: resultsGrid + + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AlwaysOff + reserveScrollbarSpace: false + gradientColor: Settings.data.ui.panelBackgroundOpacity < 1 ? "transparent" : Color.mSurface + wheelScrollMultiplier: 4.0 + trackedSelectionIndex: root.selectedIndex + + width: parent.width + height: parent.height + cellWidth: parent.width / root.targetGridColumns + cellHeight: { + var cellWidth = parent.width / root.targetGridColumns; + // Use provider's preferred ratio if available + if (root.currentProvider && root.currentProvider.preferredGridCellRatio) { + return cellWidth * root.currentProvider.preferredGridCellRatio; + } + return cellWidth; + } + leftMargin: 0 + rightMargin: 0 + topMargin: 0 + bottomMargin: 0 + model: root.results + cacheBuffer: resultsGrid.height * 2 + keyNavigationEnabled: false + focus: false + interactive: !Settings.data.appLauncher.ignoreMouseInput + + // Completely disable GridView key handling + Keys.enabled: false + + // Handle scrolling to show selected item when it changes + Connections { + target: root + enabled: root.isGridView + function onSelectedIndexChanged() { + if (!root.isGridView || root.selectedIndex < 0 || !resultsGrid) { + return; + } + + Qt.callLater(() => { + if (root.isGridView && resultsGrid && resultsGrid.cancelFlick) { + resultsGrid.cancelFlick(); + resultsGrid.positionViewAtIndex(root.selectedIndex, GridView.Contain); + } + }); + } + } + + delegate: LauncherGridDelegate { + launcher: root + } + } + } + + ColumnLayout { + Layout.leftMargin: Style.marginL + Layout.rightMargin: Style.marginL + + NDivider { + Layout.fillWidth: true + Layout.bottomMargin: Style.marginS + } + + NText { + Layout.fillWidth: true + text: { + if (root.results.length === 0) { + if (root.searchText) { + return I18n.tr("common.no-results"); + } + // Use provider's empty browsing message if available + var provider = root.currentProvider; + if (provider && provider.emptyBrowsingMessage) { + return provider.emptyBrowsingMessage; + } + return ""; + } + var prefix = root.activeProvider && root.activeProvider.name ? root.activeProvider.name + ": " : ""; + return prefix + I18n.trp("common.result-count", root.results.length); + } + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignCenter + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/LauncherGridDelegate.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/LauncherGridDelegate.qml new file mode 100644 index 0000000..67317c9 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/LauncherGridDelegate.qml @@ -0,0 +1,272 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Widgets + +import qs.Commons +import qs.Widgets + +Item { + id: gridEntryContainer + + required property var modelData + required property int index + required property var launcher + + width: GridView.view.cellWidth + height: GridView.view.cellHeight + + property bool isSelected: (!launcher.ignoreMouseHover && mouseArea.containsMouse) || (index === launcher.selectedIndex) + + // Prepare item when it becomes visible (e.g., decode images) + Component.onCompleted: { + var provider = modelData.provider; + if (provider && provider.prepareItem) { + provider.prepareItem(modelData); + } + } + + NBox { + id: gridEntry + anchors.fill: parent + anchors.margins: Style.marginXXS + color: gridEntryContainer.isSelected ? Color.mHover : Color.mSurfaceVariant + forceOpaque: gridEntryContainer.isSelected + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: launcher.isCompactDensity ? Style.marginXS : Style.marginS + anchors.bottomMargin: launcher.isCompactDensity ? Style.marginXS : Style.marginS + spacing: launcher.isCompactDensity ? 0 : Style.marginXXS + + // Icon badge or Image preview or Emoji + Item { + // Size image at 65% of cell dimensions. + Layout.preferredWidth: Math.round(gridEntry.width * 0.65) + Layout.preferredHeight: Math.round(gridEntry.height * 0.65) + Layout.alignment: Qt.AlignHCenter + + // Icon background + Rectangle { + anchors.fill: parent + radius: Style.radiusM + color: Color.mSurface + visible: Settings.data.appLauncher.showIconBackground && !modelData.isImage + } + + // Image preview - uses provider's getImageUrl if available + NImageRounded { + id: gridImagePreview + anchors.fill: parent + visible: !!modelData.isImage && !modelData.displayString + radius: Style.radiusM + + // Use provider's image revision for reactive updates + readonly property int _rev: modelData.provider && modelData.provider.imageRevision ? modelData.provider.imageRevision : 0 + + // Get image URL from provider + imagePath: { + _rev; + var provider = modelData.provider; + if (provider && provider.getImageUrl) { + return provider.getImageUrl(modelData); + } + return ""; + } + + Rectangle { + anchors.fill: parent + visible: parent.status === Image.Loading + color: Color.mSurfaceVariant + + BusyIndicator { + anchors.centerIn: parent + running: true + width: Style.baseWidgetSize * 0.5 + height: width + } + } + + onStatusChanged: status => { + if (status === Image.Error) { + gridIconLoader.visible = true; + gridImagePreview.visible = false; + } + } + } + + Loader { + id: gridIconLoader + anchors.fill: parent + anchors.margins: Style.marginXS + + visible: (!modelData.isImage && !modelData.displayString) || (!!modelData.isImage && gridImagePreview.status === Image.Error) + active: visible + + sourceComponent: Settings.data.appLauncher.iconMode === "tabler" && modelData.isTablerIcon ? gridTablerIconComponent : gridSystemIconComponent + + Component { + id: gridTablerIconComponent + NIcon { + icon: modelData.icon + pointSize: Style.fontSizeXXXL + visible: modelData.icon && !modelData.displayString + color: (gridEntryContainer.isSelected && !Settings.data.appLauncher.showIconBackground) ? Color.mOnHover : Color.mOnSurface + } + } + + Component { + id: gridSystemIconComponent + IconImage { + anchors.fill: parent + source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : "" + visible: modelData.icon && source !== "" && !modelData.displayString + asynchronous: true + } + } + } + + // String display + NText { + id: gridStringDisplay + anchors.centerIn: parent + visible: !!modelData.displayString || (!gridImagePreview.visible && !gridIconLoader.visible) + text: modelData.displayString ? modelData.displayString : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?") + pointSize: { + if (modelData.displayString) { + // Use custom size if provided, otherwise default scaling + if (modelData.displayStringSize) { + return modelData.displayStringSize * Style.uiScaleRatio; + } + if (launcher.providerHasDisplayString) { + // Scale with cell width but cap at reasonable maximum + const cellBasedSize = gridEntry.width * 0.4; + const maxSize = Style.fontSizeXXXL * Style.uiScaleRatio; + return Math.min(cellBasedSize, maxSize); + } + return Style.fontSizeXXL * 2 * Style.uiScaleRatio; + } + // Scale font size relative to cell width for low res, but cap at maximum + const cellBasedSize = gridEntry.width * 0.25; + const baseSize = Style.fontSizeXL * Style.uiScaleRatio; + const maxSize = Style.fontSizeXXL * Style.uiScaleRatio; + return Math.min(Math.max(cellBasedSize, baseSize), maxSize); + } + font.weight: Style.fontWeightBold + color: modelData.displayString ? Color.mOnSurface : Color.mOnPrimary + } + + // Badge icon overlay (generic indicator for any provider) + Rectangle { + visible: !!modelData.badgeIcon + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 2 + width: height + height: Style.fontSizeM + Style.marginXS + color: Color.mSurfaceVariant + radius: Style.radiusXXS + NIcon { + anchors.centerIn: parent + icon: modelData.badgeIcon || "" + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + } + + // Text content (hidden when hideLabel is true) + NText { + visible: !modelData.hideLabel + text: modelData.name || "Unknown" + pointSize: { + if (launcher.providerHasDisplayString && modelData.displayString) { + return Style.fontSizeS * Style.uiScaleRatio; + } + // Scale font size relative to cell width for low res, but cap at maximum + const cellBasedSize = gridEntry.width * 0.1; + const baseSize = Style.fontSizeXS * Style.uiScaleRatio; + const maxSize = Style.fontSizeS * Style.uiScaleRatio; + return Math.min(Math.max(cellBasedSize, baseSize), maxSize); + } + font.weight: Style.fontWeightSemiBold + color: gridEntryContainer.isSelected ? Color.mOnHover : Color.mOnSurface + elide: Text.ElideRight + Layout.fillWidth: true + Layout.maximumWidth: gridEntry.width - 8 + Layout.leftMargin: (launcher.providerHasDisplayString && modelData.displayString) ? Style.marginS : 0 + Layout.rightMargin: (launcher.providerHasDisplayString && modelData.displayString) ? Style.marginS : 0 + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.NoWrap + maximumLineCount: 1 + } + } + + // Action buttons (overlay in top-right corner) - dynamically populated from provider + Row { + visible: gridEntryContainer.isSelected && gridItemActions.length > 0 + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginXS + z: 10 + spacing: Style.marginXXS + + property var gridItemActions: { + if (!gridEntryContainer.isSelected) + return []; + var provider = modelData.provider || launcher.currentProvider; + if (provider && provider.getItemActions) { + return provider.getItemActions(modelData); + } + return []; + } + + Repeater { + model: parent.gridItemActions + NIconButton { + required property var modelData + icon: modelData.icon + baseSize: Style.baseWidgetSize * 0.75 + tooltipText: modelData.tooltip + z: 11 + handleWheel: true + onClicked: { + if (modelData.action) { + modelData.action(); + } + } + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + z: -1 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: !Settings.data.appLauncher.ignoreMouseInput + + onEntered: { + if (!launcher.ignoreMouseHover) { + launcher.selectedIndex = gridEntryContainer.index; + } + } + onClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + launcher.selectedIndex = gridEntryContainer.index; + launcher.activate(); + mouse.accepted = true; + } + } + acceptedButtons: Qt.LeftButton + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/LauncherListDelegate.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/LauncherListDelegate.qml new file mode 100644 index 0000000..57cdeb5 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/LauncherListDelegate.qml @@ -0,0 +1,294 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Widgets + +import qs.Commons +import qs.Widgets + +NBox { + id: entry + + required property var modelData + required property int index + required property var launcher + + property bool isSelected: (!launcher.ignoreMouseHover && mouseArea.containsMouse) || (index === launcher.selectedIndex) + + width: ListView.view.width + implicitHeight: launcher.entryHeight + clip: true + color: entry.isSelected ? Color.mHover : Color.mSurfaceVariant + forceOpaque: entry.isSelected + + // Prepare item when it becomes visible (e.g., decode images) + Component.onCompleted: { + var provider = modelData.provider; + if (provider && provider.prepareItem) { + provider.prepareItem(modelData); + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + + ColumnLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: launcher.isCompactDensity ? Style.marginXS : Style.marginM + spacing: launcher.isCompactDensity ? Style.marginXS : Style.marginM + + // Top row - Main entry content with action buttons + RowLayout { + Layout.fillWidth: true + spacing: launcher.isCompactDensity ? Style.marginS : Style.marginM + + // Icon badge or Image preview or Emoji + Item { + visible: !modelData.hideIcon + Layout.preferredWidth: modelData.hideIcon ? 0 : launcher.badgeSize + Layout.preferredHeight: modelData.hideIcon ? 0 : launcher.badgeSize + + // Icon background + Rectangle { + anchors.fill: parent + radius: Style.radiusXS + color: Color.mSurface + visible: Settings.data.appLauncher.showIconBackground && !modelData.isImage + } + + // Image preview - uses provider's getImageUrl if available + NImageRounded { + id: imagePreview + anchors.fill: parent + visible: !!modelData.isImage && !modelData.displayString + radius: Style.radiusXS + borderColor: Color.mOnSurface + borderWidth: Style.borderM + imageFillMode: Image.PreserveAspectCrop + + // Use provider's image revision for reactive updates + readonly property int _rev: modelData.provider && modelData.provider.imageRevision ? modelData.provider.imageRevision : 0 + + // Get image URL from provider + imagePath: { + _rev; + var provider = modelData.provider; + if (provider && provider.getImageUrl) { + return provider.getImageUrl(modelData); + } + return ""; + } + + Rectangle { + anchors.fill: parent + visible: parent.status === Image.Loading + color: Color.mSurfaceVariant + + BusyIndicator { + anchors.centerIn: parent + running: true + width: Style.baseWidgetSize * 0.5 + height: width + } + } + + onStatusChanged: status => { + if (status === Image.Error) { + iconLoader.visible = true; + imagePreview.visible = false; + } + } + } + + // Color swatch - shown for clipboard color entries + Rectangle { + anchors.fill: parent + radius: Style.radiusXS + color: modelData.colorHex || "transparent" + visible: !!modelData.colorHex + border.color: Color.mOnSurface + border.width: Style.borderM + } + + Loader { + id: iconLoader + anchors.fill: parent + anchors.margins: Style.marginXS + + visible: (!modelData.isImage && !modelData.displayString && !modelData.colorHex) || (!!modelData.isImage && imagePreview.status === Image.Error) + active: visible + + sourceComponent: Component { + Loader { + anchors.fill: parent + sourceComponent: Settings.data.appLauncher.iconMode === "tabler" && modelData.isTablerIcon ? tablerIconComponent : systemIconComponent + } + } + + Component { + id: tablerIconComponent + NIcon { + icon: modelData.icon + pointSize: Style.fontSizeXXXL + visible: modelData.icon && !modelData.displayString + color: (entry.isSelected && !Settings.data.appLauncher.showIconBackground) ? Color.mOnHover : Color.mOnSurface + } + } + + Component { + id: systemIconComponent + IconImage { + anchors.fill: parent + source: modelData.icon ? ThemeIcons.iconFromName(modelData.icon, "application-x-executable") : "" + visible: modelData.icon && source !== "" && !modelData.displayString + asynchronous: true + } + } + } + + // String display - takes precedence when displayString is present + NText { + id: stringDisplay + anchors.centerIn: parent + visible: !!modelData.displayString || (!imagePreview.visible && !iconLoader.visible) + text: modelData.displayString ? modelData.displayString : (modelData.name ? modelData.name.charAt(0).toUpperCase() : "?") + pointSize: modelData.displayString ? (modelData.displayStringSize || Style.fontSizeXXXL) : Style.fontSizeXXL + font.weight: Style.fontWeightBold + color: modelData.displayString ? Color.mOnSurface : Color.mOnPrimary + } + + // Image type indicator overlay + Rectangle { + visible: !!modelData.isImage && imagePreview.visible + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 2 + width: formatLabel.width + Style.marginXS + height: formatLabel.height + Style.marginXXS + color: Color.mSurfaceVariant + radius: Style.radiusXXS + NText { + id: formatLabel + anchors.centerIn: parent + text: { + if (!modelData.isImage) + return ""; + const desc = modelData.description || ""; + const parts = desc.split(" \u2022 "); + return parts[0] || "IMG"; + } + pointSize: Style.fontSizeXXS + color: Color.mOnSurfaceVariant + } + } + + // Badge icon overlay (generic indicator for any provider) + Rectangle { + visible: !!modelData.badgeIcon + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.margins: 2 + width: height + height: Style.fontSizeM + Style.marginXS + color: Color.mSurfaceVariant + radius: Style.radiusXXS + NIcon { + anchors.centerIn: parent + icon: modelData.badgeIcon || "" + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + } + + // Text content + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + NText { + text: modelData.name || "Unknown" + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: entry.isSelected ? Color.mOnHover : Color.mOnSurface + elide: Text.ElideRight + maximumLineCount: 1 + wrapMode: Text.Wrap + clip: true + Layout.fillWidth: true + } + + NText { + text: modelData.description || "" + pointSize: Style.fontSizeS + color: entry.isSelected ? Color.mOnHover : Color.mOnSurfaceVariant + elide: Text.ElideRight + maximumLineCount: 1 + Layout.fillWidth: true + visible: text !== "" && !launcher.isCompactDensity + } + } + + // Action buttons row - dynamically populated from provider + RowLayout { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + spacing: Style.marginXS + visible: entry.isSelected && itemActions.length > 0 + + property var itemActions: { + if (!entry.isSelected) + return []; + var provider = modelData.provider || launcher.currentProvider; + if (provider && provider.getItemActions) { + return provider.getItemActions(modelData); + } + return []; + } + + Repeater { + model: parent.itemActions + NIconButton { + required property var modelData + icon: modelData.icon + baseSize: Style.baseWidgetSize * 0.75 + tooltipText: modelData.tooltip + z: 1 + handleWheel: true + onClicked: { + if (modelData.action) { + modelData.action(); + } + } + } + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + z: -1 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: !Settings.data.appLauncher.ignoreMouseInput + onEntered: { + if (!launcher.ignoreMouseHover) { + launcher.selectedIndex = entry.index; + } + } + onClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + launcher.selectedIndex = entry.index; + launcher.activate(); + mouse.accepted = true; + } + } + acceptedButtons: Qt.LeftButton + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/LauncherOverlayWindow.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/LauncherOverlayWindow.qml new file mode 100644 index 0000000..4769906 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/LauncherOverlayWindow.qml @@ -0,0 +1,426 @@ +import QtQuick +import QtQuick.Shapes +import Quickshell +import Quickshell.Wayland + +import qs.Commons +import qs.Modules.MainScreen.Backgrounds +import qs.Services.UI +import qs.Widgets + +// Standalone launcher window for Overlay layer mode. +// This window appears above fullscreen windows and does not attach to the bar. +Variants { + id: launcherVariants + + model: Quickshell.screens.filter(screen => Settings.data.appLauncher.overviewLayer) + + delegate: Loader { + id: windowLoader + + required property ShellScreen modelData + + active: PanelService.overlayLauncherOpen && PanelService.overlayLauncherScreen === modelData + + sourceComponent: PanelWindow { + id: launcherWindow + screen: windowLoader.modelData + + anchors { + top: true + bottom: true + left: true + right: true + } + + color: "transparent" + + WlrLayershell.namespace: "noctalia-launcher-overlay-" + (screen?.name || "unknown") + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.exclusionMode: ExclusionMode.Ignore + + BackgroundEffect.blurRegion: Settings.data.general.enableBlurBehind ? launcherBlurRegion : null + Region { + id: launcherBlurRegion + + Region { + x: Math.round(launcherPanel.x) + y: Math.round(launcherPanel.y) + width: Math.round(launcherPanel.width) + height: Math.round(launcherPanel.height) + radius: Style.radiusL + topLeftCorner: launcherPanel.topLeftCornerState + topRightCorner: launcherPanel.topRightCornerState + bottomLeftCorner: launcherPanel.bottomLeftCornerState + bottomRightCorner: launcherPanel.bottomRightCornerState + } + + Region { + x: Math.round(previewBox.visible ? previewBox.x : 0) + y: Math.round(previewBox.visible ? previewBox.y : 0) + width: Math.round(previewBox.visible ? previewBox.width : 0) + height: Math.round(previewBox.visible ? previewBox.height : 0) + radius: Style.radiusL + } + } + + // Positioning logic (respects settings but doesn't attach to bar) + readonly property string barPosition: Settings.data.bar.position + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + readonly property int barThickness: Math.round(Style.barHeight + Style.marginL) + + readonly property string panelPosition: { + var pos = Settings.data.appLauncher.position; + if (pos === "follow_bar") { + if (barIsVertical) { + return "center_" + barPosition; + } else { + return barPosition + "_center"; + } + } + return pos; + } + + // Preview panel support + readonly property int listPanelWidth: Math.round(500 * Style.uiScaleRatio) + readonly property int previewPanelWidth: Math.round(400 * Style.uiScaleRatio) + readonly property bool previewActive: { + if (!launcherCore) + return false; + var provider = launcherCore.activeProvider; + if (!provider || !provider.hasPreview) + return false; + if (!Settings.data.appLauncher.enableClipPreview) + return false; + return launcherCore.selectedIndex >= 0 && launcherCore.results && !!launcherCore.results[launcherCore.selectedIndex]; + } + + // Dimmer background (click to close) + Rectangle { + anchors.fill: parent + color: Qt.alpha(Color.mSurface, Settings.data.general.dimmerOpacity) + + MouseArea { + anchors.fill: parent + onClicked: PanelService.closeOverlayLauncher() + } + } + + // Shadow for launcher panel + NDropShadow { + source: launcherPanel + anchors.fill: launcherPanel + autoPaddingEnabled: true + } + + // Launcher panel with position-based anchoring + Item { + id: launcherPanel + width: Math.round(Math.max(parent.width * 0.25, launcherWindow.listPanelWidth + Style.margin2L * 2)) + height: Math.round(Math.max(parent.height * 0.5, 600 * Style.uiScaleRatio)) + clip: false + + // Entrance animation + opacity: 0 + transformOrigin: { + if (touchingTop && touchingLeft) + return Item.TopLeft; + if (touchingTop && touchingRight) + return Item.TopRight; + if (touchingBottom && touchingLeft) + return Item.BottomLeft; + if (touchingBottom && touchingRight) + return Item.BottomRight; + if (touchingTop) + return Item.Top; + if (touchingBottom) + return Item.Bottom; + if (touchingLeft) + return Item.Left; + if (touchingRight) + return Item.Right; + return Item.Center; + } + + Component.onCompleted: { + opacity = 1; + } + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + // Horizontal positioning + anchors.horizontalCenter: (panelPosition === "center" || panelPosition.endsWith("_center")) ? parent.horizontalCenter : undefined + anchors.left: panelPosition.endsWith("_left") ? parent.left : undefined + anchors.right: panelPosition.endsWith("_right") ? parent.right : undefined + + // Vertical positioning + anchors.verticalCenter: (panelPosition === "center" || panelPosition.startsWith("center_")) ? parent.verticalCenter : undefined + anchors.top: panelPosition.startsWith("top_") ? parent.top : undefined + anchors.bottom: panelPosition.startsWith("bottom_") ? parent.bottom : undefined + + // Margins - only add bar clearance on the bar's edge + anchors.leftMargin: barPosition === "left" ? barThickness : 0 + anchors.rightMargin: barPosition === "right" ? barThickness : 0 + anchors.topMargin: barPosition === "top" ? barThickness : 0 + anchors.bottomMargin: barPosition === "bottom" ? barThickness : 0 + + // Edge detection - based on position setting and bar location + readonly property bool touchingLeft: panelPosition.endsWith("_left") && barPosition !== "left" + readonly property bool touchingRight: panelPosition.endsWith("_right") && barPosition !== "right" + readonly property bool touchingTop: panelPosition.startsWith("top_") && barPosition !== "top" + readonly property bool touchingBottom: panelPosition.startsWith("bottom_") && barPosition !== "bottom" + + // Corner states based on edge touching + // State 0: Normal rounded, State 1: Horizontal inversion, State 2: Vertical inversion + readonly property int topLeftCornerState: { + if (touchingLeft && touchingTop) + return 0; + if (touchingLeft) + return 2; + if (touchingTop) + return 1; + return 0; + } + readonly property int topRightCornerState: { + if (touchingRight && touchingTop) + return 0; + if (touchingRight) + return 2; + if (touchingTop) + return 1; + return 0; + } + readonly property int bottomLeftCornerState: { + if (touchingLeft && touchingBottom) + return 0; + if (touchingLeft) + return 2; + if (touchingBottom) + return 1; + return 0; + } + readonly property int bottomRightCornerState: { + if (touchingRight && touchingBottom) + return 0; + if (touchingRight) + return 2; + if (touchingBottom) + return 1; + return 0; + } + + // Background with inverted corners - extends beyond panel for inverted corners + Shape { + id: panelShape + // Extend shape to allow inverted corners to render outside panel bounds + x: -radius + y: -radius + width: launcherPanel.width + radius * 2 + height: launcherPanel.height + radius * 2 + visible: panelW > 0 && panelH > 0 + opacity: launcherPanel.opacity + layer.enabled: true + + readonly property real radius: Style.radiusL + + // Panel dimensions (for path calculations) + readonly property real panelW: launcherPanel.width + readonly property real panelH: launcherPanel.height + + // Helper functions for corner rendering + function getMultX(state) { + return state === 1 ? -1 : 1; + } + function getMultY(state) { + return state === 2 ? -1 : 1; + } + function getArcDir(multX, multY) { + return ((multX < 0) !== (multY < 0)) ? PathArc.Counterclockwise : PathArc.Clockwise; + } + + readonly property real tlMultX: getMultX(launcherPanel.topLeftCornerState) + readonly property real tlMultY: getMultY(launcherPanel.topLeftCornerState) + readonly property real trMultX: getMultX(launcherPanel.topRightCornerState) + readonly property real trMultY: getMultY(launcherPanel.topRightCornerState) + readonly property real blMultX: getMultX(launcherPanel.bottomLeftCornerState) + readonly property real blMultY: getMultY(launcherPanel.bottomLeftCornerState) + readonly property real brMultX: getMultX(launcherPanel.bottomRightCornerState) + readonly property real brMultY: getMultY(launcherPanel.bottomRightCornerState) + + ShapePath { + strokeWidth: -1 + fillColor: Qt.alpha(Color.mSurface, Color.adaptiveOpacity(Settings.data.ui.panelBackgroundOpacity)) + + // Offset by radius to account for Shape's extended bounds + startX: panelShape.radius + panelShape.radius * panelShape.tlMultX + startY: panelShape.radius + + // Top edge + PathLine { + relativeX: panelShape.panelW - panelShape.radius * panelShape.tlMultX - panelShape.radius * panelShape.trMultX + relativeY: 0 + } + // Top-right corner + PathArc { + relativeX: panelShape.radius * panelShape.trMultX + relativeY: panelShape.radius * panelShape.trMultY + radiusX: panelShape.radius + radiusY: panelShape.radius + direction: panelShape.getArcDir(panelShape.trMultX, panelShape.trMultY) + } + // Right edge + PathLine { + relativeX: 0 + relativeY: panelShape.panelH - panelShape.radius * panelShape.trMultY - panelShape.radius * panelShape.brMultY + } + // Bottom-right corner + PathArc { + relativeX: -panelShape.radius * panelShape.brMultX + relativeY: panelShape.radius * panelShape.brMultY + radiusX: panelShape.radius + radiusY: panelShape.radius + direction: panelShape.getArcDir(panelShape.brMultX, panelShape.brMultY) + } + // Bottom edge + PathLine { + relativeX: -(panelShape.panelW - panelShape.radius * panelShape.brMultX - panelShape.radius * panelShape.blMultX) + relativeY: 0 + } + // Bottom-left corner + PathArc { + relativeX: -panelShape.radius * panelShape.blMultX + relativeY: -panelShape.radius * panelShape.blMultY + radiusX: panelShape.radius + radiusY: panelShape.radius + direction: panelShape.getArcDir(panelShape.blMultX, panelShape.blMultY) + } + // Left edge + PathLine { + relativeX: 0 + relativeY: -(panelShape.panelH - panelShape.radius * panelShape.blMultY - panelShape.radius * panelShape.tlMultY) + } + // Top-left corner + PathArc { + relativeX: panelShape.radius * panelShape.tlMultX + relativeY: -panelShape.radius * panelShape.tlMultY + radiusX: panelShape.radius + radiusY: panelShape.radius + direction: panelShape.getArcDir(panelShape.tlMultX, panelShape.tlMultY) + } + } + } + + // Border + Rectangle { + anchors.fill: parent + color: "transparent" + radius: Style.radiusL + border.color: Style.boxBorderColor + border.width: Style.borderS + visible: !launcherPanel.touchingLeft && !launcherPanel.touchingRight && !launcherPanel.touchingTop && !launcherPanel.touchingBottom + } + + LauncherCore { + id: launcherCore + anchors.fill: parent + screen: windowLoader.modelData + isOpen: true + onRequestClose: PanelService.closeOverlayLauncher() + onRequestCloseImmediately: PanelService.closeOverlayLauncherImmediately() + + Component.onCompleted: PanelService.overlayLauncherCore = launcherCore + Component.onDestruction: PanelService.overlayLauncherCore = null + } + } + + // Preview Panel - positioned as sibling of launcherPanel to avoid shadow bleed + NDropShadow { + source: previewBox + anchors.fill: previewBox + autoPaddingEnabled: true + visible: previewBox.visible + z: previewBox.z - 1 + } + + NBox { + id: previewBox + visible: launcherWindow.previewActive + width: launcherWindow.previewPanelWidth + height: Math.round(400 * Style.uiScaleRatio) + forceOpaque: true + x: { + if (panelPosition.endsWith("_right")) + return launcherPanel.x - launcherWindow.previewPanelWidth - Style.marginM; + return launcherPanel.x + launcherPanel.width + Style.marginM; + } + y: { + var view = launcherCore.resultsView; + if (!view) + return launcherPanel.y + Style.marginL; + var row = launcherCore.isGridView ? Math.floor(launcherCore.selectedIndex / launcherCore.gridColumns) : launcherCore.selectedIndex; + var gridCellSize = Math.floor((launcherWindow.listPanelWidth - (2 * Style.marginXS) - ((launcherCore.targetGridColumns - 1) * Style.marginS)) / launcherCore.targetGridColumns); + var itemHeight = launcherCore.isGridView ? (gridCellSize + Style.marginXXS) : (launcherCore.entryHeight + (view.spacing || 0)); + var yPos = row * itemHeight - (view.contentY || 0); + var mapped = view.mapToItem(launcherWindow.contentItem, 0, yPos); + return Math.max(launcherPanel.y + Style.marginL, Math.min(mapped.y, launcherPanel.y + launcherPanel.height - previewBox.height - Style.marginL)); + } + + opacity: visible ? 1.0 : 0.0 + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + Behavior on y { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + Loader { + id: previewLoader + anchors.fill: parent + active: launcherWindow.previewActive + source: { + if (!active) + return ""; + var provider = launcherCore.activeProvider; + if (provider && provider.previewComponentPath) + return provider.previewComponentPath; + return ""; + } + + onLoaded: updatePreviewItem() + onItemChanged: updatePreviewItem() + + function updatePreviewItem() { + if (!item || launcherCore.selectedIndex < 0 || !launcherCore.results[launcherCore.selectedIndex]) + return; + var provider = launcherCore.activeProvider; + if (provider && provider.getPreviewData) { + item.currentItem = provider.getPreviewData(launcherCore.results[launcherCore.selectedIndex]); + } else { + item.currentItem = launcherCore.results[launcherCore.selectedIndex]; + } + } + } + } + + // Update preview when selection changes + Connections { + target: launcherCore + function onSelectedIndexChanged() { + if (previewLoader.item) + previewLoader.updatePreviewItem(); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/ApplicationsProvider.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/ApplicationsProvider.qml new file mode 100644 index 0000000..fac28e5 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/ApplicationsProvider.qml @@ -0,0 +1,654 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.Compositor +import qs.Services.System + +Item { + id: root + + property var launcher: null + property string name: I18n.tr("launcher.providers.applications") + property bool handleSearch: true + property var entries: [] + property string supportedLayouts: "both" + property bool isDefaultProvider: true // This provider handles empty search + property bool ignoreDensity: false // Apps should scale with launcher density + property bool trackUsage: true // Track usage frequency for "most used" sorting + + // Category support + property string selectedCategory: "all" + property bool showsCategories: true // Default to showing categories + property var categories: ["all", "Pinned", "AudioVideo", "Chat", "Development", "Education", "Game", "Graphics", "Network", "Office", "System", "Misc", "WebBrowser"] + property var availableCategories: ["all"] // Reactive property for available categories + + property var categoryIcons: ({ + "all": "apps", + "Pinned": "pin", + "AudioVideo": "music", + "Chat": "message-circle", + "Development": "code", + "Education": "school" // Includes Science + , + "Game": "device-gamepad", + "Graphics": "brush", + "Network": "wifi", + "Office": "file-text", + "System": "device-desktop" // Includes Settings and Utility + , + "Misc": "dots", + "WebBrowser": "world" + }) + + function getCategoryName(category) { + const names = { + "all": I18n.tr("launcher.categories.all"), + "Pinned": I18n.tr("launcher.categories.pinned"), + "AudioVideo": I18n.tr("launcher.categories.audiovideo"), + "Chat": I18n.tr("launcher.categories.chat"), + "Development": I18n.tr("launcher.categories.development"), + "Education": I18n.tr("launcher.categories.education"), + "Game": I18n.tr("launcher.categories.game"), + "Graphics": I18n.tr("launcher.categories.graphics"), + "Network": I18n.tr("common.network"), + "Office": I18n.tr("launcher.categories.office"), + "System": I18n.tr("launcher.categories.system"), + "Misc": I18n.tr("launcher.categories.misc"), + "WebBrowser": I18n.tr("launcher.categories.webbrowser") + }; + return names[category] || category; + } + + function init() { + loadApplications(); + migrateLegacyUsageKeys(); + } + + function onOpened() { + // Just update available categories in case pinned apps changed + updateAvailableCategories(); + // Default to Pinned if there are pinned apps, otherwise all + if (availableCategories.includes("Pinned")) { + selectedCategory = "Pinned"; + } else { + selectedCategory = "all"; + } + // Set category mode initially (will be updated when getResults is called) + showsCategories = true; + } + + // Reload applications when desktop entries change on disk + Connections { + target: typeof DesktopEntries !== 'undefined' ? DesktopEntries.applications : null + function onValuesChanged() { + Logger.d("ApplicationsProvider", "Desktop entries changed, reloading applications"); + loadApplications(); + } + } + + function selectCategory(category) { + selectedCategory = category; + if (launcher) { + launcher.updateResults(); + } + } + + function getAppCategories(app) { + if (!app) + return []; + + const result = []; + + if (app.categories) { + if (Array.isArray(app.categories)) { + for (let cat of app.categories) { + if (cat && cat.trim && cat.trim() !== '') { + result.push(cat.trim()); + } else if (cat && typeof cat === 'string' && cat.trim() !== '') { + result.push(cat.trim()); + } + } + } else if (typeof app.categories === 'string') { + const cats = app.categories.split(';').filter(c => c && c.trim() !== ''); + for (let cat of cats) { + const trimmed = cat.trim(); + if (trimmed && !result.includes(trimmed)) { + result.push(trimmed); + } + } + } else if (app.categories.length !== undefined) { + try { + for (let i = 0; i < app.categories.length; i++) { + const cat = app.categories[i]; + if (cat && cat.trim && typeof cat.trim === 'function' && cat.trim() !== '') { + result.push(cat.trim()); + } else if (cat && typeof cat === 'string' && cat.trim() !== '') { + result.push(cat.trim()); + } + } + } catch (e) {} + } + } + + if (app.Categories) { + const cats = app.Categories.split(';').filter(c => c && c.trim() !== ''); + for (let cat of cats) { + const trimmed = cat.trim(); + if (trimmed && !result.includes(trimmed)) { + result.push(trimmed); + } + } + } + + return result; + } + + function getAppCategory(app) { + const appCategories = getAppCategories(app); + if (appCategories.length === 0) + return null; + + const priorityCategories = ["AudioVideo", "Chat", "WebBrowser", "Game", "Development", "Graphics", "Office", "Education", "System", "Network", "Misc"]; + + for (let cat of appCategories) { + if (cat === "AudioVideo" || cat === "Audio" || cat === "Video") { + return "AudioVideo"; + } + } + + if (appCategories.includes("Chat") || appCategories.includes("InstantMessaging")) { + return "Chat"; + } + + if (appCategories.includes("WebBrowser")) { + return "WebBrowser"; + } + + // Map Science to Education + if (appCategories.includes("Science")) { + return "Education"; + } + + // Map Settings to System + if (appCategories.includes("Settings")) { + return "System"; + } + + // Map Utility to System + if (appCategories.includes("Utility")) { + return "System"; + } + + for (let priorityCat of priorityCategories) { + if (appCategories.includes(priorityCat) && root.categories.includes(priorityCat)) { + return priorityCat; + } + } + + return "Misc"; + } + + // Helper function to normalize app IDs for case-insensitive matching + function normalizeAppId(appId) { + if (!appId || typeof appId !== 'string') + return ""; + return appId.toLowerCase().trim(); + } + + // Helper function to check if an app is pinned + function isAppPinned(app) { + if (!app) + return false; + const pinnedApps = Settings.data.appLauncher.pinnedApps || []; + const appId = getAppKey(app); + const normalizedId = normalizeAppId(appId); + return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId); + } + + function appMatchesCategory(app, category) { + // Check if app matches the selected category + if (category === "all") + return true; + + // Handle Pinned category separately + if (category === "Pinned") { + return isAppPinned(app); + } + + // Get the primary category for this app (first matching standard category) + const primaryCategory = getAppCategory(app); + + // If app has no matching standard category, don't show it in any category (only in "all") + if (!primaryCategory) + return false; + + // Map Audio/Video to AudioVideo + if (category === "AudioVideo") { + const appCategories = getAppCategories(app); + // Show if app has AudioVideo, Audio, or Video + return appCategories.includes("AudioVideo") || appCategories.includes("Audio") || appCategories.includes("Video"); + } + + // Map Science to Education + if (category === "Education") { + const appCategories = getAppCategories(app); + return appCategories.includes("Education") || appCategories.includes("Science"); + } + + // Map Settings and Utility to System + if (category === "System") { + const appCategories = getAppCategories(app); + return appCategories.includes("System") || appCategories.includes("Settings") || appCategories.includes("Utility"); + } + + // Only show app in its primary category to avoid overlap + // This ensures each app appears in exactly one category tab + return category === primaryCategory; + } + + function getAvailableCategories() { + const categorySet = new Set(); + let hasAudioVideo = false; + let hasEducation = false; + let hasSystem = false; + let hasPinned = false; + + // Check if there are any pinned apps + const pinnedApps = Settings.data.appLauncher.pinnedApps || []; + if (pinnedApps.length > 0) { + // Verify that at least one pinned app exists in entries + for (let app of entries) { + if (isAppPinned(app)) { + hasPinned = true; + break; + } + } + } + + for (let app of entries) { + const appCategories = getAppCategories(app); + const primaryCategory = getAppCategory(app); + + if (appCategories.includes("AudioVideo") || appCategories.includes("Audio") || appCategories.includes("Video")) { + hasAudioVideo = true; + } else if (appCategories.includes("Education") || appCategories.includes("Science")) { + hasEducation = true; + } else if (appCategories.includes("System") || appCategories.includes("Settings") || appCategories.includes("Utility")) { + hasSystem = true; + } else if (primaryCategory && root.categories.includes(primaryCategory)) { + categorySet.add(primaryCategory); + } + } + + const result = []; + + // Add Pinned category first if there are pinned apps + if (hasPinned) { + result.push("Pinned"); + } + + result.push("all"); + + if (hasAudioVideo) { + categorySet.add("AudioVideo"); + } + if (hasEducation) { + categorySet.add("Education"); + } + if (hasSystem) { + categorySet.add("System"); + } + + for (let cat of root.categories) { + if (cat !== "all" && cat !== "Pinned" && cat !== "Misc" && categorySet.has(cat)) { + result.push(cat); + } + } + + if (categorySet.has("Misc")) { + result.push("Misc"); + } + + if (result.length === 1) { + const fallback = root.categories.filter(c => c !== "Misc"); + fallback.push("Misc"); + return fallback; + } + + return result; + } + + function loadApplications() { + if (typeof DesktopEntries === 'undefined') { + Logger.w("ApplicationsProvider", "DesktopEntries service not available"); + return; + } + + const allApps = DesktopEntries.applications.values || []; + const seen = new Map(); // Map of appId -> exec command + + entries = allApps.filter(app => { + if (!app || app.noDisplay || app.hidden) + return false; + + const appId = app.id || app.name; + const execCmd = getExecutableName(app); + + // Check if we've seen this app ID before + if (seen.has(appId)) { + const previousExec = seen.get(appId); + + // If exec is different, it's a legitimate different entry - keep it + if (previousExec !== execCmd) { + Logger.d("ApplicationsProvider", `Keeping variant of ${appId}: ${execCmd} (differs from ${previousExec})`); + // Add with modified ID to make it unique + app.id = `${appId}_${execCmd}`; + seen.set(app.id, execCmd); + return true; + } + + // Same appId AND same exec = true duplicate, skip it + Logger.d("ApplicationsProvider", `Skipping duplicate: ${appId}`); + return false; + } + + seen.set(appId, execCmd); + return true; + }).map(app => { + app.executableName = getExecutableName(app); + return app; + }); + + Logger.d("ApplicationsProvider", `Loaded ${entries.length} applications`); + updateAvailableCategories(); + } + + function updateAvailableCategories() { + availableCategories = getAvailableCategories(); + } + + Connections { + target: Settings.data.appLauncher + function onPinnedAppsChanged() { + const wasViewingPinned = selectedCategory === "Pinned"; + updateAvailableCategories(); + + // If we were viewing Pinned category and it's no longer available, switch to "all" + if (wasViewingPinned && !availableCategories.includes("Pinned")) { + selectedCategory = "all"; + } + + // Update results if we're currently viewing the Pinned category + if (selectedCategory === "Pinned" && launcher) { + launcher.updateResults(); + } else if (wasViewingPinned && selectedCategory === "all" && launcher) { + // Also update results when switching to "all" + launcher.updateResults(); + } + } + } + + function getExecutableName(app) { + if (!app) + return ""; + + // Try to get executable name from command array + if (app.command && Array.isArray(app.command) && app.command.length > 0) { + const cmd = app.command[0]; + // Extract just the executable name from the full path + const parts = cmd.split('/'); + const executable = parts[parts.length - 1]; + // Remove any arguments or parameters + return executable.split(' ')[0]; + } + + // Try to get from exec property if available + if (app.exec) { + const parts = app.exec.split('/'); + const executable = parts[parts.length - 1]; + return executable.split(' ')[0]; + } + + // Fallback to app id (desktop file name without .desktop) + if (app.id) { + return app.id.replace('.desktop', ''); + } + + return ""; + } + + function getResults(query) { + if (!entries || entries.length === 0) + return []; + + // Set category mode based on whether there's a query + const isSearching = !!(query && query.trim() !== ""); + showsCategories = !isSearching; + + // Filter by category only when NOT searching + let filteredEntries = entries; + if (!isSearching && selectedCategory && selectedCategory !== "all") { + filteredEntries = entries.filter(app => appMatchesCategory(app, selectedCategory)); + } + + if (!query || query.trim() === "") { + // Return filtered apps, optionally sorted by usage + let sorted; + if (Settings.data.appLauncher.sortByMostUsed) { + sorted = filteredEntries.slice().sort((a, b) => { + // Pinned first + const aPinned = isAppPinned(a); + const bPinned = isAppPinned(b); + if (aPinned !== bPinned) + return aPinned ? -1 : 1; + + const ua = getUsageCount(a); + const ub = getUsageCount(b); + if (ub !== ua) + return ub - ua; + return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase()); + }); + } else { + sorted = filteredEntries.slice().sort((a, b) => { + const aPinned = isAppPinned(a); + const bPinned = isAppPinned(b); + if (aPinned !== bPinned) + return aPinned ? -1 : 1; + return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase()); + }); + } + return sorted.map(app => createResultEntry(app)); + } + + // Use fuzzy search if available, fallback to simple search + if (typeof FuzzySort !== 'undefined') { + const fuzzyResults = FuzzySort.go(query, filteredEntries, { + "keys": ["name", "comment", "genericName", "executableName"], + "limit": 20 + }); + + // Sort pinned first within fuzzy results while preserving fuzzysort order otherwise + const pinned = []; + const nonPinned = []; + for (const r of fuzzyResults) { + const app = r.obj; + if (isAppPinned(app)) + pinned.push(r); + else + nonPinned.push(r); + } + return pinned.concat(nonPinned).map(result => createResultEntry(result.obj, result.score)); + } else { + // Fallback to simple search + const searchTerm = query.toLowerCase(); + return filteredEntries.filter(app => { + const name = (app.name || "").toLowerCase(); + const comment = (app.comment || "").toLowerCase(); + const generic = (app.genericName || "").toLowerCase(); + const executable = getExecutableName(app).toLowerCase(); + return name.includes(searchTerm) || comment.includes(searchTerm) || generic.includes(searchTerm) || executable.includes(searchTerm); + }).sort((a, b) => { + // Prioritize name matches, then executable matches + const aName = a.name.toLowerCase(); + const bName = b.name.toLowerCase(); + const aExecutable = getExecutableName(a).toLowerCase(); + const bExecutable = getExecutableName(b).toLowerCase(); + const aStarts = aName.startsWith(searchTerm); + const bStarts = bName.startsWith(searchTerm); + const aExecStarts = aExecutable.startsWith(searchTerm); + const bExecStarts = bExecutable.startsWith(searchTerm); + + // Prioritize name matches first + if (aStarts && !bStarts) + return -1; + if (!aStarts && bStarts) + return 1; + + // Then prioritize executable matches + if (aExecStarts && !bExecStarts) + return -1; + if (!aExecStarts && bExecStarts) + return 1; + + return aName.localeCompare(bName); + }).slice(0, 20).map(app => createResultEntry(app)); + } + } + + function createResultEntry(app, score) { + return { + "appId": getAppKey(app), + "usageKey": getAppKey(app), + "name": app.name || "Unknown", + "description": app.genericName || app.comment || "", + "icon": app.icon || "application-x-executable", + "isImage": false, + "_score": (score !== undefined ? score : 0), + "provider": root, + "onActivate": function () { + // Close the launcher/SmartPanel immediately without any animations. + // Ensures we are not preventing the future focusing of the app + launcher.closeImmediately(); + + // Defer execution to next event loop iteration to ensure panel is fully closed + Qt.callLater(() => { + Logger.d("ApplicationsProvider", `Launching: ${app.name} (App ID: ${app.id || "unknown"})`); + + const execString = (app.exec !== undefined && app.exec !== null) ? String(app.exec) : ""; + const commandArgs = Array.isArray(app.command) ? app.command : (app.command && app.command.length !== undefined) ? Array.from(app.command) : []; + let hasQuotedArgs = execString.includes("\"") || execString.includes("'"); + let hasSpaceArgs = false; + if (!hasQuotedArgs) { + hasQuotedArgs = commandArgs.some(arg => { + const text = String(arg); + return text.includes("\"") || text.includes("'"); + }); + } + if (!hasSpaceArgs) { + hasSpaceArgs = commandArgs.some(arg => String(arg).includes(" ")); + } + if (app.execute && (hasQuotedArgs || hasSpaceArgs)) { + Logger.d("ApplicationsProvider", `Detected quoted/space arguments in Exec for ${app.name}, using app.execute()`); + app.execute(); + return; + } + + if (Settings.data.appLauncher.customLaunchPrefixEnabled && Settings.data.appLauncher.customLaunchPrefix) { + // Use custom launch prefix + const prefix = Settings.data.appLauncher.customLaunchPrefix.split(" "); + Logger.d("ApplicationsProvider", `Using custom launch prefix: ${Settings.data.appLauncher.customLaunchPrefix}`); + + if (app.runInTerminal) { + const terminal = Settings.data.appLauncher.terminalCommand.split(" "); + const command = prefix.concat(terminal.concat(app.command)); + Logger.d("ApplicationsProvider", `Executing command (with prefix and terminal): ${command.join(" ")}`); + Quickshell.execDetached(command); + } else { + const command = prefix.concat(app.command); + Logger.d("ApplicationsProvider", `Executing command (with prefix): ${command.join(" ")}`); + Quickshell.execDetached(command); + } + } else { + if (app.runInTerminal) { + Logger.d("ApplicationsProvider", "Executing terminal app manually: " + app.name); + const terminal = Settings.data.appLauncher.terminalCommand.split(" "); + const command = terminal.concat(app.command); + Logger.d("ApplicationsProvider", "Executing command (manual terminal): " + command.join(" ")); + CompositorService.spawn(command); + } else if (app.command && app.command.length > 0) { + Logger.d("ApplicationsProvider", "Executing command: " + app.command.join(" ")); + CompositorService.spawn(app.command); + } else if (app.execute) { + Logger.d("ApplicationsProvider", "Calling app.execute() for: " + app.name); + app.execute(); + } else { + Logger.w("ApplicationsProvider", `Could not launch: ${app.name}. No valid launch method.`); + } + } + }); + } + }; + } + + // ------------------------- + // Item actions for launcher delegate + function getItemActions(item) { + if (!item || !item.appId) + return []; + return [ + { + "icon": isAppPinned({ + "id": item.appId + }) ? "unpin" : "pin", + "tooltip": isAppPinned({ + "id": item.appId + }) ? I18n.tr("common.unpin") : I18n.tr("common.pin"), + "action": function () { + togglePin(item.appId); + } + } + ]; + } + + function togglePin(appId) { + if (!appId) + return; + const normalizedId = normalizeAppId(appId); + let arr = (Settings.data.appLauncher.pinnedApps || []).slice(); + const idx = arr.findIndex(pinnedId => normalizeAppId(pinnedId) === normalizedId); + if (idx >= 0) + arr.splice(idx, 1); + else + arr.push(appId); + Settings.data.appLauncher.pinnedApps = arr; + } + + // ------------------------- + // Usage tracking helpers + function getAppKey(app) { + if (app && app.id) + return String(app.id); + if (app && app.command && app.command.join) + return app.command.join(" "); + return String(app && app.name ? app.name : "unknown"); + } + + function getUsageCount(app) { + return ShellState.getLauncherUsageCount(getAppKey(app)); + } + + // Migrate legacy command-based usage keys to canonical app-id keys at startup + function migrateLegacyUsageKeys() { + for (let i = 0; i < entries.length; i++) { + const app = entries[i]; + if (app && app.id && app.command && app.command.join) { + const key = getAppKey(app); + const legacyKey = app.command.join(" "); + if (legacyKey !== key && ShellState.getLauncherUsageCount(legacyKey) > 0) { + ShellState.migrateLauncherUsage(legacyKey, key); + Logger.d("ApplicationsProvider", `Migrated usage: "${legacyKey}" → "${key}"`); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/CalculatorProvider.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/CalculatorProvider.qml new file mode 100644 index 0000000..18b73d1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/CalculatorProvider.qml @@ -0,0 +1,77 @@ +import QtQuick +import Quickshell +import "../../../../Helpers/AdvancedMath.js" as AdvancedMath +import qs.Commons +import qs.Services.Keyboard +import qs.Services.UI + +Item { + id: root + + // Provider metadata + property string name: I18n.tr("launcher.providers.calculator") + property var launcher: null + property string iconMode: Settings.data.appLauncher.iconMode + property bool handleSearch: true // Contribute to regular search + property string supportedLayouts: "list" + + // Initialize provider + function init() { + Logger.d("CalculatorProvider", "Initialized"); + } + + // Get search results - evaluates math expressions inline + function getResults(query) { + if (!query) + return []; + + const trimmed = query.trim(); + if (!trimmed || !isMathExpression(trimmed)) + return []; + + try { + const result = AdvancedMath.evaluate(trimmed); + const formattedResult = AdvancedMath.formatResult(result); + return [ + { + "name": formattedResult, + "description": I18n.tr("launcher.providers.calculator-press-enter-to-copy"), + "icon": iconMode === "tabler" ? "calculator" : "accessories-calculator", + "isTablerIcon": true, + "isImage": false, + "provider": root, + "onActivate": function () { + // Copy result to clipboard via xclip + Quickshell.execDetached(["sh", "-c", "echo -n '" + formattedResult.replace(/'/g, "'\\''") + "' | wl-copy"]); + ToastService.showNotice(I18n.tr("common.copied-to-clipboard"), formattedResult); + if (launcher) + launcher.close(); + } + } + ]; + } catch (error) { + return []; + } + } + + // Check if a string is a valid math expression + function isMathExpression(expr) { + // Allow: digits, operators, parentheses, decimal points, whitespace, letters (for functions), commas + if (!/^[\d\s\+\-\*\/\(\)\.\%\^a-zA-Z,]+$/.test(expr)) + return false; + + // Must contain at least one operator OR a function call (letter followed by parenthesis) + if (!/[+\-*/%\^]/.test(expr) && !/[a-zA-Z]\s*\(/.test(expr)) + return false; + + // Reject if ends with an operator (incomplete expression) + if (/[+\-*/%\^]\s*$/.test(expr)) + return false; + + // Reject if it's just letters (would match app names) + if (/^[a-zA-Z\s]+$/.test(expr)) + return false; + + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/ClipboardProvider.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/ClipboardProvider.qml new file mode 100644 index 0000000..ddb8aa1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/ClipboardProvider.qml @@ -0,0 +1,457 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.Keyboard +import qs.Services.Noctalia + +Item { + id: root + + // Provider metadata + property string name: I18n.tr("launcher.providers.clipboard") + property var launcher: null + property string iconMode: Settings.data.appLauncher.iconMode + property string supportedLayouts: "list" // List view for clipboard content + property bool wrapNavigation: false // Don't wrap at end of list + + // Provider capabilities + property bool handleSearch: false // Don't handle regular search + + // Preview support + property bool hasPreview: Settings.data.appLauncher.enableClipPreview + property string previewComponentPath: "./ClipboardPreview.qml" + + // Image handling - expose revision for reactive updates in delegates + readonly property int imageRevision: ClipboardService.revision + + // Categories + property var availableCategories: Settings.data.appLauncher.enableClipboardChips ? ["All", "Images", "Links", "Files", "Code", "Colors"] : [] + property string selectedCategory: "All" + + function selectCategory(cat) { + if (selectedCategory !== cat) { + selectedCategory = cat; + if (launcher) { + launcher.updateResults(); + } + } + } + + property var categoryIcons: { + "All": iconMode === "tabler" ? "border-all" : "view-grid", + "Images": iconMode === "tabler" ? "photo" : "image", + "Links": iconMode === "tabler" ? "link" : "insert-link", + "Files": iconMode === "tabler" ? "file" : "text-x-generic", + "Code": iconMode === "tabler" ? "code" : "text-x-script", + "Colors": iconMode === "tabler" ? "palette" : "color-picker" + } + + // Internal state + property bool isWaitingForData: false + property bool gotResults: false + property string lastSearchText: "" + + // Listen for clipboard data updates + Connections { + target: ClipboardService + function onListCompleted() { + if (gotResults && (lastSearchText === searchText)) { + // Do not update results after the first fetch. + // This will avoid the list resetting every 2seconds when the service updates. + return; + } + // Refresh results if we're waiting for data or if clipboard plugin is active + if (isWaitingForData || (launcher && launcher.searchText.startsWith(">clip"))) { + isWaitingForData = false; + gotResults = true; + if (launcher) { + launcher.updateResults(); + } + } + } + function onActiveChanged() { + // When active state changes (e.g. dependency check completes), refresh results + if (ClipboardService.active && launcher && launcher.searchText.startsWith(">clip")) { + isWaitingForData = true; + gotResults = false; + ClipboardService.list(100); + } + } + } + + // Initialize provider + function init() { + Logger.d("ClipboardProvider", "Initialized"); + // Pre-load clipboard data if service is active + if (ClipboardService.active) { + ClipboardService.list(100); + } + } + + // Called when launcher opens + function onOpened() { + isWaitingForData = true; + gotResults = false; + lastSearchText = ""; + + // Refresh clipboard history when launcher opens + if (ClipboardService.active) { + ClipboardService.list(100); + } + } + + // Check if this provider handles the command + function handleCommand(searchText) { + return searchText.startsWith(">clip"); + } + + // Return available commands when user types ">" + function commands() { + return [ + { + "name": ">clip", + "description": I18n.tr("launcher.providers.clipboard-search-description"), + "icon": iconMode === "tabler" ? "clipboard" : "diodon", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () { + launcher.setSearchText(">clip "); + } + }, + { + "name": ">clip clear", + "description": I18n.tr("launcher.providers.clipboard-clear-description"), + "icon": iconMode === "tabler" ? "trash" : "user-trash", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () { + ClipboardService.wipeAll(); + launcher.close(); + } + } + ]; + } + + // Get search results + function getResults(searchText) { + if (!searchText.startsWith(">clip")) { + return []; + } + + lastSearchText = searchText; + const results = []; + const query = searchText.slice(5).trim(); + + // Check if clipboard service is not active + if (!ClipboardService.active) { + // If dependency check hasn't completed yet, show loading instead of disabled + if (!ClipboardService.dependencyChecked) { + return [ + { + "name": I18n.tr("launcher.providers.clipboard-loading"), + "description": I18n.tr("launcher.providers.emoji-loading-description"), + "icon": iconMode === "tabler" ? "refresh" : "view-refresh", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () {} + } + ]; + } + return [ + { + "name": I18n.tr("launcher.providers.clipboard-history-disabled"), + "description": I18n.tr("launcher.providers.clipboard-history-disabled-description"), + "icon": iconMode === "tabler" ? "refresh" : "view-refresh", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () {} + } + ]; + } + + // Special command: clear + if (query === "clear") { + return [ + { + "name": I18n.tr("launcher.providers.clipboard-clear-history"), + "description": I18n.tr("launcher.providers.clipboard-clear-description-full"), + "icon": iconMode === "tabler" ? "trash" : "user-trash", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () { + ClipboardService.wipeAll(); + launcher.close(); + } + } + ]; + } + + // Show loading state if data is being loaded + if (ClipboardService.loading || isWaitingForData) { + return [ + { + "name": I18n.tr("launcher.providers.clipboard-loading"), + "description": I18n.tr("launcher.providers.emoji-loading-description"), + "icon": iconMode === "tabler" ? "refresh" : "view-refresh", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () {} + } + ]; + } + + // Get clipboard items + const items = ClipboardService.items || []; + + // If no items and we haven't tried loading yet, trigger a load + if (items.count === 0 && !ClipboardService.loading) { + isWaitingForData = true; + ClipboardService.list(100); + return [ + { + "name": I18n.tr("launcher.providers.clipboard-loading"), + "description": I18n.tr("launcher.providers.emoji-loading-description"), + "icon": iconMode === "tabler" ? "refresh" : "view-refresh", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () {} + } + ]; + } + + // Search clipboard items + const searchTerm = query.toLowerCase(); + + const now = Date.now() / 1000; + + const catMap = { + "Images": "image", + "Links": "link", + "Files": "file", + "Code": "code", + "Colors": "color" + }; + + // Filter and format results + items.forEach(function (item) { + // Category filter + if (Settings.data.appLauncher.enableClipboardChips && root.selectedCategory !== "All") { + if (item.contentType !== catMap[root.selectedCategory]) { + return; + } + } + + const preview = (item.preview || "").toLowerCase(); + + // Skip if search term doesn't match + if (searchTerm && preview.indexOf(searchTerm) === -1) { + return; + } + + const firstSeen = ClipboardService.firstSeenById[item.id] || now; + + // Format the result based on type + let entry; + if (item.isImage) { + entry = formatImageEntry(item, firstSeen); + } else { + entry = formatTextEntry(item, firstSeen); + } + + // Add activation handler + entry.onActivate = function () { + if (Settings.data.appLauncher.autoPasteClipboard) { + launcher.closeImmediately(); + Qt.callLater(() => { + ClipboardService.pasteFromClipboard(item.id, item.mime); + }); + } else { + ClipboardService.copyToClipboard(item.id); + launcher.close(); + } + }; + + results.push(entry); + }); + + // Show empty state if no results + if (results.length === 0) { + results.push({ + "name": searchTerm ? "No matching clipboard items" : "Clipboard is empty", + "description": searchTerm ? `No items containing "${query}"` : "Copy something to see it here", + "icon": iconMode === "tabler" ? "clipboard" : "text-x-generic", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () {// Do nothing + } + }); + } + + //Logger.i("ClipboardPlugin", `Returning ${results.length} results for query: "${query}"`) + return results; + } + + function formatImageEntry(item, firstSeen) { + const meta = ClipboardService.parseImageMeta(item.preview); + const timeStr = Time.formatRelativeTime(new Date(firstSeen * 1000)); + let desc = meta ? `${meta.fmt} • ${meta.size}` : item.mime || "Image data"; + if (timeStr) + desc += ` • ${timeStr}`; + + return { + "name": meta ? `Image ${meta.w}×${meta.h}` : "Image", + "description": desc, + "icon": iconMode === "tabler" ? "photo" : "image", + "isTablerIcon": true, + "isImage": true, + "imageWidth": meta ? meta.w : 0, + "imageHeight": meta ? meta.h : 0, + "clipboardId": item.id, + "mime": item.mime, + "preview": item.preview, + "provider": root + }; + } + + function formatTextEntry(item, firstSeen) { + const preview = (item.preview || "").trim(); + const lines = preview.split('\n').filter(l => l.trim()); + + let title = lines[0] || "Empty text"; + if (title.length > 60) { + title = title.substring(0, 57) + "..."; + } + + let description = ""; + if (lines.length > 1) { + description = lines[1]; + if (description.length > 80) { + description = description.substring(0, 77) + "..."; + } + } else { + // Preview is truncated at ~100 chars, so we can't show exact count + if (preview.length >= 100) { + description = I18n.tr("toast.clipboard.long-text"); + } else { + const chars = preview.length; + const words = preview.split(/\s+/).length; + description = `${chars} characters, ${words} word${words !== 1 ? 's' : ''}`; + } + } + + const timeStr = Time.formatRelativeTime(new Date(firstSeen * 1000)); + if (timeStr) + description += ` • ${timeStr}`; + + let defaultIcon = iconMode === "tabler" ? "clipboard" : "text-x-generic"; + let colorHex = ""; + if (Settings.data.appLauncher.enableClipboardSmartIcons) { + if (item.contentType === "link") + defaultIcon = iconMode === "tabler" ? "link" : "insert-link"; + else if (item.contentType === "file") + defaultIcon = iconMode === "tabler" ? "file" : "text-x-generic"; + else if (item.contentType === "code") + defaultIcon = iconMode === "tabler" ? "code" : "text-x-script"; + else if (item.contentType === "color") { + defaultIcon = iconMode === "tabler" ? "palette" : "color-picker"; + colorHex = preview; + } + } + + return { + "name": title, + "description": description, + "icon": defaultIcon, + "isTablerIcon": true, + "isImage": false, + "clipboardId": item.id, + "preview": preview, + "contentType": item.contentType, + "colorHex": colorHex, + "provider": root + }; + } + + function getImageForItem(clipboardId) { + return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null; + } + + // ------------------------- + // Item actions for launcher delegate + function getItemActions(item) { + if (!item || !item.clipboardId) + return []; + + var actions = []; + + // Annotation tool for images + if (item.isImage && Settings.data.appLauncher.screenshotAnnotationTool !== "") { + actions.push({ + "icon": "pencil", + "tooltip": I18n.tr("tooltips.open-annotation-tool"), + "action": function () { + var tool = Settings.data.appLauncher.screenshotAnnotationTool; + Quickshell.execDetached(["sh", "-c", "cliphist decode " + item.clipboardId + " | " + tool]); + if (launcher) + launcher.close(); + } + }); + } + + // Delete action + actions.push({ + "icon": "trash", + "tooltip": I18n.tr("launcher.providers.clipboard-delete"), + "action": function () { + deleteItem(item); + } + }); + + return actions; + } + + function canDeleteItem(item) { + return item && !!item.clipboardId; + } + + function deleteItem(item) { + if (!item || !item.clipboardId) + return; + + // Set provider state before deletion so refresh works + gotResults = false; + isWaitingForData = true; + lastSearchText = launcher ? launcher.searchText : ""; + + // Delete the item + ClipboardService.deleteById(String(item.clipboardId)); + } + + // Prepare item for display (handles image decoding) + function prepareItem(item) { + if (item && item.isImage && item.clipboardId) { + if (!ClipboardService.getImageData(item.clipboardId)) { + ClipboardService.decodeToDataUrl(item.clipboardId, item.mime, null); + } + } + } + + // Get image URL for item (used by delegates) + function getImageUrl(item) { + if (!item || !item.clipboardId) + return ""; + return ClipboardService.getImageData(item.clipboardId) || ""; + } + + // Get preview data for the preview panel + function getPreviewData(item) { + if (!item) + return null; + return { + "clipboardId": item.clipboardId, + "isImage": item.isImage, + "mime": item.mime, + "preview": item.preview + }; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/CommandProvider.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/CommandProvider.qml new file mode 100644 index 0000000..12be174 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/CommandProvider.qml @@ -0,0 +1,51 @@ +import QtQuick +import Quickshell +import qs.Commons + +Item { + property var launcher: null + property string name: I18n.tr("common.command") + property string iconMode: Settings.data.appLauncher.iconMode + + function handleCommand(query) { + return query.startsWith(">cmd"); + } + + function commands() { + return [ + { + "name": ">cmd", + "description": I18n.tr("launcher.providers.command-description"), + "icon": iconMode === "tabler" ? "terminal" : "utilities-terminal", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () { + launcher.setSearchText(">cmd "); + } + } + ]; + } + + function getResults(query) { + if (!query.startsWith(">cmd")) + return []; + + let expression = query.substring(4).trim(); + return [ + { + "name": I18n.tr("common.command"), + "description": I18n.tr("launcher.providers.command-description"), + "icon": iconMode === "tabler" ? "terminal" : "utilities-terminal", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () { + launcher.closeImmediately(); + Qt.callLater(() => { + Logger.d("CommandProvider", "Executing shell command: " + expression); + Quickshell.execDetached(["sh", "-c", expression]); + }); + } + } + ]; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/EmojiProvider.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/EmojiProvider.qml new file mode 100644 index 0000000..ae64ef9 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/EmojiProvider.qml @@ -0,0 +1,165 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.Keyboard + +Item { + id: root + + // Provider metadata + property string name: I18n.tr("launcher.providers.emoji") + property var launcher: null + property string iconMode: Settings.data.appLauncher.iconMode + property bool handleSearch: false + property string supportedLayouts: "grid" // Only grid layout for emoji + property int preferredGridColumns: 7 // More columns for compact emoji display + property real preferredGridCellRatio: 1.15 // Slightly taller than wide to accommodate label + property bool supportsAutoPaste: true // Emoji can be auto-pasted + property bool ignoreDensity: false // Emoji should scale with launcher density + + property string selectedCategory: "recent" + property bool showsCategories: true // Default to showing categories + + // Empty state message for category view + readonly property string emptyBrowsingMessage: selectedCategory === "recent" ? I18n.tr("launcher.providers.emoji-no-recent") : "" + + property var categoryIcons: ({ + "all": "apps", + "recent": "clock", + "people": "user", + "animals": "paw", + "nature": "leaf", + "food": "apple", + "activity": "run", + "travel": "plane", + "objects": "home", + "symbols": "star", + "flags": "flag" + }) + + property var categories: ["all", "recent", "people", "animals", "nature", "food", "activity", "travel", "objects", "symbols", "flags"] + + function getCategoryName(category) { + const names = { + "all": I18n.tr("launcher.categories.all"), + "recent": I18n.tr("launcher.categories.emoji-recent"), + "people": I18n.tr("launcher.categories.emoji-people"), + "animals": I18n.tr("launcher.categories.emoji-animals"), + "nature": I18n.tr("launcher.categories.emoji-nature"), + "food": I18n.tr("launcher.categories.emoji-food"), + "activity": I18n.tr("launcher.categories.emoji-activity"), + "travel": I18n.tr("launcher.categories.emoji-travel"), + "objects": I18n.tr("launcher.categories.emoji-objects"), + "symbols": I18n.tr("launcher.categories.emoji-symbols"), + "flags": I18n.tr("launcher.categories.emoji-flags") + }; + return names[category] || category; + } + + // Force update results when emoji service loads + Connections { + target: EmojiService + function onLoadedChanged() { + if (EmojiService.loaded && root.launcher) { + root.launcher.updateResults(); + } + } + } + + // Initialize provider + function init() { + Logger.d("EmojiProvider", "Initialized"); + } + + function selectCategory(category) { + selectedCategory = category; + if (launcher) { + launcher.updateResults(); + } + } + + function onOpened() { + // Always reset to "recent" category when opening + selectedCategory = "recent"; + } + + // Check if this provider handles the command + function handleCommand(searchText) { + return searchText.startsWith(">emoji"); + } + + // Return available commands when user types ">" + function commands() { + return [ + { + "name": ">emoji", + "description": I18n.tr("launcher.providers.emoji-search-description"), + "icon": iconMode === "tabler" ? "mood-smile" : "face-smile", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () { + launcher.setSearchText(">emoji "); + } + } + ]; + } + + // Get search results + function getResults(searchText) { + if (!searchText.startsWith(">emoji")) { + return []; + } + + if (!EmojiService.loaded) { + return [ + { + "name": I18n.tr("launcher.providers.emoji-loading"), + "description": I18n.tr("launcher.providers.emoji-loading-description"), + "icon": iconMode === "tabler" ? "refresh" : "view-refresh", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () {} + } + ]; + } + + var query = searchText.slice(6).trim(); + var emojis = []; + + if (query !== "" || selectedCategory === "all") { + emojis = EmojiService.search(query); + } else { + emojis = EmojiService.getEmojisByCategory(selectedCategory); + } + return emojis.map(formatEmojiEntry); + } + + // Format an emoji entry for the results list + function formatEmojiEntry(emoji) { + let title = emoji.name; + let description = emoji.keywords.join(", "); + + if (emoji.category) { + description += " • Category: " + emoji.category; + } + + const emojiChar = emoji.emoji; + + return { + "name": title, + "description": description, + "icon": null, + "isImage": false, + "displayString": emojiChar, + "autoPasteText": emojiChar, + "provider": root, + "onAutoPaste": function () { + EmojiService.recordUsage(emojiChar); + }, + "onActivate": function () { + EmojiService.copy(emojiChar); + launcher.close(); + } + }; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/SessionProvider.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/SessionProvider.qml new file mode 100644 index 0000000..13a0f76 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/SessionProvider.qml @@ -0,0 +1,194 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.Compositor +import qs.Services.UI + +Item { + id: root + + // Provider metadata + property string name: I18n.tr("tooltips.session-menu") + property var launcher: null + property bool handleSearch: Settings.data.appLauncher.enableSessionSearch + property string supportedLayouts: "list" + property string iconMode: Settings.data.appLauncher.iconMode + + // Session actions with search keywords + readonly property var sessionActions: [ + { + "action": "lock", + "labelKey": "common.lock", + "icon": iconMode === "tabler" ? "lock" : "system-lock-screen", + "keywords": ["lock", "screen", "secure"] + }, + { + "action": "suspend", + "labelKey": "common.suspend", + "icon": iconMode === "tabler" ? "suspend" : "system-suspend", + "keywords": ["suspend", "sleep", "standby"] + }, + { + "action": "hibernate", + "labelKey": "common.hibernate", + "icon": iconMode === "tabler" ? "hibernate" : "system-suspend-hibernate", + "keywords": ["hibernate", "disk"] + }, + { + "action": "reboot", + "labelKey": "common.reboot", + "icon": iconMode === "tabler" ? "reboot" : "system-reboot", + "keywords": ["reboot", "restart", "reload"] + }, + { + "action": "rebootToUefi", + "labelKey": "common.reboot-to-uefi", + "icon": iconMode === "tabler" ? "reboot" : "system-reboot", + "keywords": ["reboot", "uefi", "firmware", "bios"] + }, + { + "action": "userspaceReboot", + "labelKey": "common.userspace-reboot", + "icon": iconMode === "tabler" ? "rotate" : "system-reboot", + "keywords": ["reboot", "restart", "soft", "userspace"] + }, + { + "action": "logout", + "labelKey": "common.logout", + "icon": iconMode === "tabler" ? "logout" : "system-log-out", + "keywords": ["logout", "sign out", "exit", "leave"] + }, + { + "action": "shutdown", + "labelKey": "common.shutdown", + "icon": iconMode === "tabler" ? "shutdown" : "system-shutdown", + "keywords": ["shutdown", "power off", "turn off", "poweroff"] + } + ] + + function init() { + Logger.d("SessionProvider", "Initialized"); + } + + function getEnabledActions() { + var powerOptions = Settings.data.sessionMenu.powerOptions || []; + var enabledSet = {}; + + for (var i = 0; i < powerOptions.length; i++) { + if (powerOptions[i].enabled) { + enabledSet[powerOptions[i].action] = powerOptions[i]; + } + } + + var enabled = []; + for (var j = 0; j < sessionActions.length; j++) { + var action = sessionActions[j]; + if (enabledSet[action.action]) { + enabled.push({ + "action": action.action, + "labelKey": action.labelKey, + "icon": action.icon, + "keywords": action.keywords, + "command": enabledSet[action.action].command || "" + }); + } + } + + return enabled; + } + + function getResults(query) { + if (!query) + return []; + + var trimmed = query.trim(); + if (!trimmed || trimmed.length < 2) + return []; + + var enabledActions = getEnabledActions(); + if (enabledActions.length === 0) + return []; + + // Build searchable items with resolved translations + var items = []; + for (var i = 0; i < enabledActions.length; i++) { + var action = enabledActions[i]; + var label = I18n.tr(action.labelKey); + items.push({ + "action": action.action, + "icon": action.icon, + "label": label, + "command": action.command, + "searchText": [label.toLowerCase()].concat(action.keywords).join(" ") + }); + } + + var results = FuzzySort.go(trimmed, items, { + "keys": ["label", "searchText"], + "limit": 6 + }); + + var launcherItems = []; + for (var j = 0; j < results.length; j++) { + var entry = results[j].obj; + var score = results[j].score; + + launcherItems.push({ + "name": entry.label, + "description": I18n.tr("tooltips.session-menu"), + "icon": entry.icon, + "isTablerIcon": true, + "isImage": false, + "_score": score - 1, + "provider": root, + "onActivate": createActivateHandler(entry.action, entry.command) + }); + } + + return launcherItems; + } + + function createActivateHandler(action, command) { + return function () { + if (launcher) + launcher.close(); + + // Execute via Qt.callLater, but reference only singletons + // (root may be destroyed after launcher.close() unloads the panel) + Qt.callLater(() => { + switch (action) { + case "lock": + if (PanelService.lockScreen && !PanelService.lockScreen.active) { + PanelService.lockScreen.active = true; + } + break; + case "suspend": + if (Settings.data.general.lockOnSuspend) { + CompositorService.lockAndSuspend(); + } else { + CompositorService.suspend(); + } + break; + case "hibernate": + CompositorService.hibernate(); + break; + case "reboot": + CompositorService.reboot(); + break; + case "rebootToUefi": + CompositorService.rebootToUefi(); + break; + case "userspaceReboot": + CompositorService.userspaceReboot(); + break; + case "logout": + CompositorService.logout(); + break; + case "shutdown": + CompositorService.shutdown(); + break; + } + }); + }; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/SettingsProvider.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/SettingsProvider.qml new file mode 100644 index 0000000..8993d2a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/SettingsProvider.qml @@ -0,0 +1,161 @@ +import QtQuick +import qs.Commons +import qs.Services.UI + +Item { + id: root + + // Provider metadata + property string name: I18n.tr("common.settings") + property var launcher: null + property bool handleSearch: Settings.data.appLauncher.enableSettingsSearch + property string supportedLayouts: "list" + property string iconMode: Settings.data.appLauncher.iconMode + + function init() { + Logger.d("SettingsProvider", "Initialized"); + } + + // Check if this provider handles the command + function handleCommand(searchText) { + return searchText.startsWith(">settings"); + } + + // Return available commands when user types ">" + function commands() { + return [ + { + "name": ">settings", + "description": I18n.tr("launcher.providers.settings-search-description"), + "icon": iconMode === "tabler" ? "settings" : "preferences-system", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () { + launcher.setSearchText(">settings "); + } + } + ]; + } + + function getResults(query) { + if (!query || SettingsSearchService.searchIndex.length === 0) + return []; + + var trimmed = query.trim(); + + // Handle command mode: ">settings" or ">settings " + var isCommandMode = trimmed.startsWith(">settings"); + if (isCommandMode) { + // Extract search term after ">settings " + var searchTerm = trimmed.substring(9).trim(); + // In command mode, show all settings if no search term + if (searchTerm.length === 0) { + return getAllSettings(); + } + trimmed = searchTerm; + } else { + // Regular search mode - require at least 2 chars + if (!trimmed || trimmed.length < 2) + return []; + } + + // Build searchable items with resolved translations, filtering out invisible entries + let items = []; + for (let j = 0; j < SettingsSearchService.searchIndex.length; j++) { + const entry = SettingsSearchService.searchIndex[j]; + if (!SettingsSearchService.isEntryVisible(entry)) + continue; + items.push({ + "labelKey": entry.labelKey, + "descriptionKey": entry.descriptionKey, + "widget": entry.widget, + "tab": entry.tab, + "tabLabel": entry.tabLabel, + "subTab": entry.subTab, + "subTabLabel": entry.subTabLabel || null, + "label": I18n.tr(entry.labelKey), + "description": entry.descriptionKey ? I18n.tr(entry.descriptionKey) : "", + "subTabName": entry.subTabLabel ? I18n.tr(entry.subTabLabel) : "" + }); + } + + const results = FuzzySort.go(trimmed, items, { + "keys": ["label", "subTabName", "description"], + "limit": 10, + "scoreFn": function (r) { + const labelScore = r[0].score; + const subTabScore = r[1].score * 1.5; + const descScore = r[2].score; + return Math.max(labelScore, subTabScore, descScore); + } + }); + + let launcherItems = []; + for (let i = 0; i < results.length; i++) { + const entry = results[i].obj; + const score = results[i].score; + const tabName = I18n.tr(entry.tabLabel); + const subTabName = entry.subTabName || ""; + const breadcrumb = subTabName ? (tabName + " › " + subTabName) : tabName; + + launcherItems.push({ + "name": entry.label, + "description": breadcrumb, + "icon": iconMode === "tabler" ? "settings" : "preferences-system", + "isTablerIcon": true, + "isImage": false, + "_score": score - 2, + "provider": root, + "onActivate": createActivateHandler(entry) + }); + } + + return launcherItems; + } + + function getAllSettings() { + var launcherItems = []; + + for (var j = 0; j < SettingsSearchService.searchIndex.length; j++) { + var entry = SettingsSearchService.searchIndex[j]; + if (!SettingsSearchService.isEntryVisible(entry)) + continue; + var label = I18n.tr(entry.labelKey); + var tabName = I18n.tr(entry.tabLabel); + var subTabName = entry.subTabLabel ? I18n.tr(entry.subTabLabel) : ""; + var breadcrumb = subTabName ? (tabName + " › " + subTabName) : tabName; + + launcherItems.push({ + "name": label, + "description": breadcrumb, + "icon": iconMode === "tabler" ? "settings" : "preferences-system", + "isTablerIcon": true, + "isImage": false, + "_score": 0, + "provider": root, + "onActivate": createActivateHandler({ + "labelKey": entry.labelKey, + "descriptionKey": entry.descriptionKey, + "widget": entry.widget, + "tab": entry.tab, + "tabLabel": entry.tabLabel, + "subTab": entry.subTab, + "subTabLabel": entry.subTabLabel || null + }) + }); + } + + return launcherItems; + } + + function createActivateHandler(entry) { + return function () { + if (launcher) + launcher.close(); + + Qt.callLater(() => { + SettingsPanelService.openToEntry(entry, launcher.screen); + }); + }; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/WindowsProvider.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/WindowsProvider.qml new file mode 100644 index 0000000..7627560 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Launcher/Providers/WindowsProvider.qml @@ -0,0 +1,159 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.Compositor + +Item { + id: root + + property string name: I18n.tr("common.windows") + property var launcher: null + property bool handleSearch: Settings.data.appLauncher.enableWindowsSearch + property string supportedLayouts: "list" + property string iconMode: Settings.data.appLauncher.iconMode + + function init() { + Logger.d("WindowsProvider", "Initialized"); + } + + // Check if this provider handles the command + function handleCommand(searchText) { + return searchText.startsWith(">win"); + } + + // Return available commands when user types ">" + function commands() { + return [ + { + "name": ">win", + "description": I18n.tr("launcher.providers.windows-search-description"), + "icon": iconMode === "tabler" ? "app-window" : "preferences-system-windows", + "isTablerIcon": true, + "isImage": false, + "onActivate": function () { + launcher.setSearchText(">win "); + } + } + ]; + } + + function getResults(query) { + if (!query) + return []; + + var trimmed = query.trim(); + + // Handle command mode: ">win" or ">win " + var isCommandMode = trimmed.startsWith(">win"); + if (isCommandMode) { + // Extract search term after ">win " + var searchTerm = trimmed.substring(4).trim(); + // In command mode, show all windows if no search term + if (searchTerm.length === 0) { + return getAllWindows(); + } + trimmed = searchTerm; + } else { + // Regular search mode - require at least 2 chars + if (trimmed.length < 2) + return []; + } + + var items = []; + + // Collect all windows from CompositorService + for (var i = 0; i < CompositorService.windows.count; i++) { + var win = CompositorService.windows.get(i); + items.push({ + "id": win.id, + "title": win.title || "", + "appId": win.appId || "", + "workspaceId": win.workspaceId, + "isFocused": win.isFocused, + "searchText": (win.title + " " + win.appId).toLowerCase() + }); + } + + // Fuzzy search on title and appId + var results = FuzzySort.go(trimmed, items, { + "keys": ["title", "appId"], + "limit": 10 + }); + + // Map to launcher items + var launcherItems = []; + for (var j = 0; j < results.length; j++) { + var entry = results[j].obj; + var score = results[j].score; + + // Get icon name from DesktopEntry if available, otherwise use appId + var iconName = entry.appId; + var appEntry = ThemeIcons.findAppEntry(entry.appId); + if (appEntry && appEntry.icon) { + iconName = appEntry.icon; + } + + launcherItems.push({ + "name": entry.title || entry.appId, + "description": entry.appId, + "icon": iconName || "application-x-executable", + "isTablerIcon": false, + "badgeIcon": "app-window", + "_score": score, + "provider": root, + "windowId": entry.id, + "onActivate": createActivateHandler(entry) + }); + } + + return launcherItems; + } + + function getAllWindows() { + var launcherItems = []; + + for (var i = 0; i < CompositorService.windows.count; i++) { + var win = CompositorService.windows.get(i); + + var iconName = win.appId; + var appEntry = ThemeIcons.findAppEntry(win.appId); + if (appEntry && appEntry.icon) { + iconName = appEntry.icon; + } + + launcherItems.push({ + "name": win.title || win.appId, + "description": win.appId, + "icon": iconName || "application-x-executable", + "isTablerIcon": false, + "badgeIcon": "app-window", + "_score": 0, + "provider": root, + "windowId": win.id, + "onActivate": createActivateHandler({ + "id": win.id + }) + }); + } + + return launcherItems; + } + + function createActivateHandler(windowEntry) { + return function () { + if (launcher) + launcher.close(); + + Qt.callLater(() => { + // Find the actual window object to pass to focusWindow + for (var i = 0; i < CompositorService.windows.count; i++) { + var win = CompositorService.windows.get(i); + if (win.id === windowEntry.id) { + CompositorService.focusWindow(win); + break; + } + } + }); + }; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Media/MediaPlayerPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Media/MediaPlayerPanel.qml new file mode 100644 index 0000000..400738f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Media/MediaPlayerPanel.qml @@ -0,0 +1,576 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.MainScreen +import qs.Services.Media +import qs.Services.UI +import qs.Widgets +import qs.Widgets.AudioSpectrum + +SmartPanel { + id: root + + preferredWidth: Math.round((root.isSideBySide ? 480 : 360) * Style.uiScaleRatio) + // Fallback only; SmartPanel uses panelContent.contentPreferredHeight when set. + preferredHeight: Math.round((root.compactMode ? 240 : 400) * Style.uiScaleRatio) + + property var mediaMiniSettings: { + const widget = BarService.lookupWidget("MediaMini", screen?.name); + return widget ? widget.widgetSettings : null; + } + + function refreshMediaMiniSettings() { + const widget = BarService.lookupWidget("MediaMini", screen?.name); + root.mediaMiniSettings = widget ? widget.widgetSettings : null; + } + + Connections { + target: BarService + function onActiveWidgetsChanged() { + root.refreshMediaMiniSettings(); + } + } + + Connections { + target: Settings + function onSettingsSaved() { + root.refreshMediaMiniSettings(); + } + } + + readonly property string visualizerType: (mediaMiniSettings && mediaMiniSettings.visualizerType !== undefined) ? mediaMiniSettings.visualizerType : "linear" + readonly property bool showArtistFirst: !!(mediaMiniSettings && mediaMiniSettings.showArtistFirst !== undefined ? mediaMiniSettings.showArtistFirst : true) + readonly property bool showAlbumArt: !!(mediaMiniSettings && mediaMiniSettings.panelShowAlbumArt !== undefined ? mediaMiniSettings.panelShowAlbumArt : true) + readonly property bool showVisualizer: !!(mediaMiniSettings && mediaMiniSettings.showVisualizer !== undefined ? mediaMiniSettings.showVisualizer : true) + readonly property bool compactMode: !!(mediaMiniSettings && mediaMiniSettings.compactMode !== undefined ? mediaMiniSettings.compactMode : false) + readonly property string scrollingMode: (mediaMiniSettings && mediaMiniSettings.scrollingMode !== undefined) ? mediaMiniSettings.scrollingMode : "hover" + + readonly property bool isSideBySide: root.compactMode && root.showAlbumArt + + readonly property bool needsSpectrum: root.showVisualizer && root.visualizerType !== "" && root.visualizerType !== "none" && root.isPanelOpen + + onNeedsSpectrumChanged: { + if (root.needsSpectrum) { + SpectrumService.registerComponent("mediaplayerpanel"); + } else { + SpectrumService.unregisterComponent("mediaplayerpanel"); + } + } + + Component.onCompleted: { + if (root.needsSpectrum) { + SpectrumService.registerComponent("mediaplayerpanel"); + } + } + + Component.onDestruction: { + SpectrumService.unregisterComponent("mediaplayerpanel"); + } + + panelContent: Item { + id: playerContent + anchors.fill: parent + + // implicitHeight + mainLayout vertical anchors.margins (each Style.marginL). + readonly property real contentPreferredHeight: { + const m = mainLayout.implicitHeight + 2 * Style.marginL; + const scale = Style.uiScaleRatio; + if (root.compactMode) + return Math.max(m, 240 * scale); + if (!root.showAlbumArt) + return Math.max(m, 300 * scale); + return Math.max(m, 260 * scale); + } + + property Component visualizerSource: { + switch (root.visualizerType) { + case "linear": + return linearComponent; + case "mirrored": + return mirroredComponent; + case "wave": + return waveComponent; + default: + return null; + } + } + + ColumnLayout { + id: mainLayout + anchors.fill: parent + anchors.margins: Style.marginL + spacing: root.compactMode ? Style.marginL : Style.marginM + z: 1 + + NBox { + Layout.fillWidth: true + Layout.preferredHeight: headerRow.implicitHeight + Style.margin2M + + RowLayout { + id: headerRow + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + NIcon { + icon: "music" + pointSize: Style.fontSizeL + color: Color.mPrimary + } + + NText { + text: I18n.tr("common.media-player") + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeL + color: Color.mOnSurface + Layout.fillWidth: true + } + + Rectangle { + radius: Style.radiusS + color: playerSelectorMouse.containsMouse ? Color.mPrimary : "transparent" + implicitWidth: playerRow.implicitWidth + Style.marginM + implicitHeight: Style.baseWidgetSize * 0.8 + visible: MediaService.getAvailablePlayers().length > 1 + + RowLayout { + id: playerRow + anchors.centerIn: parent + spacing: Style.marginXS + + NText { + text: MediaService.currentPlayer ? MediaService.currentPlayer.identity : "Select Player" + pointSize: Style.fontSizeXS + color: playerSelectorMouse.containsMouse ? Color.mOnPrimary : Color.mOnSurfaceVariant + } + NIcon { + icon: "chevron-down" + pointSize: Style.fontSizeXS + color: playerSelectorMouse.containsMouse ? Color.mOnPrimary : Color.mOnSurfaceVariant + } + } + + MouseArea { + id: playerSelectorMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: playerContextMenu.open() + } + + Popup { + id: playerContextMenu + x: 0 + y: parent.height + width: 160 + padding: Style.marginS + + background: Rectangle { + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Style.borderS + radius: Style.iRadiusM + } + + contentItem: ColumnLayout { + spacing: 0 + Repeater { + model: MediaService.getAvailablePlayers() + delegate: Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 30 + color: "transparent" + + Rectangle { + anchors.fill: parent + color: itemMouse.containsMouse ? Color.mPrimary : "transparent" + radius: Style.iRadiusS + } + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginS + + NIcon { + visible: MediaService.currentPlayer && MediaService.currentPlayer.identity === modelData.identity + icon: "check" + color: itemMouse.containsMouse ? Color.mOnPrimary : Color.mPrimary + pointSize: Style.fontSizeS + } + + NText { + text: modelData.identity + pointSize: Style.fontSizeS + color: itemMouse.containsMouse ? Color.mOnPrimary : Color.mOnSurface + Layout.fillWidth: true + elide: Text.ElideRight + } + } + + MouseArea { + id: itemMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + MediaService.currentPlayer = modelData; + playerContextMenu.close(); + } + } + } + } + } + } + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: root.close() + } + } + } + + NBox { + Layout.fillWidth: true + Layout.preferredHeight: mediaContentGrid.implicitHeight + Style.marginM + Style.marginM + + // Visualizer background for content area + Loader { + anchors.fill: parent + z: 0 + active: !!(root.needsSpectrum && !root.showAlbumArt) + sourceComponent: visualizerSource + } + + GridLayout { + id: mediaContentGrid + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: root.compactMode ? Style.marginL : Style.marginM + anchors.rightMargin: root.compactMode ? Style.marginL : Style.marginM + anchors.topMargin: Style.marginM + anchors.bottomMargin: Style.marginM + columns: root.isSideBySide ? 2 : 1 + columnSpacing: Style.marginL + rowSpacing: root.compactMode ? Style.marginL : Style.marginM + + // Album Art (Vertical in normal, Horizontal in compact) + Item { + id: albumArtItem + readonly property real compactArtSize: Math.round(110 * Style.uiScaleRatio) + readonly property bool artSizeKnown: artSizeProbe.status === Image.Ready && artSizeProbe.sourceSize.width > 0 && artSizeProbe.sourceSize.height > 0 + readonly property real artAspectRatio: artSizeKnown ? artSizeProbe.sourceSize.width / artSizeProbe.sourceSize.height : 1 + // Non-compact: height from width÷aspect so grid implicit height does not depend on panel height (no layout loop). + readonly property real artBoxW: root.compactMode ? compactArtSize : Math.max(parent.width, 1) + readonly property real artBoxH: root.compactMode ? compactArtSize : (artBoxW / Math.max(artAspectRatio, 0.001)) + readonly property real fitArtW: artBoxW / artBoxH > artAspectRatio ? artBoxH * artAspectRatio : artBoxW + readonly property real fitArtH: artBoxW / artBoxH > artAspectRatio ? artBoxH : artBoxW / artAspectRatio + + Layout.preferredWidth: fitArtW + Layout.preferredHeight: fitArtH + Layout.minimumWidth: fitArtW + Layout.maximumWidth: fitArtW + Layout.minimumHeight: fitArtH + Layout.maximumHeight: fitArtH + Layout.fillWidth: false + Layout.fillHeight: false + Layout.alignment: Qt.AlignHCenter + visible: root.showAlbumArt + + Image { + id: artSizeProbe + visible: false + asynchronous: true + source: MediaService.trackArtUrl + } + + NImageRounded { + anchors.fill: parent + radius: root.compactMode ? Style.radiusM : Style.radiusL + imagePath: MediaService.trackArtUrl + imageFillMode: Image.PreserveAspectCrop + fallbackIcon: "disc" + fallbackIconSize: root.compactMode ? Style.fontSizeXXXL * 3 : Style.fontSizeXXXL * 6 + borderWidth: 0 + } + + Loader { + anchors.fill: parent + anchors.margins: Style.marginS + z: 2 + active: !!(root.needsSpectrum && root.showAlbumArt) + sourceComponent: visualizerSource + } + } + + ColumnLayout { + id: controlsLayout + Layout.preferredWidth: root.compactMode ? -1 : albumArtItem.width + Layout.fillWidth: root.compactMode + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: root.compactMode + spacing: root.compactMode ? Style.marginXS : Style.marginS + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + NScrollText { + Layout.fillWidth: true + maxWidth: parent.width + text: { + if (root.showArtistFirst) { + return MediaService.trackArtist || (MediaService.trackAlbum || "Unknown Artist"); + } else { + return MediaService.trackTitle || "No Media"; + } + } + + scrollMode: { + if (root.scrollingMode === "always") + return NScrollText.ScrollMode.Always; + if (root.scrollingMode === "hover") + return NScrollText.ScrollMode.Hover; + return NScrollText.ScrollMode.Never; + } + fadeExtent: 0.01 + fadeCornerRadius: Style.radiusM + + delegate: NText { + pointSize: root.compactMode ? Style.fontSizeL : Style.fontSizeXL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + horizontalAlignment: root.isSideBySide ? Text.AlignLeft : Text.AlignHCenter + elide: Text.ElideNone + wrapMode: Text.NoWrap + } + } + + NScrollText { + Layout.fillWidth: true + maxWidth: parent.width + text: { + if (root.showArtistFirst) { + return MediaService.trackTitle || "No Media"; + } else { + return MediaService.trackArtist || (MediaService.trackAlbum || "Unknown Artist"); + } + } + + scrollMode: { + if (root.scrollingMode === "always") + return NScrollText.ScrollMode.Always; + if (root.scrollingMode === "hover") + return NScrollText.ScrollMode.Hover; + return NScrollText.ScrollMode.Never; + } + fadeExtent: 0.01 + fadeCornerRadius: Style.radiusM + + delegate: NText { + pointSize: root.compactMode ? Style.fontSizeS : Style.fontSizeM + color: Color.mOnSurfaceVariant + horizontalAlignment: root.isSideBySide ? Text.AlignLeft : Text.AlignHCenter + elide: Text.ElideNone + wrapMode: Text.NoWrap + } + } + } + + Item { + id: progressWrapper + visible: (MediaService.currentPlayer && MediaService.trackLength > 0) + Layout.fillWidth: true + Layout.preferredHeight: progressColumn.implicitHeight + + property real localSeekRatio: -1 + property real lastSentSeekRatio: -1 + property real seekEpsilon: 0.01 + property real progressRatio: { + if (!MediaService.currentPlayer || MediaService.trackLength <= 0) + return 0; + const r = MediaService.currentPosition / MediaService.trackLength; + if (isNaN(r) || !isFinite(r)) + return 0; + return Math.max(0, Math.min(1, r)); + } + + Timer { + id: seekDebounce + interval: 75 + repeat: false + onTriggered: { + if (MediaService.isSeeking && progressWrapper.localSeekRatio >= 0) { + const next = Math.max(0, Math.min(1, progressWrapper.localSeekRatio)); + if (progressWrapper.lastSentSeekRatio < 0 || Math.abs(next - progressWrapper.lastSentSeekRatio) >= progressWrapper.seekEpsilon) { + MediaService.seekByRatio(next); + progressWrapper.lastSentSeekRatio = next; + } + } + } + } + + ColumnLayout { + id: progressColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: 2 + + Item { + Layout.fillWidth: true + Layout.preferredHeight: root.compactMode ? (Style.baseWidgetSize * 0.4) : (Style.baseWidgetSize * 0.5) + + NSlider { + id: progressSlider + anchors.fill: parent + from: 0 + to: 1 + stepSize: 0 + snapAlways: false + enabled: MediaService.trackLength > 0 && MediaService.canSeek + heightRatio: 0.4 + + value: (!MediaService.isSeeking) ? progressWrapper.progressRatio : (progressWrapper.localSeekRatio >= 0 ? progressWrapper.localSeekRatio : 0) + + onMoved: { + progressWrapper.localSeekRatio = value; + seekDebounce.restart(); + } + onPressedChanged: { + if (pressed) { + MediaService.isSeeking = true; + progressWrapper.localSeekRatio = value; + MediaService.seekByRatio(value); + progressWrapper.lastSentSeekRatio = value; + } else { + seekDebounce.stop(); + MediaService.seekByRatio(value); + MediaService.isSeeking = false; + progressWrapper.localSeekRatio = -1; + progressWrapper.lastSentSeekRatio = -1; + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 0 + + NText { + text: MediaService.positionString || "0:00" + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + visible: progressWrapper.visible + } + + Item { + Layout.fillWidth: true + Layout.minimumWidth: 0 + } + + NText { + text: MediaService.lengthString || "0:00" + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignRight + visible: progressWrapper.visible + } + } + } + } + + Item { + Layout.preferredHeight: root.isSideBySide ? Style.marginM : Style.marginS + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: root.isSideBySide ? Style.marginL : Style.marginXL + + NIconButton { + icon: "media-prev" + baseSize: root.compactMode ? (Style.baseWidgetSize * 0.9) : (Style.baseWidgetSize * 1.2) + onClicked: MediaService.previous() + } + + Rectangle { + implicitWidth: root.compactMode ? (Style.baseWidgetSize * 1.3) : (Style.baseWidgetSize * 1.8) + implicitHeight: root.compactMode ? (Style.baseWidgetSize * 1.3) : (Style.baseWidgetSize * 1.8) + radius: root.compactMode ? Style.iRadiusM : Style.iRadiusL + color: Color.mPrimary + + NIcon { + anchors.centerIn: parent + icon: MediaService.isPlaying ? "media-pause" : "media-play" + pointSize: root.compactMode ? Style.fontSizeL : Style.fontSizeXXL + color: Color.mOnPrimary + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: parent.color = Color.mPrimary + onClicked: MediaService.playPause() + } + } + + NIconButton { + icon: "media-next" + baseSize: root.compactMode ? (Style.baseWidgetSize * 0.9) : (Style.baseWidgetSize * 1.2) + onClicked: MediaService.next() + } + } + } + } + } + } + } + + // Visualizer Components + Component { + id: linearComponent + NLinearSpectrum { + width: parent.width - Style.marginS + height: 20 + values: SpectrumService.values + fillColor: Color.mPrimary + opacity: 0.4 + barPosition: Settings.getBarPositionForScreen(root.screen?.name) + mirrored: Settings.data.audio.spectrumMirrored + } + } + + Component { + id: mirroredComponent + NMirroredSpectrum { + width: parent.width - Style.marginS + height: parent.height - Style.marginS + values: SpectrumService.values + fillColor: Color.mPrimary + opacity: 0.4 + mirrored: Settings.data.audio.spectrumMirrored + } + } + + Component { + id: waveComponent + NWaveSpectrum { + width: parent.width - Style.marginS + height: parent.height - Style.marginS + values: SpectrumService.values + fillColor: Color.mPrimary + opacity: 0.4 + mirrored: Settings.data.audio.spectrumMirrored + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Network/NetworkPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Network/NetworkPanel.qml new file mode 100644 index 0000000..2918c61 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Network/NetworkPanel.qml @@ -0,0 +1,950 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import "../Settings/Tabs/Connections" as WifiPrefs +import qs.Commons +import qs.Modules.MainScreen +import qs.Modules.Panels.Settings +import qs.Services.Networking +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +SmartPanel { + id: root + + preferredWidth: Math.round(440 * Style.uiScaleRatio) + preferredHeight: Math.round(500 * Style.uiScaleRatio) + + // Info panel collapsed by default, view mode persisted in settings + // Ethernet details UI state (mirrors Wi‑Fi info behavior) + property bool ethernetInfoExpanded: false + property bool ethernetDetailsGrid: (Settings.data.network.wifiDetailsViewMode === "grid") + property int ipVersion: 4 + + // Unified panel view mode: "wifi" | "ethernet" (persisted) + property string panelViewMode: "wifi" + property bool panelViewPersistEnabled: false + + onPanelViewModeChanged: { + // Persist last view (only after restored the initial value) + if (panelViewPersistEnabled) { + Settings.data.network.networkPanelView = panelViewMode; + } + if (panelViewMode === "wifi") { + ethernetInfoExpanded = false; + if (NetworkService.wifiEnabled && !NetworkService.scanningActive) { + NetworkService.scan(); + NetworkService.refreshActiveWifiDetails(); + } + } else { + if (NetworkService.ethernetConnected) { + NetworkService.refreshActiveEthernetDetails(); + } + } + } + + // Effectively visible tracking + readonly property bool effectivelyVisible: root.visible && Window.window && Window.window.visible + + onEffectivelyVisibleChanged: { + if (effectivelyVisible) { + SystemStatService.registerComponent("network-panel"); + if (NetworkService.wifiEnabled && !NetworkService.scanningActive) { + NetworkService.scan(); + NetworkService.refreshActiveWifiDetails(); + } + if (NetworkService.ethernetConnected) { + NetworkService.refreshActiveEthernetDetails(); + } + } else { + SystemStatService.unregisterComponent("network-panel"); + } + } + + onOpened: { + // Restore last view if valid, otherwise choose what's available (prefer Wi‑Fi when both exist) + if (Settings.data.network.networkPanelView) { + const last = Settings.data.network.networkPanelView; + if (last === "ethernet" && NetworkService.ethernetAvailable) { + panelViewMode = "ethernet"; + } else { + panelViewMode = "wifi"; + } + } else { + if (!NetworkService.wifiEnabled && NetworkService.ethernetAvailable) { + panelViewMode = "ethernet"; + } else { + panelViewMode = "wifi"; + } + } + panelViewPersistEnabled = true; + } + + panelContent: Rectangle { + color: "transparent" + + property real contentPreferredHeight: Math.min(root.preferredHeight, mainColumn.implicitHeight + Style.margin2L) + + ColumnLayout { + id: mainColumn + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + // Header + NBox { + Layout.fillWidth: true + Layout.preferredHeight: header.implicitHeight + Style.margin2M + + ColumnLayout { + id: header + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + RowLayout { + NIcon { + id: modeIcon + icon: panelViewMode === "wifi" ? (NetworkService.wifiEnabled ? "wifi" : "wifi-off") : (NetworkService.ethernetAvailable ? (NetworkService.ethernetConnected ? "ethernet" : "ethernet") : "ethernet-off") + pointSize: Style.fontSizeXXL + color: { + if (panelViewMode === "wifi") { + return NetworkService.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant; + } else { + return NetworkService.ethernetConnected ? Color.mPrimary : Color.mOnSurfaceVariant; + } + } + MouseArea { + anchors.fill: parent + hoverEnabled: true + onClicked: { + if (panelViewMode === "wifi") { + if (NetworkService.ethernetAvailable) { + panelViewMode = "ethernet"; + } else { + TooltipService.show(parent, I18n.tr("wifi.panel.no-ethernet-devices")); + } + } else { + panelViewMode = "wifi"; + } + } + onEntered: TooltipService.show(parent, panelViewMode === "wifi" ? I18n.tr("common.wifi") : I18n.tr("common.ethernet")) + onExited: TooltipService.hide() + } + } + + NLabel { + label: panelViewMode === "wifi" ? I18n.tr("common.wifi") : I18n.tr("common.ethernet") + Layout.fillWidth: true + } + + NToggle { + id: wifiSwitch + visible: panelViewMode === "wifi" + checked: NetworkService.wifiEnabled + enabled: !NetworkService.airplaneModeEnabled && NetworkService.wifiAvailable + onToggled: checked => NetworkService.setWifiEnabled(checked) + baseSize: Style.baseWidgetSize * 0.7 // Slightly smaller + } + + NIconButton { + icon: "settings" + tooltipText: I18n.tr("tooltips.open-settings") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 0, screen) + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: root.close() + } + } + + // Mode switch (Wi‑Fi / Ethernet) + NTabBar { + id: modeTabBar + visible: NetworkService.ethernetAvailable && NetworkService.wifiAvailable + margins: Style.marginS + Layout.fillWidth: true + spacing: Style.marginM + distributeEvenly: true + currentIndex: root.panelViewMode === "wifi" ? 0 : 1 + onCurrentIndexChanged: { + root.panelViewMode = (currentIndex === 0) ? "wifi" : "ethernet"; + } + + NTabButton { + text: I18n.tr("common.wifi") + tabIndex: 0 + checked: modeTabBar.currentIndex === 0 + } + + NTabButton { + text: I18n.tr("common.ethernet") + tabIndex: 1 + checked: modeTabBar.currentIndex === 1 + } + } + } + } + + // Unified scrollable content (Wi‑Fi or Ethernet view) + ColumnLayout { + id: wifiSectionContainer + visible: true + Layout.fillWidth: true + spacing: Style.marginM + + // Error message + Rectangle { + visible: panelViewMode === "wifi" && NetworkService.lastError.length > 0 + Layout.fillWidth: true + Layout.preferredHeight: errorRow.implicitHeight + Style.margin2M + color: Qt.alpha(Color.mError, 0.1) + radius: Style.radiusS + border.width: Style.borderS + border.color: Color.mError + + RowLayout { + id: errorRow + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + NIcon { + icon: "warning" + pointSize: Style.fontSizeL + color: Color.mError + } + + NText { + text: NetworkService.lastError + color: Color.mError + pointSize: Style.fontSizeS + wrapMode: Text.Wrap + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + baseSize: Style.baseWidgetSize * 0.6 + onClicked: NetworkService.lastError = "" + } + } + } + + // Unified scrollable content + NScrollView { + id: contentScroll + Layout.fillWidth: true + Layout.fillHeight: true + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + reserveScrollbarSpace: false + gradientColor: Color.mSurface + + ColumnLayout { + id: contentColumn + width: contentScroll.availableWidth + spacing: Style.marginM + + // Wi‑Fi disabled state + NBox { + id: disabledBox + visible: panelViewMode === "wifi" && !NetworkService.wifiEnabled + Layout.fillWidth: true + Layout.preferredHeight: disabledColumn.implicitHeight + Style.margin2M + + ColumnLayout { + id: disabledColumn + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginL + + Item { + Layout.fillHeight: true + } + + NIcon { + icon: "wifi-off" + pointSize: 48 + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: I18n.tr("wifi.panel.disabled") + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: I18n.tr("wifi.panel.enable-message") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + Item { + Layout.fillHeight: true + } + } + } + + // Scanning state (show when no networks and we haven't had any yet) + NBox { + id: scanningBox + visible: panelViewMode === "wifi" && NetworkService.wifiEnabled && Object.keys(NetworkService.networks).length === 0 && NetworkService.scanningActive + Layout.fillWidth: true + Layout.preferredHeight: scanningColumn.implicitHeight + Style.margin2M + + ColumnLayout { + id: scanningColumn + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginL + + Item { + Layout.fillHeight: true + } + + NBusyIndicator { + running: visible && root.effectivelyVisible + color: Color.mPrimary + size: Style.baseWidgetSize + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: I18n.tr("wifi.panel.searching") + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + Item { + Layout.fillHeight: true + } + } + } + + // Empty state when no networks (only show after we've had networks before, meaning a real empty result) + NBox { + id: emptyBox + visible: panelViewMode === "wifi" && NetworkService.wifiEnabled && Object.keys(NetworkService.networks).length === 0 && !NetworkService.scanningActive + Layout.fillWidth: true + Layout.preferredHeight: emptyColumn.implicitHeight + Style.margin2M + + ColumnLayout { + id: emptyColumn + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginL + + Item { + Layout.fillHeight: true + } + + NIcon { + icon: "wifi-question" + pointSize: 48 + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: I18n.tr("wifi.panel.no-networks") + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + Item { + Layout.fillHeight: true + } + } + } + + // Networks list container (Wi‑Fi) + ColumnLayout { + id: networksList + visible: panelViewMode === "wifi" && NetworkService.wifiEnabled && Object.keys(NetworkService.networks).length > 0 + width: parent.width + spacing: Style.marginM + + WifiPrefs.WifiSubTab { + showOnlyLists: true + } + } + + // Ethernet view + NBox { + id: ethernetSection + visible: panelViewMode === "ethernet" + Layout.fillWidth: true + Layout.preferredHeight: ethernetColumn.implicitHeight + Style.margin2M + + ColumnLayout { + id: ethernetColumn + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + // Section label + NLabel { + label: I18n.tr("wifi.panel.available-interfaces") + visible: (NetworkService.ethernetInterfaces && NetworkService.ethernetInterfaces.length > 0) + } + + // Empty state when no Ethernet devices + ColumnLayout { + id: emptyEthColumn + + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter + Layout.preferredHeight: emptyEthColumn.implicitHeight + Style.margin2M + visible: !(NetworkService.ethernetInterfaces && NetworkService.ethernetInterfaces.length > 0) + spacing: Style.marginL + + Item { + Layout.fillHeight: true + } + + NIcon { + icon: "ethernet-off" + pointSize: 48 + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: I18n.tr("wifi.panel.no-ethernet-devices") + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + Item { + Layout.fillHeight: true + } + } + + // Interfaces list + ColumnLayout { + id: ethIfacesList + visible: NetworkService.ethernetInterfaces && NetworkService.ethernetInterfaces.length > 0 + width: parent.width + spacing: Style.marginXS + + Repeater { + model: NetworkService.ethernetInterfaces || [] + delegate: NBox { + id: ethItem + + function getContentColors(defaultColors = [Color.mSurface, Color.mOnSurface]) { + if (modelData.connected) { + return [Color.mPrimary, Color.mOnPrimary]; + } + return defaultColors; + } + + Layout.fillWidth: true + Layout.leftMargin: Style.marginXS + Layout.rightMargin: Style.marginXS + implicitHeight: ethItemColumn.implicitHeight + Style.margin2M + radius: Style.radiusM + forceOpaque: true + color: ethItem.getContentColors()[0] + + ColumnLayout { + id: ethItemColumn + width: parent.width - Style.margin2M + x: Style.marginM + y: Style.marginM + spacing: Style.marginS + + // Main row matching Wi‑Fi card style + // Click handling for the whole header row is provided by a sibling MouseArea + // anchored to this row (defined right after this RowLayout). + RowLayout { + id: ethHeaderRow + Layout.fillWidth: true + spacing: Style.marginS + + NIcon { + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + horizontalAlignment: Text.AlignLeft + icon: NetworkService.getIcon(true) + pointSize: Style.fontSizeXXL + color: ethItem.getContentColors()[1] + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + NText { + text: modelData.connectionName || modelData.ifname + pointSize: Style.fontSizeM + font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium + color: ethItem.getContentColors()[1] + elide: Text.ElideRight + Layout.fillWidth: true + } + + RowLayout { + spacing: Style.marginXS + + NText { + text: { + if (modelData.connected) { + switch (NetworkService.networkConnectivity) { + case "full": + return I18n.tr("common.connected"); + case "limited": + case "unknown": + return I18n.tr("wifi.panel.internet-limited"); + case "portal": + return I18n.tr("wifi.panel.action-required"); + default: + return NetworkService.networkConnectivity; + } + } + return I18n.tr("common.disconnected"); + } + pointSize: Style.fontSizeXXS + color: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy) + } + + // Network speed indicators (visible when connected and speed > 0) + RowLayout { + visible: (modelData.connected && NetworkService.networkConnectivity === "full") && (SystemStatService.rxSpeed > 0 || SystemStatService.txSpeed > 0) + spacing: 2 + Layout.leftMargin: Style.marginXS + Layout.fillWidth: false + + NIcon { + visible: SystemStatService.rxSpeed > 0 + icon: "arrow-down" + pointSize: Style.fontSizeXXS + color: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy) + } + + NText { + visible: SystemStatService.rxSpeed > 0 + text: SystemStatService.formatSpeed(SystemStatService.rxSpeed) + pointSize: Style.fontSizeXXS + color: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy) + elide: Text.ElideNone + } + + Item { + visible: SystemStatService.rxSpeed > 0 && SystemStatService.txSpeed > 0 + width: Style.marginXS + height: 1 + } + + NIcon { + visible: SystemStatService.txSpeed > 0 + icon: "arrow-up" + pointSize: Style.fontSizeXXS + color: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy) + } + + NText { + visible: SystemStatService.txSpeed > 0 + text: SystemStatService.formatSpeed(SystemStatService.txSpeed) + pointSize: Style.fontSizeXXS + color: Qt.alpha(ethItem.getContentColors()[1], Style.opacityHeavy) + elide: Text.ElideNone + } + } + } + } + + // Info button on the right + NIconButton { + icon: "info" + tooltipText: I18n.tr("common.info") + baseSize: Style.baseWidgetSize * 0.75 + colorBg: Color.mSurfaceVariant + colorFg: Color.mOnSurface + colorBorder: "transparent" + colorBorderHover: "transparent" + enabled: true + visible: NetworkService.ethernetConnected + onClicked: { + if (NetworkService.activeEthernetIf === modelData.ifname && ethernetInfoExpanded) { + ethernetInfoExpanded = false; + return; + } + if (NetworkService.activeEthernetIf !== modelData.ifname) { + NetworkService.activeEthernetIf = modelData.ifname; + NetworkService.activeEthernetDetailsTimestamp = 0; + } + ethernetInfoExpanded = true; + NetworkService.refreshActiveEthernetDetails(); + } + } + } + + // Click handling without anchors in a Layout-managed item + TapHandler { + target: ethHeaderRow + onTapped: { + if (NetworkService.activeEthernetIf === modelData.ifname && ethernetInfoExpanded) { + ethernetInfoExpanded = false; + return; + } + if (NetworkService.activeEthernetIf !== modelData.ifname) { + NetworkService.activeEthernetIf = modelData.ifname; + NetworkService.activeEthernetDetailsTimestamp = 0; + } + ethernetInfoExpanded = true; + NetworkService.refreshActiveEthernetDetails(); + } + } + + // Inline Ethernet details + Rectangle { + id: ethInfoInline + visible: ethernetInfoExpanded && NetworkService.activeEthernetIf === modelData.ifname + Layout.fillWidth: true + color: Color.mSurfaceVariant + radius: Style.radiusXS + border.width: Style.borderS + border.color: Style.boxBorderColor + implicitHeight: ethInfoGrid.implicitHeight + Style.margin2S + clip: true + Layout.topMargin: Style.marginXS + + // Grid/List toggle + NIconButton { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginS + icon: ethernetDetailsGrid ? "layout-list" : "layout-grid" + tooltipText: ethernetDetailsGrid ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view") + baseSize: Style.baseWidgetSize * 0.65 + onClicked: { + ethernetDetailsGrid = !ethernetDetailsGrid; + Settings.data.network.wifiDetailsViewMode = ethernetDetailsGrid ? "grid" : "list"; + } + z: 1 + } + + GridLayout { + id: ethInfoGrid + anchors.fill: parent + anchors.margins: Style.marginS + anchors.rightMargin: Style.baseWidgetSize + flow: ethernetDetailsGrid ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: ethernetDetailsGrid ? 3 : 6 + columns: ethernetDetailsGrid ? 2 : 1 + columnSpacing: Style.marginM + rowSpacing: Style.marginXS + onColumnsChanged: { + if (ethInfoGrid.forceLayout) { + Qt.callLater(function () { + ethInfoGrid.forceLayout(); + }); + } + } + + // --- Item 1: Interface --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "ethernet" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.alignment: Qt.AlignVCenter + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, I18n.tr("wifi.panel.interface")) + onExited: TooltipService.hide() + } + } + NText { + text: (NetworkService.activeEthernetDetails.ifname && NetworkService.activeEthernetDetails.ifname.length > 0) ? NetworkService.activeEthernetDetails.ifname : (NetworkService.activeEthernetIf || "-") + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + wrapMode: ethernetDetailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere + elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone + maximumLineCount: ethernetDetailsGrid ? 1 : 6 + clip: true + + // Click-to-copy Ethernet interface name + MouseArea { + anchors.fill: parent + // Guard against undefined by normalizing to empty strings + enabled: ((NetworkService.activeEthernetDetails.ifname || "").length > 0) || ((NetworkService.activeEthernetIf || "").length > 0) + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) + onExited: TooltipService.hide() + onClicked: { + const value = (NetworkService.activeEthernetDetails.ifname && NetworkService.activeEthernetDetails.ifname.length > 0) ? NetworkService.activeEthernetDetails.ifname : (NetworkService.activeEthernetIf || ""); + if (value.length > 0) { + Quickshell.execDetached(["wl-copy", value]); + ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("common.copied-to-clipboard"), "ethernet"); + } + } + } + } + } + + // --- Item 2: Hardware Address --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "hash" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.alignment: Qt.AlignVCenter + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, I18n.tr("bluetooth.panel.device-address")) + onExited: TooltipService.hide() + } + } + NText { + text: NetworkService.activeEthernetDetails.hwAddr || "-" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + wrapMode: ethernetDetailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere + elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone + maximumLineCount: ethernetDetailsGrid ? 1 : 6 + clip: true + + MouseArea { + anchors.fill: parent + enabled: (NetworkService.activeEthernetDetails.hwAddr || "").length > 0 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) + onExited: TooltipService.hide() + onClicked: { + const value = NetworkService.activeEthernetDetails.hwAddr || ""; + if (value.length > 0) { + Quickshell.execDetached(["wl-copy", value]); + ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("common.copied-to-clipboard"), "ethernet"); + } + } + } + } + } + + // --- Item 3: Link speed --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "gauge" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.alignment: Qt.AlignVCenter + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, I18n.tr("wifi.panel.link-speed")) + onExited: TooltipService.hide() + } + } + NText { + text: (NetworkService.activeEthernetDetails.speed && NetworkService.activeEthernetDetails.speed.length > 0) ? NetworkService.activeEthernetDetails.speed : "-" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + wrapMode: ethernetDetailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere + elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone + maximumLineCount: ethernetDetailsGrid ? 1 : 6 + clip: true + } + } + + // --- Item 4: IPv4 || IPv6 --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "network" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.alignment: Qt.AlignVCenter + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("wifi.panel.ipv4") : I18n.tr("wifi.panel.ipv6")) + onExited: TooltipService.hide() + onClicked: { + root.ipVersion = root.ipVersion === 4 ? 6 : 4; + TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("wifi.panel.ipv4") : I18n.tr("wifi.panel.ipv6")); + } + } + } + NText { + text: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.ipv4 || "-") : ((NetworkService.activeEthernetDetails.ipv6 || []).join(", ") || "-") + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + wrapMode: ethernetDetailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere + elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone + maximumLineCount: ethernetDetailsGrid ? 1 : 6 + clip: true + + // Click-to-copy Ethernet IP address + MouseArea { + anchors.fill: parent + enabled: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.ipv4 || "").length > 0 : (NetworkService.activeEthernetDetails.ipv6 || []).length > 0 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) + onExited: TooltipService.hide() + onClicked: { + const value = root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.ipv4 || "") : ((NetworkService.activeEthernetDetails.ipv6 || []).join(", ") || ""); + if (value.length > 0) { + Quickshell.execDetached(["wl-copy", value]); + ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("common.copied-to-clipboard"), "ethernet"); + } + } + } + } + } + + // --- Item 5: DNS --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "world" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.alignment: Qt.AlignVCenter + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("wifi.panel.dns") + " (" + I18n.tr("wifi.panel.ipv4") + ")" : I18n.tr("wifi.panel.dns") + " (" + I18n.tr("wifi.panel.ipv6") + ")") + onExited: TooltipService.hide() + onClicked: { + root.ipVersion = root.ipVersion === 4 ? 6 : 4; + TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("wifi.panel.dns") + " (" + I18n.tr("wifi.panel.ipv4") + ")" : I18n.tr("wifi.panel.dns") + " (" + I18n.tr("wifi.panel.ipv6") + ")"); + } + } + } + NText { + text: root.ipVersion === 4 ? ((NetworkService.activeEthernetDetails.dns4 || []).join(", ") || "-") : ((NetworkService.activeEthernetDetails.dns6 || []).join(", ") || "-") + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + wrapMode: ethernetDetailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere + elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone + maximumLineCount: ethernetDetailsGrid ? 1 : 6 + clip: true + + // Click-to-copy Ethernet DNS + MouseArea { + anchors.fill: parent + enabled: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.dns4 || []).length > 0 : (NetworkService.activeEthernetDetails.dns6 || []).length > 0 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) + onExited: TooltipService.hide() + onClicked: { + const value = root.ipVersion === 4 ? ((NetworkService.activeEthernetDetails.dns4 || []).join(", ") || "") : ((NetworkService.activeEthernetDetails.dns6 || []).join(", ") || ""); + if (value.length > 0) { + Quickshell.execDetached(["wl-copy", value]); + ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("common.copied-to-clipboard"), "ethernet"); + } + } + } + } + } + + // --- Item 6: Gateway --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "router" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.alignment: Qt.AlignVCenter + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("common.gateway") + " (" + I18n.tr("wifi.panel.ipv4") + ")" : I18n.tr("common.gateway") + " (" + I18n.tr("wifi.panel.ipv6") + ")") + onExited: TooltipService.hide() + onClicked: { + root.ipVersion = root.ipVersion === 4 ? 6 : 4; + TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("common.gateway") + " (" + I18n.tr("wifi.panel.ipv4") + ")" : I18n.tr("common.gateway") + " (" + I18n.tr("wifi.panel.ipv6") + ")"); + } + } + } + NText { + text: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.gateway4 || "-") : ((NetworkService.activeEthernetDetails.gateway6 || []).join(", ") || "-") + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + wrapMode: ethernetDetailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere + elide: ethernetDetailsGrid ? Text.ElideRight : Text.ElideNone + maximumLineCount: ethernetDetailsGrid ? 1 : 6 + clip: true + + // Click-to-copy Ethernet Gateway + MouseArea { + anchors.fill: parent + enabled: root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.gateway4 || "").length > 0 : (NetworkService.activeEthernetDetails.gateway6 || []).length > 0 + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) + onExited: TooltipService.hide() + onClicked: { + const value = root.ipVersion === 4 ? (NetworkService.activeEthernetDetails.gateway4 || "") : ((NetworkService.activeEthernetDetails.gateway6 || []).join(", ") || ""); + if (value.length > 0) { + Quickshell.execDetached(["wl-copy", value]); + ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("common.copied-to-clipboard"), "ethernet"); + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/NotificationHistory/NotificationHistoryPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/NotificationHistory/NotificationHistoryPanel.qml new file mode 100644 index 0000000..64968ff --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/NotificationHistory/NotificationHistoryPanel.qml @@ -0,0 +1,1022 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Notifications +import Quickshell.Wayland +import qs.Commons +import qs.Modules.MainScreen +import qs.Modules.Panels.Settings +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +// Notification History panel +SmartPanel { + id: root + + preferredWidth: Math.round((Settings.data.notifications.enableMarkdown ? 540 : 440) * Style.uiScaleRatio) + preferredHeight: Math.round((Settings.data.notifications.enableMarkdown ? 640 : 540) * Style.uiScaleRatio) + + onOpened: { + NotificationService.updateLastSeenTs(); + } + + panelContent: Rectangle { + id: panelContent + color: "transparent" + focus: true + + // Force focus when opened + Connections { + target: root + function onOpened() { + panelContent.forceActiveFocus(); + } + } + + Keys.onPressed: event => { + // Tab navigation for categories + if (event.key === Qt.Key_Tab) { + currentRange = (currentRange + 1) % 4; + event.accepted = true; + return; + } + + if (event.key === Qt.Key_Backtab) { // Shift+Tab + currentRange = (currentRange - 1 + 4) % 4; + event.accepted = true; + return; + } + + // Navigation Up/Down + if (checkKey(event, 'up')) { + moveSelection(-1); + event.accepted = true; + return; + } + if (checkKey(event, 'down')) { + moveSelection(1); + event.accepted = true; + return; + } + + // Action Navigation Left/Right + if (checkKey(event, 'left')) { + moveAction(-1); + event.accepted = true; + return; + } + if (checkKey(event, 'right')) { + moveAction(1); + event.accepted = true; + return; + } + + // Activation (Enter) + if (checkKey(event, 'enter')) { + activateSelection(); + event.accepted = true; + return; + } + + // Removal (Delete/Remove) + if (checkKey(event, 'remove') || event.key === Qt.Key_Delete) { + removeSelection(); + event.accepted = true; + return; + } + } + + function parseActions(actions) { + try { + return JSON.parse(actions || "[]"); + } catch (e) { + return []; + } + } + + function moveSelection(dir) { + var m = NotificationService.historyModel; + if (!m || m.count === 0) + return; + + var newIndex = focusIndex; + var found = false; + var count = m.count; + + // If no selection yet, start from beginning (or end if up) + if (focusIndex === -1) { + if (dir > 0) + newIndex = -1; + else + newIndex = count; + } + + // Loop to find next visible item + var loopCount = 0; + while (loopCount < count) { + newIndex += dir; + + // Bounds check + if (newIndex < 0 || newIndex >= count) { + break; // Stop at edges + } + + var item = m.get(newIndex); + if (item && isInCurrentRange(item.timestamp)) { + found = true; + break; + } + loopCount++; + } + + if (found) { + focusIndex = newIndex; + actionIndex = -1; // Reset action selection + scrollToItem(focusIndex); + } + } + + function moveAction(dir) { + if (focusIndex === -1) + return; + var item = NotificationService.historyModel.get(focusIndex); + if (!item) + return; + + var actions = parseActions(item.actionsJson); + + if (actions.length === 0) + return; + + var newActionIndex = actionIndex + dir; + + // Clamp between -1 (body) and actions.length - 1 + if (newActionIndex < -1) + newActionIndex = -1; + if (newActionIndex >= actions.length) + newActionIndex = actions.length - 1; + + actionIndex = newActionIndex; + } + + function activateSelection() { + if (focusIndex === -1) + return; + var item = NotificationService.historyModel.get(focusIndex); + if (!item) + return; + + if (actionIndex >= 0) { + var actions = parseActions(item.actionsJson); + if (actionIndex < actions.length) { + if (NotificationService.invokeAction(item.id, actions[actionIndex].identifier)) + root.close(); + } + } else { + var delegate = notificationColumn.children[focusIndex]; + if (!delegate) + return; + if (!(delegate.canExpand || delegate.isExpanded)) + return; + + if (scrollView.expandedId === item.id) { + scrollView.expandedId = ""; + } else { + scrollView.expandedId = item.id; + } + } + } + + function removeSelection() { + if (focusIndex === -1) + return; + var item = NotificationService.historyModel.get(focusIndex); + if (!item) + return; + + NotificationService.removeFromHistory(item.id); + // selection updates automatically? + // If we remove item at index i, the next item becomes index i. + // So focusIndex is still valid (unless it was last item). + // But we should re-verify if it exists. + // Actually NotificationService removal might be async or immediate. + // If immediate, model count decreases. + // We might need to clamp focusIndex. + // Let's handle this in a helper or just let the user navigate again. + // Better UX: select next available or previous if last. + } + + function scrollToItem(index) { + // Find the delegate item + if (index < 0 || index >= notificationColumn.children.length) + return; + + var item = notificationColumn.children[index]; + if (item && item.visible) { + // Use the internal flickable from NScrollView for accurate scrolling + var flickable = scrollView._internalFlickable; + if (!flickable || !flickable.contentItem) + return; + + var pos = flickable.contentItem.mapFromItem(item, 0, 0); + var itemY = pos.y; + var itemHeight = item.height; + + var currentContentY = flickable.contentY; + var viewHeight = flickable.height; + + // Check if above visible area + if (itemY < currentContentY) { + flickable.contentY = Math.max(0, itemY - Style.marginM); + } else + // Check if below visible area + if (itemY + itemHeight > currentContentY + viewHeight) { + flickable.contentY = (itemY + itemHeight) - viewHeight + Style.marginM; + } + } + } + + // Calculate content height based on header + tabs (if visible) + content + property real calculatedHeight: { + if (NotificationService.historyModel.count === 0) { + return headerBox.implicitHeight + scrollView.implicitHeight + Style.margin2L + Style.marginM; + } + return headerBox.implicitHeight + scrollView.implicitHeight + Style.margin2L + Style.marginM; + } + property real contentPreferredHeight: Math.min(root.preferredHeight, Math.ceil(calculatedHeight)) + + property real layoutWidth: Math.max(1, root.preferredWidth - Style.margin2L) + + // State (lazy-loaded with panelContent) + property var rangeCounts: [0, 0, 0, 0] + property var lastKnownDate: null // Track the current date to detect day changes + + // UI state (lazy-loaded with panelContent) + // 0 = All, 1 = Today, 2 = Yesterday, 3 = Earlier + property int currentRange: 1 // start on Today by default + property bool groupByDate: true + onCurrentRangeChanged: resetFocus() + + // Keyboard navigation state + property int focusIndex: -1 + property int actionIndex: -1 // For actions within a notification + + function resetFocus() { + focusIndex = -1; + actionIndex = -1; + } + + function checkKey(event, settingName) { + return Keybinds.checkKey(event, settingName, Settings); + } + + // Helper functions (lazy-loaded with panelContent) + function dateOnly(d) { + return new Date(d.getFullYear(), d.getMonth(), d.getDate()); + } + + function getDateKey(d) { + // Returns a string key for the date (YYYY-MM-DD) for comparison + var date = dateOnly(d); + return date.getFullYear() + "-" + date.getMonth() + "-" + date.getDate(); + } + + function rangeForTimestamp(ts) { + var dt = new Date(ts); + var today = dateOnly(new Date()); + var thatDay = dateOnly(dt); + + var diffMs = today - thatDay; + var diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) + return 0; + if (diffDays === 1) + return 1; + return 2; + } + + function recalcRangeCounts() { + var m = NotificationService.historyModel; + if (!m || typeof m.count === "undefined" || m.count <= 0) { + panelContent.rangeCounts = [0, 0, 0, 0]; + return; + } + + var counts = [0, 0, 0, 0]; + + counts[0] = m.count; + + for (var i = 0; i < m.count; ++i) { + var item = m.get(i); + if (!item || typeof item.timestamp === "undefined") + continue; + var r = rangeForTimestamp(item.timestamp); + counts[r + 1] = counts[r + 1] + 1; + } + + panelContent.rangeCounts = counts; + } + + function isInCurrentRange(ts) { + if (currentRange === 0) + return true; + return rangeForTimestamp(ts) === (currentRange - 1); + } + + function countForRange(range) { + return rangeCounts[range] || 0; + } + + function hasNotificationsInCurrentRange() { + var m = NotificationService.historyModel; + if (!m || m.count === 0) { + return false; + } + for (var i = 0; i < m.count; ++i) { + var item = m.get(i); + if (item && isInCurrentRange(item.timestamp)) + return true; + } + return false; + } + + Component.onCompleted: { + recalcRangeCounts(); + // Initialize lastKnownDate + lastKnownDate = getDateKey(new Date()); + } + + Connections { + target: NotificationService.historyModel + function onCountChanged() { + panelContent.recalcRangeCounts(); + } + } + + // Timer to check for day changes at midnight + Timer { + id: dayChangeTimer + interval: 60000 // Check every minute + repeat: true + running: true // Always runs when panelContent exists (panel is open) + onTriggered: { + var currentDateKey = panelContent.getDateKey(new Date()); + if (panelContent.lastKnownDate !== null && panelContent.lastKnownDate !== currentDateKey) { + // Day has changed, recalculate counts + panelContent.recalcRangeCounts(); + } + panelContent.lastKnownDate = currentDateKey; + } + } + + ColumnLayout { + id: mainColumn + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + // Header section + NBox { + id: headerBox + Layout.fillWidth: true + implicitHeight: header.implicitHeight + Style.margin2M + + ColumnLayout { + id: header + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + RowLayout { + id: headerRow + NIcon { + icon: "bell" + pointSize: Style.fontSizeXXL + color: Color.mPrimary + } + + NText { + text: I18n.tr("common.notifications") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NIconButton { + icon: NotificationService.doNotDisturb ? "bell-off" : "bell" + tooltipText: NotificationService.doNotDisturb ? I18n.tr("tooltips.do-not-disturb-enabled") : I18n.tr("tooltips.do-not-disturb-enabled") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: NotificationService.doNotDisturb = !NotificationService.doNotDisturb + } + + NIconButton { + icon: "trash" + tooltipText: I18n.tr("actions.clear-history") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + NotificationService.clearHistory(); + // Close panel as there is nothing more to see. + root.close(); + } + } + + NIconButton { + icon: "settings" + tooltipText: I18n.tr("common.settings") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + SettingsPanelService.openToTab(SettingsPanel.Tab.Notifications, 0, screen); + root.close(); + } + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: root.close() + } + } + + // Time range tabs ([All] / [Today] / [Yesterday] / [Earlier]) + NTabBar { + id: tabsBox + Layout.fillWidth: true + visible: NotificationService.historyModel.count > 0 && panelContent.groupByDate + currentIndex: panelContent.currentRange + tabHeight: Style.toOdd(Style.baseWidgetSize * 0.8) + spacing: Style.marginXS + distributeEvenly: true + + NTabButton { + tabIndex: 0 + text: I18n.tr("launcher.categories.all") + " (" + panelContent.countForRange(0) + ")" + checked: tabsBox.currentIndex === 0 + onClicked: panelContent.currentRange = 0 + pointSize: Style.fontSizeXS + } + + NTabButton { + tabIndex: 1 + text: I18n.tr("notifications.range.today") + " (" + panelContent.countForRange(1) + ")" + checked: tabsBox.currentIndex === 1 + onClicked: panelContent.currentRange = 1 + pointSize: Style.fontSizeXS + } + + NTabButton { + tabIndex: 2 + text: I18n.tr("notifications.range.yesterday") + " (" + panelContent.countForRange(2) + ")" + checked: tabsBox.currentIndex === 2 + onClicked: panelContent.currentRange = 2 + pointSize: Style.fontSizeXS + } + + NTabButton { + tabIndex: 3 + text: I18n.tr("notifications.range.earlier") + " (" + panelContent.countForRange(3) + ")" + checked: tabsBox.currentIndex === 3 + onClicked: panelContent.currentRange = 3 + pointSize: Style.fontSizeXS + } + } + } + } + + // Notification list container with gradient overlay + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + NScrollView { + id: scrollView + anchors.fill: parent + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + reserveScrollbarSpace: false + gradientColor: Color.mSurface + + // Track which notification is expanded + property string expandedId: "" + + ColumnLayout { + width: panelContent.layoutWidth + spacing: Style.marginM + + // Empty state when no notifications + NBox { + visible: !panelContent.hasNotificationsInCurrentRange() + Layout.fillWidth: true + Layout.preferredHeight: emptyState.implicitHeight + Style.marginXL + + ColumnLayout { + id: emptyState + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + Item { + Layout.fillHeight: true + } + + NIcon { + icon: "bell-off" + pointSize: (NotificationService.historyModel.count === 0) ? 48 : Style.baseWidgetSize + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: I18n.tr("notifications.panel.no-notifications") + pointSize: (NotificationService.historyModel.count === 0) ? Style.fontSizeL : Style.fontSizeM + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + visible: NotificationService.historyModel.count === 0 + text: I18n.tr("notifications.panel.description") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + Item { + Layout.fillHeight: true + } + } + } + + // Notification list container + Item { + visible: panelContent.hasNotificationsInCurrentRange() + Layout.fillWidth: true + Layout.preferredHeight: notificationColumn.implicitHeight + + Column { + id: notificationColumn + width: panelContent.layoutWidth + spacing: Style.marginM + + Repeater { + model: NotificationService.historyModel + + delegate: Item { + id: notificationDelegate + width: parent.width + visible: panelContent.isInCurrentRange(model.timestamp) + height: visible && !isRemoving ? contentColumn.height + Style.margin2M : 0 + + property int listIndex: index + property string notificationId: model.id + property string appName: model.appName || "" + property bool isExpanded: scrollView.expandedId === notificationId + property bool canExpand: summaryText.truncated || bodyText.truncated + property real swipeOffset: 0 + property real pressGlobalX: 0 + property real pressGlobalY: 0 + property bool isSwiping: false + property bool isRemoving: false + property string pendingLink: "" + readonly property real swipeStartThreshold: Math.round(16 * Style.uiScaleRatio) + readonly property real swipeDismissThreshold: Math.max(110, width * 0.3) + readonly property int removeAnimationDuration: Style.animationNormal + readonly property int notificationTextFormat: (Settings.data.notifications.enableMarkdown && notificationDelegate.isExpanded) ? Text.MarkdownText : Text.StyledText + readonly property real actionButtonSize: Style.baseWidgetSize * 0.7 + readonly property real buttonClusterWidth: notificationDelegate.actionButtonSize * 2 + Style.marginXS + readonly property real iconSize: Math.round(40 * Style.uiScaleRatio) + + function isSafeLink(link) { + if (!link) + return false; + const lower = link.toLowerCase(); + const schemes = ["http://", "https://", "mailto:"]; + return schemes.some(scheme => lower.startsWith(scheme)); + } + + function linkAtPoint(x, y) { + if (!Settings.data.notifications.enableMarkdown || !notificationDelegate.isExpanded) + return ""; + + if (summaryText) { + const summaryPoint = summaryText.mapFromItem(historyInteractionArea, x, y); + if (summaryPoint.x >= 0 && summaryPoint.y >= 0 && summaryPoint.x <= summaryText.width && summaryPoint.y <= summaryText.height) { + const summaryLink = summaryText.linkAt ? summaryText.linkAt(summaryPoint.x, summaryPoint.y) : ""; + if (isSafeLink(summaryLink)) + return summaryLink; + } + } + + if (bodyText) { + const bodyPoint = bodyText.mapFromItem(historyInteractionArea, x, y); + if (bodyPoint.x >= 0 && bodyPoint.y >= 0 && bodyPoint.x <= bodyText.width && bodyPoint.y <= bodyText.height) { + const bodyLink = bodyText.linkAt ? bodyText.linkAt(bodyPoint.x, bodyPoint.y) : ""; + if (isSafeLink(bodyLink)) + return bodyLink; + } + } + + return ""; + } + + function updateCursorAt(x, y) { + if (notificationDelegate.isExpanded && notificationDelegate.linkAtPoint(x, y)) { + historyInteractionArea.cursorShape = Qt.PointingHandCursor; + } else { + historyInteractionArea.cursorShape = Qt.ArrowCursor; + } + } + + transform: Translate { + x: notificationDelegate.swipeOffset + } + + function dismissBySwipe() { + if (isRemoving) + return; + isRemoving = true; + isSwiping = false; + + if (Settings.data.general.animationDisabled) { + NotificationService.removeFromHistory(notificationId); + return; + } + + swipeOffset = swipeOffset >= 0 ? width + Style.marginL : -width - Style.marginL; + opacity = 0; + removeTimer.restart(); + } + + Timer { + id: removeTimer + interval: notificationDelegate.removeAnimationDuration + repeat: false + onTriggered: NotificationService.removeFromHistory(notificationId) + } + + Behavior on swipeOffset { + enabled: !Settings.data.general.animationDisabled && !notificationDelegate.isSwiping + NumberAnimation { + duration: notificationDelegate.removeAnimationDuration + easing.type: Easing.OutCubic + } + } + + Behavior on opacity { + enabled: !Settings.data.general.animationDisabled && notificationDelegate.isRemoving + NumberAnimation { + duration: notificationDelegate.removeAnimationDuration + easing.type: Easing.OutCubic + } + } + + Behavior on height { + enabled: !Settings.data.general.animationDisabled && notificationDelegate.isRemoving + NumberAnimation { + duration: notificationDelegate.removeAnimationDuration + easing.type: Easing.OutCubic + } + } + + Behavior on y { + enabled: !Settings.data.general.animationDisabled && notificationDelegate.isRemoving + NumberAnimation { + duration: notificationDelegate.removeAnimationDuration + easing.type: Easing.OutCubic + } + } + + // Parse actions safely + property var actionsList: parseActions(model.actionsJson) + + property bool isFocused: index === panelContent.focusIndex + + Rectangle { + anchors.fill: parent + radius: Style.radiusM + color: Color.mSurfaceVariant + border.color: { + if (notificationDelegate.isFocused) + return Color.mPrimary; + if (Settings.data.ui.boxBorderEnabled) + return Qt.alpha(Color.mOutline, Style.opacityHeavy); + return "transparent"; + } + border.width: notificationDelegate.isFocused ? Style.borderM : Style.borderS + + Behavior on color { + enabled: !Settings.data.general.animationDisabled + ColorAnimation { + duration: Style.animationFast + } + } + } + + // Click to expand/collapse + MouseArea { + id: historyInteractionArea + anchors.fill: parent + anchors.rightMargin: notificationDelegate.buttonClusterWidth + Style.marginM + enabled: !notificationDelegate.isRemoving + hoverEnabled: true + cursorShape: Qt.ArrowCursor + onPressed: mouse => { + panelContent.focusIndex = index; + panelContent.actionIndex = -1; + + if (notificationDelegate.isExpanded) { + const link = notificationDelegate.linkAtPoint(mouse.x, mouse.y); + if (link) { + notificationDelegate.pendingLink = link; + } else { + notificationDelegate.pendingLink = ""; + } + } + + if (mouse.button !== Qt.LeftButton) + return; + const globalPoint = historyInteractionArea.mapToGlobal(mouse.x, mouse.y); + notificationDelegate.pressGlobalX = globalPoint.x; + notificationDelegate.pressGlobalY = globalPoint.y; + notificationDelegate.isSwiping = false; + } + onPositionChanged: mouse => { + if (!(mouse.buttons & Qt.LeftButton) || notificationDelegate.isRemoving) + return; + + const globalPoint = historyInteractionArea.mapToGlobal(mouse.x, mouse.y); + const deltaX = globalPoint.x - notificationDelegate.pressGlobalX; + const deltaY = globalPoint.y - notificationDelegate.pressGlobalY; + + if (!notificationDelegate.isSwiping) { + if (Math.abs(deltaX) < notificationDelegate.swipeStartThreshold) + return; + + // Only start a swipe-dismiss when horizontal movement is dominant. + if (Math.abs(deltaX) <= Math.abs(deltaY) * 1.15) { + return; + } + notificationDelegate.isSwiping = true; + } + + if (notificationDelegate.pendingLink && Math.abs(deltaX) >= notificationDelegate.swipeStartThreshold) { + notificationDelegate.pendingLink = ""; + } + + notificationDelegate.swipeOffset = deltaX; + } + onReleased: mouse => { + if (mouse.button !== Qt.LeftButton) + return; + + if (notificationDelegate.isSwiping) { + if (Math.abs(notificationDelegate.swipeOffset) >= notificationDelegate.swipeDismissThreshold) { + notificationDelegate.dismissBySwipe(); + } else { + notificationDelegate.swipeOffset = 0; + } + notificationDelegate.isSwiping = false; + notificationDelegate.pendingLink = ""; + return; + } + + if (notificationDelegate.pendingLink) { + Qt.openUrlExternally(notificationDelegate.pendingLink); + notificationDelegate.pendingLink = ""; + return; + } + + // Without a default action, or if invoking it fails, + // fall back to focusing the sender window by app identity. + var actions = notificationDelegate.actionsList; + var hasDefault = actions.some(function (a) { + return a.identifier === "default"; + }); + if (hasDefault && NotificationService.invokeAction(notificationDelegate.notificationId, "default")) { + root.close(); + } else { + NotificationService.focusSenderWindow(notificationDelegate.appName); + root.close(); + } + } + onCanceled: { + notificationDelegate.isSwiping = false; + notificationDelegate.swipeOffset = 0; + notificationDelegate.pendingLink = ""; + historyInteractionArea.cursorShape = Qt.ArrowCursor; + } + } + + HoverHandler { + target: historyInteractionArea + onPointChanged: notificationDelegate.updateCursorAt(point.position.x, point.position.y) + onActiveChanged: { + if (!active) { + historyInteractionArea.cursorShape = Qt.ArrowCursor; + } + } + } + + onVisibleChanged: { + if (!visible) { + notificationDelegate.isSwiping = false; + notificationDelegate.swipeOffset = 0; + notificationDelegate.opacity = 1; + notificationDelegate.isRemoving = false; + removeTimer.stop(); + } + } + + Component.onDestruction: removeTimer.stop() + + Column { + id: contentColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginM + spacing: Style.marginM + + Row { + width: parent.width + spacing: Style.marginM + + // Icon + NImageRounded { + anchors.verticalCenter: notificationDelegate.isExpanded ? undefined : parent.verticalCenter + width: notificationDelegate.iconSize + height: notificationDelegate.iconSize + radius: Math.min(Style.radiusL, width / 2) + imagePath: model.cachedImage || model.originalImage || "" + borderColor: "transparent" + borderWidth: 0 + fallbackIcon: "bell" + fallbackIconSize: 24 + } + + // Content + Column { + width: parent.width - notificationDelegate.iconSize - notificationDelegate.buttonClusterWidth - Style.margin2M + spacing: Style.marginXS + + // Header row with app name and timestamp + Row { + width: parent.width + spacing: Style.marginS + + // Urgency indicator + Rectangle { + width: 6 + height: 6 + anchors.verticalCenter: parent.verticalCenter + radius: 3 + visible: model.urgency !== 1 + color: { + if (model.urgency === 2) + return Color.mError; + else if (model.urgency === 0) + return Color.mOnSurfaceVariant; + else + return "transparent"; + } + } + + NText { + text: model.appName || "Unknown App" + pointSize: Style.fontSizeXS + font.weight: Style.fontWeightBold + color: Color.mSecondary + } + + NText { + textFormat: Text.PlainText + text: " " + Time.formatRelativeTime(model.timestamp) + pointSize: Style.fontSizeXXS + color: Color.mOnSurfaceVariant + anchors.bottom: parent.bottom + } + } + + // Summary + NText { + id: summaryText + width: parent.width + text: (Settings.data.notifications.enableMarkdown && notificationDelegate.isExpanded) ? (model.summaryMarkdown || I18n.tr("common.no-summary")) : (model.summary || I18n.tr("common.no-summary")) + pointSize: Style.fontSizeM + color: Color.mOnSurface + textFormat: notificationDelegate.notificationTextFormat + wrapMode: Text.Wrap + maximumLineCount: notificationDelegate.isExpanded ? 999 : 2 + elide: Text.ElideRight + } + + // Body + NText { + id: bodyText + width: parent.width + text: (Settings.data.notifications.enableMarkdown && notificationDelegate.isExpanded) ? (model.bodyMarkdown || "") : (model.body || "") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + textFormat: notificationDelegate.notificationTextFormat + wrapMode: Text.Wrap + maximumLineCount: notificationDelegate.isExpanded ? 999 : 3 + elide: Text.ElideRight + visible: text.length > 0 + } + + // Actions Flow + Flow { + width: parent.width + spacing: Style.marginS + visible: notificationDelegate.actionsList.length > 0 + + Repeater { + model: notificationDelegate.actionsList + + delegate: NButton { + text: modelData.text + fontSize: Style.fontSizeS + + readonly property bool actionNavActive: notificationDelegate.isFocused && panelContent.actionIndex !== -1 + readonly property bool isSelected: actionNavActive && panelContent.actionIndex === index + + backgroundColor: isSelected ? Color.mSecondary : Color.mPrimary + textColor: isSelected ? Color.mOnSecondary : Color.mOnPrimary + + outlined: false + implicitHeight: 24 + + onHoveredChanged: { + if (hovered) { + panelContent.focusIndex = notificationDelegate.listIndex; + } + } + + // Capture modelData in a property to avoid reference errors + property var actionData: modelData + onClicked: { + if (NotificationService.invokeAction(notificationDelegate.notificationId, actionData.identifier)) + root.close(); + } + } + } + } + } + + Item { + width: notificationDelegate.buttonClusterWidth + height: notificationDelegate.actionButtonSize + + Row { + anchors.right: parent.right + spacing: Style.marginXS + + NIconButton { + id: expandButton + icon: notificationDelegate.isExpanded ? "chevron-up" : "chevron-down" + tooltipText: notificationDelegate.isExpanded ? I18n.tr("notifications.panel.click-to-collapse") || "Click to collapse" : I18n.tr("notifications.panel.click-to-expand") || "Click to expand" + baseSize: notificationDelegate.actionButtonSize + opacity: (notificationDelegate.canExpand || notificationDelegate.isExpanded) ? 1.0 : 0.0 + enabled: notificationDelegate.canExpand || notificationDelegate.isExpanded + + onClicked: { + notificationDelegate.pendingLink = ""; + historyInteractionArea.cursorShape = Qt.ArrowCursor; + if (scrollView.expandedId === notificationId) { + scrollView.expandedId = ""; + } else { + scrollView.expandedId = notificationId; + } + } + } + + // Delete button + NIconButton { + icon: "trash" + tooltipText: I18n.tr("tooltips.delete-notification") + baseSize: notificationDelegate.actionButtonSize + + onClicked: { + NotificationService.removeFromHistory(notificationId); + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Plugins/PluginPanelSlot.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Plugins/PluginPanelSlot.qml new file mode 100644 index 0000000..098feac --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Plugins/PluginPanelSlot.qml @@ -0,0 +1,228 @@ +import QtQuick +import Quickshell +import qs.Commons +import qs.Modules.MainScreen +import qs.Services.Noctalia +import qs.Services.UI + +/** +* Generic plugin panel slot that can be reused for different plugins +*/ +SmartPanel { + id: root + + // Which plugin slot this is (1 or 2) + property int slotNumber: 1 + + // Currently loaded plugin ID (empty if no plugin using this slot) + property string currentPluginId: "" + + // Plugin instance + property var pluginInstance: null + + // Reference to the plugin content loader (set when panel content is created) + property var contentLoader: null + + // Pass through anchor properties from plugin panel content + panelAnchorHorizontalCenter: pluginInstance?.panelAnchorHorizontalCenter ?? false + panelAnchorVerticalCenter: pluginInstance?.panelAnchorVerticalCenter ?? false + panelAnchorTop: pluginInstance?.panelAnchorTop ?? false + panelAnchorBottom: pluginInstance?.panelAnchorBottom ?? false + panelAnchorLeft: pluginInstance?.panelAnchorLeft ?? false + panelAnchorRight: pluginInstance?.panelAnchorRight ?? false + + // Panel background color + panelBackgroundColor: pluginInstance?.panelBackgroundColor ?? Color.mSurface + + // Panel content is dynamically loaded + panelContent: Component { + Item { + id: panelContainer + + // Required by SmartPanel for background rendering geometry + readonly property var geometryPlaceholder: pluginContentItem + + // Panel properties expected by SmartPanel + // A plugin can opt out of attachment (allowAttach: false) but cannot override + // the global "attach panels to bar" setting — if that setting is off, panels + // stay detached regardless of what the plugin requests. + readonly property bool allowAttach: { + var globalAllow = Settings.data.ui.panelsAttachedToBar || root.forceAttachToBar; + if (!globalAllow) + return false; + if (pluginContentLoader.item && pluginContentLoader.item.allowAttach !== undefined) + return pluginContentLoader.item.allowAttach; + return globalAllow; + } + // Expose preferred dimensions from plugin panel content + // Only define these if the plugin provides them + property var contentPreferredWidth: { + if (pluginContentLoader.item && pluginContentLoader.item.contentPreferredWidth !== undefined && pluginContentLoader.item.contentPreferredWidth > 0) { + return pluginContentLoader.item.contentPreferredWidth; + } + return undefined; + } + + property var contentPreferredHeight: { + if (pluginContentLoader.item && pluginContentLoader.item.contentPreferredHeight !== undefined && pluginContentLoader.item.contentPreferredHeight > 0) { + return pluginContentLoader.item.contentPreferredHeight; + } + return undefined; + } + + anchors.fill: parent + + // Dynamic plugin content + Item { + id: pluginContentItem + anchors.fill: parent + + // Plugin content loader, pluginApi is injected synchronously in loadPluginPanel() + Loader { + id: pluginContentLoader + anchors.fill: parent + active: false + } + } + + Component.onCompleted: { + // Store reference to the loader so loadPluginPanel can access it + root.contentLoader = pluginContentLoader; + + // Load plugin panel content if assigned + if (root.currentPluginId !== "") { + root.loadPluginPanel(root.currentPluginId); + } + } + } + } + + // Load a plugin's panel content + function loadPluginPanel(pluginId) { + if (!PluginService.isPluginLoaded(pluginId)) { + Logger.w("PluginPanelSlot", "Plugin not loaded:", pluginId); + return false; + } + + var plugin = PluginService.loadedPlugins[pluginId]; + if (!plugin || !plugin.manifest) { + Logger.w("PluginPanelSlot", "Plugin data not found:", pluginId); + return false; + } + + if (!plugin.manifest.entryPoints || !plugin.manifest.entryPoints.panel) { + Logger.w("PluginPanelSlot", "Plugin does not provide a panel:", pluginId); + return false; + } + + // Check if loader is available + if (!root.contentLoader) { + Logger.e("PluginPanelSlot", "Content loader not available yet"); + return false; + } + + // Clear any stale pluginInstance before loading new content + root.pluginInstance = null; + + var pluginDir = PluginRegistry.getPluginDir(pluginId); + var panelPath = pluginDir + "/" + plugin.manifest.entryPoints.panel; + + Logger.i("PluginPanelSlot", "Loading panel for plugin:", pluginId, "in slot", root.slotNumber); + + // Load the panel component with cache-busting version parameter + var loadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0; + var component = Qt.createComponent("file://" + panelPath + "?v=" + loadVersion); + + if (component.status === Component.Ready) { + finalizePluginLoad(pluginId, component); + return true; + } else if (component.status === Component.Loading) { + // Handle async component loading - wait for it to be ready + Logger.d("PluginPanelSlot", "Component loading asynchronously for:", pluginId); + component.statusChanged.connect(function () { + if (component.status === Component.Ready) { + finalizePluginLoad(pluginId, component); + // Force SmartPanel to recalculate position now that content is ready + if (root.isPanelVisible) { + root.setPosition(); + } + } else if (component.status === Component.Error) { + PluginService.recordPluginError(pluginId, "panel", component.errorString()); + } + }); + return true; // Will be loaded asynchronously + } else if (component.status === Component.Error) { + PluginService.recordPluginError(pluginId, "panel", component.errorString()); + return false; + } + + return false; + } + + // Helper function to finalize plugin content loading + function finalizePluginLoad(pluginId, component) { + // Get plugin API + var api = PluginService.getPluginAPI(pluginId); + + // Use setSource with initial properties so pluginApi is available + // from the first binding evaluation (before onLoaded) + root.contentLoader.active = true; + root.contentLoader.setSource(component.url, api ? { + "pluginApi": api + } : {}); + + if (root.contentLoader.item) { + root.pluginInstance = root.contentLoader.item; + root.currentPluginId = pluginId; + + Logger.i("PluginPanelSlot", "Panel loaded for:", pluginId); + } else { + Logger.e("PluginPanelSlot", "Failed to get loader item for:", pluginId); + } + } + + // Unload current plugin panel + function unloadPluginPanel() { + if (root.currentPluginId === "") { + return; + } + + Logger.i("PluginPanelSlot", "Unloading panel from slot", root.slotNumber); + + if (root.contentLoader) { + root.contentLoader.active = false; + root.contentLoader.sourceComponent = null; + } + root.pluginInstance = null; + root.currentPluginId = ""; + } + + // Register with PanelService + Component.onCompleted: { + PanelService.registerPanel(root); + } + + // Update plugin's panelOpenScreen when this panel opens/closes + onOpened: { + if (root.currentPluginId !== "") { + var api = PluginService.getPluginAPI(root.currentPluginId); + if (api) { + api.panelOpenScreen = root.screen; + Logger.d("PluginPanelSlot", "Set panelOpenScreen for", root.currentPluginId, "to", root.screen?.name); + } + } + } + + onClosed: { + if (root.currentPluginId !== "") { + var api = PluginService.getPluginAPI(root.currentPluginId); + if (api) { + api.panelOpenScreen = null; + Logger.d("PluginPanelSlot", "Cleared panelOpenScreen for", root.currentPluginId); + } + } + // Clear stale references when panel closes (content is destroyed by SmartPanel) + root.pluginInstance = null; + root.contentLoader = null; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SessionMenu/SessionMenu.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SessionMenu/SessionMenu.qml new file mode 100644 index 0000000..7d508ad --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SessionMenu/SessionMenu.qml @@ -0,0 +1,1136 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets +import qs.Commons +import qs.Modules.MainScreen +import qs.Services.Compositor +import qs.Services.UI +import qs.Widgets + +SmartPanel { + id: root + + readonly property bool largeButtonsStyle: Settings.data.sessionMenu.largeButtonsStyle || false + readonly property bool largeButtonsLayout: Settings.data.sessionMenu.largeButtonsLayout || "grid" + + // Large buttons style is fullscreen — disable blur behind it + blurEnabled: !largeButtonsStyle + + // Make panel background transparent for large buttons style + panelBackgroundColor: largeButtonsStyle ? "transparent" : Color.mSurface + + preferredWidth: largeButtonsStyle ? 0 : Math.round(440 * Style.uiScaleRatio) + preferredWidthRatio: largeButtonsStyle ? 1.0 : 0 + preferredHeight: { + if (largeButtonsStyle) { + return 0; // Use ratio instead + } + var headerHeight = Settings.data.sessionMenu.showHeader ? Style.baseWidgetSize * 0.6 : 0; + + var dividerHeight = Settings.data.sessionMenu.showHeader ? Style.marginS : 0; + var buttonHeight = Style.baseWidgetSize * 1.3 * Style.uiScaleRatio; + var buttonSpacing = Style.marginS; + var enabledCount = powerOptions.length; + + var headerSpacing = Settings.data.sessionMenu.showHeader ? Style.margin2L : 0; + var baseHeight = (Style.marginL * 4) + headerHeight + dividerHeight + headerSpacing; + var buttonsHeight = enabledCount > 0 ? (buttonHeight * enabledCount) + (buttonSpacing * (enabledCount - 1)) : 0; + + return Math.round(baseHeight + buttonsHeight); + } + preferredHeightRatio: largeButtonsStyle ? 1.0 : 0 + + // Positioning - large buttons style is always centered and fullscreen + readonly property string panelPosition: Settings.data.sessionMenu.position + + panelAnchorHorizontalCenter: largeButtonsStyle || panelPosition === "center" || panelPosition.endsWith("_center") + panelAnchorVerticalCenter: largeButtonsStyle || panelPosition === "center" + panelAnchorLeft: !largeButtonsStyle && panelPosition !== "center" && panelPosition.endsWith("_left") + panelAnchorRight: !largeButtonsStyle && panelPosition !== "center" && panelPosition.endsWith("_right") + panelAnchorBottom: !largeButtonsStyle && panelPosition.startsWith("bottom_") + panelAnchorTop: !largeButtonsStyle && panelPosition.startsWith("top_") + + // SessionMenu handle it's own closing logic + property bool closeWithEscape: false + + // Timer properties + readonly property int timerDuration: Settings.data.sessionMenu.countdownDuration + property string pendingAction: "" + property bool timerActive: false + property int timeRemaining: 0 + + // Navigation properties + property int selectedIndex: -1 + property bool ignoreMouseHover: true // Transient flag, should always be true on init + + // Global mouse tracking for movement detection across delegates + property real globalLastMouseX: 0 + property real globalLastMouseY: 0 + property bool globalMouseInitialized: false + property bool mouseTrackingReady: false // Delay tracking until panel is settled + + Timer { + id: mouseTrackingDelayTimer + interval: Style.animationNormal + 50 // Wait for panel animation to complete + safety margin + repeat: false + onTriggered: { + root.mouseTrackingReady = true; + root.globalMouseInitialized = false; // Reset so we get fresh initial position + } + } + + // Action metadata mapping + readonly property var actionMetadata: { + "lock": { + "icon": "lock", + "title": I18n.tr("common.lock"), + "isShutdown": false + }, + "suspend": { + "icon": "suspend", + "title": I18n.tr("common.suspend"), + "isShutdown": false + }, + "hibernate": { + "icon": "hibernate", + "title": I18n.tr("common.hibernate"), + "isShutdown": false + }, + "reboot": { + "icon": "reboot", + "title": I18n.tr("common.reboot"), + "isShutdown": false + }, + "userspaceReboot": { + "icon": "rotate", + "title": I18n.tr("common.userspace-reboot"), + "isShutdown": false + }, + "rebootToUefi": { + "icon": "reboot", + "title": I18n.tr("common.reboot-to-uefi"), + "isShutdown": false + }, + "logout": { + "icon": "logout", + "title": I18n.tr("common.logout"), + "isShutdown": false + }, + "shutdown": { + "icon": "shutdown", + "title": I18n.tr("common.shutdown"), + "isShutdown": true + } + } + + // Build powerOptions from settings, filtering enabled ones and adding metadata + // _powerOptionsVersion forces re-evaluation when settings change + property int _powerOptionsVersion: 0 + property var powerOptions: { + // Reference version to trigger re-evaluation + void (_powerOptionsVersion); + var options = []; + var settingsOptions = Settings.data.sessionMenu.powerOptions || []; + + for (var i = 0; i < settingsOptions.length; i++) { + var settingOption = settingsOptions[i]; + if (settingOption.enabled && actionMetadata[settingOption.action]) { + var metadata = actionMetadata[settingOption.action]; + options.push({ + "action": settingOption.action, + "icon": metadata.icon, + "title": metadata.title, + "isShutdown": metadata.isShutdown, + "countdownEnabled": settingOption.countdownEnabled !== undefined ? settingOption.countdownEnabled : true, + "command": settingOption.command || "", + "keybind": settingOption.keybind || "" + }); + } + } + + return options; + } + + Connections { + target: Settings.data.sessionMenu + function onPowerOptionsChanged() { + root._powerOptionsVersion++; + } + } + + // Lifecycle handlers + onOpened: { + if (powerOptions.length > 0) { + selectedIndex = -1; + ignoreMouseHover = true; + globalMouseInitialized = false; + mouseTrackingReady = false; + mouseTrackingDelayTimer.restart(); + } else { + Logger.w("SessionMenu", "Trying to open an empty session menu"); + root.closeImmediately(); + } + } + + onClosed: { + cancelTimer(); + selectedIndex = -1; + ignoreMouseHover = true; + } + + // Timer management + function startTimer(action) { + // Check if global countdown is disabled + if (!Settings.data.sessionMenu.enableCountdown) { + executeAction(action); + return; + } + + // Check per-item countdown setting + var option = null; + for (var i = 0; i < powerOptions.length; i++) { + if (powerOptions[i].action === action) { + option = powerOptions[i]; + break; + } + } + + // If this specific action has countdown disabled, execute immediately + if (option && option.countdownEnabled === false) { + executeAction(action); + return; + } + + if (timerActive && pendingAction === action) { + // Second click - execute immediately + executeAction(action); + return; + } + + pendingAction = action; + timeRemaining = timerDuration; + timerActive = true; + countdownTimer.start(); + } + + function cancelTimer() { + timerActive = false; + pendingAction = ""; + timeRemaining = 0; + countdownTimer.stop(); + } + + function executeAction(action) { + // Stop timer but don't reset other properties yet + countdownTimer.stop(); + + // Use default behavior or custom command handled by CompositorService + switch (action) { + case "lock": + CompositorService.lock(); + break; + case "suspend": + // Check if we should lock before suspending + if (Settings.data.general.lockOnSuspend) { + CompositorService.lockAndSuspend(); + } else { + CompositorService.suspend(); + } + break; + case "hibernate": + CompositorService.hibernate(); + break; + case "reboot": + CompositorService.reboot(); + break; + case "userspaceReboot": + CompositorService.userspaceReboot(); + break; + case "rebootToUefi": + CompositorService.rebootToUefi(); + break; + case "logout": + CompositorService.logout(); + break; + case "shutdown": + CompositorService.shutdown(); + break; + } + + // Reset timer state and close panel + cancelTimer(); + root.close(); + } + + // Navigation functions + function selectNextWrapped() { + if (powerOptions.length > 0) { + if (selectedIndex < 0) { + selectedIndex = 0; + } else { + selectedIndex = (selectedIndex + 1) % powerOptions.length; + } + } + } + + function selectPreviousWrapped() { + if (powerOptions.length > 0) { + if (selectedIndex < 0) { + selectedIndex = powerOptions.length - 1; + } else { + selectedIndex = (((selectedIndex - 1) % powerOptions.length) + powerOptions.length) % powerOptions.length; + } + } + } + + function selectFirst() { + if (powerOptions.length > 0) { + selectedIndex = 0; + } else { + selectedIndex = -1; + } + } + + function selectLast() { + if (powerOptions.length > 0) { + selectedIndex = powerOptions.length - 1; + } else { + selectedIndex = -1; + } + } + + function getGridInfo() { + let columns, rows; + if (Settings.data.sessionMenu.largeButtonsLayout === "single-row") { + columns = powerOptions.length; + rows = 1; + } else { + columns = Math.min(3, Math.ceil(Math.sqrt(powerOptions.length))); + rows = Math.ceil(powerOptions.length / columns); + } + + return { + columns, + rows, + currentRow: selectedIndex >= 0 ? Math.floor(selectedIndex / columns) : -1, + currentCol: selectedIndex >= 0 ? selectedIndex % columns : -1, + itemsInRow: row => Math.min(columns, powerOptions.length - row * columns) + }; + } + + // Unified navigation function + function navigateGrid(direction) { + if (powerOptions.length === 0) + return; + + const grid = getGridInfo(); + // If no selection, start at first item + let newRow = grid.currentRow >= 0 ? grid.currentRow : 0; + let newCol = grid.currentCol >= 0 ? grid.currentCol : 0; + + switch (direction) { + case "left": + newCol = newCol - 1 < 0 ? grid.itemsInRow(newRow) - 1 : newCol - 1; + break; + case "right": + // We already moved to newCol to 0 if grid.currentCol was negative + newCol = grid.currentCol < 0 ? newRow : newCol + 1 >= grid.itemsInRow(newRow) ? 0 : newCol + 1; + break; + case "up": + newRow = newRow - 1 < 0 ? grid.rows - 1 : newRow - 1; + break; + case "down": + // We already moved to newRow to 0 if grid.currentRow was negative + newRow = grid.currentRow < 0 ? newRow : newRow + 1 >= grid.rows ? 0 : newRow + 1; + break; + } + + // For vertical movement, clamp column if row has fewer items + if (direction === "up" || direction === "down") { + const itemsInNewRow = grid.itemsInRow(newRow); + newCol = Math.min(newCol, itemsInNewRow - 1); + } + + const newIndex = newRow * grid.columns + newCol; + if (newIndex < powerOptions.length) { + selectedIndex = newIndex; + } + } + + function activate() { + if (powerOptions.length > 0 && selectedIndex >= 0 && powerOptions[selectedIndex]) { + const option = powerOptions[selectedIndex]; + startTimer(option.action); + } + } + + function checkKey(event, settingName) { + return Keybinds.checkKey(event, settingName, Settings); + } + + function handleUp() { + if (largeButtonsStyle) { + navigateGrid("up"); + } else { + selectPreviousWrapped(); + } + } + + function handleDown() { + if (largeButtonsStyle) { + navigateGrid("down"); + } else { + selectNextWrapped(); + } + } + + function handleLeft() { + if (largeButtonsStyle) { + navigateGrid("left"); + } else { + selectPreviousWrapped(); + } + } + + function handleRight() { + if (largeButtonsStyle) { + navigateGrid("right"); + } else { + selectNextWrapped(); + } + } + + function handleEnter() { + activate(); + } + + function handleEscape() { + if (timerActive) { + cancelTimer(); + } else { + root.close(); + } + } + + // Override keyboard handlers from SmartPanel + function onEscapePressed() { + handleEscape(); + } + function onTabPressed() { + selectNextWrapped(); + } + function onBackTabPressed() { + selectPreviousWrapped(); + } + function onLeftPressed() { + handleLeft(); + } + function onRightPressed() { + handleRight(); + } + function onUpPressed() { + handleUp(); + } + function onDownPressed() { + handleDown(); + } + function onEnterPressed() { + handleEnter(); + } + function onHomePressed() { + selectFirst(); + } + function onEndPressed() { + selectLast(); + } + + function checkKeybind(event) { + if (powerOptions.length === 0) + return false; + + // Construct key string in the same format as the recorder + // Ignore modifier keys by themselves + if (event.key === Qt.Key_Control || event.key === Qt.Key_Shift || event.key === Qt.Key_Alt || event.key === Qt.Key_Meta) { + return false; + } + + const pressedKeybind = Keybinds.getKeybindString(event); + if (!pressedKeybind) + return false; + + for (var i = 0; i < powerOptions.length; i++) { + const option = powerOptions[i]; + if (option.keybind === pressedKeybind) { + selectedIndex = i; + startTimer(option.action); + return true; + } + } + return false; + } + + // Countdown timer + Timer { + id: countdownTimer + interval: 100 + repeat: true + onTriggered: { + timeRemaining -= interval; + if (timeRemaining <= 0) { + executeAction(pendingAction); + } + } + } + + panelContent: Rectangle { + id: panelContent + color: "transparent" + focus: true + + // For large buttons style, use full screen dimensions + readonly property var contentPreferredWidth: largeButtonsStyle ? (root.screen?.width || root.width || 0) : undefined + readonly property var contentPreferredHeight: largeButtonsStyle ? (root.screen?.height || root.height || 0) : undefined + + // Focus management + Connections { + target: root + function onOpened() { + Qt.callLater(() => { + panelContent.forceActiveFocus(); + }); + } + } + + Keys.onPressed: event => { + // Check custom entry keybinds first + if (root.checkKeybind(event)) { + event.accepted = true; + return; + } + + // Check global navigation keybinds + if (checkKey(event, 'up')) { + handleUp(); + event.accepted = true; + return; + } + if (checkKey(event, 'down')) { + handleDown(); + event.accepted = true; + return; + } + if (checkKey(event, 'left')) { + handleLeft(); + event.accepted = true; + return; + } + if (checkKey(event, 'right')) { + handleRight(); + event.accepted = true; + return; + } + if (checkKey(event, 'enter')) { + handleEnter(); + event.accepted = true; + return; + } + if (checkKey(event, 'escape')) { + handleEscape(); + event.accepted = true; + return; + } + + // Block default keys if they weren't matched above + // This prevents 'Up' from working if rebinned to something else + if (event.key === Qt.Key_Up || event.key === Qt.Key_Down || event.key === Qt.Key_Left || event.key === Qt.Key_Right || event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Escape) { + event.accepted = true; + return; + } + } + + HoverHandler { + id: globalHoverHandler + + onPointChanged: { + if (!root.mouseTrackingReady) { + return; + } + + if (!root.globalMouseInitialized) { + root.globalLastMouseX = point.position.x; + root.globalLastMouseY = point.position.y; + root.globalMouseInitialized = true; + return; + } + + const deltaX = Math.abs(point.position.x - root.globalLastMouseX); + const deltaY = Math.abs(point.position.y - root.globalLastMouseY); + if (deltaX + deltaY >= 5) { + root.ignoreMouseHover = false; + root.globalLastMouseX = point.position.x; + root.globalLastMouseY = point.position.y; + } + } + } + + // Timer text for large buttons style, positioned absolutely with background + Rectangle { + id: timerTextContainer + visible: largeButtonsStyle && timerActive + anchors.bottom: largeButtonsContainer.top + anchors.horizontalCenter: largeButtonsContainer.horizontalCenter + anchors.bottomMargin: Style.marginM + width: timerText.width + Style.margin2XL + height: timerText.height + Style.margin2L + radius: Style.radiusM + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Style.borderS + z: 1000 + + NText { + id: timerText + anchors.centerIn: parent + text: I18n.tr("session-menu.action-in-seconds", { + "action": root.actionMetadata[pendingAction] ? root.actionMetadata[pendingAction].title : "", + "seconds": Math.ceil(timeRemaining / 1000) + }) + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + } + } + + // Large buttons style layout container + ColumnLayout { + id: largeButtonsContainer + visible: largeButtonsStyle + anchors.centerIn: parent + + // Large buttons style layout (grid) + GridLayout { + id: largeButtonsGrid + Layout.alignment: Qt.AlignHCenter + columns: Settings.data.sessionMenu.largeButtonsLayout === "single-row" ? powerOptions.length : Math.min(3, Math.ceil(Math.sqrt(powerOptions.length))) + rowSpacing: Style.marginXL + columnSpacing: Style.marginXL + width: columns * 200 * Style.uiScaleRatio + (columns - 1) * Style.marginXL + height: Math.ceil(powerOptions.length / columns) * 200 * Style.uiScaleRatio + (Math.ceil(powerOptions.length / columns) - 1) * Style.marginXL + + Repeater { + model: powerOptions + delegate: LargeButton { + Layout.preferredWidth: Math.round(200 * Style.uiScaleRatio) + Layout.preferredHeight: Math.round(200 * Style.uiScaleRatio) + icon: modelData.icon + title: modelData.title + isShutdown: modelData.isShutdown || false + isSelected: index === selectedIndex + number: index + 1 + buttonIndex: index + onClicked: { + selectedIndex = index; + startTimer(modelData.action); + } + pending: timerActive && pendingAction === modelData.action + keybind: modelData.keybind || "" + } + } + } + } + + // Normal style layout + NBox { + visible: !largeButtonsStyle + anchors.fill: parent + anchors.margins: Style.marginL + color: Color.mSurfaceVariant + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginL + + // Header with title and close button + RowLayout { + visible: Settings.data.sessionMenu.showHeader + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize * 0.6 + + NText { + text: timerActive ? I18n.tr("session-menu.action-in-seconds", { + "action": root.actionMetadata[pendingAction] ? root.actionMetadata[pendingAction].title : "", + "seconds": Math.ceil(timeRemaining / 1000) + }) : I18n.tr("session-menu.title") + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeL + color: timerActive ? Color.mPrimary : Color.mOnSurface + Layout.alignment: Qt.AlignVCenter + verticalAlignment: Text.AlignVCenter + } + + Item { + Layout.fillWidth: true + } + + NIconButton { + icon: timerActive ? "stop" : "close" + tooltipText: timerActive ? I18n.tr("session-menu.cancel-timer") : I18n.tr("common.close") + Layout.alignment: Qt.AlignVCenter + baseSize: Style.baseWidgetSize * 0.7 + colorBg: timerActive ? Qt.alpha(Color.mError, 0.08) : "transparent" + colorFg: timerActive ? Color.mError : Color.mOnSurface + onClicked: { + if (timerActive) { + cancelTimer(); + } else { + cancelTimer(); + root.close(); + } + } + } + } + + NDivider { + visible: Settings.data.sessionMenu.showHeader + Layout.fillWidth: true + } + + // Power options + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + + Repeater { + model: powerOptions + delegate: PowerButton { + Layout.fillWidth: true + icon: modelData.icon + title: modelData.title + isShutdown: modelData.isShutdown || false + isSelected: index === selectedIndex + number: index + 1 + buttonIndex: index + onClicked: { + selectedIndex = index; + startTimer(modelData.action); + } + pending: timerActive && pendingAction === modelData.action + keybind: modelData.keybind || "" + } + } + } + } + } + + // Background MouseArea for large buttons style - closes panel when clicking outside buttons + MouseArea { + visible: largeButtonsStyle + anchors.fill: parent + z: -1 + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onClicked: mouse => { + // Only close if not clicking on a button + // The buttons are above this MouseArea, so clicks on them won't reach here + if (timerActive) { + // Cancel countdown if active + cancelTimer(); + } else { + root.close(); + } + } + } + } + + // Custom power button component + component PowerButton: Rectangle { + id: buttonRoot + + property string icon: "" + property string title: "" + property bool pending: false + property bool isShutdown: false + property bool isSelected: false + property int number: 0 + property string keybind: "" + property int buttonIndex: -1 + + // Effective hover state that respects ignoreMouseHover + readonly property bool effectiveHover: !root.ignoreMouseHover && mouseArea.containsMouse + + signal clicked + + height: Style.baseWidgetSize * 1.3 * Style.uiScaleRatio + radius: Style.radiusS + color: { + if (pending) { + return Qt.alpha(Color.mPrimary, 0.08); + } + if (isSelected || effectiveHover) { + return Color.mHover; + } + return "transparent"; + } + + border.width: pending ? Math.max(Style.borderM) : 0 + border.color: pending ? Color.mPrimary : Color.mOutline + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + + Item { + id: contentItem + anchors.fill: parent + anchors.margins: Style.marginM + + // Icon on the left + NIcon { + id: iconElement + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + icon: buttonRoot.icon + color: { + if (buttonRoot.pending) + return Color.mPrimary; + if (buttonRoot.isShutdown && !buttonRoot.isSelected && !buttonRoot.effectiveHover) + return Color.mError; + if (buttonRoot.isSelected || buttonRoot.effectiveHover) + return Color.mOnHover; + return Color.mOnSurface; + } + pointSize: Style.fontSizeXXL + width: Style.baseWidgetSize * 0.5 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + } + + // Keybind indicator and countdown text at far right + Item { + id: indicatorGroup + width: (countdownText.visible ? countdownText.width + Style.marginXS : 0) + numberIndicatorRect.width + height: numberIndicatorRect.height + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + z: 20 + + // Countdown as plain text (left of keybind) + NText { + id: countdownText + visible: !Settings.data.sessionMenu.showHeader && buttonRoot.pending && timerActive && pendingAction === modelData.action + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + text: Math.ceil(timeRemaining / 1000) + pointSize: Style.fontSizeS + color: Color.mPrimary + font.weight: Style.fontWeightBold + } + + // Keybind indicator + Rectangle { + id: numberIndicatorRect + anchors.left: countdownText.visible ? countdownText.right : parent.left + anchors.leftMargin: countdownText.visible ? Style.marginXS : 0 + anchors.verticalCenter: parent.verticalCenter + width: labelText.implicitWidth + Style.margin2M + height: labelText.height + Style.margin2XS + radius: Math.min(Style.radiusM, height / 2) + color: (buttonRoot.isSelected || buttonRoot.effectiveHover) ? Color.mOnPrimary : Color.mSurface + border.width: Style.borderS + border.color: (buttonRoot.isSelected || buttonRoot.effectiveHover) ? Color.mOnPrimary : Color.mOutline + visible: Settings.data.sessionMenu.showKeybinds && (buttonRoot.keybind !== "") && !buttonRoot.pending + + NText { + id: labelText + anchors.centerIn: parent + text: buttonRoot.keybind + pointSize: Style.fontSizeXS + color: (buttonRoot.isSelected || buttonRoot.effectiveHover) ? Color.mPrimary : Color.mOnSurface + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + } + } + } + + // Text content in the middle + ColumnLayout { + anchors.left: iconElement.right + anchors.right: indicatorGroup.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Style.marginL + anchors.rightMargin: Style.marginM + spacing: 0 + + NText { + text: buttonRoot.title + font.weight: Style.fontWeightMedium + pointSize: Style.fontSizeM + color: { + if (buttonRoot.pending) + return Color.mPrimary; + if (buttonRoot.isShutdown && !buttonRoot.isSelected && !buttonRoot.effectiveHover) + return Color.mError; + if (buttonRoot.isSelected || buttonRoot.effectiveHover) + return Color.mOnHover; + return Color.mOnSurface; + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onEntered: { + if (!root.ignoreMouseHover) { + selectedIndex = buttonRoot.buttonIndex; + } + } + + onExited: { + if (!root.ignoreMouseHover && selectedIndex === buttonRoot.buttonIndex) { + selectedIndex = -1; + } + } + + onClicked: buttonRoot.clicked() + } + } + + // Large buttons style button component + component LargeButton: Rectangle { + id: largeButtonRoot + + property string icon: "" + property string title: "" + property bool pending: false + property bool isShutdown: false + property bool isSelected: false + property int number: 0 + property string keybind: "" + property int buttonIndex: -1 + + // Effective hover state that respects ignoreMouseHover + readonly property bool effectiveHover: !root.ignoreMouseHover && mouseArea.containsMouse + readonly property real hoveredScale: 1.05 + + signal clicked + + property real hoverScale: (isSelected || effectiveHover) ? hoveredScale : 1.0 + + radius: Style.radiusL + color: { + if (pending) { + return Qt.alpha(Color.mPrimary, 1.0); + } + if (isSelected || effectiveHover) { + return Qt.alpha(Color.mPrimary, 1.0); + } + return Color.mSurface; + } + + border.width: Style.borderS + border.color: Color.mOutline + + // Always enable layer to fix nvidia bug, render at 2x size to avoid blur when scaling up + layer.enabled: true + layer.smooth: true + layer.textureSize: Qt.size(Math.ceil(width * 2), Math.ceil(height * 2)) + + // Scale transform for hover effect + transform: Scale { + origin.x: largeButtonRoot.width / 2 + origin.y: largeButtonRoot.height / 2 + xScale: hoverScale + yScale: hoverScale + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + + Behavior on border.width { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + + Behavior on hoverScale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + easing.overshoot: 0.5 + } + } + + ColumnLayout { + anchors.centerIn: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + // Large icon with scale animation + NIcon { + id: iconElement + Layout.alignment: Qt.AlignHCenter + icon: largeButtonRoot.icon + color: { + if (largeButtonRoot.pending) + return Color.mOnPrimary; + if (largeButtonRoot.isShutdown && !largeButtonRoot.isSelected && !largeButtonRoot.effectiveHover) + return Color.mError; + if (largeButtonRoot.isSelected || largeButtonRoot.effectiveHover) + return Color.mOnPrimary; + return Color.mOnSurface; + } + pointSize: Style.fontSizeXXXL * 2.25 + width: 90 * Style.uiScaleRatio + height: 90 * Style.uiScaleRatio + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + readonly property real hoveredIconScale: 1.15 + property real iconScale: (largeButtonRoot.isSelected || largeButtonRoot.effectiveHover) ? hoveredIconScale : 1.0 + + // Always enable layer to fix nvidia bug, render at 2x size to avoid blur when scaling up + layer.enabled: true + layer.smooth: true + layer.textureSize: Qt.size(Math.ceil(width * 2), Math.ceil(height * 2)) + + transform: Scale { + origin.x: iconElement.width / 2 + origin.y: iconElement.height / 2 + xScale: iconElement.iconScale + yScale: iconElement.iconScale + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + + Behavior on iconScale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutBack + easing.overshoot: 0.6 + } + } + } + + // Title text + NText { + Layout.alignment: Qt.AlignHCenter + text: largeButtonRoot.title + font.weight: Style.fontWeightMedium + pointSize: Style.fontSizeL + color: { + if (largeButtonRoot.pending) + return Color.mOnPrimary; + if (largeButtonRoot.isShutdown && !largeButtonRoot.isSelected && !largeButtonRoot.effectiveHover) + return Color.mError; + if (largeButtonRoot.isSelected || largeButtonRoot.effectiveHover) + return Color.mOnPrimary; + return Color.mOnSurface; + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + } + } + + // Keybind/Number indicator in top-right corner + Rectangle { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginM + width: largeNumberText.implicitWidth + Style.margin2M + height: largeNumberText.implicitHeight + Style.margin2XS + radius: Math.min(Style.radiusM, height / 2) + color: (largeButtonRoot.isSelected || largeButtonRoot.effectiveHover) ? Color.mOnPrimary : Qt.alpha(Color.mSurfaceVariant, 0.7) + border.width: Style.borderS + border.color: (largeButtonRoot.isSelected || largeButtonRoot.effectiveHover) ? Color.mOnPrimary : Color.mOutline + visible: Settings.data.sessionMenu.showKeybinds && (largeButtonRoot.keybind !== "") && !largeButtonRoot.pending + z: 10 + + NText { + id: largeNumberText + anchors.centerIn: parent + text: largeButtonRoot.keybind + pointSize: Style.fontSizeS + color: { + if (largeButtonRoot.isSelected || largeButtonRoot.effectiveHover) + return Color.mPrimary; + return Color.mOnSurfaceVariant; + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCirc + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onEntered: { + if (!root.ignoreMouseHover) { + selectedIndex = largeButtonRoot.buttonIndex; + } + } + + onExited: { + if (!root.ignoreMouseHover && selectedIndex === largeButtonRoot.buttonIndex) { + selectedIndex = -1; + } + } + + onClicked: largeButtonRoot.clicked() + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml new file mode 100644 index 0000000..c218356 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml @@ -0,0 +1,193 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import qs.Commons +import qs.Services.UI +import qs.Widgets + +// Widget Settings Dialog Component +Popup { + id: root + + property int widgetIndex: -1 + property var widgetData: null + property string widgetId: "" + property string sectionId: "" + property var screen: null + property var settingsCache: ({}) + + readonly property real maxHeight: (screen ? screen.height : (parent ? parent.height : 800)) * 0.8 + + signal updateWidgetSettings(string section, int index, var settings) + + width: Math.max(content.implicitWidth + padding * 2, 640) + height: Math.min(content.implicitHeight + padding * 2, maxHeight) + padding: Style.marginXL + modal: true + closePolicy: Popup.NoAutoClose + dim: false + anchors.centerIn: parent + + onOpened: { + // Load settings when popup opens with data + if (widgetData && widgetId) { + loadWidgetSettings(); + } + // Request focus to ensure keyboard input works + forceActiveFocus(); + } + + background: Rectangle { + id: bgRect + + color: Color.mSurface + radius: Style.radiusL + border.color: Color.mPrimary + border.width: Style.borderM + } + + contentItem: FocusScope { + id: focusScope + focus: true + + ColumnLayout { + id: content + anchors.fill: parent + spacing: Style.marginM + + // Title + RowLayout { + id: titleRow + Layout.fillWidth: true + Layout.preferredHeight: implicitHeight + + NText { + text: I18n.tr("system.widget-settings-title", { + "widget": root.widgetId + }) + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + onClicked: saveAndClose() + } + } + + // Separator + Rectangle { + id: separator + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + } + + // Scrollable settings area + NScrollView { + id: scrollView + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 100 + gradientColor: Color.mSurface + + ColumnLayout { + width: scrollView.availableWidth + spacing: Style.marginM + + // Settings based on widget type + // Will be triggered via settingsLoader.setSource() + Loader { + id: settingsLoader + Layout.fillWidth: true + onLoaded: { + // Try to focus the first focusable item in the loaded settings + if (item) { + Qt.callLater(() => { + var firstInput = findFirstFocusable(item); + if (firstInput) { + firstInput.forceActiveFocus(); + } else { + focusScope.forceActiveFocus(); + } + }); + } + } + + function findFirstFocusable(item) { + if (!item) + return null; + // Check if this item can accept focus + if (item.focus !== undefined && item.focus === true) + return item; + // Check children + if (item.children) { + for (var i = 0; i < item.children.length; i++) { + var child = item.children[i]; + if (child && child.focus !== undefined && child.focus === true) + return child; + var found = findFirstFocusable(child); + if (found) + return found; + } + } + return null; + } + } + } + } + } + } + + Timer { + id: saveTimer + running: false + interval: 150 + onTriggered: { + root.updateWidgetSettings(root.sectionId, root.widgetIndex, root.settingsCache); + } + } + + Connections { + target: settingsLoader.item + ignoreUnknownSignals: true + function onSettingsChanged(newSettings) { + if (newSettings) { + root.settingsCache = newSettings; + saveTimer.start(); + } + } + } + + function saveAndClose() { + if (settingsLoader.item && typeof settingsLoader.item.saveSettings === 'function') { + var newSettings = settingsLoader.item.saveSettings(); + if (newSettings) { + root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings); + } + } + root.close(); + } + + function loadWidgetSettings() { + const source = BarWidgetRegistry.widgetSettingsMap[widgetId]; + if (source) { + var currentWidgetData = widgetData; + if (sectionId && widgetIndex >= 0) { + var widgets = Settings.getBarWidgetsForScreen(screen?.name || "")[sectionId]; + if (widgets && widgetIndex < widgets.length) { + currentWidgetData = widgets[widgetIndex]; + } + } + settingsLoader.setSource(source, { + "screen": screen, + "widgetData": currentWidgetData, + "widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId] + }); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/MonitorWidgetsConfig.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/MonitorWidgetsConfig.qml new file mode 100644 index 0000000..fe4627e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/MonitorWidgetsConfig.qml @@ -0,0 +1,230 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import "../../../../Helpers/QtObj2JS.js" as QtObj2JS +import qs.Commons +import qs.Services.Noctalia +import qs.Services.UI +import qs.Widgets + +// Monitor Widgets Configuration (inline) +NBox { + id: root + + required property var screen + readonly property string screenName: screen?.name || "" + // determine bar orientation + readonly property string barPosition: Settings.getBarPositionForScreen(screenName) + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + + color: Color.mSurfaceVariant + Layout.fillWidth: true + implicitHeight: content.implicitHeight + Style.margin2L + + // Helper to get widgets for this screen (ensures override exists) + function _getWidgetsContainer() { + if (!Settings.hasScreenOverride(screenName, "widgets")) { + var currentWidgets = Settings.getBarWidgetsForScreen(screenName); + var widgetsCopy = QtObj2JS.qtObjectToPlainObject(currentWidgets); + Settings.setScreenOverride(screenName, "widgets", widgetsCopy); + } + var entry = Settings.getScreenOverrideEntry(screenName); + return entry ? entry.widgets : Settings.data.bar.widgets; + } + + // Persist widget changes by reassigning the override (triggers change detection) + function _saveWidgets(widgets) { + Settings.setScreenOverride(screenName, "widgets", widgets); + } + + // Widget manipulation functions + function _addWidgetToSection(widgetId, section) { + var newWidget = { + "id": widgetId + }; + if (BarWidgetRegistry.widgetHasUserSettings(widgetId)) { + var metadata = BarWidgetRegistry.widgetMetadata[widgetId]; + if (metadata) { + Object.keys(metadata).forEach(function (key) { + newWidget[key] = metadata[key]; + }); + } + } + var widgets = _getWidgetsContainer(); + widgets[section].push(newWidget); + _saveWidgets(widgets); + BarService.widgetsRevision++; + } + + function _removeWidgetFromSection(section, index) { + var widgets = _getWidgetsContainer(); + if (index >= 0 && index < widgets[section].length) { + var newArray = widgets[section].slice(); + var removedWidgets = newArray.splice(index, 1); + widgets[section] = newArray; + _saveWidgets(widgets); + BarService.widgetsRevision++; + + if (removedWidgets[0].id === "ControlCenter" && BarService.lookupWidget("ControlCenter") === undefined) { + ToastService.showWarning(I18n.tr("toast.missing-control-center.label"), I18n.tr("toast.missing-control-center.description"), 12000); + } + } + } + + function _reorderWidgetInSection(section, fromIndex, toIndex) { + var widgets = _getWidgetsContainer(); + if (fromIndex >= 0 && fromIndex < widgets[section].length && toIndex >= 0 && toIndex < widgets[section].length) { + var newArray = widgets[section].slice(); + var item = newArray[fromIndex]; + newArray.splice(fromIndex, 1); + newArray.splice(toIndex, 0, item); + widgets[section] = newArray; + _saveWidgets(widgets); + BarService.widgetsRevision++; + } + } + + // Note: _updateWidgetSettingsInSection does NOT increment revision + // because it only changes settings, not widget structure + function _updateWidgetSettingsInSection(section, index, settings) { + var widgets = _getWidgetsContainer(); + widgets[section][index] = settings; + _saveWidgets(widgets); + } + + function _moveWidgetBetweenSections(fromSection, index, toSection) { + var widgets = _getWidgetsContainer(); + if (index >= 0 && index < widgets[fromSection].length) { + var widget = widgets[fromSection][index]; + var sourceArray = widgets[fromSection].slice(); + sourceArray.splice(index, 1); + widgets[fromSection] = sourceArray; + var targetArray = widgets[toSection].slice(); + targetArray.push(widget); + widgets[toSection] = targetArray; + _saveWidgets(widgets); + BarService.widgetsRevision++; + } + } + + // Available widgets ListModel + function updateAvailableWidgetsModel() { + availableWidgetsModel.clear(); + var widgetIds = BarWidgetRegistry.getAvailableWidgets(); + if (!widgetIds) + return; + for (var i = 0; i < widgetIds.length; i++) { + var id = widgetIds[i]; + var displayName = id; + const badges = []; + if (BarWidgetRegistry.isPluginWidget(id)) { + var pluginId = id.replace("plugin:", ""); + var manifest = PluginRegistry.getPluginManifest(pluginId); + if (manifest && manifest.name) { + displayName = manifest.name; + } else { + displayName = pluginId; + } + badges.push({ + "icon": "plugin", + "color": Color.mSecondary + }); + } + if (BarWidgetRegistry.isCpuIntensive(id)) { + badges.push({ + "icon": "cpu-intensive", + "color": Color.mSecondary + }); + } + availableWidgetsModel.append({ + "key": id, + "name": displayName, + "badges": badges + }); + } + } + + Component.onCompleted: Qt.callLater(updateAvailableWidgetsModel) + + ListModel { + id: availableWidgetsModel + } + + // Get effective widgets for this screen + readonly property var effectiveWidgets: Settings.getBarWidgetsForScreen(screenName) + + ColumnLayout { + id: content + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + NText { + text: I18n.tr("panels.bar.widgets-desc") + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + // Left Section + NSectionEditor { + sectionName: root.barIsVertical ? I18n.tr("positions.top") : I18n.tr("positions.left") + sectionId: "left" + barIsVertical: root.barIsVertical + screen: root.screen + settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml") + widgetRegistry: BarWidgetRegistry + widgetModel: root.effectiveWidgets.left + availableWidgets: availableWidgetsModel + onAddWidget: (widgetId, section) => root._addWidgetToSection(widgetId, section) + onRemoveWidget: (section, index) => root._removeWidgetFromSection(section, index) + onReorderWidget: (section, fromIndex, toIndex) => root._reorderWidgetInSection(section, fromIndex, toIndex) + onUpdateWidgetSettings: (section, index, settings) => root._updateWidgetSettingsInSection(section, index, settings) + onMoveWidget: (fromSection, index, toSection) => root._moveWidgetBetweenSections(fromSection, index, toSection) + onOpenPluginSettingsRequested: manifest => pluginSettingsDialog.openPluginSettings(manifest) + } + + // Center Section + NSectionEditor { + sectionName: I18n.tr("positions.center") + sectionId: "center" + barIsVertical: root.barIsVertical + screen: root.screen + settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml") + widgetRegistry: BarWidgetRegistry + widgetModel: root.effectiveWidgets.center + availableWidgets: availableWidgetsModel + onAddWidget: (widgetId, section) => root._addWidgetToSection(widgetId, section) + onRemoveWidget: (section, index) => root._removeWidgetFromSection(section, index) + onReorderWidget: (section, fromIndex, toIndex) => root._reorderWidgetInSection(section, fromIndex, toIndex) + onUpdateWidgetSettings: (section, index, settings) => root._updateWidgetSettingsInSection(section, index, settings) + onMoveWidget: (fromSection, index, toSection) => root._moveWidgetBetweenSections(fromSection, index, toSection) + onOpenPluginSettingsRequested: manifest => pluginSettingsDialog.openPluginSettings(manifest) + } + + // Right Section + NSectionEditor { + sectionName: root.barIsVertical ? I18n.tr("positions.bottom") : I18n.tr("positions.right") + sectionId: "right" + barIsVertical: root.barIsVertical + screen: root.screen + settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml") + widgetRegistry: BarWidgetRegistry + widgetModel: root.effectiveWidgets.right + availableWidgets: availableWidgetsModel + onAddWidget: (widgetId, section) => root._addWidgetToSection(widgetId, section) + onRemoveWidget: (section, index) => root._removeWidgetFromSection(section, index) + onReorderWidget: (section, fromIndex, toIndex) => root._reorderWidgetInSection(section, fromIndex, toIndex) + onUpdateWidgetSettings: (section, index, settings) => root._updateWidgetSettingsInSection(section, index, settings) + onMoveWidget: (fromSection, index, toSection) => root._moveWidgetBetweenSections(fromSection, index, toSection) + onOpenPluginSettingsRequested: manifest => pluginSettingsDialog.openPluginSettings(manifest) + } + } + + // Plugin settings dialog + NPluginSettingsPopup { + id: pluginSettingsDialog + parent: Overlay.overlay + showToastOnSave: false + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml new file mode 100644 index 0000000..f997fd6 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/ActiveWindowSettings.qml @@ -0,0 +1,168 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property bool valueShowIcon: widgetData.showIcon !== undefined ? widgetData.showIcon : widgetMetadata.showIcon + property bool valueShowText: widgetData.showText !== undefined ? widgetData.showText : widgetMetadata.showText + property string valueHideMode: widgetData.hideMode !== undefined ? widgetData.hideMode : widgetMetadata.hideMode + property string valueScrollingMode: widgetData.scrollingMode || widgetMetadata.scrollingMode + property int valueMaxWidth: widgetData.maxWidth !== undefined ? widgetData.maxWidth : widgetMetadata.maxWidth + property bool valueUseFixedWidth: widgetData.useFixedWidth !== undefined ? widgetData.useFixedWidth : widgetMetadata.useFixedWidth + property bool valueColorizeIcons: widgetData.colorizeIcons !== undefined ? widgetData.colorizeIcons : widgetMetadata.colorizeIcons + property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor + + Component.onCompleted: { + if (widgetData && widgetData.hideMode !== undefined) { + valueHideMode = widgetData.hideMode; + } + } + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.hideMode = valueHideMode; + settings.showIcon = valueShowIcon; + settings.showText = valueShowText; + settings.scrollingMode = valueScrollingMode; + settings.maxWidth = parseInt(widthInput.text) || widgetMetadata.maxWidth; + settings.useFixedWidth = valueUseFixedWidth; + settings.colorizeIcons = valueColorizeIcons; + settings.textColor = valueTextColor; + settingsChanged(settings); + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("bar.taskbar.hide-mode-label") + description: I18n.tr("bar.active-window.hide-mode-description") + model: [ + { + "key": "visible", + "name": I18n.tr("hide-modes.visible") + }, + { + "key": "hidden", + "name": I18n.tr("hide-modes.hidden") + }, + { + "key": "transparent", + "name": I18n.tr("hide-modes.transparent") + } + ] + currentKey: root.valueHideMode + onSelected: key => { + root.valueHideMode = key; + saveSettings(); + } + defaultValue: widgetMetadata.hideMode + } + + NColorChoice { + label: I18n.tr("common.select-color") + currentKey: valueTextColor + onSelected: key => { + valueTextColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.textColor + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.active-window.show-app-text-label") + description: I18n.tr("bar.active-window.show-app-text-description") + checked: root.valueShowText + onToggled: checked => { + root.valueShowText = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showText + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.active-window.show-app-icon-label") + description: I18n.tr("bar.active-window.show-app-icon-description") + checked: root.valueShowIcon + onToggled: checked => { + root.valueShowIcon = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showIcon + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.tray.colorize-icons-label") + description: I18n.tr("bar.active-window.colorize-icons-description") + checked: root.valueColorizeIcons + onToggled: checked => { + root.valueColorizeIcons = checked; + saveSettings(); + } + visible: root.valueShowIcon + defaultValue: widgetMetadata.colorizeIcons + } + + NTextInput { + id: widthInput + Layout.fillWidth: true + label: I18n.tr("bar.taskbar.max-width-label") + description: I18n.tr("bar.media-mini.max-width-description") + placeholderText: widgetMetadata.maxWidth + text: valueMaxWidth + onTextChanged: saveSettings() + defaultValue: String(widgetMetadata.maxWidth) + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.media-mini.use-fixed-width-label") + description: I18n.tr("bar.media-mini.use-fixed-width-description") + checked: valueUseFixedWidth + onToggled: checked => { + valueUseFixedWidth = checked; + saveSettings(); + } + defaultValue: widgetMetadata.useFixedWidth + } + + NComboBox { + label: I18n.tr("bar.media-mini.scrolling-mode-label") + description: I18n.tr("bar.active-window.scrolling-mode-description") + model: [ + { + "key": "always", + "name": I18n.tr("options.scrolling-modes.always") + }, + { + "key": "hover", + "name": I18n.tr("options.scrolling-modes.hover") + }, + { + "key": "never", + "name": I18n.tr("options.scrolling-modes.never") + } + ] + currentKey: valueScrollingMode + defaultValue: widgetMetadata.scrollingMode + onSelected: key => { + valueScrollingMode = key; + saveSettings(); + } + minimumWidth: 200 + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/AudioVisualizerSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/AudioVisualizerSettings.qml new file mode 100644 index 0000000..499db6a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/AudioVisualizerSettings.qml @@ -0,0 +1,63 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property bool valueHideWhenIdle: widgetData.hideWhenIdle !== undefined ? widgetData.hideWhenIdle : widgetMetadata.hideWhenIdle + property string valueColorName: widgetData.colorName !== undefined ? widgetData.colorName : widgetMetadata.colorName + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.width = parseInt(widthInput.text) || widgetMetadata.width; + settings.hideWhenIdle = valueHideWhenIdle; + settings.colorName = valueColorName; + settingsChanged(settings); + } + + NTextInput { + id: widthInput + Layout.fillWidth: true + label: I18n.tr("common.width") + description: I18n.tr("bar.audio-visualizer.width-description") + text: widgetData.width || widgetMetadata.width + placeholderText: I18n.tr("placeholders.enter-width-pixels") + onTextChanged: saveSettings() + defaultValue: String(widgetMetadata.width) + } + + NColorChoice { + Layout.fillWidth: true + label: I18n.tr("bar.audio-visualizer.color-name-label") + description: I18n.tr("bar.audio-visualizer.color-name-description") + currentKey: root.valueColorName + onSelected: key => { + root.valueColorName = key; + saveSettings(); + } + defaultValue: widgetMetadata.colorName + } + + NToggle { + label: I18n.tr("bar.audio-visualizer.hide-when-idle-label") + description: I18n.tr("bar.audio-visualizer.hide-when-idle-description") + checked: valueHideWhenIdle + onToggled: checked => { + valueHideWhenIdle = checked; + saveSettings(); + } + defaultValue: widgetMetadata.hideWhenIdle + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/BatterySettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/BatterySettings.qml new file mode 100644 index 0000000..7c59cd2 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/BatterySettings.qml @@ -0,0 +1,138 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.Hardware +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode + property string valueDeviceNativePath: widgetData.deviceNativePath !== undefined ? widgetData.deviceNativePath : widgetMetadata.deviceNativePath + property bool valueShowPowerProfiles: widgetData.showPowerProfiles !== undefined ? widgetData.showPowerProfiles : widgetMetadata.showPowerProfiles + property bool valueShowNoctaliaPerformance: widgetData.showNoctaliaPerformance !== undefined ? widgetData.showNoctaliaPerformance : widgetMetadata.showNoctaliaPerformance + property bool valueHideIfNotDetected: widgetData.hideIfNotDetected !== undefined ? widgetData.hideIfNotDetected : widgetMetadata.hideIfNotDetected + property bool valueHideIfIdle: widgetData.hideIfIdle !== undefined ? widgetData.hideIfIdle : widgetMetadata.hideIfIdle + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + if (widgetData && widgetData.id) { + settings.id = widgetData.id; + } + settings.displayMode = valueDisplayMode; + settings.showPowerProfiles = valueShowPowerProfiles; + settings.showNoctaliaPerformance = valueShowNoctaliaPerformance; + settings.hideIfNotDetected = valueHideIfNotDetected; + settings.hideIfIdle = valueHideIfIdle; + settings.deviceNativePath = valueDeviceNativePath; + settingsChanged(settings); + } + + NComboBox { + id: deviceComboBox + Layout.fillWidth: true + label: I18n.tr("bar.battery.device-label") + description: I18n.tr("bar.battery.device-description") + minimumWidth: 240 + model: BatteryService.deviceModel + currentKey: root.valueDeviceNativePath + onSelected: key => { + root.valueDeviceNativePath = key; + saveSettings(); + } + defaultValue: widgetMetadata.deviceNativePath + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("common.display-mode") + description: I18n.tr("bar.battery.display-mode-description") + minimumWidth: 240 + model: [ + { + "key": "graphic", + "name": I18n.tr("bar.battery.display-mode-graphic") + }, + { + "key": "graphic-clean", + "name": I18n.tr("bar.battery.display-mode-graphic-clean") + }, + { + "key": "icon-hover", + "name": I18n.tr("bar.battery.display-mode-icon-hover") + }, + { + "key": "icon-always", + "name": I18n.tr("bar.battery.display-mode-icon-always") + }, + { + "key": "icon-only", + "name": I18n.tr("bar.battery.display-mode-icon-only") + } + ] + currentKey: root.valueDisplayMode + onSelected: key => { + root.valueDisplayMode = key; + saveSettings(); + } + defaultValue: widgetMetadata.displayMode + } + + NToggle { + label: I18n.tr("bar.battery.hide-if-not-detected-label") + description: I18n.tr("bar.battery.hide-if-not-detected-description") + checked: valueHideIfNotDetected + onToggled: checked => { + valueHideIfNotDetected = checked; + saveSettings(); + } + defaultValue: widgetMetadata.hideIfNotDetected + } + + NToggle { + label: I18n.tr("bar.battery.hide-if-idle-label") + description: I18n.tr("bar.battery.hide-if-idle-description") + checked: valueHideIfIdle + onToggled: checked => { + valueHideIfIdle = checked; + saveSettings(); + } + defaultValue: widgetMetadata.hideIfIdle + } + + NDivider { + Layout.fillWidth: true + } + + NToggle { + label: I18n.tr("bar.battery.show-power-profile-label") + description: I18n.tr("bar.battery.show-power-profile-description") + checked: valueShowPowerProfiles + onToggled: checked => { + valueShowPowerProfiles = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showPowerProfiles + } + + NToggle { + label: I18n.tr("bar.battery.show-noctalia-performance-label") + description: I18n.tr("bar.battery.show-noctalia-performance-description") + checked: valueShowNoctaliaPerformance + onToggled: checked => { + valueShowNoctaliaPerformance = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showNoctaliaPerformance + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/BluetoothSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/BluetoothSettings.qml new file mode 100644 index 0000000..cd1c53d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/BluetoothSettings.qml @@ -0,0 +1,74 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.displayMode = valueDisplayMode; + settings.iconColor = valueIconColor; + settings.textColor = valueTextColor; + settingsChanged(settings); + } + + NComboBox { + label: I18n.tr("common.display-mode") + description: I18n.tr("bar.volume.display-mode-description") + minimumWidth: 200 + model: [ + { + "key": "onhover", + "name": I18n.tr("display-modes.on-hover") + }, + { + "key": "alwaysShow", + "name": I18n.tr("display-modes.always-show") + }, + { + "key": "alwaysHide", + "name": I18n.tr("display-modes.always-hide") + } + ] + currentKey: root.valueDisplayMode + onSelected: key => { + root.valueDisplayMode = key; + saveSettings(); + } + defaultValue: widgetMetadata.displayMode + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: valueIconColor + onSelected: key => { + valueIconColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.iconColor + } + + NColorChoice { + currentKey: valueTextColor + onSelected: key => { + valueTextColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.textColor + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/BrightnessSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/BrightnessSettings.qml new file mode 100644 index 0000000..38a3494 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/BrightnessSettings.qml @@ -0,0 +1,93 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor + property bool valueApplyToAllMonitors: widgetData.applyToAllMonitors !== undefined ? widgetData.applyToAllMonitors : widgetMetadata.applyToAllMonitors + + readonly property bool hasMultipleMonitors: (Quickshell.screens || []).length > 1 + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.displayMode = valueDisplayMode; + settings.iconColor = valueIconColor; + settings.textColor = valueTextColor; + settings.applyToAllMonitors = valueApplyToAllMonitors; + settingsChanged(settings); + } + + NComboBox { + label: I18n.tr("common.display-mode") + description: I18n.tr("bar.volume.display-mode-description") + minimumWidth: 200 + model: [ + { + "key": "onhover", + "name": I18n.tr("display-modes.on-hover") + }, + { + "key": "alwaysShow", + "name": I18n.tr("display-modes.always-show") + }, + { + "key": "alwaysHide", + "name": I18n.tr("display-modes.always-hide") + } + ] + currentKey: valueDisplayMode + onSelected: key => { + valueDisplayMode = key; + saveSettings(); + } + defaultValue: widgetMetadata.displayMode + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: valueIconColor + onSelected: key => { + valueIconColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.iconColor + } + + NColorChoice { + currentKey: valueTextColor + onSelected: key => { + valueTextColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.textColor + } + + NToggle { + visible: hasMultipleMonitors + Layout.fillWidth: true + label: I18n.tr("bar.brightness.apply-all-label") + description: I18n.tr("bar.brightness.apply-all-description") + checked: valueApplyToAllMonitors + onToggled: checked => { + valueApplyToAllMonitors = checked; + saveSettings(); + } + defaultValue: widgetMetadata.applyToAllMonitors + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/ClockSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/ClockSettings.qml new file mode 100644 index 0000000..6d2f9c3 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/ClockSettings.qml @@ -0,0 +1,315 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.System +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + width: 700 + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property string valueClockColor: widgetData.clockColor !== undefined ? widgetData.clockColor : widgetMetadata.clockColor + property bool valueUseCustomFont: widgetData.useCustomFont !== undefined ? widgetData.useCustomFont : widgetMetadata.useCustomFont + property string valueCustomFont: widgetData.customFont !== undefined ? widgetData.customFont : widgetMetadata.customFont + property string valueFormatHorizontal: widgetData.formatHorizontal !== undefined ? widgetData.formatHorizontal : widgetMetadata.formatHorizontal + property string valueFormatVertical: widgetData.formatVertical !== undefined ? widgetData.formatVertical : widgetMetadata.formatVertical + property string valueTooltipFormat: widgetData.tooltipFormat !== undefined ? widgetData.tooltipFormat : widgetMetadata.tooltipFormat + + readonly property color textColor: Color.resolveColorKey(valueClockColor) + + // Track the currently focused input field + property var focusedInput: null + property int focusedLineIndex: -1 + + readonly property var now: Time.now + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.clockColor = valueClockColor; + settings.useCustomFont = valueUseCustomFont; + settings.customFont = valueCustomFont; + settings.formatHorizontal = valueFormatHorizontal.trim(); + settings.formatVertical = valueFormatVertical.trim(); + settings.tooltipFormat = valueTooltipFormat.trim(); + settingsChanged(settings); + } + + // Function to insert token at cursor position in the focused input + function insertToken(token) { + if (!focusedInput || !focusedInput.inputItem) { + // If no input is focused, default to horiz + if (inputHoriz.inputItem) { + inputHoriz.inputItem.focus = true; + focusedInput = inputHoriz; + } + } + + if (focusedInput && focusedInput.inputItem) { + var input = focusedInput.inputItem; + var cursorPos = input.cursorPosition; + var currentText = input.text; + + // Insert token at cursor position + var newText = currentText.substring(0, cursorPos) + token + currentText.substring(cursorPos); + input.text = newText + " "; + + // Move cursor after the inserted token + input.cursorPosition = cursorPos + token.length + 1; + + // Ensure the input keeps focus + input.focus = true; + } + } + + NColorChoice { + currentKey: valueClockColor + onSelected: key => { + valueClockColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.clockColor + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.clock.use-custom-font-label") + description: I18n.tr("bar.clock.use-custom-font-description") + checked: valueUseCustomFont + onToggled: checked => { + valueUseCustomFont = checked; + saveSettings(); + } + defaultValue: widgetMetadata.useCustomFont + } + + NSearchableComboBox { + Layout.fillWidth: true + visible: valueUseCustomFont + label: I18n.tr("bar.clock.custom-font-label") + description: I18n.tr("bar.clock.custom-font-description") + model: FontService.availableFonts + currentKey: valueCustomFont + placeholder: I18n.tr("bar.clock.custom-font-placeholder") + searchPlaceholder: I18n.tr("bar.clock.custom-font-search-placeholder") + popupHeight: 420 + minimumWidth: 300 + onSelected: function (key) { + valueCustomFont = key; + saveSettings(); + } + defaultValue: Settings.data.ui.fontDefault + } + + NDivider { + Layout.fillWidth: true + } + + NHeader { + label: I18n.tr("bar.clock.clock-display-label") + description: I18n.tr("bar.clock.clock-display-description") + } + + RowLayout { + id: main + + spacing: Style.marginL + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + + ColumnLayout { + spacing: Style.marginM + + Layout.fillWidth: true + Layout.preferredWidth: 1 // Equal sizing hint + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + + NTextInput { + id: inputHoriz + Layout.fillWidth: true + label: I18n.tr("bar.clock.horizontal-bar-label") + description: I18n.tr("bar.clock.horizontal-bar-description") + placeholderText: "HH:mm ddd, MMM dd" + text: valueFormatHorizontal + onTextChanged: { + valueFormatHorizontal = text; + saveSettings(); + } + Component.onCompleted: { + if (inputItem) { + inputItem.onActiveFocusChanged.connect(function () { + if (inputItem.activeFocus) { + root.focusedInput = inputHoriz; + } + }); + } + } + defaultValue: widgetMetadata.formatHorizontal + } + + Item { + Layout.fillHeight: true + } + + NTextInput { + id: inputVert + Layout.fillWidth: true + label: I18n.tr("bar.clock.vertical-bar-label") + description: I18n.tr("bar.clock.vertical-bar-description") + // Tokens are Qt format tokens and must not be localized + placeholderText: "HH mm dd MM" + text: valueFormatVertical + onTextChanged: { + valueFormatVertical = text; + saveSettings(); + } + Component.onCompleted: { + if (inputItem) { + inputItem.onActiveFocusChanged.connect(function () { + if (inputItem.activeFocus) { + root.focusedInput = inputVert; + } + }); + } + } + defaultValue: widgetMetadata.formatVertical + } + + NTextInput { + id: inputTooltip + Layout.fillWidth: true + label: I18n.tr("bar.clock.tooltip-format-label") + description: I18n.tr("bar.clock.tooltip-format-description") + placeholderText: "HH:mm, ddd MMM dd" + text: valueTooltipFormat + onTextChanged: { + valueTooltipFormat = text; + saveSettings(); + } + Component.onCompleted: { + if (inputItem) { + inputItem.onActiveFocusChanged.connect(function () { + if (inputItem.activeFocus) { + root.focusedInput = inputTooltip; + } + }); + } + } + defaultValue: widgetMetadata.tooltipFormat + } + } + + // -------------- + // Preview + ColumnLayout { + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.fillWidth: false + + NLabel { + label: I18n.tr("bar.clock.preview") + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + } + + Rectangle { + Layout.preferredWidth: 320 + Layout.preferredHeight: 160 // Fixed height instead of fillHeight + + color: Color.mSurfaceVariant + radius: Style.radiusM + border.color: Color.mSecondary + border.width: Style.borderS + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + ColumnLayout { + spacing: Style.marginM + anchors.centerIn: parent + + ColumnLayout { + spacing: -2 + Layout.alignment: Qt.AlignHCenter + + // Horizontal + Repeater { + Layout.topMargin: Style.marginM + model: I18n.locale.toString(now, valueFormatHorizontal.trim()).split("\\n") + delegate: NText { + visible: text !== "" + text: modelData + family: valueUseCustomFont && valueCustomFont ? valueCustomFont : Settings.data.ui.fontDefault + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + color: textColor + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + } + + NDivider { + Layout.fillWidth: true + } + + // Vertical + ColumnLayout { + spacing: -2 + Layout.alignment: Qt.AlignHCenter + + Repeater { + Layout.topMargin: Style.marginM + model: I18n.locale.toString(now, valueFormatVertical.trim()).split(" ") + delegate: NText { + visible: text !== "" + text: modelData + family: valueUseCustomFont && valueCustomFont ? valueCustomFont : Settings.data.ui.fontDefault + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + color: textColor + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + } + } + } + } + } + + NDivider { + Layout.topMargin: Style.marginM + Layout.bottomMargin: Style.marginM + } + + NDateTimeTokens { + Layout.fillWidth: true + Layout.preferredHeight: 300 + + // Connect to token clicked signal if NDateTimeTokens provides it + onTokenClicked: token => root.insertToken(token) + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/ControlCenterSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/ControlCenterSettings.qml new file mode 100644 index 0000000..d24602d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/ControlCenterSettings.qml @@ -0,0 +1,134 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon + property bool valueUseDistroLogo: widgetData.useDistroLogo !== undefined ? widgetData.useDistroLogo : widgetMetadata.useDistroLogo + property string valueCustomIconPath: widgetData.customIconPath !== undefined ? widgetData.customIconPath : widgetMetadata.customIconPath + property bool valueEnableColorization: widgetData.enableColorization !== undefined ? widgetData.enableColorization : widgetMetadata.enableColorization + property string valueColorizeSystemIcon: widgetData.colorizeSystemIcon !== undefined ? widgetData.colorizeSystemIcon : widgetMetadata.colorizeSystemIcon + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.icon = valueIcon; + settings.useDistroLogo = valueUseDistroLogo; + settings.customIconPath = valueCustomIconPath; + settings.enableColorization = valueEnableColorization; + settings.colorizeSystemIcon = valueColorizeSystemIcon; + settingsChanged(settings); + } + + NToggle { + label: I18n.tr("bar.control-center.use-distro-logo-label") + description: I18n.tr("bar.control-center.use-distro-logo-description") + checked: valueUseDistroLogo + onToggled: checked => { + valueUseDistroLogo = checked; + saveSettings(); + } + defaultValue: widgetMetadata.useDistroLogo + } + + NToggle { + label: I18n.tr("bar.custom-button.enable-colorization-label") + description: I18n.tr("bar.control-center.enable-colorization-description") + checked: valueEnableColorization + onToggled: checked => { + valueEnableColorization = checked; + saveSettings(); + } + defaultValue: widgetMetadata.enableColorization + } + + NColorChoice { + visible: valueEnableColorization + label: I18n.tr("common.select-icon-color") + description: I18n.tr("bar.control-center.color-selection-description") + currentKey: valueColorizeSystemIcon + onSelected: function (key) { + valueColorizeSystemIcon = key; + saveSettings(); + } + defaultValue: widgetMetadata.colorizeSystemIcon + } + + RowLayout { + spacing: Style.marginM + + NLabel { + label: I18n.tr("common.icon") + description: I18n.tr("bar.control-center.icon-description") + } + + NImageRounded { + Layout.preferredWidth: Style.fontSizeXL * 2 + Layout.preferredHeight: Style.fontSizeXL * 2 + Layout.alignment: Qt.AlignVCenter + radius: Math.min(Style.radiusL, Layout.preferredWidth / 2) + imagePath: valueCustomIconPath + visible: valueCustomIconPath !== "" && !valueUseDistroLogo + } + + NIcon { + Layout.alignment: Qt.AlignVCenter + icon: valueIcon + pointSize: Style.fontSizeXXL * 1.5 + visible: valueIcon !== "" && valueCustomIconPath === "" && !valueUseDistroLogo + } + } + + RowLayout { + spacing: Style.marginM + NButton { + enabled: !valueUseDistroLogo + text: I18n.tr("bar.control-center.browse-library") + onClicked: iconPicker.open() + } + + NButton { + enabled: !valueUseDistroLogo + text: I18n.tr("bar.control-center.browse-file") + onClicked: imagePicker.openFilePicker() + } + } + + NIconPicker { + id: iconPicker + initialIcon: valueIcon + onIconSelected: iconName => { + valueIcon = iconName; + valueCustomIconPath = ""; + saveSettings(); + } + } + + NFilePicker { + id: imagePicker + title: I18n.tr("bar.control-center.select-custom-icon") + selectionMode: "files" + nameFilters: ImageCacheService.basicImageFilters.concat(["*.svg"]) + initialPath: Quickshell.env("HOME") + onAccepted: paths => { + if (paths.length > 0) { + valueCustomIconPath = paths[0]; // Use first selected file + saveSettings(); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/CustomButtonSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/CustomButtonSettings.qml new file mode 100644 index 0000000..0b8e1ce --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/CustomButtonSettings.qml @@ -0,0 +1,512 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import qs.Commons +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon + property bool valueTextStream: widgetData.textStream !== undefined ? widgetData.textStream : widgetMetadata.textStream + property bool valueParseJson: widgetData.parseJson !== undefined ? widgetData.parseJson : widgetMetadata.parseJson + property int valueMaxTextLengthHorizontal: widgetData?.maxTextLength?.horizontal ?? widgetMetadata?.maxTextLength?.horizontal + property int valueMaxTextLengthVertical: widgetData?.maxTextLength?.vertical ?? widgetMetadata?.maxTextLength?.vertical + property string valueHideMode: (widgetData.hideMode !== undefined) ? widgetData.hideMode : widgetMetadata.hideMode + property bool valueShowIcon: (widgetData.showIcon !== undefined) ? widgetData.showIcon : widgetMetadata.showIcon + property bool valueShowExecTooltip: widgetData.showExecTooltip !== undefined ? widgetData.showExecTooltip : widgetMetadata.showExecTooltip + property bool valueShowTextTooltip: widgetData.showTextTooltip !== undefined ? widgetData.showTextTooltip : widgetMetadata.showTextTooltip + property bool valueEnableColorization: widgetData.enableColorization !== undefined ? widgetData.enableColorization : widgetMetadata.enableColorization + property string valueColorizeSystemIcon: widgetData.colorizeSystemIcon !== undefined ? widgetData.colorizeSystemIcon : widgetMetadata.colorizeSystemIcon + property string valueIpcIdentifier: widgetData.ipcIdentifier !== undefined ? widgetData.ipcIdentifier : widgetMetadata.ipcIdentifier + property string valueGeneralTooltipText: widgetData.generalTooltipText !== undefined ? widgetData.generalTooltipText : widgetMetadata.generalTooltipText + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.icon = valueIcon; + settings.leftClickExec = leftClickExecInput.text; + settings.leftClickUpdateText = leftClickUpdateText.checked; + settings.rightClickExec = rightClickExecInput.text; + settings.rightClickUpdateText = rightClickUpdateText.checked; + settings.middleClickExec = middleClickExecInput.text; + settings.middleClickUpdateText = middleClickUpdateText.checked; + settings.wheelMode = separateWheelToggle.internalChecked ? "separate" : "unified"; + settings.wheelExec = wheelExecInput.text; + settings.wheelUpExec = wheelUpExecInput.text; + settings.wheelDownExec = wheelDownExecInput.text; + settings.wheelUpdateText = wheelUpdateText.checked; + settings.wheelUpUpdateText = wheelUpUpdateText.checked; + settings.wheelDownUpdateText = wheelDownUpdateText.checked; + settings.textCommand = textCommandInput.text; + settings.textCollapse = textCollapseInput.text; + settings.textStream = valueTextStream; + settings.parseJson = valueParseJson; + settings.showIcon = valueShowIcon; + settings.showExecTooltip = valueShowExecTooltip; + settings.showTextTooltip = valueShowTextTooltip; + settings.hideMode = valueHideMode; + settings.maxTextLength = { + "horizontal": valueMaxTextLengthHorizontal, + "vertical": valueMaxTextLengthVertical + }; + settings.textIntervalMs = parseInt(textIntervalInput.text || textIntervalInput.placeholderText, 10); + settings.enableColorization = valueEnableColorization; + settings.colorizeSystemIcon = valueColorizeSystemIcon; + settings.ipcIdentifier = valueIpcIdentifier; + settings.generalTooltipText = valueGeneralTooltipText; + settingsChanged(settings); + } + + RowLayout { + spacing: Style.marginM + + NLabel { + label: I18n.tr("common.icon") + description: I18n.tr("bar.custom-button.icon-description") + } + + NIcon { + Layout.alignment: Qt.AlignVCenter + icon: valueIcon + pointSize: Style.fontSizeXL + visible: valueIcon !== "" + } + + NButton { + text: I18n.tr("common.browse") + onClicked: iconPicker.open() + } + } + + NIconPicker { + id: iconPicker + initialIcon: valueIcon + onIconSelected: function (iconName) { + valueIcon = iconName; + saveSettings(); + } + } + + NToggle { + id: showIconToggle + label: I18n.tr("bar.custom-button.show-icon-label") + description: I18n.tr("bar.custom-button.show-icon-description") + checked: valueShowIcon + onToggled: checked => { + valueShowIcon = checked; + saveSettings(); + } + visible: textCommandInput.text !== "" + defaultValue: widgetMetadata.showIcon + } + + NToggle { + label: I18n.tr("bar.custom-button.enable-colorization-label") + description: I18n.tr("bar.custom-button.enable-colorization-description") + checked: valueEnableColorization + onToggled: checked => { + valueEnableColorization = checked; + saveSettings(); + } + defaultValue: widgetMetadata.enableColorization + } + + NColorChoice { + visible: valueEnableColorization + label: I18n.tr("common.select-icon-color") + description: I18n.tr("bar.custom-button.color-selection-description") + currentKey: valueColorizeSystemIcon + onSelected: key => { + valueColorizeSystemIcon = key; + saveSettings(); + } + defaultValue: widgetMetadata.colorizeSystemIcon + } + + NTextInput { + Layout.fillWidth: true + label: I18n.tr("bar.custom-button.general-tooltip-text-label") + description: I18n.tr("bar.custom-button.general-tooltip-text-description") + placeholderText: I18n.tr("placeholders.enter-tooltip") + text: valueGeneralTooltipText + onTextChanged: { + valueGeneralTooltipText = text; + saveSettings(); + } + defaultValue: widgetMetadata.generalTooltipText + } + + NToggle { + id: showExecTooltipToggle + label: I18n.tr("bar.custom-button.show-exec-tooltip-label") + description: I18n.tr("bar.custom-button.show-exec-tooltip-description") + checked: valueShowExecTooltip + onToggled: checked => { + valueShowExecTooltip = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showExecTooltip + } + + NToggle { + id: showTextTooltipToggle + label: I18n.tr("bar.custom-button.show-text-tooltip-label") + description: I18n.tr("bar.custom-button.show-text-tooltip-description") + checked: valueShowTextTooltip + onToggled: checked => { + valueShowTextTooltip = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showTextTooltip + } + + NTextInput { + Layout.fillWidth: true + label: I18n.tr("bar.custom-button.ipc-identifier-label") + description: I18n.tr("bar.custom-button.ipc-identifier-description") + placeholderText: I18n.tr("placeholders.enter-ipc-identifier") + text: valueIpcIdentifier + onTextChanged: { + valueIpcIdentifier = text; + saveSettings(); + } + defaultValue: widgetMetadata.ipcIdentifier + } + + RowLayout { + spacing: Style.marginM + + NTextInput { + id: leftClickExecInput + Layout.fillWidth: true + label: I18n.tr("bar.custom-button.left-click-label") + description: I18n.tr("bar.custom-button.left-click-description") + placeholderText: I18n.tr("placeholders.enter-command") + text: widgetData?.leftClickExec || widgetMetadata.leftClickExec + onTextChanged: saveSettings() + defaultValue: widgetMetadata.leftClickExec + } + + NToggle { + id: leftClickUpdateText + enabled: !valueTextStream + Layout.alignment: Qt.AlignRight | Qt.AlignBottom + Layout.bottomMargin: Style.marginS + onEntered: TooltipService.show(leftClickUpdateText, I18n.tr("bar.custom-button.left-click-update-text")) + onExited: TooltipService.hide() + checked: widgetData?.leftClickUpdateText ?? widgetMetadata.leftClickUpdateText + onToggled: isChecked => { + checked = isChecked; + saveSettings(); + } + defaultValue: widgetMetadata.leftClickUpdateText + } + } + + RowLayout { + spacing: Style.marginM + + NTextInput { + id: rightClickExecInput + Layout.fillWidth: true + label: I18n.tr("bar.custom-button.right-click-label") + description: I18n.tr("bar.custom-button.right-click-description") + placeholderText: I18n.tr("placeholders.enter-command") + text: widgetData?.rightClickExec || widgetMetadata.rightClickExec + onTextChanged: saveSettings() + defaultValue: widgetMetadata.rightClickExec + } + + NToggle { + id: rightClickUpdateText + enabled: !valueTextStream + Layout.alignment: Qt.AlignRight | Qt.AlignBottom + Layout.bottomMargin: Style.marginS + onEntered: TooltipService.show(rightClickUpdateText, I18n.tr("bar.custom-button.right-click-update-text")) + onExited: TooltipService.hide() + checked: widgetData?.rightClickUpdateText ?? widgetMetadata.rightClickUpdateText + onToggled: isChecked => { + checked = isChecked; + saveSettings(); + } + defaultValue: widgetMetadata.rightClickUpdateText + } + } + + RowLayout { + spacing: Style.marginM + + NTextInput { + id: middleClickExecInput + Layout.fillWidth: true + label: I18n.tr("bar.custom-button.middle-click-label") + description: I18n.tr("bar.custom-button.middle-click-description") + placeholderText: I18n.tr("placeholders.enter-command") + text: widgetData.middleClickExec || widgetMetadata.middleClickExec + onTextChanged: saveSettings() + defaultValue: widgetMetadata.middleClickExec + } + + NToggle { + id: middleClickUpdateText + enabled: !valueTextStream + Layout.alignment: Qt.AlignRight | Qt.AlignBottom + Layout.bottomMargin: Style.marginS + onEntered: TooltipService.show(middleClickUpdateText, I18n.tr("bar.custom-button.middle-click-update-text")) + onExited: TooltipService.hide() + checked: widgetData?.middleClickUpdateText ?? widgetMetadata.middleClickUpdateText + onToggled: isChecked => { + checked = isChecked; + saveSettings(); + } + defaultValue: widgetMetadata.middleClickUpdateText + } + } + + // Wheel command settings + NToggle { + id: separateWheelToggle + Layout.fillWidth: true + label: I18n.tr("bar.custom-button.wheel-mode-separate-label") + description: I18n.tr("bar.custom-button.wheel-mode-separate-description") + property bool internalChecked: (widgetData?.wheelMode || widgetMetadata?.wheelMode) === "separate" + checked: internalChecked + onToggled: checked => { + internalChecked = checked; + saveSettings(); + } + defaultValue: widgetMetadata.wheelMode === "separate" + } + + ColumnLayout { + Layout.fillWidth: true + + RowLayout { + id: unifiedWheelLayout + visible: !separateWheelToggle.checked + spacing: Style.marginM + + NTextInput { + id: wheelExecInput + Layout.fillWidth: true + label: I18n.tr("bar.custom-button.wheel-label") + description: I18n.tr("bar.custom-button.wheel-description") + placeholderText: I18n.tr("placeholders.enter-command") + text: widgetData?.wheelExec || widgetMetadata?.wheelExec + onTextChanged: saveSettings() + defaultValue: widgetMetadata.wheelExec + } + + NToggle { + id: wheelUpdateText + enabled: !valueTextStream + Layout.alignment: Qt.AlignRight | Qt.AlignBottom + Layout.bottomMargin: Style.marginS + onEntered: TooltipService.show(wheelUpdateText, I18n.tr("bar.custom-button.wheel-update-text")) + onExited: TooltipService.hide() + checked: widgetData?.wheelUpdateText ?? widgetMetadata?.wheelUpdateText + onToggled: isChecked => { + checked = isChecked; + saveSettings(); + } + defaultValue: widgetMetadata.wheelUpdateText + } + } + + ColumnLayout { + id: separatedWheelLayout + Layout.fillWidth: true + visible: separateWheelToggle.checked + + RowLayout { + spacing: Style.marginM + + NTextInput { + id: wheelUpExecInput + Layout.fillWidth: true + label: I18n.tr("bar.custom-button.wheel-up-label") + description: I18n.tr("bar.custom-button.wheel-up-description") + placeholderText: I18n.tr("placeholders.enter-command") + text: widgetData?.wheelUpExec || widgetMetadata?.wheelUpExec + onTextChanged: saveSettings() + defaultValue: widgetMetadata.wheelUpExec + } + + NToggle { + id: wheelUpUpdateText + enabled: !valueTextStream + Layout.alignment: Qt.AlignRight | Qt.AlignBottom + Layout.bottomMargin: Style.marginS + onEntered: TooltipService.show(wheelUpUpdateText, I18n.tr("bar.custom-button.wheel-update-text")) + onExited: TooltipService.hide() + checked: (widgetData?.wheelUpUpdateText !== undefined) ? widgetData.wheelUpUpdateText : widgetMetadata?.wheelUpUpdateText + onToggled: isChecked => { + checked = isChecked; + saveSettings(); + } + defaultValue: widgetMetadata.wheelUpUpdateText + } + } + + RowLayout { + spacing: Style.marginM + + NTextInput { + id: wheelDownExecInput + Layout.fillWidth: true + label: I18n.tr("bar.custom-button.wheel-down-label") + description: I18n.tr("bar.custom-button.wheel-down-description") + placeholderText: I18n.tr("placeholders.enter-command") + text: widgetData?.wheelDownExec || widgetMetadata?.wheelDownExec + onTextChanged: saveSettings() + defaultValue: widgetMetadata.wheelDownExec + } + + NToggle { + id: wheelDownUpdateText + enabled: !valueTextStream + Layout.alignment: Qt.AlignRight | Qt.AlignBottom + Layout.bottomMargin: Style.marginS + onEntered: TooltipService.show(wheelDownUpdateText, I18n.tr("bar.custom-button.wheel-update-text")) + onExited: TooltipService.hide() + checked: (widgetData?.wheelDownUpdateText !== undefined) ? widgetData.wheelDownUpdateText : widgetMetadata?.wheelDownUpdateText + onToggled: isChecked => { + checked = isChecked; + saveSettings(); + } + defaultValue: widgetMetadata.wheelDownUpdateText + } + } + } + } + + NDivider { + Layout.fillWidth: true + } + + NHeader { + label: I18n.tr("bar.custom-button.dynamic-text") + } + + NSpinBox { + label: I18n.tr("bar.custom-button.max-text-length-horizontal-label") + description: I18n.tr("bar.custom-button.max-text-length-horizontal-description") + from: 0 + to: 100 + value: valueMaxTextLengthHorizontal + onValueChanged: { + valueMaxTextLengthHorizontal = value; + saveSettings(); + } + defaultValue: widgetMetadata.maxTextLength.horizontal + } + + NSpinBox { + label: I18n.tr("bar.custom-button.max-text-length-vertical-label") + description: I18n.tr("bar.custom-button.max-text-length-vertical-description") + from: 0 + to: 100 + value: valueMaxTextLengthVertical + onValueChanged: { + valueMaxTextLengthVertical = value; + saveSettings(); + } + defaultValue: widgetMetadata.maxTextLength.vertical + } + + NToggle { + id: textStreamInput + label: I18n.tr("bar.custom-button.text-stream-label") + description: I18n.tr("bar.custom-button.text-stream-description") + checked: valueTextStream + onToggled: checked => { + valueTextStream = checked; + saveSettings(); + } + defaultValue: widgetMetadata.textStream + } + + NToggle { + id: parseJsonInput + label: I18n.tr("bar.custom-button.parse-json-label") + description: I18n.tr("bar.custom-button.parse-json-description") + checked: valueParseJson + onToggled: checked => { + valueParseJson = checked; + saveSettings(); + } + defaultValue: widgetMetadata.parseJson + } + + NTextInput { + id: textCommandInput + Layout.fillWidth: true + label: I18n.tr("bar.custom-button.display-command-output-label") + description: valueTextStream ? I18n.tr("bar.custom-button.display-command-output-stream-description") : I18n.tr("bar.custom-button.display-command-output-description") + placeholderText: I18n.tr("placeholders.command-example") + text: widgetData?.textCommand || widgetMetadata.textCommand + onTextChanged: saveSettings() + defaultValue: widgetMetadata.textCommand + } + + NTextInput { + id: textCollapseInput + Layout.fillWidth: true + visible: valueTextStream + label: I18n.tr("bar.custom-button.collapse-condition-label") + description: I18n.tr("bar.custom-button.collapse-condition-description") + placeholderText: I18n.tr("placeholders.enter-text-to-collapse") + text: widgetData?.textCollapse || widgetMetadata.textCollapse + onTextChanged: saveSettings() + defaultValue: widgetMetadata.textCollapse + } + + NTextInput { + id: textIntervalInput + Layout.fillWidth: true + visible: !valueTextStream + label: I18n.tr("bar.custom-button.refresh-interval-label") + description: I18n.tr("bar.custom-button.refresh-interval-description") + placeholderText: String(widgetMetadata.textIntervalMs) + text: widgetData && widgetData.textIntervalMs !== undefined ? String(widgetData.textIntervalMs) : "" + onTextChanged: saveSettings() + defaultValue: String(widgetMetadata.textIntervalMs) + } + + NComboBox { + id: hideModeComboBox + label: I18n.tr("bar.custom-button.hide-mode-label") + description: I18n.tr("bar.custom-button.hide-mode-description") + model: [ + { + name: I18n.tr("bar.custom-button.hide-mode-always-expanded"), + key: "alwaysExpanded" + }, + { + name: I18n.tr("bar.custom-button.hide-mode-expand-with-output"), + key: "expandWithOutput" + }, + { + name: I18n.tr("bar.custom-button.hide-mode-max-transparent"), + key: "maxTransparent" + } + ] + currentKey: valueHideMode + onSelected: key => { + valueHideMode = key; + saveSettings(); + } + visible: textCommandInput.text !== "" && valueTextStream == true + defaultValue: widgetMetadata.hideMode + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/DarkModeSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/DarkModeSettings.qml new file mode 100644 index 0000000..f6546e8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/DarkModeSettings.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.iconColor = valueIconColor; + settingsChanged(settings); + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: valueIconColor + onSelected: key => { + valueIconColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.iconColor + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/KeepAwakeSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/KeepAwakeSettings.qml new file mode 100644 index 0000000..fc94235 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/KeepAwakeSettings.qml @@ -0,0 +1,45 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.iconColor = valueIconColor; + settings.textColor = valueTextColor; + settingsChanged(settings); + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: valueIconColor + onSelected: key => { + valueIconColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.iconColor + } + + NColorChoice { + currentKey: valueTextColor + onSelected: key => { + valueTextColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.textColor + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/KeyboardLayoutSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/KeyboardLayoutSettings.qml new file mode 100644 index 0000000..9b40c80 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/KeyboardLayoutSettings.qml @@ -0,0 +1,89 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode + property bool valueShowIcon: widgetData.showIcon !== undefined ? widgetData.showIcon : widgetMetadata.showIcon + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.displayMode = valueDisplayMode; + settings.showIcon = valueShowIcon; + settings.iconColor = valueIconColor; + settings.textColor = valueTextColor; + settingsChanged(settings); + } + + NComboBox { + visible: valueShowIcon // Hide display mode setting when icon is disabled + label: I18n.tr("common.display-mode") + description: I18n.tr("bar.volume.display-mode-description") + minimumWidth: 200 + model: [ + { + "key": "onhover", + "name": I18n.tr("display-modes.on-hover") + }, + { + "key": "forceOpen", + "name": I18n.tr("display-modes.force-open") + }, + { + "key": "alwaysHide", + "name": I18n.tr("display-modes.always-hide") + } + ] + currentKey: valueDisplayMode + onSelected: key => { + valueDisplayMode = key; + saveSettings(); + } + defaultValue: widgetMetadata.displayMode + } + + NToggle { + label: I18n.tr("bar.custom-button.show-icon-label") + description: I18n.tr("bar.keyboard-layout.show-icon-description") + checked: valueShowIcon + onToggled: checked => { + valueShowIcon = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showIcon + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: valueIconColor + onSelected: key => { + valueIconColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.iconColor + } + + NColorChoice { + currentKey: valueTextColor + onSelected: key => { + valueTextColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.textColor + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/LauncherSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/LauncherSettings.qml new file mode 100644 index 0000000..ff91dc6 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/LauncherSettings.qml @@ -0,0 +1,134 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property string valueIcon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon + property bool valueUseDistroLogo: widgetData.useDistroLogo !== undefined ? widgetData.useDistroLogo : widgetMetadata.useDistroLogo + property string valueCustomIconPath: widgetData.customIconPath !== undefined ? widgetData.customIconPath : widgetMetadata.customIconPath + property bool valueEnableColorization: widgetData.enableColorization !== undefined ? widgetData.enableColorization : widgetMetadata.enableColorization + property string valueColorizeSystemIcon: widgetData.colorizeSystemIcon !== undefined ? widgetData.colorizeSystemIcon : widgetMetadata.colorizeSystemIcon + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.icon = valueIcon; + settings.useDistroLogo = valueUseDistroLogo; + settings.customIconPath = valueCustomIconPath; + settings.enableColorization = valueEnableColorization; + settings.colorizeSystemIcon = valueColorizeSystemIcon; + settingsChanged(settings); + } + + NToggle { + label: I18n.tr("bar.control-center.use-distro-logo-label") + description: I18n.tr("bar.control-center.use-distro-logo-description") + checked: valueUseDistroLogo + onToggled: checked => { + valueUseDistroLogo = checked; + saveSettings(); + } + defaultValue: widgetMetadata.useDistroLogo + } + + NToggle { + label: I18n.tr("bar.custom-button.enable-colorization-label") + description: I18n.tr("bar.control-center.enable-colorization-description") + checked: valueEnableColorization + onToggled: checked => { + valueEnableColorization = checked; + saveSettings(); + } + defaultValue: widgetMetadata.enableColorization + } + + NColorChoice { + visible: valueEnableColorization + label: I18n.tr("common.select-icon-color") + description: I18n.tr("bar.control-center.color-selection-description") + currentKey: valueColorizeSystemIcon + onSelected: function (key) { + valueColorizeSystemIcon = key; + saveSettings(); + } + defaultValue: widgetMetadata.colorizeSystemIcon + } + + RowLayout { + spacing: Style.marginM + + NLabel { + label: I18n.tr("common.icon") + description: I18n.tr("bar.control-center.icon-description") + } + + NImageRounded { + Layout.preferredWidth: Style.fontSizeXL * 2 + Layout.preferredHeight: Style.fontSizeXL * 2 + Layout.alignment: Qt.AlignVCenter + radius: Math.min(Style.radiusL, Layout.preferredWidth / 2) + imagePath: valueCustomIconPath + visible: valueCustomIconPath !== "" && !valueUseDistroLogo + } + + NIcon { + Layout.alignment: Qt.AlignVCenter + icon: valueIcon + pointSize: Style.fontSizeXXL * 1.5 + visible: valueIcon !== "" && valueCustomIconPath === "" && !valueUseDistroLogo + } + } + + RowLayout { + spacing: Style.marginM + NButton { + enabled: !valueUseDistroLogo + text: I18n.tr("bar.control-center.browse-library") + onClicked: iconPicker.open() + } + + NButton { + enabled: !valueUseDistroLogo + text: I18n.tr("bar.control-center.browse-file") + onClicked: imagePicker.openFilePicker() + } + } + + NIconPicker { + id: iconPicker + initialIcon: valueIcon + onIconSelected: iconName => { + valueIcon = iconName; + valueCustomIconPath = ""; + saveSettings(); + } + } + + NFilePicker { + id: imagePicker + title: I18n.tr("bar.control-center.select-custom-icon") + selectionMode: "files" + nameFilters: ImageCacheService.basicImageFilters.concat(["*.svg"]) + initialPath: Quickshell.env("HOME") + onAccepted: paths => { + if (paths.length > 0) { + valueCustomIconPath = paths[0]; + saveSettings(); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/LockKeysSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/LockKeysSettings.qml new file mode 100644 index 0000000..f5373a3 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/LockKeysSettings.qml @@ -0,0 +1,170 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property bool valueShowCapsLock: widgetData.showCapsLock !== undefined ? widgetData.showCapsLock : widgetMetadata.showCapsLock + property bool valueShowNumLock: widgetData.showNumLock !== undefined ? widgetData.showNumLock : widgetMetadata.showNumLock + property bool valueShowScrollLock: widgetData.showScrollLock !== undefined ? widgetData.showScrollLock : widgetMetadata.showScrollLock + + property string capsIcon: widgetData.capsLockIcon !== undefined ? widgetData.capsLockIcon : widgetMetadata.capsLockIcon + property string numIcon: widgetData.numLockIcon !== undefined ? widgetData.numLockIcon : widgetMetadata.numLockIcon + property string scrollIcon: widgetData.scrollLockIcon !== undefined ? widgetData.scrollLockIcon : widgetMetadata.scrollLockIcon + + property bool valueHideWhenOff: widgetData.hideWhenOff !== undefined ? widgetData.hideWhenOff : (widgetMetadata.hideWhenOff !== undefined ? widgetMetadata.hideWhenOff : false) + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.showCapsLock = valueShowCapsLock; + settings.showNumLock = valueShowNumLock; + settings.showScrollLock = valueShowScrollLock; + settings.capsLockIcon = capsIcon; + settings.numLockIcon = numIcon; + settings.scrollLockIcon = scrollIcon; + settings.hideWhenOff = valueHideWhenOff; + settingsChanged(settings); + } + + RowLayout { + spacing: Style.marginM + + NToggle { + label: I18n.tr("bar.lock-keys.show-caps-lock-label") + description: I18n.tr("bar.lock-keys.show-caps-lock-description") + checked: valueShowCapsLock + onToggled: checked => { + valueShowCapsLock = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showCapsLock + } + + NIcon { + Layout.alignment: Qt.AlignVCenter + icon: capsIcon + pointSize: Style.fontSizeXL + visible: capsIcon !== "" + } + + NButton { + text: I18n.tr("common.browse") + onClicked: capsPicker.open() + enabled: valueShowCapsLock + } + } + + NIconPicker { + id: capsPicker + initialIcon: capsIcon + query: "letter-c" + onIconSelected: function (iconName) { + capsIcon = iconName; + saveSettings(); + } + } + + RowLayout { + spacing: Style.marginM + + NToggle { + label: I18n.tr("bar.lock-keys.show-num-lock-label") + description: I18n.tr("bar.lock-keys.show-num-lock-description") + checked: valueShowNumLock + onToggled: checked => { + valueShowNumLock = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showNumLock + } + + NIcon { + Layout.alignment: Qt.AlignVCenter + icon: numIcon + pointSize: Style.fontSizeXL + visible: numIcon !== "" + } + + NButton { + text: I18n.tr("common.browse") + onClicked: numPicker.open() + enabled: valueShowNumLock + } + } + + NIconPicker { + id: numPicker + initialIcon: numIcon + query: "letter-n" + onIconSelected: function (iconName) { + numIcon = iconName; + saveSettings(); + } + } + + RowLayout { + spacing: Style.marginM + + NToggle { + label: I18n.tr("bar.lock-keys.show-scroll-lock-label") + description: I18n.tr("bar.lock-keys.show-scroll-lock-description") + checked: valueShowScrollLock + onToggled: checked => { + valueShowScrollLock = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showScrollLock + } + + NIcon { + Layout.alignment: Qt.AlignVCenter + icon: scrollIcon + pointSize: Style.fontSizeXL + visible: scrollIcon !== "" + } + + NButton { + text: I18n.tr("common.browse") + onClicked: scrollPicker.open() + enabled: valueShowScrollLock + } + } + + NIconPicker { + id: scrollPicker + initialIcon: scrollIcon + query: "letter-s" + onIconSelected: function (iconName) { + scrollIcon = iconName; + saveSettings(); + } + } + + NDivider { + Layout.fillWidth: true + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.lock-keys.hide-when-off-label") + description: I18n.tr("bar.lock-keys.hide-when-off-description") + checked: valueHideWhenOff + onToggled: checked => { + valueHideWhenOff = checked; + saveSettings(); + } + defaultValue: widgetMetadata.hideWhenOff + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/MediaMiniSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/MediaMiniSettings.qml new file mode 100644 index 0000000..da2f741 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/MediaMiniSettings.qml @@ -0,0 +1,248 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property string valueHideMode: widgetData.hideMode !== undefined ? widgetData.hideMode : widgetMetadata.hideMode + // Deprecated: hideWhenIdle now folded into hideMode = "idle" + property bool valueHideWhenIdle: widgetData.hideWhenIdle !== undefined ? widgetData.hideWhenIdle : widgetMetadata.hideWhenIdle + property bool valueShowAlbumArt: widgetData.showAlbumArt !== undefined ? widgetData.showAlbumArt : widgetMetadata.showAlbumArt + property bool valuePanelShowAlbumArt: widgetData.panelShowAlbumArt !== undefined ? widgetData.panelShowAlbumArt : widgetMetadata.panelShowAlbumArt + property bool valueShowArtistFirst: widgetData.showArtistFirst !== undefined ? widgetData.showArtistFirst : widgetMetadata.showArtistFirst + property bool valueShowVisualizer: widgetData.showVisualizer !== undefined ? widgetData.showVisualizer : widgetMetadata.showVisualizer + property string valueVisualizerType: widgetData.visualizerType !== undefined ? widgetData.visualizerType : widgetMetadata.visualizerType + property string valueScrollingMode: widgetData.scrollingMode !== undefined ? widgetData.scrollingMode : widgetMetadata.scrollingMode + property int valueMaxWidth: widgetData.maxWidth !== undefined ? widgetData.maxWidth : widgetMetadata.maxWidth + property bool valueUseFixedWidth: widgetData.useFixedWidth !== undefined ? widgetData.useFixedWidth : widgetMetadata.useFixedWidth + property bool valueShowProgressRing: widgetData.showProgressRing !== undefined ? widgetData.showProgressRing : widgetMetadata.showProgressRing + property bool valueCompactMode: widgetData.compactMode !== undefined ? widgetData.compactMode : widgetMetadata.compactMode + property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor + + Component.onCompleted: { + if (widgetData && widgetData.hideMode !== undefined) { + valueHideMode = widgetData.hideMode; + } + } + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.hideMode = valueHideMode; + // No longer store hideWhenIdle separately; kept for backward compatibility only + settings.showAlbumArt = valueShowAlbumArt; + settings.panelShowAlbumArt = valuePanelShowAlbumArt; + settings.showArtistFirst = valueShowArtistFirst; + settings.showVisualizer = valueShowVisualizer; + settings.visualizerType = valueVisualizerType; + settings.scrollingMode = valueScrollingMode; + settings.maxWidth = parseInt(widthInput.text) || widgetMetadata.maxWidth; + settings.useFixedWidth = valueUseFixedWidth; + settings.showProgressRing = valueShowProgressRing; + settings.compactMode = valueCompactMode; + settings.textColor = valueTextColor; + settingsChanged(settings); + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("bar.taskbar.hide-mode-label") + description: I18n.tr("bar.media-mini.hide-mode-description") + model: [ + { + "key": "visible", + "name": I18n.tr("hide-modes.visible") + }, + { + "key": "hidden", + "name": I18n.tr("hide-modes.hidden") + }, + { + "key": "transparent", + "name": I18n.tr("hide-modes.transparent") + }, + { + "key": "idle", + "name": I18n.tr("hide-modes.idle") + } + ] + currentKey: root.valueHideMode + onSelected: key => { + root.valueHideMode = key; + saveSettings(); + } + defaultValue: widgetMetadata.hideMode + } + + NToggle { + label: I18n.tr("bar.media-mini.show-album-art-label") + description: I18n.tr("bar.media-mini.show-album-art-description") + checked: valueShowAlbumArt + onToggled: checked => { + valueShowAlbumArt = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showAlbumArt + } + + NToggle { + label: I18n.tr("bar.media-mini.show-artist-first-label") + description: I18n.tr("bar.media-mini.show-artist-first-description") + checked: valueShowArtistFirst + onToggled: checked => { + valueShowArtistFirst = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showArtistFirst + } + + NToggle { + label: I18n.tr("bar.media-mini.show-visualizer-label") + description: I18n.tr("bar.media-mini.show-visualizer-description") + checked: valueShowVisualizer + onToggled: checked => { + valueShowVisualizer = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showVisualizer + } + + NComboBox { + visible: valueShowVisualizer + label: I18n.tr("bar.media-mini.visualizer-type-label") + description: I18n.tr("bar.media-mini.visualizer-type-description") + model: [ + { + "key": "linear", + "name": I18n.tr("options.visualizer-types.linear") + }, + { + "key": "mirrored", + "name": I18n.tr("options.visualizer-types.mirrored") + }, + { + "key": "wave", + "name": I18n.tr("options.visualizer-types.wave") + } + ] + currentKey: valueVisualizerType + onSelected: key => { + valueVisualizerType = key; + saveSettings(); + } + minimumWidth: 200 + defaultValue: widgetMetadata.visualizerType + } + + NTextInput { + id: widthInput + Layout.fillWidth: true + label: I18n.tr("bar.taskbar.max-width-label") + description: I18n.tr("bar.media-mini.max-width-description") + placeholderText: widgetMetadata.maxWidth + text: valueMaxWidth + onTextChanged: saveSettings() + defaultValue: String(widgetMetadata.maxWidth) + } + + NToggle { + label: I18n.tr("bar.media-mini.use-fixed-width-label") + description: I18n.tr("bar.media-mini.use-fixed-width-description") + checked: valueUseFixedWidth + onToggled: checked => { + valueUseFixedWidth = checked; + saveSettings(); + } + defaultValue: widgetMetadata.useFixedWidth + } + + NToggle { + label: I18n.tr("bar.media-mini.show-progress-ring-label") + description: I18n.tr("bar.media-mini.show-progress-ring-description") + checked: valueShowProgressRing + onToggled: checked => { + valueShowProgressRing = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showProgressRing + } + + NColorChoice { + currentKey: valueTextColor + onSelected: key => { + valueTextColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.textColor + } + + NComboBox { + label: I18n.tr("bar.media-mini.scrolling-mode-label") + description: I18n.tr("bar.media-mini.scrolling-mode-description") + model: [ + { + "key": "always", + "name": I18n.tr("options.scrolling-modes.always") + }, + { + "key": "hover", + "name": I18n.tr("options.scrolling-modes.hover") + }, + { + "key": "never", + "name": I18n.tr("options.scrolling-modes.never") + } + ] + currentKey: valueScrollingMode + onSelected: key => { + valueScrollingMode = key; + saveSettings(); + } + minimumWidth: 200 + defaultValue: widgetMetadata.scrollingMode + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginS + } + + NLabel { + label: I18n.tr("bar.media-mini.panel-section-label") + description: I18n.tr("bar.media-mini.panel-section-description") + labelColor: Color.mPrimary + } + + NToggle { + label: I18n.tr("bar.media-mini.show-album-art-label") + description: I18n.tr("bar.media-mini.show-album-art-description") + checked: valuePanelShowAlbumArt + onToggled: checked => { + valuePanelShowAlbumArt = checked; + saveSettings(); + } + defaultValue: widgetMetadata.panelShowAlbumArt + } + + NToggle { + label: I18n.tr("bar.media-mini.compact-mode-label") + description: I18n.tr("bar.media-mini.compact-mode-description") + checked: valueCompactMode + onToggled: checked => { + valueCompactMode = checked; + saveSettings(); + } + defaultValue: widgetMetadata.compactMode + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/MicrophoneSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/MicrophoneSettings.qml new file mode 100644 index 0000000..35e2b05 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/MicrophoneSettings.qml @@ -0,0 +1,90 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode + property string valueMiddleClickCommand: widgetData.middleClickCommand !== undefined ? widgetData.middleClickCommand : widgetMetadata.middleClickCommand + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.displayMode = valueDisplayMode; + settings.middleClickCommand = valueMiddleClickCommand; + settings.iconColor = valueIconColor; + settings.textColor = valueTextColor; + settingsChanged(settings); + } + + NComboBox { + label: I18n.tr("common.display-mode") + description: I18n.tr("bar.volume.display-mode-description") + minimumWidth: 200 + model: [ + { + "key": "onhover", + "name": I18n.tr("display-modes.on-hover") + }, + { + "key": "alwaysShow", + "name": I18n.tr("display-modes.always-show") + }, + { + "key": "alwaysHide", + "name": I18n.tr("display-modes.always-hide") + } + ] + currentKey: valueDisplayMode + onSelected: key => { + valueDisplayMode = key; + saveSettings(); + } + defaultValue: widgetMetadata.displayMode + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: valueIconColor + onSelected: key => { + valueIconColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.iconColor + } + + NColorChoice { + currentKey: valueTextColor + onSelected: key => { + valueTextColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.textColor + } + + // Middle click command + NTextInput { + label: I18n.tr("bar.custom-button.middle-click-label") + description: I18n.tr("panels.audio.on-middle-clicked-description") + placeholderText: I18n.tr("panels.audio.external-mixer-placeholder") + text: valueMiddleClickCommand + onTextChanged: { + valueMiddleClickCommand = text; + saveSettings(); + } + defaultValue: widgetMetadata.middleClickCommand + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/NetworkSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/NetworkSettings.qml new file mode 100644 index 0000000..cd1c53d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/NetworkSettings.qml @@ -0,0 +1,74 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.displayMode = valueDisplayMode; + settings.iconColor = valueIconColor; + settings.textColor = valueTextColor; + settingsChanged(settings); + } + + NComboBox { + label: I18n.tr("common.display-mode") + description: I18n.tr("bar.volume.display-mode-description") + minimumWidth: 200 + model: [ + { + "key": "onhover", + "name": I18n.tr("display-modes.on-hover") + }, + { + "key": "alwaysShow", + "name": I18n.tr("display-modes.always-show") + }, + { + "key": "alwaysHide", + "name": I18n.tr("display-modes.always-hide") + } + ] + currentKey: root.valueDisplayMode + onSelected: key => { + root.valueDisplayMode = key; + saveSettings(); + } + defaultValue: widgetMetadata.displayMode + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: valueIconColor + onSelected: key => { + valueIconColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.iconColor + } + + NColorChoice { + currentKey: valueTextColor + onSelected: key => { + valueTextColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.textColor + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/NightLightSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/NightLightSettings.qml new file mode 100644 index 0000000..f6546e8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/NightLightSettings.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.iconColor = valueIconColor; + settingsChanged(settings); + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: valueIconColor + onSelected: key => { + valueIconColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.iconColor + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/NoctaliaPerformanceSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/NoctaliaPerformanceSettings.qml new file mode 100644 index 0000000..f6546e8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/NoctaliaPerformanceSettings.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.iconColor = valueIconColor; + settingsChanged(settings); + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: valueIconColor + onSelected: key => { + valueIconColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.iconColor + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/NotificationHistorySettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/NotificationHistorySettings.qml new file mode 100644 index 0000000..4a59f1a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/NotificationHistorySettings.qml @@ -0,0 +1,90 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property bool valueShowUnreadBadge: widgetData.showUnreadBadge !== undefined ? widgetData.showUnreadBadge : widgetMetadata.showUnreadBadge + property bool valueHideWhenZero: widgetData.hideWhenZero !== undefined ? widgetData.hideWhenZero : widgetMetadata.hideWhenZero + property bool valueHideWhenZeroUnread: widgetData.hideWhenZeroUnread !== undefined ? widgetData.hideWhenZeroUnread : widgetMetadata.hideWhenZeroUnread + property string valueUnreadBadgeColor: widgetData.unreadBadgeColor !== undefined ? widgetData.unreadBadgeColor : widgetMetadata.unreadBadgeColor + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.showUnreadBadge = valueShowUnreadBadge; + settings.hideWhenZero = valueHideWhenZero; + settings.hideWhenZeroUnread = valueHideWhenZeroUnread; + settings.unreadBadgeColor = valueUnreadBadgeColor; + settings.iconColor = valueIconColor; + settingsChanged(settings); + } + + NToggle { + label: I18n.tr("bar.notification-history.show-unread-badge-label") + description: I18n.tr("bar.notification-history.show-unread-badge-description") + checked: valueShowUnreadBadge + onToggled: checked => { + valueShowUnreadBadge = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showUnreadBadge + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: valueIconColor + onSelected: key => { + valueIconColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.iconColor + } + + NColorChoice { + label: I18n.tr("bar.notification-history.unread-badge-color-label") + description: I18n.tr("bar.notification-history.unread-badge-color-description") + currentKey: valueUnreadBadgeColor + onSelected: key => { + valueUnreadBadgeColor = key; + saveSettings(); + } + visible: valueShowUnreadBadge + defaultValue: widgetMetadata.unreadBadgeColor + } + + NToggle { + label: I18n.tr("bar.notification-history.hide-widget-when-zero-label") + description: I18n.tr("bar.notification-history.hide-widget-when-zero-description") + checked: valueHideWhenZero + onToggled: checked => { + valueHideWhenZero = checked; + saveSettings(); + } + enabled: !valueHideWhenZeroUnread + defaultValue: widgetMetadata.hideWhenZero + } + + NToggle { + label: I18n.tr("bar.notification-history.hide-widget-when-zero-unread-label") + description: I18n.tr("bar.notification-history.hide-widget-when-zero-unread-description") + checked: valueHideWhenZeroUnread + onToggled: checked => { + valueHideWhenZeroUnread = checked; + saveSettings(); + } + defaultValue: widgetMetadata.hideWhenZeroUnread + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/PowerProfileSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/PowerProfileSettings.qml new file mode 100644 index 0000000..f6546e8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/PowerProfileSettings.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.iconColor = valueIconColor; + settingsChanged(settings); + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: valueIconColor + onSelected: key => { + valueIconColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.iconColor + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/SessionMenuSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/SessionMenuSettings.qml new file mode 100644 index 0000000..258d0ab --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/SessionMenuSettings.qml @@ -0,0 +1,38 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.System +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + width: 700 + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.iconColor = valueIconColor; + settingsChanged(settings); + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: root.valueIconColor + onSelected: key => { + root.valueIconColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.iconColor + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/SettingsSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/SettingsSettings.qml new file mode 100644 index 0000000..d5f54b1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/SettingsSettings.qml @@ -0,0 +1,35 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.iconColor = valueIconColor; + settingsChanged(settings); + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: valueIconColor + onSelected: key => { + valueIconColor = key; + saveSettings(); + } + valueDefault: widgetMetadata.iconColor + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/SpacerSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/SpacerSettings.qml new file mode 100644 index 0000000..818f74f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/SpacerSettings.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.width = parseInt(widthInput.text) || widgetMetadata.width; + settingsChanged(settings); + } + + NTextInput { + id: widthInput + Layout.fillWidth: true + label: I18n.tr("common.width") + description: I18n.tr("bar.spacer.width-description") + text: widgetData.width || widgetMetadata.width + placeholderText: I18n.tr("placeholders.enter-width-pixels") + onTextChanged: saveSettings() + defaultValue: String(widgetMetadata.width) + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml new file mode 100644 index 0000000..9d4f1e9 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/SystemMonitorSettings.qml @@ -0,0 +1,325 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.System +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name) + readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right" + + // Local, editable state for checkboxes + property bool valueCompactMode: widgetData.compactMode !== undefined ? widgetData.compactMode : widgetMetadata.compactMode + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor + property bool valueUseMonospaceFont: widgetData.useMonospaceFont !== undefined ? widgetData.useMonospaceFont : widgetMetadata.useMonospaceFont + property bool valueUsePadding: widgetData.usePadding !== undefined ? widgetData.usePadding : widgetMetadata.usePadding + property bool valueShowCpuUsage: widgetData.showCpuUsage !== undefined ? widgetData.showCpuUsage : widgetMetadata.showCpuUsage + property bool valueShowCpuCores: widgetData.showCpuCores !== undefined ? widgetData.showCpuCores : widgetMetadata.showCpuCores + property bool valueShowCpuFreq: widgetData.showCpuFreq !== undefined ? widgetData.showCpuFreq : widgetMetadata.showCpuFreq + property bool valueShowCpuTemp: widgetData.showCpuTemp !== undefined ? widgetData.showCpuTemp : widgetMetadata.showCpuTemp + property bool valueShowGpuTemp: widgetData.showGpuTemp !== undefined ? widgetData.showGpuTemp : widgetMetadata.showGpuTemp + property bool valueShowLoadAverage: widgetData.showLoadAverage !== undefined ? widgetData.showLoadAverage : widgetMetadata.showLoadAverage + property bool valueShowMemoryUsage: widgetData.showMemoryUsage !== undefined ? widgetData.showMemoryUsage : widgetMetadata.showMemoryUsage + property bool valueShowMemoryAsPercent: widgetData.showMemoryAsPercent !== undefined ? widgetData.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent + property bool valueShowSwapUsage: widgetData.showSwapUsage !== undefined ? widgetData.showSwapUsage : widgetMetadata.showSwapUsage + property bool valueShowNetworkStats: widgetData.showNetworkStats !== undefined ? widgetData.showNetworkStats : widgetMetadata.showNetworkStats + property bool valueShowDiskUsage: widgetData.showDiskUsage !== undefined ? widgetData.showDiskUsage : widgetMetadata.showDiskUsage + property bool valueShowDiskUsageAsPercent: widgetData.showDiskUsageAsPercent !== undefined ? widgetData.showDiskUsageAsPercent : widgetMetadata.showDiskUsageAsPercent + property bool valueShowDiskAvailable: widgetData.showDiskAvailable !== undefined ? widgetData.showDiskAvailable : widgetMetadata.showDiskAvailable + property string valueDiskPath: widgetData.diskPath !== undefined ? widgetData.diskPath : widgetMetadata.diskPath + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.compactMode = valueCompactMode; + settings.iconColor = valueIconColor; + settings.textColor = valueTextColor; + settings.useMonospaceFont = valueUseMonospaceFont; + settings.usePadding = valueUsePadding; + settings.showCpuUsage = valueShowCpuUsage; + settings.showCpuCores = valueShowCpuCores; + settings.showCpuFreq = valueShowCpuFreq; + settings.showCpuTemp = valueShowCpuTemp; + settings.showGpuTemp = valueShowGpuTemp; + settings.showLoadAverage = valueShowLoadAverage; + settings.showMemoryUsage = valueShowMemoryUsage; + settings.showMemoryAsPercent = valueShowMemoryAsPercent; + settings.showSwapUsage = valueShowSwapUsage; + settings.showNetworkStats = valueShowNetworkStats; + settings.showDiskUsage = valueShowDiskUsage; + settings.showDiskUsageAsPercent = valueShowDiskUsageAsPercent; + settings.showDiskAvailable = valueShowDiskAvailable; + settings.diskPath = valueDiskPath; + + settingsChanged(settings); + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.system-monitor.compact-mode-label") + description: I18n.tr("bar.system-monitor.compact-mode-description") + checked: valueCompactMode + onToggled: checked => { + valueCompactMode = checked; + saveSettings(); + } + defaultValue: widgetMetadata.compactMode + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: valueIconColor + onSelected: key => { + valueIconColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.iconColor + } + + NColorChoice { + currentKey: valueTextColor + onSelected: key => { + valueTextColor = key; + saveSettings(); + } + visible: !valueCompactMode + defaultValue: widgetMetadata.textColor + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.system-monitor.use-monospace-font-label") + description: I18n.tr("bar.system-monitor.use-monospace-font-description") + checked: valueUseMonospaceFont + onToggled: checked => { + valueUseMonospaceFont = checked; + saveSettings(); + } + visible: !valueCompactMode + defaultValue: widgetMetadata.useMonospaceFont + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.system-monitor.use-padding-label") + description: isVerticalBar ? I18n.tr("bar.system-monitor.use-padding-description-disabled-vertical") : !valueUseMonospaceFont ? I18n.tr("bar.system-monitor.use-padding-description-disabled-monospace-font") : I18n.tr("bar.system-monitor.use-padding-description") + checked: valueUsePadding && !isVerticalBar && valueUseMonospaceFont + onToggled: checked => { + valueUsePadding = checked; + saveSettings(); + } + visible: !valueCompactMode + enabled: !isVerticalBar && valueUseMonospaceFont + defaultValue: widgetMetadata.usePadding + } + + NDivider { + Layout.fillWidth: true + } + + NToggle { + id: showCpuUsage + Layout.fillWidth: true + label: I18n.tr("bar.system-monitor.cpu-usage-label") + description: I18n.tr("bar.system-monitor.cpu-usage-description") + checked: valueShowCpuUsage + onToggled: checked => { + valueShowCpuUsage = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showCpuUsage + } + + NToggle { + id: showCpuCores + Layout.fillWidth: true + label: I18n.tr("bar.system-monitor.cpu-cores-label") + description: I18n.tr("bar.system-monitor.cpu-cores-description") + checked: valueShowCpuCores + onToggled: checked => { + valueShowCpuCores = checked; + saveSettings(); + } + visible: valueCompactMode + } + + NToggle { + id: showCpuFreq + Layout.fillWidth: true + label: I18n.tr("bar.system-monitor.cpu-frequency-label") + description: I18n.tr("bar.system-monitor.cpu-frequency-description") + checked: valueShowCpuFreq + onToggled: checked => { + valueShowCpuFreq = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showCpuFreq + } + + NToggle { + id: showCpuTemp + Layout.fillWidth: true + label: I18n.tr("bar.system-monitor.cpu-temperature-label") + description: I18n.tr("bar.system-monitor.cpu-temperature-description") + checked: valueShowCpuTemp + onToggled: checked => { + valueShowCpuTemp = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showCpuTemp + } + + NToggle { + id: showLoadAverage + Layout.fillWidth: true + label: I18n.tr("bar.system-monitor.load-average-label") + description: I18n.tr("bar.system-monitor.load-average-description") + checked: valueShowLoadAverage + onToggled: checked => { + valueShowLoadAverage = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showLoadAverage + } + + NToggle { + id: showGpuTemp + Layout.fillWidth: true + label: I18n.tr("panels.system-monitor.gpu-section-label") + description: I18n.tr("bar.system-monitor.gpu-temperature-description") + checked: valueShowGpuTemp + onToggled: checked => { + valueShowGpuTemp = checked; + saveSettings(); + } + visible: SystemStatService.gpuAvailable + defaultValue: widgetMetadata.showGpuTemp + } + + NToggle { + id: showMemoryUsage + Layout.fillWidth: true + label: I18n.tr("bar.system-monitor.memory-usage-label") + description: I18n.tr("bar.system-monitor.memory-usage-description") + checked: valueShowMemoryUsage + onToggled: checked => { + valueShowMemoryUsage = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showMemoryUsage + } + + NToggle { + id: showMemoryAsPercent + Layout.fillWidth: true + label: I18n.tr("bar.system-monitor.memory-percentage-label") + description: I18n.tr("bar.system-monitor.memory-percentage-description") + checked: valueShowMemoryAsPercent + onToggled: checked => { + valueShowMemoryAsPercent = checked; + saveSettings(); + } + visible: valueShowMemoryUsage + defaultValue: widgetMetadata.showMemoryAsPercent + } + + NToggle { + id: showSwapUsage + Layout.fillWidth: true + label: I18n.tr("bar.system-monitor.swap-usage-label") + description: I18n.tr("bar.system-monitor.swap-usage-description") + checked: valueShowSwapUsage + onToggled: checked => { + valueShowSwapUsage = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showSwapUsage + } + + NToggle { + id: showNetworkStats + Layout.fillWidth: true + label: I18n.tr("bar.system-monitor.network-traffic-label") + description: I18n.tr("bar.system-monitor.network-traffic-description") + checked: valueShowNetworkStats + onToggled: checked => { + valueShowNetworkStats = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showNetworkStats + } + + NDivider { + Layout.fillWidth: true + } + + NToggle { + id: showDiskUsage + Layout.fillWidth: true + label: I18n.tr("bar.system-monitor.storage-usage-label") + description: I18n.tr("bar.system-monitor.storage-usage-description") + checked: valueShowDiskUsage + onToggled: checked => { + valueShowDiskUsage = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showDiskUsage + } + + NToggle { + id: showDiskUsageAsPercent + Layout.fillWidth: true + label: I18n.tr("bar.system-monitor.storage-as-percentage-label") + description: I18n.tr("bar.system-monitor.storage-as-percentage-description") + checked: valueShowDiskUsageAsPercent + onToggled: checked => { + valueShowDiskUsageAsPercent = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showDiskUsageAsPercent + } + + NToggle { + id: showDiskAvailable + Layout.fillWidth: true + label: I18n.tr("bar.system-monitor.storage-available-label") + description: I18n.tr("bar.system-monitor.storage-available-description") + checked: valueShowDiskAvailable + onToggled: checked => { + valueShowDiskAvailable = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showDiskAvailable + } + + NComboBox { + id: diskPathComboBox + Layout.fillWidth: true + label: I18n.tr("bar.system-monitor.disk-path-label") + description: I18n.tr("bar.system-monitor.disk-path-description") + model: { + const paths = Object.keys(SystemStatService.diskPercents).sort(); + return paths.map(path => ({ + key: path, + name: path + })); + } + currentKey: valueDiskPath + onSelected: key => { + valueDiskPath = key; + saveSettings(); + } + defaultValue: widgetMetadata.diskPath + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/TaskbarSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/TaskbarSettings.qml new file mode 100644 index 0000000..2628e59 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/TaskbarSettings.qml @@ -0,0 +1,201 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + readonly property bool isVerticalBar: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + + // Local state + property string valueHideMode: widgetData.hideMode !== undefined ? widgetData.hideMode : widgetMetadata.hideMode + property bool valueOnlyActiveWorkspaces: widgetData.onlyActiveWorkspaces !== undefined ? widgetData.onlyActiveWorkspaces : widgetMetadata.onlyActiveWorkspaces + property bool valueOnlySameOutput: widgetData.onlySameOutput !== undefined ? widgetData.onlySameOutput : widgetMetadata.onlySameOutput + property bool valueColorizeIcons: widgetData.colorizeIcons !== undefined ? widgetData.colorizeIcons : widgetMetadata.colorizeIcons + property bool valueShowTitle: isVerticalBar ? false : widgetData.showTitle !== undefined ? widgetData.showTitle : widgetMetadata.showTitle + property bool valueSmartWidth: widgetData.smartWidth !== undefined ? widgetData.smartWidth : widgetMetadata.smartWidth + property int valueMaxTaskbarWidth: widgetData.maxTaskbarWidth !== undefined ? widgetData.maxTaskbarWidth : widgetMetadata.maxTaskbarWidth + property int valueTitleWidth: widgetData.titleWidth !== undefined ? widgetData.titleWidth : widgetMetadata.titleWidth + property bool valueShowPinnedApps: widgetData.showPinnedApps !== undefined ? widgetData.showPinnedApps : widgetMetadata.showPinnedApps + property real valueIconScale: widgetData.iconScale !== undefined ? widgetData.iconScale : widgetMetadata.iconScale + + Component.onCompleted: { + if (widgetData && widgetData.hideMode !== undefined) { + valueHideMode = widgetData.hideMode; + } else if (widgetMetadata && widgetMetadata.hideMode !== undefined) { + valueHideMode = widgetMetadata.hideMode; + } + } + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.hideMode = valueHideMode; + settings.onlySameOutput = valueOnlySameOutput; + settings.onlyActiveWorkspaces = valueOnlyActiveWorkspaces; + settings.colorizeIcons = valueColorizeIcons; + settings.showTitle = valueShowTitle; + settings.smartWidth = valueSmartWidth; + settings.maxTaskbarWidth = valueMaxTaskbarWidth; + settings.titleWidth = parseInt(titleWidthInput.text) || widgetMetadata.titleWidth; + settings.showPinnedApps = valueShowPinnedApps; + settings.iconScale = valueIconScale; + settingsChanged(settings); + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("bar.taskbar.hide-mode-label") + description: I18n.tr("bar.taskbar.hide-mode-description") + model: [ + { + "key": "visible", + "name": I18n.tr("hide-modes.visible") + }, + { + "key": "hidden", + "name": I18n.tr("hide-modes.hidden") + }, + { + "key": "transparent", + "name": I18n.tr("hide-modes.transparent") + } + ] + currentKey: root.valueHideMode + onSelected: key => { + root.valueHideMode = key; + saveSettings(); + } + defaultValue: widgetMetadata.hideMode + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.taskbar.only-same-monitor-label") + description: I18n.tr("bar.taskbar.only-same-monitor-description") + checked: root.valueOnlySameOutput + onToggled: checked => { + root.valueOnlySameOutput = checked; + saveSettings(); + } + defaultValue: widgetMetadata.onlySameOutput + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.taskbar.only-active-workspaces-label") + description: I18n.tr("bar.taskbar.only-active-workspaces-description") + checked: root.valueOnlyActiveWorkspaces + onToggled: checked => { + root.valueOnlyActiveWorkspaces = checked; + saveSettings(); + } + defaultValue: widgetMetadata.onlyActiveWorkspaces + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.tray.colorize-icons-label") + description: I18n.tr("bar.taskbar.colorize-icons-description") + checked: root.valueColorizeIcons + onToggled: checked => { + root.valueColorizeIcons = checked; + saveSettings(); + } + defaultValue: widgetMetadata.colorizeIcons + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.taskbar.show-pinned-apps-label") + description: I18n.tr("bar.taskbar.show-pinned-apps-description") + checked: root.valueShowPinnedApps + onToggled: checked => { + root.valueShowPinnedApps = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showPinnedApps + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("bar.taskbar.icon-scale-label") + description: I18n.tr("bar.taskbar.icon-scale-description") + from: 0.5 + to: 1 + stepSize: 0.01 + showReset: true + value: root.valueIconScale + defaultValue: widgetMetadata.iconScale + onMoved: value => { + root.valueIconScale = value; + saveSettings(); + } + text: Math.round(root.valueIconScale * 100) + "%" + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.taskbar.show-title-label") + description: isVerticalBar ? I18n.tr("bar.taskbar.show-title-description-disabled") : I18n.tr("bar.taskbar.show-title-description") + checked: root.valueShowTitle + onToggled: checked => { + root.valueShowTitle = checked; + saveSettings(); + } + enabled: !isVerticalBar + defaultValue: widgetMetadata.showTitle + } + + NTextInput { + id: titleWidthInput + visible: root.valueShowTitle && !isVerticalBar + Layout.fillWidth: true + label: I18n.tr("bar.taskbar.title-width-label") + description: I18n.tr("bar.taskbar.title-width-description") + text: widgetData.titleWidth || widgetMetadata.titleWidth + placeholderText: I18n.tr("placeholders.enter-width-pixels") + onTextChanged: saveSettings() + defaultValue: String(widgetMetadata.titleWidth) + } + + NToggle { + Layout.fillWidth: true + visible: !isVerticalBar && root.valueShowTitle + label: I18n.tr("bar.taskbar.smart-width-label") + description: I18n.tr("bar.taskbar.smart-width-description") + checked: root.valueSmartWidth + onToggled: checked => { + root.valueSmartWidth = checked; + saveSettings(); + } + defaultValue: widgetMetadata.smartWidth + } + + NValueSlider { + visible: root.valueSmartWidth && !isVerticalBar + Layout.fillWidth: true + label: I18n.tr("bar.taskbar.max-width-label") + description: I18n.tr("bar.taskbar.max-width-description") + from: 10 + to: 100 + stepSize: 5 + showReset: true + value: root.valueMaxTaskbarWidth + defaultValue: widgetMetadata.maxTaskbarWidth + onMoved: value => { + root.valueMaxTaskbarWidth = Math.round(value); + saveSettings(); + } + text: Math.round(root.valueMaxTaskbarWidth) + "%" + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/TraySettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/TraySettings.qml new file mode 100644 index 0000000..e3fb99b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/TraySettings.qml @@ -0,0 +1,199 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property var localBlacklist: widgetData.blacklist || [] + property bool valueColorizeIcons: widgetData.colorizeIcons !== undefined ? widgetData.colorizeIcons : widgetMetadata.colorizeIcons + property string valueChevronColor: widgetData.chevronColor !== undefined ? widgetData.chevronColor : widgetMetadata.chevronColor + property bool valueDrawerEnabled: widgetData.drawerEnabled !== undefined ? widgetData.drawerEnabled : widgetMetadata.drawerEnabled + property bool valueHidePassive: widgetData.hidePassive !== undefined ? widgetData.hidePassive : widgetMetadata.hidePassive + + ListModel { + id: blacklistModel + } + + function populateBlacklist() { + for (var i = 0; i < localBlacklist.length; i++) { + blacklistModel.append({ + "rule": localBlacklist[i] + }); + } + } + + Component.onCompleted: { + Qt.callLater(populateBlacklist); + } + + spacing: Style.marginM + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.tray.drawer-enabled-label") + description: I18n.tr("bar.tray.drawer-enabled-description") + checked: root.valueDrawerEnabled + onToggled: checked => { + root.valueDrawerEnabled = checked; + saveSettings(); + } + defaultValue: widgetMetadata.drawerEnabled + } + + NColorChoice { + label: I18n.tr("bar.tray.chevron-color-label") + description: I18n.tr("bar.tray.chevron-color-description") + currentKey: root.valueChevronColor + onSelected: key => { + root.valueChevronColor = key; + saveSettings(); + } + visible: root.valueDrawerEnabled + defaultValue: widgetMetadata.chevronColor + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.tray.colorize-icons-label") + description: I18n.tr("bar.tray.colorize-icons-description") + checked: root.valueColorizeIcons + onToggled: checked => { + root.valueColorizeIcons = checked; + saveSettings(); + } + defaultValue: widgetMetadata.colorizeIcons + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.tray.hide-passive-label") + description: I18n.tr("bar.tray.hide-passive-description") + checked: root.valueHidePassive + onToggled: checked => { + root.valueHidePassive = checked; + saveSettings(); + } + defaultValue: widgetMetadata.hidePassive + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NLabel { + label: I18n.tr("panels.bar.tray-blacklist-label") + description: I18n.tr("panels.bar.tray-blacklist-description") + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NTextInputButton { + id: newRuleInput + Layout.fillWidth: true + placeholderText: I18n.tr("panels.bar.tray-blacklist-placeholder") + buttonIcon: "add" + onButtonClicked: { + if (newRuleInput.text.length > 0) { + var newRule = newRuleInput.text.trim(); + var exists = false; + for (var i = 0; i < blacklistModel.count; i++) { + if (blacklistModel.get(i).rule === newRule) { + exists = true; + break; + } + } + if (!exists) { + blacklistModel.append({ + "rule": newRule + }); + newRuleInput.text = ""; + saveSettings(); + } + } + } + } + } + } + + // List of current blacklist items + NListView { + Layout.fillWidth: true + Layout.preferredHeight: 150 + Layout.topMargin: Style.marginL // Increased top margin + gradientColor: Color.mSurface + + model: blacklistModel + delegate: Item { + width: ListView.width + height: 40 + + Rectangle { + id: itemBackground + anchors.fill: parent + anchors.margins: Style.marginXS + color: "transparent" // Make background transparent + border.color: Color.mOutline + border.width: Style.borderS + radius: Style.radiusS + visible: model.rule !== undefined && model.rule !== "" // Only visible if rule exists + } + + Row { + anchors.fill: parent + anchors.leftMargin: Style.marginS + anchors.rightMargin: Style.marginS + spacing: Style.marginS + + NText { + anchors.verticalCenter: parent.verticalCenter + text: model.rule + elide: Text.ElideRight + } + + NIconButton { + anchors.verticalCenter: parent.verticalCenter + icon: "close" + baseSize: 12 * Style.uiScaleRatio + colorBg: Color.mSurfaceVariant + colorFg: Color.mOnSurfaceVariant + colorBgHover: Color.mError + colorFgHover: Color.mOnError + onClicked: { + blacklistModel.remove(index); + saveSettings(); + } + } + } + } + } + + // This function will be called by the dialog to get the new settings + function saveSettings() { + var newBlacklist = []; + for (var i = 0; i < blacklistModel.count; i++) { + newBlacklist.push(blacklistModel.get(i).rule); + } + + // Return the updated settings for this widget instance + var settings = Object.assign({}, widgetData || {}); + settings.blacklist = newBlacklist; + settings.colorizeIcons = root.valueColorizeIcons; + settings.chevronColor = root.valueChevronColor; + settings.drawerEnabled = root.valueDrawerEnabled; + settings.hidePassive = root.valueHidePassive; + settingsChanged(settings); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/VPNSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/VPNSettings.qml new file mode 100644 index 0000000..1b94322 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/VPNSettings.qml @@ -0,0 +1,70 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.displayMode = valueDisplayMode; + settings.iconColor = valueIconColor; + settings.textColor = valueTextColor; + settingsChanged(settings); + } + + NComboBox { + label: I18n.tr("common.display-mode") + description: I18n.tr("bar.volume.display-mode-description") + minimumWidth: 200 + model: [ + { + "key": "onhover", + "name": I18n.tr("display-modes.on-hover") + }, + { + "key": "alwaysShow", + "name": I18n.tr("display-modes.always-show") + }, + { + "key": "alwaysHide", + "name": I18n.tr("display-modes.always-hide") + } + ] + currentKey: root.valueDisplayMode + onSelected: key => { + root.valueDisplayMode = key; + saveSettings(); + } + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: valueIconColor + onSelected: key => { + valueIconColor = key; + saveSettings(); + } + } + + NColorChoice { + currentKey: valueTextColor + onSelected: key => { + valueTextColor = key; + saveSettings(); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/VolumeSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/VolumeSettings.qml new file mode 100644 index 0000000..35e2b05 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/VolumeSettings.qml @@ -0,0 +1,90 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + // Local state + property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode + property string valueMiddleClickCommand: widgetData.middleClickCommand !== undefined ? widgetData.middleClickCommand : widgetMetadata.middleClickCommand + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + property string valueTextColor: widgetData.textColor !== undefined ? widgetData.textColor : widgetMetadata.textColor + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.displayMode = valueDisplayMode; + settings.middleClickCommand = valueMiddleClickCommand; + settings.iconColor = valueIconColor; + settings.textColor = valueTextColor; + settingsChanged(settings); + } + + NComboBox { + label: I18n.tr("common.display-mode") + description: I18n.tr("bar.volume.display-mode-description") + minimumWidth: 200 + model: [ + { + "key": "onhover", + "name": I18n.tr("display-modes.on-hover") + }, + { + "key": "alwaysShow", + "name": I18n.tr("display-modes.always-show") + }, + { + "key": "alwaysHide", + "name": I18n.tr("display-modes.always-hide") + } + ] + currentKey: valueDisplayMode + onSelected: key => { + valueDisplayMode = key; + saveSettings(); + } + defaultValue: widgetMetadata.displayMode + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: valueIconColor + onSelected: key => { + valueIconColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.iconColor + } + + NColorChoice { + currentKey: valueTextColor + onSelected: key => { + valueTextColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.textColor + } + + // Middle click command + NTextInput { + label: I18n.tr("bar.custom-button.middle-click-label") + description: I18n.tr("panels.audio.on-middle-clicked-description") + placeholderText: I18n.tr("panels.audio.external-mixer-placeholder") + text: valueMiddleClickCommand + onTextChanged: { + valueMiddleClickCommand = text; + saveSettings(); + } + defaultValue: widgetMetadata.middleClickCommand + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/WallpaperSelectorSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/WallpaperSelectorSettings.qml new file mode 100644 index 0000000..ec0584b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/WallpaperSelectorSettings.qml @@ -0,0 +1,33 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + property string valueIconColor: widgetData.iconColor !== undefined ? widgetData.iconColor : widgetMetadata.iconColor + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.iconColor = valueIconColor; + settingsChanged(settings); + } + + NColorChoice { + label: I18n.tr("common.select-icon-color") + currentKey: valueIconColor + onSelected: key => { + valueIconColor = key; + saveSettings(); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/WorkspaceSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/WorkspaceSettings.qml new file mode 100644 index 0000000..5606df0 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Bar/WidgetSettings/WorkspaceSettings.qml @@ -0,0 +1,323 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + // Properties to receive data from parent + property var screen: null + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + property string valueLabelMode: widgetData.labelMode !== undefined ? widgetData.labelMode : widgetMetadata.labelMode + property bool valueHideUnoccupied: widgetData.hideUnoccupied !== undefined ? widgetData.hideUnoccupied : widgetMetadata.hideUnoccupied + property bool valueFollowFocusedScreen: widgetData.followFocusedScreen !== undefined ? widgetData.followFocusedScreen : widgetMetadata.followFocusedScreen + property int valueCharacterCount: widgetData.characterCount !== undefined ? widgetData.characterCount : widgetMetadata.characterCount + + // Grouped mode settings + property bool valueShowApplications: widgetData.showApplications !== undefined ? widgetData.showApplications : widgetMetadata.showApplications + property bool valueShowApplicationsHover: widgetData.showApplicationsHover !== undefined ? widgetData.showApplicationsHover : widgetMetadata.showApplicationsHover + property bool valueShowLabelsOnlyWhenOccupied: widgetData.showLabelsOnlyWhenOccupied !== undefined ? widgetData.showLabelsOnlyWhenOccupied : widgetMetadata.showLabelsOnlyWhenOccupied + property bool valueColorizeIcons: widgetData.colorizeIcons !== undefined ? widgetData.colorizeIcons : widgetMetadata.colorizeIcons + property real valueUnfocusedIconsOpacity: widgetData.unfocusedIconsOpacity !== undefined ? widgetData.unfocusedIconsOpacity : widgetMetadata.unfocusedIconsOpacity + property real valueGroupedBorderOpacity: widgetData.groupedBorderOpacity !== undefined ? widgetData.groupedBorderOpacity : widgetMetadata.groupedBorderOpacity + property bool valueEnableScrollWheel: widgetData.enableScrollWheel !== undefined ? widgetData.enableScrollWheel : widgetMetadata.enableScrollWheel + property real valueIconScale: widgetData.iconScale !== undefined ? widgetData.iconScale : widgetMetadata.iconScale + property string valueFocusedColor: widgetData.focusedColor !== undefined ? widgetData.focusedColor : widgetMetadata.focusedColor + property string valueOccupiedColor: widgetData.occupiedColor !== undefined ? widgetData.occupiedColor : widgetMetadata.occupiedColor + property string valueEmptyColor: widgetData.emptyColor !== undefined ? widgetData.emptyColor : widgetMetadata.emptyColor + property bool valueShowBadge: widgetData.showBadge !== undefined ? widgetData.showBadge : widgetMetadata.showBadge + property real valuePillSize: widgetData.pillSize !== undefined ? widgetData.pillSize : widgetMetadata.pillSize + property string valueFontWeight: widgetData.fontWeight !== undefined ? widgetData.fontWeight : widgetMetadata.fontWeight + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.labelMode = valueLabelMode; + settings.hideUnoccupied = valueHideUnoccupied; + settings.characterCount = valueCharacterCount; + settings.followFocusedScreen = valueFollowFocusedScreen; + settings.showApplications = valueShowApplications; + settings.showApplicationsHover = valueShowApplicationsHover; + settings.showLabelsOnlyWhenOccupied = valueShowLabelsOnlyWhenOccupied; + settings.colorizeIcons = valueColorizeIcons; + settings.unfocusedIconsOpacity = valueUnfocusedIconsOpacity; + settings.groupedBorderOpacity = valueGroupedBorderOpacity; + settings.enableScrollWheel = valueEnableScrollWheel; + settings.iconScale = valueIconScale; + settings.focusedColor = valueFocusedColor; + settings.occupiedColor = valueOccupiedColor; + settings.emptyColor = valueEmptyColor; + settings.showBadge = valueShowBadge; + settings.pillSize = valuePillSize; + settings.fontWeight = valueFontWeight; + settingsChanged(settings); + } + + NComboBox { + id: labelModeCombo + label: I18n.tr("bar.workspace.label-mode-label") + description: I18n.tr("bar.workspace.label-mode-description") + model: [ + { + "key": "none", + "name": I18n.tr("common.none") + }, + { + "key": "index", + "name": I18n.tr("options.workspace-labels.index") + }, + { + "key": "name", + "name": I18n.tr("options.workspace-labels.name") + }, + { + "key": "index+name", + "name": I18n.tr("options.workspace-labels.index-and-name") + } + ] + currentKey: widgetData.labelMode || widgetMetadata.labelMode + onSelected: key => { + valueLabelMode = key; + saveSettings(); + } + minimumWidth: 200 + } + + NSpinBox { + label: I18n.tr("bar.workspace.character-count-label") + description: I18n.tr("bar.workspace.character-count-description") + from: 1 + to: 10 + value: valueCharacterCount + onValueChanged: { + valueCharacterCount = value; + saveSettings(); + } + visible: valueLabelMode === "name" + } + + NValueSlider { + label: I18n.tr("bar.workspace.pill-size-label") + description: I18n.tr("bar.workspace.pill-size-description") + from: 0.4 + to: 1.0 + stepSize: 0.01 + value: valuePillSize + defaultValue: widgetMetadata.pillSize + showReset: true + onMoved: value => { + valuePillSize = value; + saveSettings(); + } + text: Math.round(valuePillSize * 100) + "%" + visible: !valueShowApplications + } + + NComboBox { + id: fontWeightCombo + label: I18n.tr("bar.workspace.font-weight-label") + description: I18n.tr("bar.workspace.font-weight-description") + model: [ + { + "key": "regular", + "name": I18n.tr("common.font-weight-regular") + }, + { + "key": "medium", + "name": I18n.tr("common.font-weight-medium") + }, + { + "key": "semibold", + "name": I18n.tr("common.font-weight-semibold") + }, + { + "key": "bold", + "name": I18n.tr("common.font-weight-bold") + }, + ] + currentKey: widgetData.fontWeight || widgetMetadata.fontWeight + onSelected: key => { + valueFontWeight = key; + saveSettings(); + } + minimumWidth: 200 + } + + NToggle { + label: I18n.tr("bar.workspace.hide-unoccupied-label") + description: I18n.tr("bar.workspace.hide-unoccupied-description") + checked: valueHideUnoccupied + onToggled: checked => { + valueHideUnoccupied = checked; + saveSettings(); + } + } + + NToggle { + label: I18n.tr("bar.workspace.show-labels-only-when-occupied-label") + description: I18n.tr("bar.workspace.show-labels-only-when-occupied-description") + checked: valueShowLabelsOnlyWhenOccupied + onToggled: checked => { + valueShowLabelsOnlyWhenOccupied = checked; + saveSettings(); + } + } + + NToggle { + label: I18n.tr("bar.workspace.follow-focused-screen-label") + description: I18n.tr("bar.workspace.follow-focused-screen-description") + checked: valueFollowFocusedScreen + onToggled: checked => { + valueFollowFocusedScreen = checked; + saveSettings(); + } + } + + NToggle { + label: I18n.tr("bar.workspace.enable-scrollwheel-label") + description: I18n.tr("bar.workspace.enable-scrollwheel-description") + checked: valueEnableScrollWheel + onToggled: checked => { + valueEnableScrollWheel = checked; + saveSettings(); + } + } + + NDivider { + Layout.fillWidth: true + } + + NToggle { + label: I18n.tr("bar.workspace.show-applications-label") + description: I18n.tr("bar.workspace.show-applications-description") + checked: valueShowApplications + onToggled: checked => { + valueShowApplications = checked; + saveSettings(); + } + } + + NToggle { + label: I18n.tr("bar.workspace.show-applications-hover-label") + description: I18n.tr("bar.workspace.show-applications-hover-description") + checked: valueShowApplicationsHover + onToggled: checked => { + valueShowApplicationsHover = checked; + saveSettings(); + } + visible: valueShowApplications + } + + NToggle { + label: I18n.tr("bar.workspace.show-badge-label") + description: I18n.tr("bar.workspace.show-badge-description") + checked: valueShowBadge + onToggled: checked => { + valueShowBadge = checked; + saveSettings(); + } + visible: valueShowApplications + } + + NToggle { + label: I18n.tr("bar.tray.colorize-icons-label") + description: I18n.tr("bar.active-window.colorize-icons-description") + checked: valueColorizeIcons + onToggled: checked => { + valueColorizeIcons = checked; + saveSettings(); + } + visible: valueShowApplications + } + + NValueSlider { + label: I18n.tr("bar.workspace.unfocused-icons-opacity-label") + description: I18n.tr("bar.workspace.unfocused-icons-opacity-description") + from: 0 + to: 1 + stepSize: 0.01 + showReset: true + value: valueUnfocusedIconsOpacity + defaultValue: widgetMetadata.unfocusedIconsOpacity + onMoved: value => { + valueUnfocusedIconsOpacity = value; + saveSettings(); + } + text: Math.floor(valueUnfocusedIconsOpacity * 100) + "%" + visible: valueShowApplications + } + + NValueSlider { + label: I18n.tr("bar.workspace.grouped-border-opacity-label") + description: I18n.tr("bar.workspace.grouped-border-opacity-description") + from: 0 + to: 1 + stepSize: 0.01 + showReset: true + value: valueGroupedBorderOpacity + defaultValue: widgetMetadata.groupedBorderOpacity + onMoved: value => { + valueGroupedBorderOpacity = value; + saveSettings(); + } + text: Math.floor(valueGroupedBorderOpacity * 100) + "%" + visible: valueShowApplications + } + + NValueSlider { + label: I18n.tr("bar.taskbar.icon-scale-label") + description: I18n.tr("bar.taskbar.icon-scale-description") + from: 0.5 + to: 1 + stepSize: 0.01 + showReset: true + value: valueIconScale + defaultValue: widgetMetadata.iconScale + onMoved: value => { + valueIconScale = value; + saveSettings(); + } + text: Math.round(valueIconScale * 100) + "%" + visible: valueShowApplications + } + + NDivider { + Layout.fillWidth: true + } + + NColorChoice { + label: I18n.tr("bar.workspace.focused-color-label") + description: I18n.tr("bar.workspace.focused-color-description") + currentKey: valueFocusedColor + onSelected: key => { + valueFocusedColor = key; + saveSettings(); + } + } + + NColorChoice { + label: I18n.tr("bar.workspace.occupied-color-label") + description: I18n.tr("bar.workspace.occupied-color-description") + currentKey: valueOccupiedColor + onSelected: key => { + valueOccupiedColor = key; + saveSettings(); + } + } + + NColorChoice { + label: I18n.tr("bar.workspace.empty-color-label") + description: I18n.tr("bar.workspace.empty-color-description") + currentKey: valueEmptyColor + onSelected: key => { + valueEmptyColor = key; + saveSettings(); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml new file mode 100644 index 0000000..53aa8b6 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml @@ -0,0 +1,160 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.UI +import qs.Widgets + +// Widget Settings Dialog Component +Popup { + id: root + + property int widgetIndex: -1 + property var widgetData: null + property string widgetId: "" + property string sectionId: "" + property var screen: null + property var settingsCache: ({}) + + signal updateWidgetSettings(string section, int index, var settings) + + readonly property real maxHeight: (screen ? screen.height : (parent ? parent.height : 800)) * 0.8 + readonly property real defaultContentWidth: Math.round(600 * Style.uiScaleRatio) + readonly property real settingsContentWidth: { + if (settingsLoader.item && settingsLoader.item.implicitWidth > 0) { + return settingsLoader.item.implicitWidth; + } + return defaultContentWidth; + } + + readonly property real dialogPadding: Style.marginXL + width: Math.max(settingsContentWidth + dialogPadding * 2, 500) + height: Math.min(content.implicitHeight + dialogPadding * 2, maxHeight) + padding: 0 + modal: true + closePolicy: Popup.NoAutoClose + anchors.centerIn: parent + + onOpened: { + if (widgetData && widgetId) { + loadWidgetSettings(); + } + } + + background: Rectangle { + color: Color.mSurface + radius: Style.radiusL + border.color: Color.mPrimary + border.width: Style.borderM + } + + contentItem: ColumnLayout { + id: content + anchors.fill: parent + anchors.margins: dialogPadding + spacing: Style.marginM + + // Title + RowLayout { + id: titleRow + Layout.fillWidth: true + Layout.preferredHeight: implicitHeight + + NText { + text: I18n.tr("system.widget-settings-title", { + "widget": root.widgetId + }) + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + onClicked: saveAndClose() + } + } + + // Separator + Rectangle { + id: separator + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + } + + // Scrollable settings area + NScrollView { + id: scrollView + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 100 + gradientColor: Color.mSurface + reserveScrollbarSpace: false + + ColumnLayout { + width: scrollView.availableWidth + spacing: Style.marginM + + Loader { + id: settingsLoader + Layout.fillWidth: true + onItemChanged: { + if (item) { + // Force width recalculation when content loads + Qt.callLater(() => { + root.width = Math.max(root.settingsContentWidth + root.dialogPadding * 2, 500); + }); + } + } + } + } + } + } + + Timer { + id: saveTimer + running: false + interval: 150 + onTriggered: { + root.updateWidgetSettings(root.sectionId, root.widgetIndex, root.settingsCache); + } + } + + Connections { + target: settingsLoader.item + ignoreUnknownSignals: true + function onSettingsChanged(newSettings) { + if (newSettings) { + root.settingsCache = newSettings; + saveTimer.start(); + } + } + } + + function saveAndClose() { + if (settingsLoader.item && typeof settingsLoader.item.saveSettings === 'function') { + var newSettings = settingsLoader.item.saveSettings(); + if (newSettings) { + root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings); + } + } + root.close(); + } + + function loadWidgetSettings() { + const widgetSettingsMap = { + "CustomButton": "WidgetSettings/CustomButtonSettings.qml" + }; + + const source = widgetSettingsMap[widgetId]; + if (source) { + settingsLoader.setSource(source, { + "widgetData": widgetData, + "widgetMetadata": ControlCenterWidgetRegistry.widgetMetadata[widgetId] + }); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml new file mode 100644 index 0000000..d27e285 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/ControlCenter/WidgetSettings/CustomButtonSettings.qml @@ -0,0 +1,303 @@ +import QtQml.Models +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + Layout.preferredWidth: Math.round(600 * Style.uiScaleRatio) + implicitWidth: Layout.preferredWidth + + property var widgetData: null + property var widgetMetadata: null + property var rootSettings: null + + signal settingsChanged(var settings) + + QtObject { + id: _settings + + property string icon: widgetData.icon !== undefined ? widgetData.icon : widgetMetadata.icon + property string onClicked: widgetData.onClicked !== undefined ? widgetData.onClicked : widgetMetadata.onClicked + property string onRightClicked: widgetData.onRightClicked !== undefined ? widgetData.onRightClicked : widgetMetadata.onRightClicked + property string onMiddleClicked: widgetData.onMiddleClicked !== undefined ? widgetData.onMiddleClicked : widgetMetadata.onMiddleClicked + property ListModel _stateChecksListModel: ListModel {} + property string stateChecksJson: "[]" + property string generalTooltipText: widgetData.generalTooltipText !== undefined ? widgetData.generalTooltipText : widgetMetadata.generalTooltipText + property bool enableOnStateLogic: widgetData.enableOnStateLogic !== undefined ? widgetData.enableOnStateLogic : widgetMetadata.enableOnStateLogic + property bool showExecTooltip: widgetData.showExecTooltip !== undefined ? widgetData.showExecTooltip : widgetMetadata.showExecTooltip + + function populateStateChecks() { + try { + var initialChecks = JSON.parse(stateChecksJson); + if (initialChecks && Array.isArray(initialChecks)) { + for (var i = 0; i < initialChecks.length; i++) { + var item = initialChecks[i]; + if (item && typeof item === "object") { + _settings._stateChecksListModel.append({ + "command": item.command || "", + "icon": item.icon || "" + }); + } else { + Logger.w("CustomButtonSettings", "Invalid stateChecks entry at index " + i + ":", item); + } + } + } + } catch (e) { + Logger.e("CustomButtonSettings", "Failed to parse stateChecksJson:", e.message); + } + } + + Component.onCompleted: { + root.rootSettings = _settings; + stateChecksJson = (widgetData && widgetData.stateChecksJson !== undefined) ? widgetData.stateChecksJson : (widgetMetadata && widgetMetadata.stateChecksJson ? widgetMetadata.stateChecksJson : "[]"); + Qt.callLater(populateStateChecks); + } + + function saveSettings() { + var savedStateChecksArray = []; + for (var i = 0; i < _settings._stateChecksListModel.count; i++) { + savedStateChecksArray.push(_settings._stateChecksListModel.get(i)); + } + _settings.stateChecksJson = JSON.stringify(savedStateChecksArray); + + return { + "id": widgetData.id, + "icon": _settings.icon, + "onClicked": _settings.onClicked, + "onRightClicked": _settings.onRightClicked, + "onMiddleClicked": _settings.onMiddleClicked, + "stateChecksJson": _settings.stateChecksJson, + "generalTooltipText": _settings.generalTooltipText, + "enableOnStateLogic": _settings.enableOnStateLogic, + "showExecTooltip": _settings.showExecTooltip + }; + } + } + + function saveSettings() { + return _settings.saveSettings(); + } + + function updateStateCheck(index, command, icon) { + _settings._stateChecksListModel.set(index, { + "command": command, + "icon": icon + }); + _settings.saveSettings(); + } + + function removeStateCheck(index) { + _settings._stateChecksListModel.remove(index); + _settings.saveSettings(); + } + + function addStateCheck() { + _settings._stateChecksListModel.append({ + "command": "", + "icon": "" + }); + _settings.saveSettings(); + } + + RowLayout { + spacing: Style?.marginM ?? 8 + + NLabel { + label: I18n.tr("common.icon") + description: I18n.tr("panels.control-center.shortcuts-custom-button-icon-description") + } + + NIcon { + Layout.alignment: Qt.AlignVCenter + icon: _settings.icon || (widgetMetadata && widgetMetadata.icon ? widgetMetadata.icon : "") + pointSize: Style?.fontSizeXL ?? 24 + visible: (_settings.icon || (widgetMetadata && widgetMetadata.icon ? widgetMetadata.icon : "")) !== "" + } + + NButton { + text: I18n.tr("common.browse") + onClicked: iconPicker.open() + } + } + + NIconPicker { + id: iconPicker + initialIcon: _settings.icon + onIconSelected: function (iconName) { + _settings.icon = iconName; + saveSettings(); + } + } + + NTextInput { + Layout.fillWidth: true + label: I18n.tr("bar.custom-button.general-tooltip-text-label") + description: I18n.tr("bar.custom-button.general-tooltip-text-description") + placeholderText: I18n.tr("placeholders.enter-tooltip") + text: _settings.generalTooltipText + onTextChanged: { + _settings.generalTooltipText = text; + saveSettings(); + } + defaultValue: widgetMetadata.generalTooltipText + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.custom-button.show-exec-tooltip-label") + description: I18n.tr("bar.custom-button.show-exec-tooltip-description") + checked: _settings.showExecTooltip + onToggled: checked => { + _settings.showExecTooltip = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showExecTooltip + } + + NTextInput { + Layout.fillWidth: true + label: I18n.tr("bar.custom-button.left-click-label") + description: I18n.tr("bar.custom-button.left-click-description") + placeholderText: I18n.tr("placeholders.enter-command") + text: _settings.onClicked + onTextChanged: { + _settings.onClicked = text; + saveSettings(); + } + defaultValue: widgetMetadata.onClicked + } + + NTextInput { + Layout.fillWidth: true + label: I18n.tr("bar.custom-button.right-click-label") + description: I18n.tr("bar.custom-button.right-click-description") + placeholderText: I18n.tr("placeholders.enter-command") + text: _settings.onRightClicked + onTextChanged: { + _settings.onRightClicked = text; + saveSettings(); + } + defaultValue: widgetMetadata.onRightClicked + } + + NTextInput { + Layout.fillWidth: true + label: I18n.tr("bar.custom-button.middle-click-label") + description: I18n.tr("bar.custom-button.middle-click-description") + placeholderText: I18n.tr("placeholders.enter-command") + text: _settings.onMiddleClicked + onTextChanged: { + _settings.onMiddleClicked = text; + saveSettings(); + } + defaultValue: widgetMetadata.onMiddleClicked + } + + NDivider {} + + NToggle { + id: enableOnStateLogicToggle + Layout.fillWidth: true + label: I18n.tr("panels.control-center.shortcuts-custom-button-enable-on-state-logic-label") + description: I18n.tr("panels.control-center.shortcuts-custom-button-enable-on-state-logic-description") + checked: _settings.enableOnStateLogic + onToggled: checked => { + _settings.enableOnStateLogic = checked; + saveSettings(); + } + defaultValue: widgetMetadata.enableOnStateLogic + } + + ColumnLayout { + Layout.fillWidth: true + visible: root.rootSettings && root.rootSettings.enableOnStateLogic + spacing: Style?.marginM ?? 8 + + NLabel { + label: I18n.tr("panels.control-center.shortcuts-custom-button-state-checks-label") + } + + Repeater { + model: root.rootSettings ? root.rootSettings._stateChecksListModel : null + delegate: Item { + property int currentIndex: index + + implicitHeight: contentRow.implicitHeight + Layout.fillWidth: true + + RowLayout { + id: contentRow + anchors.fill: parent + spacing: Style?.marginM ?? 8 + + NTextInput { + Layout.fillWidth: true + placeholderText: I18n.tr("panels.control-center.shortcuts-custom-button-state-checks-command") + text: model.command + onTextChanged: { + updateStateCheck(currentIndex, text, model.icon); + } + } + + RowLayout { + Layout.alignment: Qt.AlignVCenter + spacing: Style?.marginS ?? 4 + + NIcon { + icon: model.icon + pointSize: Style?.fontSizeL ?? 20 + visible: model.icon !== undefined && model.icon !== "" + } + + NIconButton { + icon: "folder" + tooltipText: I18n.tr("common.browse") + baseSize: Style?.buttonSizeS ?? 24 + onClicked: iconPickerDelegate.open() + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("panels.control-center.shortcuts-custom-button-state-checks-remove") + baseSize: Style?.buttonSizeS ?? 24 + colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight) + colorBg: Color.mError + colorFg: Color.mOnError + colorBgHover: Qt.alpha(Color.mError, Style.opacityMedium) + colorFgHover: Color.mOnError + onClicked: { + removeStateCheck(currentIndex); + } + } + } + } + + NIconPicker { + id: iconPickerDelegate + initialIcon: model.icon + onIconSelected: function (iconName) { + updateStateCheck(currentIndex, model.command, iconName); + } + } + } + } + + Item { + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + spacing: Style?.marginM ?? 8 + + NButton { + text: I18n.tr("panels.control-center.shortcuts-custom-button-state-checks-add") + onClicked: addStateCheck() + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/DesktopWidgetSettingsDialog.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/DesktopWidgetSettingsDialog.qml new file mode 100644 index 0000000..93c7892 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/DesktopWidgetSettingsDialog.qml @@ -0,0 +1,276 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Noctalia +import qs.Services.UI +import qs.Widgets + +Popup { + id: root + + property int widgetIndex: -1 + property var widgetData: null + property string widgetId: "" + property string sectionId: "" // Not used for desktop widgets, but required by NSectionEditor + property var screen: null + property var settingsCache: ({}) + + readonly property real maxHeight: (screen ? screen.height : (parent ? parent.height : 800)) * 0.8 + + signal updateWidgetSettings(string section, int index, var settings) + + width: Math.max(content.implicitWidth + padding * 2, 500) + height: Math.min(content.implicitHeight + padding * 2, maxHeight) + padding: Style.marginXL + modal: true + closePolicy: Popup.NoAutoClose + dim: false + + // Center in parent + x: Math.round((parent.width - width) / 2) + y: Math.round((parent.height - height) / 2) + + onOpened: { + if (widgetData && widgetId) { + loadWidgetSettings(); + } + forceActiveFocus(); + } + + background: Rectangle { + id: bgRect + color: Color.mSurface + radius: Style.radiusL + border.color: Color.mPrimary + border.width: Style.borderM + } + + contentItem: FocusScope { + id: focusScope + focus: true + + ColumnLayout { + id: content + anchors.fill: parent + spacing: Style.marginM + + RowLayout { + id: titleRow + Layout.fillWidth: true + Layout.preferredHeight: implicitHeight + + NText { + text: I18n.tr("system.widget-settings-title", { + "widget": DesktopWidgetRegistry.getWidgetDisplayName(root.widgetId) + }) + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + onClicked: saveAndClose() + } + } + + Rectangle { + id: separator + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + } + + // Scrollable settings area + NScrollView { + id: scrollView + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 100 + gradientColor: Color.mSurface + + ColumnLayout { + width: scrollView.availableWidth + spacing: Style.marginM + + Loader { + id: settingsLoader + Layout.fillWidth: true + onLoaded: { + if (item) { + Qt.callLater(() => { + var firstInput = findFirstFocusable(item); + if (firstInput) { + firstInput.forceActiveFocus(); + } else { + focusScope.forceActiveFocus(); + } + }); + } + } + + function findFirstFocusable(item) { + if (!item) + return null; + if (item.focus !== undefined && item.focus === true) + return item; + if (item.children) { + for (var i = 0; i < item.children.length; i++) { + var child = item.children[i]; + if (child && child.focus !== undefined && child.focus === true) + return child; + var found = findFirstFocusable(child); + if (found) + return found; + } + } + return null; + } + } + } + } + } + } + + // Mouse area for a draggable popup + MouseArea { + x: titleRow.x + y: titleRow.y + width: titleRow.width + height: titleRow.height + z: -1 + + cursorShape: Qt.OpenHandCursor + + property real pressX: 0 + property real pressY: 0 + + onPressed: mouse => { + pressX = mouse.x; + pressY = mouse.y; + cursorShape = Qt.ClosedHandCursor; + } + + onReleased: { + cursorShape = Qt.OpenHandCursor; + } + + onPositionChanged: mouse => { + if (pressed) { + var deltaX = mouse.x - pressX; + var deltaY = mouse.y - pressY; + + root.x += deltaX; + root.y += deltaY; + } + } + } + + Timer { + id: saveTimer + running: false + interval: 150 + onTriggered: { + root.updateWidgetSettings(root.sectionId, root.widgetIndex, root.settingsCache); + } + } + + Connections { + target: settingsLoader.item + ignoreUnknownSignals: true + function onSettingsChanged(newSettings) { + if (newSettings) { + root.settingsCache = newSettings; + saveTimer.start(); + } + } + + function onSettingsSaved(newSettings) { + if (newSettings) { + root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings); + } + } + } + + function saveAndClose() { + if (settingsLoader.item && typeof settingsLoader.item.saveSettings === 'function') { + var newSettings = settingsLoader.item.saveSettings(); + if (newSettings) { + root.updateWidgetSettings(root.sectionId, root.widgetIndex, newSettings); + } + } + root.close(); + } + + function loadWidgetSettings() { + // Handle plugin widgets + if (DesktopWidgetRegistry.isPluginWidget(widgetId)) { + var pluginId = widgetId.replace("plugin:", ""); + var manifest = PluginRegistry.getPluginManifest(pluginId); + + var pluginDir = PluginRegistry.getPluginDir(pluginId); + var loadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0; + var api = PluginService.getPluginAPI(pluginId); + + var settingsPath; + if (manifest && manifest.entryPoints && manifest.entryPoints.desktopWidgetSettings) { + settingsPath = "file://" + pluginDir + "/" + manifest.entryPoints.desktopWidgetSettings; + + var widgetSettings = {}; + widgetSettings.data = widgetData || {}; + widgetSettings.metadata = DesktopWidgetRegistry.widgetMetadata[widgetId] || {}; + widgetSettings.save = function () { + var newSettings = Object.assign({}, widgetSettings.data); + root.settingsCache = newSettings; + saveTimer.start(); + }; + + settingsLoader.setSource(settingsPath + "?v=" + loadVersion, { + "pluginApi": api, + "widgetSettings": widgetSettings + }); + } else { + Logger.w("DesktopWidgetSettingsDialog", "Plugin does not have desktop widget settings:", pluginId); + + // Fallback to the plugin settings + if (manifest && manifest.entryPoints && manifest.entryPoints.settings) { + settingsPath = "file://" + pluginDir + "/" + manifest.entryPoints.settings; + + settingsLoader.setSource(settingsPath + "?v=" + loadVersion, { + "pluginApi": api + }); + } else { + Logger.w("DesktopWidgetSettingsDialog", "Plugin does not have settings:", pluginId); + } + } + + return; + } + + // Handle core widgets + const source = DesktopWidgetRegistry.widgetSettingsMap[widgetId]; + if (source) { + var currentWidgetData = widgetData; + var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || []; + for (var i = 0; i < monitorWidgets.length; i++) { + if (monitorWidgets[i].name === sectionId) { + var widgets = monitorWidgets[i].widgets || []; + if (widgetIndex >= 0 && widgetIndex < widgets.length) { + currentWidgetData = widgets[widgetIndex]; + } + break; + } + } + var fullPath = Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/DesktopWidgets/" + source); + settingsLoader.setSource(fullPath, { + "widgetData": currentWidgetData, + "widgetMetadata": DesktopWidgetRegistry.widgetMetadata[widgetId] + }); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/AudioVisualizerSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/AudioVisualizerSettings.qml new file mode 100644 index 0000000..ea65462 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/AudioVisualizerSettings.qml @@ -0,0 +1,154 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + property int valueWidth: widgetData.width !== undefined ? widgetData.width : widgetMetadata.width + property int valueHeight: widgetData.height !== undefined ? widgetData.height : widgetMetadata.height + property string valueVisualizerType: widgetData.visualizerType !== undefined ? widgetData.visualizerType : widgetMetadata.visualizerType + property string valueColorName: widgetData.colorName !== undefined ? widgetData.colorName : widgetMetadata.colorName + property bool valueHideWhenIdle: widgetData.hideWhenIdle !== undefined ? widgetData.hideWhenIdle : widgetMetadata.hideWhenIdle + property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : widgetMetadata.showBackground + property bool valueRoundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : widgetMetadata.roundedCorners + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.width = valueWidth; + settings.height = valueHeight; + settings.visualizerType = valueVisualizerType; + settings.colorName = valueColorName; + settings.hideWhenIdle = valueHideWhenIdle; + settings.showBackground = valueShowBackground; + settings.roundedCorners = valueRoundedCorners; + settingsChanged(settings); + } + + NTextInput { + id: widthInput + Layout.fillWidth: true + label: I18n.tr("common.width") + description: I18n.tr("bar.audio-visualizer.width-description") + text: String(valueWidth) + placeholderText: I18n.tr("placeholders.enter-width-pixels") + inputMethodHints: Qt.ImhDigitsOnly + onEditingFinished: { + const parsed = parseInt(text); + if (!isNaN(parsed) && parsed > 0) { + valueWidth = parsed; + saveSettings(); + } else { + text = String(valueWidth); + } + } + defaultValue: String(widgetMetadata.width) + } + + NTextInput { + id: heightInput + Layout.fillWidth: true + label: I18n.tr("common.height") + description: I18n.tr("bar.audio-visualizer.height-description") + text: String(valueHeight) + placeholderText: I18n.tr("placeholders.enter-width-pixels") + inputMethodHints: Qt.ImhDigitsOnly + onEditingFinished: { + const parsed = parseInt(text); + if (!isNaN(parsed) && parsed > 0) { + valueHeight = parsed; + saveSettings(); + } else { + text = String(valueHeight); + } + } + defaultValue: String(widgetMetadata.height) + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.audio.visualizer-type-label") + description: I18n.tr("panels.desktop-widgets.media-player-visualizer-type-description") + model: [ + { + "key": "linear", + "name": I18n.tr("options.visualizer-types.linear") + }, + { + "key": "mirrored", + "name": I18n.tr("options.visualizer-types.mirrored") + }, + { + "key": "wave", + "name": I18n.tr("options.visualizer-types.wave") + } + ] + currentKey: valueVisualizerType + onSelected: key => { + valueVisualizerType = key; + saveSettings(); + } + defaultValue: widgetMetadata.visualizerType + } + + NColorChoice { + Layout.fillWidth: true + label: I18n.tr("bar.audio-visualizer.color-name-label") + description: I18n.tr("bar.audio-visualizer.color-name-description") + currentKey: valueColorName + onSelected: key => { + valueColorName = key; + saveSettings(); + } + defaultValue: widgetMetadata.colorName + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.audio-visualizer.hide-when-idle-label") + description: I18n.tr("bar.audio-visualizer.hide-when-idle-description") + checked: valueHideWhenIdle + onToggled: checked => { + valueHideWhenIdle = checked; + saveSettings(); + } + defaultValue: widgetMetadata.hideWhenIdle + } + + NDivider { + Layout.fillWidth: true + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.desktop-widgets.clock-show-background-label") + description: I18n.tr("panels.desktop-widgets.media-player-show-background-description") + checked: valueShowBackground + onToggled: checked => { + valueShowBackground = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showBackground + } + + NToggle { + Layout.fillWidth: true + visible: valueShowBackground + label: I18n.tr("panels.desktop-widgets.clock-rounded-corners-label") + description: I18n.tr("panels.desktop-widgets.media-player-rounded-corners-description") + checked: valueRoundedCorners + onToggled: checked => { + valueRoundedCorners = checked; + saveSettings(); + } + defaultValue: widgetMetadata.roundedCorners + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/ClockSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/ClockSettings.qml new file mode 100644 index 0000000..f86b1a1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/ClockSettings.qml @@ -0,0 +1,293 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.System +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + width: 700 + + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : widgetMetadata.showBackground + property bool valueRoundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : widgetMetadata.roundedCorners + property string valueClockStyle: widgetData.clockStyle !== undefined ? widgetData.clockStyle : widgetMetadata.clockStyle + property string valueClockColor: widgetData.clockColor !== undefined ? widgetData.clockColor : widgetMetadata.clockColor + property bool valueUseCustomFont: widgetData.useCustomFont !== undefined ? widgetData.useCustomFont : widgetMetadata.useCustomFont + property string valueCustomFont: widgetData.customFont !== undefined ? widgetData.customFont : widgetMetadata.custonFont + property string valueFormat: widgetData.format !== undefined ? widgetData.format : widgetMetadata.format + + // Track the currently focused input field + property var focusedInput: null + + readonly property bool isMinimalMode: valueClockStyle === "minimal" + readonly property var now: Time.now + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.showBackground = valueShowBackground; + settings.roundedCorners = valueRoundedCorners; + settings.clockStyle = valueClockStyle; + settings.clockColor = valueClockColor; + settings.useCustomFont = valueUseCustomFont; + settings.customFont = valueCustomFont; + settings.format = valueFormat.trim(); + settingsChanged(settings); + } + + // Function to insert token at cursor position in the focused input + function insertToken(token) { + if (!focusedInput || !focusedInput.inputItem) { + // If no input is focused, default to format input + if (formatInput.inputItem) { + formatInput.inputItem.focus = true; + focusedInput = formatInput; + } + } + + if (focusedInput && focusedInput.inputItem) { + var input = focusedInput.inputItem; + var cursorPos = input.cursorPosition; + var currentText = input.text; + + // Insert token at cursor position + var newText = currentText.substring(0, cursorPos) + token + currentText.substring(cursorPos); + input.text = newText + " "; + + // Move cursor after the inserted token + input.cursorPosition = cursorPos + token.length + 1; + + // Ensure the input keeps focus + input.focus = true; + saveSettings(); + } + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.desktop-widgets.clock-style-label") + description: I18n.tr("panels.desktop-widgets.clock-style-description") + currentKey: valueClockStyle + minimumWidth: 260 * Style.uiScaleRatio + model: [ + { + "key": "minimal", + "name": I18n.tr("panels.desktop-widgets.clock-style-minimal") + }, + { + "key": "digital", + "name": I18n.tr("panels.desktop-widgets.clock-style-digital") + }, + { + "key": "analog", + "name": I18n.tr("panels.desktop-widgets.clock-style-analog") + }, + { + "key": "binary", + "name": I18n.tr("panels.desktop-widgets.clock-style-binary") + } + ] + onSelected: key => { + valueClockStyle = key; + saveSettings(); + } + defaultValue: widgetMetadata.clockStyle + } + + NColorChoice { + label: I18n.tr("common.select-color") + description: I18n.tr("common.select-color-description") + currentKey: valueClockColor + onSelected: key => { + valueClockColor = key; + saveSettings(); + } + defaultValue: widgetMetadata.clockColor + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.clock.use-custom-font-label") + description: I18n.tr("bar.clock.use-custom-font-description") + checked: valueUseCustomFont + onToggled: checked => { + valueUseCustomFont = checked; + saveSettings(); + } + defaultValue: widgetMetadata.useCustomFont + } + + NSearchableComboBox { + Layout.fillWidth: true + visible: valueUseCustomFont + label: I18n.tr("bar.clock.custom-font-label") + description: I18n.tr("bar.clock.custom-font-description") + model: FontService.availableFonts + currentKey: valueCustomFont + placeholder: I18n.tr("bar.clock.custom-font-placeholder") + searchPlaceholder: I18n.tr("bar.clock.custom-font-search-placeholder") + popupHeight: 420 + minimumWidth: 300 + onSelected: function (key) { + valueCustomFont = key; + saveSettings(); + } + enabled: valueClockStyle === "minimal" + defaultValue: Settings.data.ui.fontDefault + } + + NDivider { + Layout.fillWidth: true + visible: isMinimalMode + } + + NHeader { + visible: isMinimalMode + label: I18n.tr("bar.clock.clock-display-label") + description: I18n.tr("bar.clock.clock-display-description") + } + + // Format editor - only visible in minimal mode + RowLayout { + id: main + visible: isMinimalMode + spacing: Style.marginL + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + + ColumnLayout { + spacing: Style.marginM + Layout.fillWidth: true + Layout.preferredWidth: 1 + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + + NTextInput { + id: formatInput + Layout.fillWidth: true + label: I18n.tr("panels.desktop-widgets.clock-format-label") + description: I18n.tr("bar.clock.horizontal-bar-description") + placeholderText: "HH:mm\\nd MMMM yyyy" + text: valueFormat + onTextChanged: { + valueFormat = text; + settingsChanged(saveSettings()); + } + Component.onCompleted: { + if (inputItem) { + inputItem.onActiveFocusChanged.connect(function () { + if (inputItem.activeFocus) { + root.focusedInput = formatInput; + } + }); + } + } + defaultValue: widgetMetadata.format + } + } + + // Preview + ColumnLayout { + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.fillWidth: false + + NLabel { + label: I18n.tr("bar.clock.preview") + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + } + + Rectangle { + Layout.preferredWidth: 320 + Layout.preferredHeight: 160 + color: Color.mSurfaceVariant + radius: Style.radiusM + border.color: Color.mSecondary + border.width: Style.borderS + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + ColumnLayout { + spacing: Style.marginM + anchors.centerIn: parent + + ColumnLayout { + spacing: -2 + Layout.alignment: Qt.AlignHCenter + + Repeater { + Layout.topMargin: Style.marginM + model: I18n.locale.toString(now, valueFormat.trim()).split("\\n") + delegate: NText { + visible: text !== "" + text: modelData + family: valueUseCustomFont && valueCustomFont ? valueCustomFont : Settings.data.ui.fontDefault + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + color: Color.resolveColorKey(valueClockColor) + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + } + } + } + } + } + + NDivider { + Layout.topMargin: Style.marginM + Layout.bottomMargin: Style.marginM + visible: isMinimalMode + } + + NDateTimeTokens { + Layout.fillWidth: true + height: 200 + visible: isMinimalMode + onTokenClicked: token => root.insertToken(token) + } + + NDivider { + Layout.fillWidth: true + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.desktop-widgets.clock-show-background-label") + description: I18n.tr("panels.desktop-widgets.clock-show-background-description") + checked: valueShowBackground + onToggled: checked => { + valueShowBackground = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showBackground + } + + NToggle { + Layout.fillWidth: true + visible: valueShowBackground + label: I18n.tr("panels.desktop-widgets.clock-rounded-corners-label") + description: I18n.tr("panels.desktop-widgets.clock-rounded-corners-description") + checked: valueRoundedCorners + onToggled: checked => { + valueRoundedCorners = checked; + saveSettings(); + } + defaultValue: widgetMetadata.roundedCorners + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/MediaPlayerSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/MediaPlayerSettings.qml new file mode 100644 index 0000000..9bd9a6c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/MediaPlayerSettings.qml @@ -0,0 +1,148 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : widgetMetadata.showBackground + property string valueVisualizerType: widgetData.visualizerType ? widgetData.visualizerType : widgetMetadata.visualizerType + property string valueHideMode: widgetData.hideMode !== undefined ? widgetData.hideMode : widgetMetadata.hideMode + property bool valueShowButtons: widgetData.showButtons !== undefined ? widgetData.showButtons : widgetMetadata.showButtons + property bool valueShowAlbumArt: widgetData.showAlbumArt !== undefined ? widgetData.showAlbumArt : widgetMetadata.showAlbumArt + property bool valueShowVisualizer: widgetData.showVisualizer !== undefined ? widgetData.showVisualizer : widgetMetadata.showVisualizer + property bool valueRoundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : widgetMetadata.roundedCorners + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.showBackground = valueShowBackground; + settings.visualizerType = valueVisualizerType; + settings.hideMode = valueHideMode; + settings.showButtons = valueShowButtons; + settings.showAlbumArt = valueShowAlbumArt; + settings.showVisualizer = valueShowVisualizer; + settings.roundedCorners = valueRoundedCorners; + settingsChanged(settings); + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.desktop-widgets.clock-show-background-label") + description: I18n.tr("panels.desktop-widgets.media-player-show-background-description") + checked: valueShowBackground + onToggled: checked => { + valueShowBackground = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showBackground + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.desktop-widgets.clock-rounded-corners-label") + description: I18n.tr("panels.desktop-widgets.media-player-rounded-corners-description") + checked: valueRoundedCorners + onToggled: checked => { + valueRoundedCorners = checked; + saveSettings(); + } + defaultValue: widgetMetadata.roundedCorners + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.desktop-widgets.media-player-show-album-art-label") + description: I18n.tr("panels.desktop-widgets.media-player-show-album-art-description") + checked: valueShowAlbumArt + onToggled: checked => { + valueShowAlbumArt = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showAlbumArt + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("bar.media-mini.show-visualizer-label") + description: I18n.tr("panels.desktop-widgets.media-player-show-visualizer-description") + checked: valueShowVisualizer + onToggled: checked => { + valueShowVisualizer = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showVisualizer + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.desktop-widgets.media-player-show-buttons-label") + description: I18n.tr("panels.desktop-widgets.media-player-show-buttons-description") + checked: valueShowButtons + onToggled: checked => { + valueShowButtons = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showButtons + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.audio.visualizer-type-label") + description: I18n.tr("panels.desktop-widgets.media-player-visualizer-type-description") + enabled: valueShowVisualizer + model: [ + { + "key": "linear", + "name": I18n.tr("options.visualizer-types.linear") + }, + { + "key": "mirrored", + "name": I18n.tr("options.visualizer-types.mirrored") + }, + { + "key": "wave", + "name": I18n.tr("options.visualizer-types.wave") + } + ] + currentKey: valueVisualizerType + onSelected: key => { + valueVisualizerType = key; + saveSettings(); + } + defaultValue: widgetMetadata.visualizerType + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("bar.taskbar.hide-mode-label") + description: I18n.tr("bar.media-mini.hide-mode-description") + model: [ + { + "key": "hidden", + "name": I18n.tr("hide-modes.hidden") + }, + { + "key": "idle", + "name": I18n.tr("hide-modes.idle") + }, + { + "key": "visible", + "name": I18n.tr("hide-modes.visible") + } + ] + currentKey: valueHideMode + onSelected: key => { + valueHideMode = key; + saveSettings(); + } + defaultValue: widgetMetadata.hideMode + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/SystemStatSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/SystemStatSettings.qml new file mode 100644 index 0000000..fe7e135 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/SystemStatSettings.qml @@ -0,0 +1,148 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.System +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + width: 700 + + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + property string valueStatType: widgetData.statType !== undefined ? widgetData.statType : widgetMetadata.statType + property string valueDiskPath: widgetData.diskPath !== undefined ? widgetData.diskPath : widgetMetadata.diskPath + property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : widgetMetadata.showBackground + property bool valueRoundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : widgetMetadata.roundedCorners + property string valueLayout: widgetData.layout !== undefined ? widgetData.layout : widgetMetadata.layout + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.statType = valueStatType; + settings.diskPath = valueDiskPath; + settings.showBackground = valueShowBackground; + settings.roundedCorners = valueRoundedCorners; + settings.layout = valueLayout; + settingsChanged(settings); + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.desktop-widgets.system-stat-stat-type-label") + description: I18n.tr("panels.desktop-widgets.system-stat-stat-type-description") + currentKey: valueStatType + minimumWidth: 260 * Style.uiScaleRatio + model: { + let items = [ + { + "key": "CPU", + "name": I18n.tr("system-monitor.cpu-usage") + }, + { + "key": "Memory", + "name": I18n.tr("common.memory") + }, + { + "key": "Network", + "name": I18n.tr("bar.system-monitor.network-traffic-label") + }, + { + "key": "Disk", + "name": I18n.tr("system-monitor.disk") + } + ]; + if (Settings.data.systemMonitor.enableDgpuMonitoring) + items.push({ + "key": "GPU", + "name": I18n.tr("panels.system-monitor.gpu-section-label") + }); + return items; + } + onSelected: key => { + valueStatType = key; + saveSettings(); + } + defaultValue: widgetMetadata.statType + } + + NComboBox { + Layout.fillWidth: true + visible: valueStatType === "Disk" + label: I18n.tr("bar.system-monitor.disk-path-label") + description: I18n.tr("bar.system-monitor.disk-path-description") + model: { + const paths = Object.keys(SystemStatService.diskPercents).sort(); + return paths.map(path => ({ + key: path, + name: path + })); + } + currentKey: valueDiskPath + onSelected: key => { + valueDiskPath = key; + saveSettings(); + } + defaultValue: widgetMetadata.diskPath + } + + NDivider { + Layout.fillWidth: true + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.desktop-widgets.system-stat-show-background-label") + description: I18n.tr("panels.desktop-widgets.system-stat-show-background-description") + checked: valueShowBackground + onToggled: checked => { + valueShowBackground = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showBackground + } + + NToggle { + Layout.fillWidth: true + visible: valueShowBackground + label: I18n.tr("panels.desktop-widgets.system-stat-rounded-corners-label") + description: I18n.tr("panels.desktop-widgets.system-stat-rounded-corners-description") + checked: valueRoundedCorners + onToggled: checked => { + valueRoundedCorners = checked; + saveSettings(); + } + defaultValue: widgetMetadata.roundedCorners + } + + NDivider { + Layout.fillWidth: true + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.desktop-widgets.system-stat-layout-label") + description: I18n.tr("panels.desktop-widgets.system-stat-layout-description") + currentKey: valueLayout + minimumWidth: 260 * Style.uiScaleRatio + model: [ + { + "key": "side", + "name": I18n.tr("panels.desktop-widgets.system-stat-layout-side") + }, + { + "key": "bottom", + "name": I18n.tr("panels.desktop-widgets.system-stat-layout-bottom") + } + ] + onSelected: key => { + valueLayout = key; + saveSettings(); + } + defaultValue: widgetMetadata.layout + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/WeatherSettings.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/WeatherSettings.qml new file mode 100644 index 0000000..4406921 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/DesktopWidgets/WidgetSettings/WeatherSettings.qml @@ -0,0 +1,50 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + property var widgetData: null + property var widgetMetadata: null + + signal settingsChanged(var settings) + + property bool valueShowBackground: widgetData.showBackground !== undefined ? widgetData.showBackground : widgetMetadata.showBackground + property bool valueRoundedCorners: widgetData.roundedCorners !== undefined ? widgetData.roundedCorners : widgetMetadata.roundedCorners + + function saveSettings() { + var settings = Object.assign({}, widgetData || {}); + settings.showBackground = valueShowBackground; + settings.roundedCorners = valueRoundedCorners; + settingsChanged(settings); + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.desktop-widgets.clock-show-background-label") + description: I18n.tr("panels.desktop-widgets.weather-show-background-description") + checked: valueShowBackground + onToggled: checked => { + valueShowBackground = checked; + saveSettings(); + } + defaultValue: widgetMetadata.showBackground + } + + NToggle { + Layout.fillWidth: true + visible: valueShowBackground + label: I18n.tr("panels.desktop-widgets.clock-rounded-corners-label") + description: I18n.tr("panels.desktop-widgets.clock-rounded-corners-description") + checked: valueRoundedCorners + onToggled: checked => { + valueRoundedCorners = checked; + saveSettings(); + } + defaultValue: widgetMetadata.roundedCorners + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/SettingsContent.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/SettingsContent.qml new file mode 100644 index 0000000..61e023a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/SettingsContent.qml @@ -0,0 +1,1331 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.Panels.Settings.Tabs +import qs.Modules.Panels.Settings.Tabs.About +import qs.Modules.Panels.Settings.Tabs.Audio +import qs.Modules.Panels.Settings.Tabs.Bar +import qs.Modules.Panels.Settings.Tabs.ColorScheme +import qs.Modules.Panels.Settings.Tabs.Connections +import qs.Modules.Panels.Settings.Tabs.ControlCenter +import qs.Modules.Panels.Settings.Tabs.Display +import qs.Modules.Panels.Settings.Tabs.Dock +import qs.Modules.Panels.Settings.Tabs.Hooks +import qs.Modules.Panels.Settings.Tabs.Idle +import qs.Modules.Panels.Settings.Tabs.Launcher +import qs.Modules.Panels.Settings.Tabs.LockScreen +import qs.Modules.Panels.Settings.Tabs.Notifications +import qs.Modules.Panels.Settings.Tabs.Osd +import qs.Modules.Panels.Settings.Tabs.Plugins +import qs.Modules.Panels.Settings.Tabs.Region +import qs.Modules.Panels.Settings.Tabs.SessionMenu +import qs.Modules.Panels.Settings.Tabs.SystemMonitor +import qs.Modules.Panels.Settings.Tabs.UserInterface +import qs.Modules.Panels.Settings.Tabs.Wallpaper +import qs.Services.Compositor +import qs.Services.Power +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + Component.onDestruction: SystemStatService.unregisterComponent("settings") + + // Screen reference for child components + property var screen + + // Input: which tab to show initially + property int requestedTab: 0 + + // Exposed state for parent to access + property int currentTabIndex: 0 + property var tabsModel: [] + property var activeScrollView: null + property var activeTabContent: null + property bool sidebarExpanded: true + // Track if sidebar was collapsed before searching started + property bool wasCollapsedBeforeSearch: false + + // Search state + property string searchText: "" + property var searchResults: [] + property int searchSelectedIndex: 0 + property string highlightLabelKey: "" + property bool navigatingFromSearch: false + + // Mouse hover suppression during keyboard navigation + property bool ignoreMouseHover: false + property real _lastMouseX: 0 + property real _lastMouseY: 0 + property bool _mouseInitialized: false + + readonly property bool sidebarCardStyle: Settings.data.ui.settingsPanelSideBarCardStyle + + onSearchResultsChanged: { + searchSelectedIndex = 0; + ignoreMouseHover = true; + _mouseInitialized = false; + } + + // Signal when close button is clicked + signal closeRequested + + // Search function + onSearchTextChanged: { + if (searchText.trim() === "") { + searchResults = []; + if (wasCollapsedBeforeSearch) { + root.sidebarExpanded = false; + wasCollapsedBeforeSearch = false; + } + return; + } + + // Auto-expand sidebar when searching + if (!root.sidebarExpanded) { + if (root.activeFocus) { + // If we are typing and the sidebar is collapsed and focused, we assume the user is typing to search + wasCollapsedBeforeSearch = true; + } + root.sidebarExpanded = true; + } + + if (SettingsSearchService.searchIndex.length === 0) + return; + + // Build searchable items with resolved translations, filtering out invisible entries + let items = []; + for (let j = 0; j < SettingsSearchService.searchIndex.length; j++) { + const entry = SettingsSearchService.searchIndex[j]; + if (!SettingsSearchService.isEntryVisible(entry)) + continue; + items.push({ + "labelKey": entry.labelKey, + "descriptionKey": entry.descriptionKey, + "widget": entry.widget, + "tab": entry.tab, + "tabLabel": entry.tabLabel, + "subTab": entry.subTab, + "subTabLabel": entry.subTabLabel || null, + "label": I18n.tr(entry.labelKey), + "description": entry.descriptionKey ? I18n.tr(entry.descriptionKey) : "", + "subTabName": entry.subTabLabel ? I18n.tr(entry.subTabLabel) : "" + }); + } + + const results = FuzzySort.go(searchText.trim(), items, { + "keys": ["label", "subTabName", "description"], + "limit": 20, + "scoreFn": function (r) { + // r[0]=label, r[1]=subTabName, r[2]=description + // Boost subTabName matches by 1.5x + const labelScore = r[0].score; + const subTabScore = r[1].score * 1.5; + const descScore = r[2].score; + return Math.max(labelScore, subTabScore, descScore); + } + }); + + let extracted = []; + for (let i = 0; i < results.length; i++) { + extracted.push(results[i].obj); + } + searchResults = extracted; + } + + // Navigate to a search result + property int _pendingSubTab: -1 + + function navigateToResult(entry) { + if (entry.tab < 0 || entry.tab >= tabsModel.length) + return; + + highlightLabelKey = entry.labelKey; + _pendingSubTab = (entry.subTab !== null && entry.subTab !== undefined) ? entry.subTab : -1; + + const alreadyOnTab = (currentTabIndex === entry.tab); + navigatingFromSearch = true; + currentTabIndex = entry.tab; + navigatingFromSearch = false; + + if (alreadyOnTab && activeTabContent) { + if (_pendingSubTab >= 0) { + navigatingFromSearch = true; + setSubTabIndex(_pendingSubTab); + navigatingFromSearch = false; + _pendingSubTab = -1; + } + highlightScrollTimer.targetKey = highlightLabelKey; + highlightScrollTimer.restart(); + } + + // Clear highlight after a delay + highlightClearTimer.restart(); + } + + // Navigate to a tab and optionally a subtab (simpler than navigateToResult, no highlighting) + function navigateToTab(tabId, subTabIndex) { + // Find the tab index by tab ID + let tabIndex = -1; + for (let i = 0; i < tabsModel.length; i++) { + if (tabsModel[i].id === tabId) { + tabIndex = i; + break; + } + } + + if (tabIndex < 0) + return; + + const hasSubTab = subTabIndex !== null && subTabIndex !== undefined && subTabIndex >= 0; + _pendingSubTab = hasSubTab ? subTabIndex : -1; + + // Check if we're already on this tab + const alreadyOnTab = (currentTabIndex === tabIndex); + + currentTabIndex = tabIndex; + + if (alreadyOnTab && activeTabContent && hasSubTab) { + // Tab is already loaded, apply subtab directly + setSubTabIndex(subTabIndex); + _pendingSubTab = -1; + } + } + + function searchSelectNext() { + if (searchResults.length === 0) + return; + ignoreMouseHover = true; + _mouseInitialized = false; + searchSelectedIndex = Math.min(searchSelectedIndex + 1, searchResults.length - 1); + searchResultsList.positionViewAtIndex(searchSelectedIndex, ListView.Contain); + } + + function searchSelectPrevious() { + if (searchResults.length === 0) + return; + ignoreMouseHover = true; + _mouseInitialized = false; + searchSelectedIndex = Math.max(searchSelectedIndex - 1, 0); + searchResultsList.positionViewAtIndex(searchSelectedIndex, ListView.Contain); + } + + function searchActivate() { + if (searchSelectedIndex >= 0 && searchSelectedIndex < searchResults.length) { + navigateToResult(searchResults[searchSelectedIndex]); + searchInput.text = ""; + } + } + + // Set sub-tab on the currently loaded tab content. Returns true if an NTabBar was found. + function setSubTabIndex(subTabIndex) { + if (activeTabContent) { + return setSubTabRecursive(activeTabContent, subTabIndex); + } + return false; + } + + function setSubTabRecursive(item, subTabIndex) { + if (!item) + return false; + + if (item.objectName === "NTabBar") { + // Prepare the sibling NTabView so the index change doesn't animate + if (item.parent) { + for (let j = 0; j < item.parent.children.length; j++) { + const sibling = item.parent.children[j]; + if (sibling.objectName === "NTabView" && sibling.setIndexWithoutAnimation) { + sibling.setIndexWithoutAnimation(subTabIndex); + break; + } + } + } + item.currentIndex = subTabIndex; + return true; + } + + const childCount = item.children ? item.children.length : 0; + for (let i = 0; i < childCount; i++) { + if (setSubTabRecursive(item.children[i], subTabIndex)) + return true; + } + return false; + } + + onCurrentTabIndexChanged: { + if (!navigatingFromSearch) { + clearHighlightImmediately(); + } + } + + property var currentSubTabBar: null + + onActiveTabContentChanged: { + if (currentSubTabBar) { + try { + currentSubTabBar.currentIndexChanged.disconnect(onSubTabChanged); + } catch (e) {} + currentSubTabBar = null; + } + + if (activeTabContent) { + const tabBar = findNTabBar(activeTabContent); + if (tabBar) { + currentSubTabBar = tabBar; + currentSubTabBar.currentIndexChanged.connect(onSubTabChanged); + } + } + } + + function onSubTabChanged() { + if (!navigatingFromSearch) { + clearHighlightImmediately(); + } + } + + function findNTabBar(item) { + if (!item) + return null; + + if (item.objectName === "NTabBar") { + return item; + } + + const childCount = item.children ? item.children.length : 0; + for (let i = 0; i < childCount; i++) { + const found = findNTabBar(item.children[i]); + if (found) + return found; + } + return null; + } + + function clearHighlightImmediately() { + highlightClearTimer.stop(); + highlightScrollTimer.stop(); + highlightAnimation.stop(); + highlightLabelKey = ""; + highlightOverlay.opacity = 0; + } + + function isEffectivelyVisible(item) { + var current = item; + while (current) { + if (current.visible === false) + return false; + if (current.opacity !== undefined && current.opacity <= 0) + return false; + current = current.parent; + } + return true; + } + + // Find and highlight a widget by its label key. + function findAndHighlightWidget(item, labelKey) { + if (!item) + return null; + + // Skip hidden branches to avoid highlighting controls that are not on screen. + if (!isEffectivelyVisible(item)) + return null; + + // Check if this item has a matching label. + if (item.hasOwnProperty("label") && item.label === I18n.tr(labelKey) && item.width > 0 && item.height > 0) { + return item; + } + + // Recursively search children + if (item.children) { + for (let i = 0; i < item.children.length; i++) { + const found = findAndHighlightWidget(item.children[i], labelKey); + if (found) + return found; + } + } + return null; + } + + Timer { + id: highlightClearTimer + interval: 3000 + onTriggered: root.highlightLabelKey = "" + } + + Timer { + id: highlightScrollTimer + interval: 333 + property string targetKey: "" + onTriggered: { + if (root.activeTabContent && targetKey) { + const widget = root.findAndHighlightWidget(root.activeTabContent, targetKey); + if (widget && root.activeScrollView) { + // Scroll widget into view using the Flickable directly + const flickable = root.activeScrollView.contentItem; + const mapped = widget.mapToItem(flickable.contentItem, 0, 0); + const targetY = mapped.y - flickable.height / 3; + flickable.contentY = Math.max(0, Math.min(targetY, flickable.contentHeight - flickable.height)); + + // Position highlight overlay after scroll layout has settled + Qt.callLater(function () { + const overlayPos = widget.mapToItem(tabContentArea, 0, 0); + highlightOverlay.x = overlayPos.x - Style.marginM; + highlightOverlay.y = overlayPos.y - Style.marginM; + highlightOverlay.width = widget.width + Style.margin2M; + highlightOverlay.height = widget.height + Style.margin2M; + highlightAnimation.restart(); + }); + } + } + targetKey = ""; + } + } + + // Clear highlight when the user scrolls so the outline doesn't stay in place + Connections { + target: root.activeScrollView ? root.activeScrollView.contentItem : null + enabled: root.highlightLabelKey !== "" && !highlightScrollTimer.running + function onContentYChanged() { + root.clearHighlightImmediately(); + } + } + + // Save sidebar state when it changes + onSidebarExpandedChanged: { + ShellState.setSettingsSidebarExpanded(sidebarExpanded); + if (!sidebarExpanded) { + root.searchText = ""; + searchInput.text = ""; + root.forceActiveFocus(); + } + } + + Component.onCompleted: { + SystemStatService.registerComponent("settings"); + // Restore sidebar state + sidebarExpanded = ShellState.getSettingsSidebarExpanded(); + } + + // Tab components + Component { + id: generalTab + GeneralTab {} + } + Component { + id: launcherTab + LauncherTab {} + } + Component { + id: barTab + BarTab {} + } + Component { + id: audioTab + AudioTab {} + } + Component { + id: displayTab + DisplayTab {} + } + Component { + id: osdTab + OsdTab {} + } + Component { + id: connectionsTab + ConnectionsTab {} + } + Component { + id: regionTab + RegionTab {} + } + Component { + id: colorSchemeTab + ColorSchemeTab {} + } + Component { + id: wallpaperTab + WallpaperTab {} + } + Component { + id: aboutTab + AboutTab {} + } + Component { + id: hooksTab + HooksTab {} + } + Component { + id: idleTab + IdleTab {} + } + Component { + id: dockTab + DockTab {} + } + Component { + id: notificationsTab + NotificationsTab {} + } + Component { + id: controlCenterTab + ControlCenterTab {} + } + Component { + id: userInterfaceTab + UserInterfaceTab {} + } + Component { + id: lockScreenTab + LockScreenTab {} + } + Component { + id: sessionMenuTab + SessionMenuTab {} + } + Component { + id: systemMonitorTab + SystemMonitorTab {} + } + Component { + id: pluginsTab + PluginsTab {} + } + Component { + id: desktopWidgetsTab + DesktopWidgetsTab {} + } + + function updateTabsModel() { + let newTabs = [ + { + "id": SettingsPanel.Tab.General, + "label": "common.general", + "icon": "settings-general", + "source": generalTab + }, + { + "id": SettingsPanel.Tab.UserInterface, + "label": "panels.user-interface.title", + "icon": "settings-user-interface", + "source": userInterfaceTab + }, + { + "id": SettingsPanel.Tab.ColorScheme, + "label": "panels.color-scheme.title", + "icon": "settings-color-scheme", + "source": colorSchemeTab + }, + { + "id": SettingsPanel.Tab.Wallpaper, + "label": "common.wallpaper", + "icon": "settings-wallpaper", + "source": wallpaperTab + }, + { + "id": SettingsPanel.Tab.Bar, + "label": "panels.bar.title", + "icon": "settings-bar", + "source": barTab + }, + { + "id": SettingsPanel.Tab.Dock, + "label": "panels.dock.title", + "icon": "settings-dock", + "source": dockTab + }, + { + "id": SettingsPanel.Tab.DesktopWidgets, + "label": "panels.desktop-widgets.title", + "icon": "clock", + "source": desktopWidgetsTab + }, + { + "id": SettingsPanel.Tab.ControlCenter, + "label": "panels.control-center.title", + "icon": "settings-control-center", + "source": controlCenterTab + }, + { + "id": SettingsPanel.Tab.Launcher, + "label": "panels.launcher.title", + "icon": "settings-launcher", + "source": launcherTab + }, + { + "id": SettingsPanel.Tab.Notifications, + "label": "common.notifications", + "icon": "settings-notifications", + "source": notificationsTab + }, + { + "id": SettingsPanel.Tab.OSD, + "label": "panels.osd.title", + "icon": "settings-osd", + "source": osdTab + }, + { + "id": SettingsPanel.Tab.LockScreen, + "label": "panels.lock-screen.title", + "icon": "settings-lock-screen", + "source": lockScreenTab + }, + { + "id": SettingsPanel.Tab.SessionMenu, + "label": "session-menu.title", + "icon": "settings-session-menu", + "source": sessionMenuTab + }, + { + "id": SettingsPanel.Tab.Idle, + "label": "panels.idle.title", + "icon": "settings-idle", + "source": idleTab + }, + { + "id": SettingsPanel.Tab.Audio, + "label": "panels.audio.title", + "icon": "settings-audio", + "source": audioTab + }, + { + "id": SettingsPanel.Tab.Display, + "label": "panels.display.title", + "icon": "settings-display", + "source": displayTab + }, + { + "id": SettingsPanel.Tab.Connections, + "label": "panels.connections.title", + "icon": "settings-network", + "source": connectionsTab + }, + { + "id": SettingsPanel.Tab.Location, + "label": "panels.region.title", + "icon": "settings-location", + "source": regionTab + }, + { + "id": SettingsPanel.Tab.System, + "label": "panels.system.title", + "icon": "settings-system-monitor", + "source": systemMonitorTab + }, + { + "id": SettingsPanel.Tab.Plugins, + "label": "panels.plugins.title", + "icon": "plugin", + "source": pluginsTab + }, + { + "id": SettingsPanel.Tab.Hooks, + "label": "panels.hooks.title", + "icon": "settings-hooks", + "source": hooksTab + }, + { + "id": SettingsPanel.Tab.About, + "label": "panels.about.title", + "icon": "settings-about", + "source": aboutTab + } + ]; + + root.tabsModel = newTabs; + } + + function selectTabById(tabId) { + for (var i = 0; i < tabsModel.length; i++) { + if (tabsModel[i].id === tabId) { + currentTabIndex = i; + return; + } + } + currentTabIndex = 0; + } + + function initialize() { + ProgramCheckerService.checkAllPrograms(); + // Guard _pendingSubTab during model rebuild: updateTabsModel() triggers + // a ListView model reset which can set currentTabIndex=0 via the sidebar + // sync handler, causing the wrong tab to load and consume _pendingSubTab. + const savedPendingSubTab = _pendingSubTab; + _pendingSubTab = -1; + updateTabsModel(); + _pendingSubTab = savedPendingSubTab; + selectTabById(requestedTab); + // Skip auto-focus on Nvidia GPUs - cursor blink causes UI choppiness + const isNvidia = SystemStatService.gpuType === "nvidia"; + if (sidebarExpanded && !isNvidia) { + Qt.callLater(() => { + if (searchInput.inputItem) + searchInput.inputItem.forceActiveFocus(); + }); + } else { + // Ensure root has focus so it can catch typing + Qt.callLater(() => root.forceActiveFocus()); + } + } + + // Handle typing when sidebar is collapsed + focus: true + Keys.onPressed: event => { + if (!sidebarExpanded && event.text.length > 0 && event.text.trim() !== "") { + // Only capture if it looks like visible text + if (event.modifiers & (Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier)) + return; + + // Explicitly ignore backspace and similar keys that might have text but shouldn't trigger search + if (event.key === Qt.Key_Backspace || event.key === Qt.Key_Delete || event.key === Qt.Key_Escape) + return; + + wasCollapsedBeforeSearch = true; + sidebarExpanded = true; + searchInput.text = event.text; + Qt.callLater(() => { + if (searchInput.inputItem) { + searchInput.inputItem.forceActiveFocus(); + // Cursor moves to end automatically usually, but let's be safe + searchInput.inputItem.cursorPosition = 1; + } + }); + event.accepted = true; + } + } + + // Scroll functions + function scrollDown() { + if (activeScrollView && activeScrollView.ScrollBar.vertical) { + const scrollBar = activeScrollView.ScrollBar.vertical; + const stepSize = activeScrollView.height * 0.1; + scrollBar.position = Math.min(scrollBar.position + stepSize / activeScrollView.contentHeight, 1.0 - scrollBar.size); + } + } + + function scrollUp() { + if (activeScrollView && activeScrollView.ScrollBar.vertical) { + const scrollBar = activeScrollView.ScrollBar.vertical; + const stepSize = activeScrollView.height * 0.1; + scrollBar.position = Math.max(scrollBar.position - stepSize / activeScrollView.contentHeight, 0); + } + } + + function scrollPageDown() { + if (activeScrollView && activeScrollView.ScrollBar.vertical) { + const scrollBar = activeScrollView.ScrollBar.vertical; + const pageSize = activeScrollView.height * 0.9; + scrollBar.position = Math.min(scrollBar.position + pageSize / activeScrollView.contentHeight, 1.0 - scrollBar.size); + } + } + + function scrollPageUp() { + if (activeScrollView && activeScrollView.ScrollBar.vertical) { + const scrollBar = activeScrollView.ScrollBar.vertical; + const pageSize = activeScrollView.height * 0.9; + scrollBar.position = Math.max(scrollBar.position - pageSize / activeScrollView.contentHeight, 0); + } + } + + // Tab navigation functions + function selectNextTab() { + if (tabsModel.length > 0) { + currentTabIndex = (currentTabIndex + 1) % tabsModel.length; + } + } + + function selectPreviousTab() { + if (tabsModel.length > 0) { + currentTabIndex = (currentTabIndex - 1 + tabsModel.length) % tabsModel.length; + } + } + + // Main UI + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL + spacing: 0 + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: Style.marginL + + // Sidebar + NBox { + id: sidebar + + clip: true + Layout.preferredWidth: Math.round(root.sidebarExpanded ? 200 * Style.uiScaleRatio : sidebarToggle.width + (root.sidebarCardStyle ? Style.margin2M : 0) + (sidebarList.verticalScrollBarActive ? Style.marginM : 0)) + Layout.fillHeight: true + Layout.alignment: Qt.AlignTop + + radius: root.sidebarCardStyle ? Style.radiusM : 0 + color: root.sidebarCardStyle ? Color.mSurfaceVariant : "transparent" + border.color: root.sidebarCardStyle ? Style.boxBorderColor : "transparent" + + Behavior on Layout.preferredWidth { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + + // Sidebar content + ColumnLayout { + anchors.fill: parent + spacing: Style.marginS + anchors.margins: root.sidebarCardStyle ? Style.marginM : 0 + + // Sidebar toggle button + Item { + id: toggleContainer + Layout.fillWidth: true + Layout.preferredHeight: Math.round(toggleRow.implicitHeight + Style.margin2S) + + Rectangle { + id: sidebarToggle + width: Math.round(toggleRow.implicitWidth + Style.margin2S) + height: parent.height + anchors.left: parent.left + radius: Style.radiusS + color: toggleMouseArea.containsMouse ? Color.mHover : "transparent" + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + + RowLayout { + id: toggleRow + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: Style.marginS + spacing: 0 + + NIcon { + icon: root.sidebarExpanded ? "layout-sidebar-right-expand" : "layout-sidebar-left-expand" + color: toggleMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface + pointSize: Style.fontSizeXL + } + } + + MouseArea { + id: toggleMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: { + TooltipService.show(sidebarToggle, root.sidebarExpanded ? I18n.tr("tooltips.collapse") : I18n.tr("tooltips.expand")); + } + onExited: { + TooltipService.hide(); + } + onClicked: { + TooltipService.hide(); + root.sidebarExpanded = !root.sidebarExpanded; + } + } + } + } + + // Search container wrapper to prevent layout jumps + Item { + id: searchContainerWrapper + Layout.fillWidth: true + Layout.preferredHeight: searchInput.implicitHeight > 0 ? searchInput.implicitHeight : (Style.fontSizeXL + Style.margin2M) + + // Search input + NTextInput { + id: searchInput + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + placeholderText: I18n.tr("common.search") + inputIconName: "search" + visible: opacity > 0 + opacity: root.sidebarExpanded ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + + onTextChanged: root.searchText = text + onEditingFinished: { + if (root.searchText.trim() !== "") + root.searchActivate(); + } + } + + // Search button for collapsed sidebar + Item { + id: searchCollapsedContainer + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + height: Math.round(searchCollapsedRow.implicitHeight + Style.margin2S) + visible: opacity > 0 + opacity: !root.sidebarExpanded ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + + Rectangle { + id: searchCollapsedButton + width: Math.round(searchCollapsedRow.implicitWidth + Style.margin2S) + height: parent.height + anchors.left: parent.left + radius: Style.radiusS + color: searchCollapsedMouseArea.containsMouse ? Color.mHover : "transparent" + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + + RowLayout { + id: searchCollapsedRow + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: Style.marginS + spacing: 0 + + NIcon { + icon: "search" + color: searchCollapsedMouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface + pointSize: Style.fontSizeXL + } + } + + MouseArea { + id: searchCollapsedMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + root.sidebarExpanded = true; + root.wasCollapsedBeforeSearch = false; // Expanding manually resets this + Qt.callLater(() => searchInput.inputItem.forceActiveFocus()); + } + onEntered: { + TooltipService.show(searchCollapsedButton, I18n.tr("common.search")); + } + onExited: { + TooltipService.hide(); + } + } + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.bottomMargin: Style.marginXL + + // Search results list + NListView { + id: searchResultsList + anchors.fill: parent + model: root.searchResults + spacing: Style.marginXS + visible: root.searchText.trim() !== "" + verticalPolicy: ScrollBar.AsNeeded + gradientColor: "transparent" + reserveScrollbarSpace: false + + HoverHandler { + onPointChanged: { + if (!root._mouseInitialized) { + root._lastMouseX = point.position.x; + root._lastMouseY = point.position.y; + root._mouseInitialized = true; + return; + } + + const deltaX = Math.abs(point.position.x - root._lastMouseX); + const deltaY = Math.abs(point.position.y - root._lastMouseY); + if (deltaX + deltaY >= 5) { + root.ignoreMouseHover = false; + root._lastMouseX = point.position.x; + root._lastMouseY = point.position.y; + } + } + } + + delegate: Rectangle { + id: resultItem + width: searchResultsList.width - (searchResultsList.verticalScrollBarActive ? Style.marginM : 0) + height: resultColumn.implicitHeight + Style.margin2M + radius: Style.iRadiusS + readonly property bool selected: index === root.searchSelectedIndex + readonly property bool effectiveHover: !root.ignoreMouseHover && resultMouseArea.containsMouse + color: (effectiveHover || selected) ? Color.mHover : "transparent" + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + + ColumnLayout { + id: resultColumn + anchors.fill: parent + anchors.leftMargin: Style.marginL + anchors.rightMargin: Style.marginL + anchors.topMargin: Style.marginM + anchors.bottomMargin: Style.marginM + spacing: 0 + + NText { + text: I18n.tr(modelData.labelKey) + pointSize: Style.fontSizeM + font.weight: Style.fontWeightSemiBold + color: (resultItem.effectiveHover || resultItem.selected) ? Color.mOnHover : Color.mOnSurface + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 + } + + NText { + text: { + let t = I18n.tr(modelData.tabLabel); + if (modelData.subTabLabel) + t += " › " + I18n.tr(modelData.subTabLabel); + return t; + } + pointSize: Style.fontSizeXS + color: (resultItem.effectiveHover || resultItem.selected) ? Color.mOnHover : Color.mOnSurfaceVariant + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 + } + } + + MouseArea { + id: resultMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: { + if (!root.ignoreMouseHover) + root.searchSelectedIndex = index; + } + onClicked: { + root.searchSelectedIndex = index; + root.navigateToResult(modelData); + searchInput.text = ""; + } + } + } + } + + // Tab list + NListView { + id: sidebarList + visible: root.searchText.trim() === "" + anchors.fill: parent + model: root.tabsModel + spacing: Style.marginXS + currentIndex: root.currentTabIndex + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AlwaysOff + gradientColor: "transparent" + reserveScrollbarSpace: false + + delegate: Rectangle { + id: tabItem + width: sidebarList.width + height: tabEntryRow.implicitHeight + Style.margin2XS + radius: Style.iRadiusS + color: selected ? Color.mPrimary : (tabItem.hovering ? Color.mHover : "transparent") + readonly property bool selected: index === root.currentTabIndex + property bool hovering: false + property color tabTextColor: selected ? Color.mOnPrimary : (tabItem.hovering ? Color.mOnHover : Color.mOnSurface) + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + + Behavior on tabTextColor { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + + RowLayout { + id: tabEntryRow + anchors.fill: parent + anchors.leftMargin: Style.marginS + anchors.rightMargin: Style.marginS + spacing: Style.marginM + + NIcon { + icon: modelData.icon + color: tabTextColor + pointSize: Style.fontSizeXL + Layout.alignment: Qt.AlignVCenter + } + + NText { + text: I18n.tr(modelData.label) + color: tabTextColor + pointSize: Style.fontSizeM + font.weight: Style.fontWeightSemiBold + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + visible: root.sidebarExpanded + opacity: root.sidebarExpanded ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton + cursorShape: Qt.PointingHandCursor + onEntered: { + tabItem.hovering = true; + // Show tooltip when sidebar is collapsed + if (!root.sidebarExpanded) { + TooltipService.show(tabItem, I18n.tr(modelData.label)); + } + } + onExited: { + tabItem.hovering = false; + // Hide tooltip when sidebar is collapsed + if (!root.sidebarExpanded) { + TooltipService.hide(); + } + } + onCanceled: { + tabItem.hovering = false; + if (!root.sidebarExpanded) { + TooltipService.hide(); + } + } + onClicked: { + root.currentTabIndex = index; + // Hide tooltip on click + if (!root.sidebarExpanded) { + TooltipService.hide(); + } + } + } + } + + onCurrentIndexChanged: { + if (currentIndex !== root.currentTabIndex) { + root.currentTabIndex = currentIndex; + } + } + + Connections { + target: root + function onCurrentTabIndexChanged() { + if (sidebarList.currentIndex !== root.currentTabIndex) { + sidebarList.currentIndex = root.currentTabIndex; + sidebarList.positionViewAtIndex(root.currentTabIndex, ListView.Contain); + } + } + } + } + } + } + } + + // Content pane + NBox { + id: contentPane + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignTop + radius: Style.radiusM + color: Color.mSurfaceVariant + + ColumnLayout { + id: contentLayout + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.margins: Style.marginL + width: Math.min(parent.width - Style.marginL * 2, 780 * Style.uiScaleRatio) + spacing: Style.marginS + + // Header row + RowLayout { + id: headerRow + Layout.fillWidth: true + spacing: Style.marginS + + NIcon { + icon: root.tabsModel[currentTabIndex]?.icon ?? "" + color: Color.mPrimary + pointSize: Style.fontSizeXXL + } + + NText { + text: root.tabsModel[root.currentTabIndex]?.label ? I18n.tr(root.tabsModel[root.currentTabIndex].label) : "" + pointSize: Style.fontSizeXL + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + Layout.alignment: Qt.AlignVCenter + onClicked: root.closeRequested() + } + } + + // Tab content area + Rectangle { + id: tabContentArea + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: -Style.marginM + Layout.rightMargin: -Style.marginL + color: "transparent" + + Repeater { + id: contentRepeater + model: root.tabsModel + delegate: Loader { + anchors.fill: parent + active: index === root.currentTabIndex + opacity: 0 + + NumberAnimation on opacity { + id: fadeInAnim + from: 0 + to: 1 + duration: Style.animationSlowest + easing.type: Easing.OutCubic + running: false + } + + onStatusChanged: { + if (status === Loader.Ready && item) { + fadeInAnim.start(); + const scrollView = item.children[0]; + if (scrollView && scrollView.toString().includes("ScrollView")) { + root.activeScrollView = scrollView; + } + } + } + + sourceComponent: NScrollView { + id: scrollView + anchors.fill: parent + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + leftPadding: Style.marginL + topPadding: Style.marginL + bottomPadding: Style.marginL + userRightPadding: Style.marginL + reserveScrollbarSpace: false + + Component.onCompleted: { + root.activeScrollView = scrollView; + } + + Loader { + active: true + sourceComponent: root.tabsModel[index]?.source + width: scrollView.availableWidth + onLoaded: { + if (item && item.hasOwnProperty("screen")) { + item.screen = root.screen; + } + root.activeTabContent = item; + if (root._pendingSubTab >= 0) { + root.navigatingFromSearch = true; + if (root.setSubTabIndex(root._pendingSubTab)) + root._pendingSubTab = -1; + root.navigatingFromSearch = false; + } + if (root.highlightLabelKey) { + highlightScrollTimer.targetKey = root.highlightLabelKey; + highlightScrollTimer.restart(); + } + } + } + } + } + } + + // Highlight overlay for search results + Rectangle { + id: highlightOverlay + visible: opacity > 0 + opacity: 0 + color: Qt.alpha(Color.mSecondary, 0.2) + border.color: Qt.alpha(Color.mSecondary, 0.6) + border.width: Style.borderM + radius: Style.radiusS + z: 100 + + SequentialAnimation { + id: highlightAnimation + + NumberAnimation { + target: highlightOverlay + property: "opacity" + to: 1.0 + duration: Style.animationSlow + easing.type: Easing.OutQuad + } + + PauseAnimation { + duration: 2000 + } + + NumberAnimation { + target: highlightOverlay + property: "opacity" + to: 0 + duration: Style.animationSlowest + easing.type: Easing.InQuad + } + } + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/SettingsPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/SettingsPanel.qml new file mode 100644 index 0000000..f4de52e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/SettingsPanel.qml @@ -0,0 +1,290 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.MainScreen +import qs.Services.UI +import qs.Widgets + +SmartPanel { + id: root + + preferredWidth: Math.round(840 * Style.uiScaleRatio) + preferredHeight: Math.round(910 * Style.uiScaleRatio) + + // Settings panel mode: "centered", "attached", "window" + readonly property string settingsPanelMode: Settings.data.ui.settingsPanelMode + readonly property bool isWindowMode: settingsPanelMode === "window" + readonly property bool attachToBar: settingsPanelMode === "attached" + + readonly property string barDensity: Settings.data.bar.density + readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name) + readonly property bool barFloating: Settings.data.bar.barType === "floating" + readonly property real barMarginH: barFloating ? Math.ceil(Settings.data.bar.marginHorizontal) : 0 + readonly property real barMarginV: barFloating ? Math.ceil(Settings.data.bar.marginVertical) : 0 + + forceAttachToBar: attachToBar + panelAnchorHorizontalCenter: !root.useButtonPosition && (attachToBar ? (barPosition === "top" || barPosition === "bottom") : true) + panelAnchorVerticalCenter: !root.useButtonPosition && (attachToBar ? (barPosition === "left" || barPosition === "right") : true) + panelAnchorTop: !root.useButtonPosition && attachToBar && barPosition === "top" + panelAnchorBottom: !root.useButtonPosition && attachToBar && barPosition === "bottom" + panelAnchorLeft: !root.useButtonPosition && attachToBar && barPosition === "left" + panelAnchorRight: !root.useButtonPosition && attachToBar && barPosition === "right" + + onAttachToBarChanged: { + if (isPanelOpen) { + Qt.callLater(root.setPosition); + } + } + + onBarPositionChanged: { + if (isPanelOpen) { + Qt.callLater(root.setPosition); + } + } + + onBarDensityChanged: { + if (isPanelOpen) { + Qt.callLater(root.setPosition); + } + } + + onBarFloatingChanged: { + if (isPanelOpen) { + Qt.callLater(root.setPosition); + } + } + + onBarMarginHChanged: { + if (isPanelOpen) { + Qt.callLater(root.setPosition); + } + } + + onBarMarginVChanged: { + if (isPanelOpen) { + Qt.callLater(root.setPosition); + } + } + + // Tabs enumeration, order is NOT relevant + enum Tab { + About, + Audio, + Bar, + ColorScheme, + LockScreen, + ControlCenter, + DesktopWidgets, + OSD, + Display, + Dock, + General, + Hooks, + Idle, + Launcher, + Location, + Connections, + Notifications, + Plugins, + SessionMenu, + System, + UserInterface, + Wallpaper + } + + property int requestedTab: SettingsPanel.Tab.General + property int requestedSubTab: -1 + property var requestedEntry: null + + // Content state - these are synced with SettingsContent when panel opens + property int currentTabIndex: 0 + property var tabsModel: [] + property var activeScrollView: null + + // Internal reference to the content (set when panel content loads) + property var _settingsContent: null + + // Override toggle to handle window mode + function toggle(buttonItem, buttonName) { + if (isWindowMode) { + SettingsPanelService.toggleWindow(requestedTab); + return; + } + // Call parent toggle + if (isPanelOpen) { + close(); + } else { + open(buttonItem, buttonName); + } + } + + // Override open to handle window mode + function open(buttonItem, buttonName) { + if (isWindowMode) { + SettingsPanelService.openWindow(requestedTab); + return; + } + + // Panel mode: replicate SmartPanel.open() logic + if (!buttonItem && buttonName) { + if (typeof buttonName === "object" && buttonName.x !== undefined && buttonName.y !== undefined) { + root.buttonItem = null; + root.buttonPosition = buttonName; + root.buttonWidth = 0; + root.buttonHeight = 0; + root.useButtonPosition = true; + } else { + buttonItem = BarService.lookupWidget(buttonName, screen.name); + } + } + + if (buttonItem) { + root.buttonItem = buttonItem; + var buttonPos = buttonItem.mapToItem(null, 0, 0); + root.buttonPosition = Qt.point(buttonPos.x, buttonPos.y); + root.buttonWidth = buttonItem.width; + root.buttonHeight = buttonItem.height; + root.useButtonPosition = true; + } else if (!(buttonName && typeof buttonName === "object" && buttonName.x !== undefined && buttonName.y !== undefined)) { + root.buttonItem = null; + root.useButtonPosition = false; + } + + isPanelOpen = true; + PanelService.willOpenPanel(root); + } + + // Open to a specific tab and optionally a subtab + function openToTab(tab, subTab, buttonItem, buttonName) { + requestedTab = tab !== undefined ? tab : SettingsPanel.Tab.General; + requestedSubTab = subTab !== undefined ? subTab : -1; + open(buttonItem, buttonName); + } + + // When the panel opens, initialize content + onOpened: { + if (_settingsContent) { + if (requestedEntry) { + _settingsContent.requestedTab = requestedEntry.tab; + _settingsContent.initialize(); + const entry = requestedEntry; + requestedEntry = null; + Qt.callLater(() => _settingsContent.navigateToResult(entry)); + } else { + _settingsContent.requestedTab = requestedTab; + if (requestedSubTab >= 0) { + _settingsContent._pendingSubTab = requestedSubTab; + requestedSubTab = -1; + } + _settingsContent.initialize(); + } + } + } + + // Scroll functions - delegate to content + function scrollDown() { + if (_settingsContent) + _settingsContent.scrollDown(); + } + + function scrollUp() { + if (_settingsContent) + _settingsContent.scrollUp(); + } + + function scrollPageDown() { + if (_settingsContent) + _settingsContent.scrollPageDown(); + } + + function scrollPageUp() { + if (_settingsContent) + _settingsContent.scrollPageUp(); + } + + // Navigation functions - delegate to content + function selectNextTab() { + if (_settingsContent) + _settingsContent.selectNextTab(); + } + + function selectPreviousTab() { + if (_settingsContent) + _settingsContent.selectPreviousTab(); + } + + // Override keyboard handlers from SmartPanel + function onTabPressed() { + selectNextTab(); + } + + function onBackTabPressed() { + selectPreviousTab(); + } + + function onUpPressed() { + if (_settingsContent && _settingsContent.searchText.trim() !== "") { + _settingsContent.searchSelectPrevious(); + } else { + scrollUp(); + } + } + + function onDownPressed() { + if (_settingsContent && _settingsContent.searchText.trim() !== "") { + _settingsContent.searchSelectNext(); + } else { + scrollDown(); + } + } + + function onPageUpPressed() { + scrollPageUp(); + } + + function onPageDownPressed() { + scrollPageDown(); + } + + function onCtrlJPressed() { + if (_settingsContent && _settingsContent.searchText.trim() !== "") { + _settingsContent.searchSelectNext(); + } else { + scrollDown(); + } + } + + function onCtrlKPressed() { + if (_settingsContent && _settingsContent.searchText.trim() !== "") { + _settingsContent.searchSelectPrevious(); + } else { + scrollUp(); + } + } + + panelContent: Rectangle { + id: panelContent + color: "transparent" + + SettingsContent { + id: settingsContent + anchors.fill: parent + screen: root.screen + onCloseRequested: root.close() + Component.onCompleted: { + root._settingsContent = settingsContent; + root.tabsModel = Qt.binding(function () { + return settingsContent.tabsModel; + }); + root.currentTabIndex = Qt.binding(function () { + return settingsContent.currentTabIndex; + }); + root.activeScrollView = Qt.binding(function () { + return settingsContent.activeScrollView; + }); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/SettingsPanelWindow.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/SettingsPanelWindow.qml new file mode 100644 index 0000000..58d394b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/SettingsPanelWindow.qml @@ -0,0 +1,130 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services.UI +import qs.Widgets + +FloatingWindow { + id: root + + title: "Noctalia" + minimumSize: Qt.size(840 * Style.uiScaleRatio, 910 * Style.uiScaleRatio) + implicitWidth: Math.round(840 * Style.uiScaleRatio) + implicitHeight: Math.round(910 * Style.uiScaleRatio) + color: "transparent" + + visible: false + + // Register with SettingsPanelService + Component.onCompleted: { + SettingsPanelService.settingsWindow = root; + } + + property bool isInitialized: false + + // Navigate to a specific tab and optional subtab. + // Works whether the window is already visible or just becoming visible. + function navigateTo(tab, subTab) { + const tabId = tab !== undefined ? tab : 0; + const subTabId = (subTab !== undefined && subTab !== null && subTab >= 0) ? subTab : -1; + if (isInitialized) { + settingsContent.navigateToTab(tabId, subTabId); + } else { + settingsContent.requestedTab = tabId; + if (subTabId >= 0) + settingsContent._pendingSubTab = subTabId; + settingsContent.initialize(); + isInitialized = true; + // Tab content persists in window mode; if no subtab specified and the + // tab content is still loaded (same tab), reset to first subtab + if (subTabId < 0 && settingsContent.activeTabContent) + settingsContent.setSubTabIndex(0); + } + } + + // Navigate to a search result entry. + // Works whether the window is already visible or just becoming visible. + function navigateToEntry(entry) { + if (isInitialized) { + Qt.callLater(() => settingsContent.navigateToResult(entry)); + } else { + settingsContent.requestedTab = entry.tab; + settingsContent.initialize(); + Qt.callLater(() => settingsContent.navigateToResult(entry)); + isInitialized = true; + } + } + + // Sync visibility with service + onVisibleChanged: { + if (visible) { + SettingsPanelService.isWindowOpen = true; + } else { + isInitialized = false; + SettingsPanelService.isWindowOpen = false; + } + } + + // Keyboard shortcuts + Shortcut { + sequence: "Escape" + enabled: !PanelService.isKeybindRecording + onActivated: SettingsPanelService.closeWindow() + } + + Shortcut { + sequence: "Tab" + enabled: !PanelService.isKeybindRecording + onActivated: settingsContent.selectNextTab() + } + + Shortcut { + sequence: "Backtab" + enabled: !PanelService.isKeybindRecording + onActivated: settingsContent.selectPreviousTab() + } + + Instantiator { + model: Settings.data.general.keybinds.keyUp || [] + Shortcut { + sequence: modelData + enabled: !PanelService.isKeybindRecording + onActivated: { + if (settingsContent.searchText.trim() !== "") + settingsContent.searchSelectPrevious(); + else + settingsContent.scrollUp(); + } + } + } + + Instantiator { + model: Settings.data.general.keybinds.keyDown || [] + Shortcut { + sequence: modelData + enabled: !PanelService.isKeybindRecording + onActivated: { + if (settingsContent.searchText.trim() !== "") + settingsContent.searchSelectNext(); + else + settingsContent.scrollDown(); + } + } + } + + // Main content + Rectangle { + anchors.fill: parent + color: Qt.alpha(Color.mSurface, Settings.data.ui.panelBackgroundOpacity) + radius: Style.radiusL + + SettingsContent { + id: settingsContent + anchors.fill: parent + onCloseRequested: SettingsPanelService.closeWindow() + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/About/AboutTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/About/AboutTab.qml new file mode 100644 index 0000000..3172c77 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/About/AboutTab.qml @@ -0,0 +1,48 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.info") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.contributors") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + NTabButton { + text: I18n.tr("common.supporters") + tabIndex: 2 + checked: subTabBar.currentIndex === 2 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + VersionSubTab {} + ContributorsSubTab {} + SupportersSubTab {} + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/About/ContributorsSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/About/ContributorsSubTab.qml new file mode 100644 index 0000000..2448483 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/About/ContributorsSubTab.qml @@ -0,0 +1,253 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Noctalia +import qs.Widgets + +ColumnLayout { + id: root + + property var contributors: GitHubService.contributors + property int avatarCacheVersion: 0 + + readonly property int topContributorsCount: 20 + + Connections { + target: GitHubService + function onCachedAvatarsChanged() { + root.avatarCacheVersion++; + } + } + + spacing: Style.marginL + + NHeader { + description: I18n.trp("panels.about.contributors-description", root.contributors.length) + enableDescriptionRichText: true + } + + // Top 20 contributors with full cards (avoids GridView shader crashes on Qt 6.8) + Flow { + id: topContributorsFlow + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + spacing: Style.marginM + + Repeater { + model: Math.min(root.contributors.length, root.topContributorsCount) + + delegate: Rectangle { + width: Math.max(Math.round(topContributorsFlow.width / 2 - Style.marginM - 1), Math.round(Style.baseWidgetSize * 4)) + height: Math.round(Style.baseWidgetSize * 2.3) + radius: Style.radiusM + color: contributorArea.containsMouse ? Color.mHover : "transparent" + border.width: 1 + border.color: contributorArea.containsMouse ? Color.mPrimary : Color.mOutline + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + // Avatar container with rectangular design (modern, no shader issues) + Item { + id: wrapper + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: Style.baseWidgetSize * 1.8 + Layout.preferredHeight: Style.baseWidgetSize * 1.8 + + property bool isRounded: false + + // Background and image container + Item { + anchors.fill: parent + + // Simple circular image (pre-rendered, no shaders) + Image { + anchors.fill: parent + source: { + // Depend on avatarCacheVersion to trigger re-evaluation + var _ = root.avatarCacheVersion; + // Try cached circular version first + var username = root.contributors[index].login; + var cached = GitHubService.getAvatarPath(username); + if (cached) { + wrapper.isRounded = true; + return cached; + } + + // Fall back to original avatar URL + return root.contributors[index].avatar_url || ""; + } + fillMode: Image.PreserveAspectFit // Fit since image is already circular with transparency + mipmap: true + smooth: true + asynchronous: true + visible: root.contributors[index].avatar_url !== undefined && root.contributors[index].avatar_url !== "" + opacity: status === Image.Ready ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + + // Fallback icon + NIcon { + anchors.centerIn: parent + visible: !root.contributors[index].avatar_url || root.contributors[index].avatar_url === "" + icon: "person" + pointSize: Style.fontSizeL + color: Color.mPrimary + } + } + + Rectangle { + visible: wrapper.isRounded + anchors.fill: parent + color: "transparent" + radius: width * 0.5 + border.width: Style.borderM + border.color: Color.mPrimary + } + } + + // Info column + ColumnLayout { + spacing: 2 + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + + NText { + text: root.contributors[index].login || "Unknown" + font.weight: Style.fontWeightBold + color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurface + elide: Text.ElideRight + Layout.fillWidth: true + pointSize: Style.fontSizeS + } + + RowLayout { + spacing: Style.marginXS + Layout.fillWidth: true + + NIcon { + icon: "git-commit" + pointSize: Style.fontSizeXS + color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant + } + + NText { + text: `${(root.contributors[index].contributions || 0).toString()} commits` + pointSize: Style.fontSizeXS + color: contributorArea.containsMouse ? Color.mOnHover : Color.mOnSurfaceVariant + } + } + } + + // Hover indicator + NIcon { + Layout.alignment: Qt.AlignVCenter + icon: "arrow-right" + pointSize: Style.fontSizeS + color: Color.mPrimary + opacity: contributorArea.containsMouse ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + } + + MouseArea { + id: contributorArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (root.contributors[index].html_url) + Quickshell.execDetached(["xdg-open", root.contributors[index].html_url]); + } + } + } + } + } + + // Remaining contributors (simple text links) + Flow { + id: remainingContributorsFlow + visible: root.contributors.length > root.topContributorsCount + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.topMargin: Style.marginL + spacing: Style.marginS + + Repeater { + model: Math.max(0, root.contributors.length - root.topContributorsCount) + + delegate: Rectangle { + width: nameText.implicitWidth + Style.margin2M + height: nameText.implicitHeight + Style.margin2S + radius: Style.radiusS + color: nameArea.containsMouse ? Color.mHover : "transparent" + border.width: Style.borderS + border.color: nameArea.containsMouse ? Color.mPrimary : Color.mOutline + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + NText { + id: nameText + anchors.centerIn: parent + text: root.contributors[index + root.topContributorsCount].login || "Unknown" + pointSize: Style.fontSizeXS + color: nameArea.containsMouse ? Color.mOnHover : Color.mOnSurface + font.weight: Style.fontWeightMedium + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + MouseArea { + id: nameArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + if (root.contributors[index + root.topContributorsCount].html_url) + Quickshell.execDetached(["xdg-open", root.contributors[index + root.topContributorsCount].html_url]); + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/About/SupportersSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/About/SupportersSubTab.qml new file mode 100644 index 0000000..c23e987 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/About/SupportersSubTab.qml @@ -0,0 +1,229 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Noctalia +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + + property var supporters: SupporterService.supporters + property int avatarCacheVersion: 0 + + Connections { + target: SupporterService + function onCachedAvatarsChanged() { + root.avatarCacheVersion++; + } + } + + spacing: Style.marginL + + NHeader { + description: root.supporters.length === 0 ? I18n.tr("panels.about.supporters-loading") : I18n.trp("panels.about.supporters-desc", root.supporters.length) + enableDescriptionRichText: true + } + + NButton { + Layout.alignment: Qt.AlignHCenter + icon: "heart" + text: I18n.tr("panels.about.become-supporter") + outlined: true + onClicked: { + Quickshell.execDetached(["xdg-open", "https://buymeacoffee.com/noctalia"]); + ToastService.showNotice(I18n.tr("panels.about.support"), I18n.tr("toast.donation-opened")); + } + } + + NDivider { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + } + + // Supporter cards + Flow { + id: supportersFlow + visible: root.supporters.length > 0 + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + spacing: Style.marginL + + Repeater { + model: root.supporters.length + + delegate: Rectangle { + id: supporterCard + + property bool hasGithub: !!root.supporters[index].github_username + + width: Math.max(Math.round(supportersFlow.width / 2 - Style.marginL - 1), Math.round(Style.baseWidgetSize * 4.5)) + height: Math.round(Style.baseWidgetSize * 2.6) + radius: Style.radiusM + color: supporterArea.containsMouse && hasGithub ? Color.mHover : Qt.alpha(Color.mPrimary, 0.05) + border.width: Style.borderM + border.color: supporterArea.containsMouse && hasGithub ? Color.mPrimary : Qt.alpha(Color.mPrimary, 0.5) + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + // Avatar with heart badge + Item { + id: avatarWrapper + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: Style.baseWidgetSize * 2.0 + Layout.preferredHeight: Style.baseWidgetSize * 2.0 + + property bool isRounded: false + + Item { + anchors.fill: parent + + Image { + anchors.fill: parent + visible: supporterCard.hasGithub + source: { + if (!supporterCard.hasGithub) + return ""; + var _ = root.avatarCacheVersion; + var username = root.supporters[index].github_username; + var cached = SupporterService.getAvatarPath(username); + if (cached) { + avatarWrapper.isRounded = true; + return cached; + } + return "https://github.com/" + username + ".png?size=256"; + } + fillMode: Image.PreserveAspectFit + mipmap: true + smooth: true + asynchronous: true + opacity: status === Image.Ready ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + + // Fallback logo for supporters without GitHub + Rectangle { + anchors.fill: parent + visible: !supporterCard.hasGithub + radius: width * 0.5 + color: Qt.alpha(Color.mPrimary, 0.15) + + Image { + anchors.centerIn: parent + source: "../../../../../Assets/noctalia.svg" + width: parent.width * 0.75 + height: width + fillMode: Image.PreserveAspectFit + mipmap: true + smooth: true + } + } + } + + Rectangle { + visible: avatarWrapper.isRounded || !supporterCard.hasGithub + anchors.fill: parent + color: "transparent" + radius: width * 0.5 + border.width: Style.borderM + border.color: Color.mPrimary + } + + // Heart badge + Rectangle { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: -2 + anchors.bottomMargin: -2 + width: Style.fontSizeM + Style.marginS + height: width + radius: width * 0.5 + color: Color.mPrimary + + NIcon { + anchors.centerIn: parent + icon: "heart-filled" + pointSize: Style.fontSizeXS + color: Color.mOnPrimary + } + } + } + + // Info column + ColumnLayout { + spacing: 2 + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + + NText { + text: root.supporters[index].name || root.supporters[index].github_username || "Unknown" + font.weight: Style.fontWeightBold + color: supporterArea.containsMouse && supporterCard.hasGithub ? Color.mOnHover : Color.mOnSurface + elide: Text.ElideRight + Layout.fillWidth: true + pointSize: Style.fontSizeS + } + + NText { + text: I18n.tr("panels.about.supporter-badge") + pointSize: Style.fontSizeXS + color: Color.mPrimary + font.weight: Style.fontWeightMedium + } + } + + // Hover indicator (only for supporters with GitHub) + NIcon { + Layout.alignment: Qt.AlignVCenter + icon: "arrow-right" + pointSize: Style.fontSizeS + color: Color.mPrimary + visible: supporterCard.hasGithub + opacity: supporterArea.containsMouse ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + } + + MouseArea { + id: supporterArea + anchors.fill: parent + hoverEnabled: true + cursorShape: supporterCard.hasGithub ? Qt.PointingHandCursor : Qt.ArrowCursor + onClicked: { + var username = root.supporters[index].github_username; + if (username) { + Quickshell.execDetached(["xdg-open", "https://github.com/" + username]); + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/About/VersionSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/About/VersionSubTab.qml new file mode 100644 index 0000000..7e5e82b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/About/VersionSubTab.qml @@ -0,0 +1,985 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.Compositor +import qs.Services.Noctalia +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + opacity: 0 + + onSystemInfoLoadingChanged: { + if (!systemInfoLoading) + tabAppearAnim.start(); + } + + NumberAnimation on opacity { + id: tabAppearAnim + from: 0 + to: 1 + duration: Style.animationSlowest + easing.type: Easing.OutCubic + running: false + } + + property string latestVersion: GitHubService.latestVersion + property string currentVersion: UpdateService.currentVersion + property string commitInfo: "" + property string qsVersion: "" + property string qsRevision: "" + + readonly property bool isGitVersion: root.currentVersion.endsWith("-git") + readonly property int gigaB: (1024 * 1024 * 1024) + readonly property int gigaD: (1000 * 1000 * 1000) + + // Update status: compare versions + readonly property bool updateAvailable: { + if (!root.latestVersion || !root.currentVersion || root.latestVersion === I18n.tr("common.unknown")) + return false; + return UpdateService.compareVersions(root.latestVersion, root.currentVersion) > 0 && !root.isGitVersion; + } + readonly property bool isUpToDate: { + if (!root.latestVersion || !root.currentVersion || root.latestVersion === I18n.tr("common.unknown")) + return false; + return UpdateService.compareVersions(root.latestVersion, root.currentVersion) <= 0; + } + + readonly property bool qsUpdateAvailable: { + if (!GitHubService.latestQSVersion || !root.qsVersion || GitHubService.latestQSVersion === I18n.tr("common.unknown")) + return false; + return UpdateService.compareVersions(GitHubService.latestQSVersion, root.qsVersion) > 0; + } + + readonly property bool qsIsUpToDate: { + if (!GitHubService.latestQSVersion || !root.qsVersion || GitHubService.latestQSVersion === I18n.tr("common.unknown")) + return false; + return UpdateService.compareVersions(GitHubService.latestQSVersion, root.qsVersion) <= 0; + } + + // System info properties + property var systemInfo: null + property bool systemInfoLoading: true + property bool systemInfoAvailable: true + + spacing: Style.marginL + + function getModule(type) { + if (!root.systemInfo) + return null; + return root.systemInfo.find(m => m.type === type); + } + + function getMonitorsText(separator) { + const sep = separator || "\n"; + const screens = Quickshell.screens || []; + const scales = CompositorService.displayScales || {}; + let lines = []; + for (let i = 0; i < screens.length; i++) { + const screen = screens[i]; + const name = screen.name || "Unknown"; + const scaleData = scales[name]; + const scaleValue = (typeof scaleData === "object" && scaleData !== null) ? (scaleData.scale || 1.0) : (scaleData || 1.0); + lines.push(name + ": " + screen.width + "x" + screen.height + " @ " + scaleValue + "x"); + } + return lines.join(sep); + } + + function getTelemetryPayload() { + const screens = Quickshell.screens || []; + const scales = CompositorService.displayScales || {}; + const monitors = []; + for (let i = 0; i < screens.length; i++) { + const screen = screens[i]; + const name = screen.name || "Unknown"; + const scaleData = scales[name]; + const scaleValue = (typeof scaleData === "object" && scaleData !== null) ? (scaleData.scale || 1.0) : (scaleData || 1.0); + monitors.push({ + width: screen.width || 0, + height: screen.height || 0, + scale: scaleValue + }); + } + return { + instanceId: TelemetryService.getInstanceId(), + version: UpdateService.currentVersion, + compositor: TelemetryService.getCompositorType(), + os: HostService.osPretty || "Unknown", + ramGb: Math.round((root.getModule("Memory")?.result?.total || 0) / root.gigaB), + monitors: monitors, + ui: { + scaleRatio: Settings.data.general.scaleRatio, + fontDefaultScale: Settings.data.ui.fontDefaultScale, + fontFixedScale: Settings.data.ui.fontFixedScale + } + }; + } + + function copyTelemetryData() { + const payload = getTelemetryPayload(); + const json = JSON.stringify(payload, null, 2); + Quickshell.execDetached(["wl-copy", json]); + ToastService.showNotice(I18n.tr("panels.about.telemetry-title"), I18n.tr("panels.about.telemetry-data-copied")); + } + + function copyInfoToClipboard() { + let info = "Noctalia Shell: " + root.currentVersion; + if (root.isGitVersion && root.commitInfo) { + info += " (" + root.commitInfo + ")"; + } + info += "\n"; + + if (root.qsVersion) { + let qsV = root.qsVersion.startsWith("v") ? root.qsVersion : "v" + root.qsVersion; + info += "Noctalia QS: " + qsV; + if (root.qsRevision) { + info += " (" + root.qsRevision + ")"; + } + info += "\n"; + } + + info += "\nSystem Information\n"; + info += "==================\n"; + if (root.systemInfo) { + const os = root.getModule("OS"); + const kernel = root.getModule("Kernel"); + const title = root.getModule("Title"); + const product = root.getModule("Host"); + const board = root.getModule("Board"); + const cpu = root.getModule("CPU"); + const gpu = root.getModule("GPU"); + const mem = root.getModule("Memory"); + const wm = root.getModule("WM"); + info += "OS: " + (os?.result?.prettyName || "N/A") + "\n"; + info += "Kernel: " + (kernel?.result?.release || "N/A") + "\n"; + info += "Host: " + (title?.result?.hostName || "N/A") + "\n"; + info += "Product: " + (product?.result?.name || "N/A") + "\n"; + info += "Board: " + (board?.result?.name || "N/A") + "\n"; + info += "CPU: " + (cpu?.result?.cpu || "N/A") + "\n"; + if (gpu?.result && Array.isArray(gpu.result) && gpu.result.length > 0) { + info += "GPU: " + gpu.result.map(g => g.name || "Unknown").join(", ") + "\n"; + } + if (mem?.result) { + info += "Memory: " + (mem.result.total / root.gigaB).toFixed(1) + " GB \n"; + } + if (wm?.result) { + info += "WM: " + (wm.result.prettyName || wm.result.processName || "N/A") + "\n"; + } + } + const monitors = getMonitorsText("\n").split("\n"); + for (const mon of monitors) { + info += "Monitor: " + mon + "\n"; + } + info += "\nSettings\n"; + info += "========\n"; + info += "UI Scale: " + Settings.data.general.scaleRatio + "\n"; + info += "Default Font: " + (Settings.data.ui.fontDefault || "default") + " @ " + Settings.data.ui.fontDefaultScale + "x\n"; + info += "Fixed Font: " + (Settings.data.ui.fontFixed || "default") + " @ " + Settings.data.ui.fontFixedScale + "x\n"; + Quickshell.execDetached(["wl-copy", info]); + ToastService.showNotice(I18n.tr("panels.about.title"), I18n.tr("panels.about.info-copied")); + } + + Component.onCompleted: { + // Check if fastfetch is available before trying to run it + checkFastfetchProcess.running = true; + qsVersionProcess.running = true; + + Logger.d("VersionSubTab", "Current version:", root.currentVersion); + Logger.d("VersionSubTab", "Is git version:", root.isGitVersion); + // Only fetch commit info for -git versions + if (root.isGitVersion) { + // On NixOS, extract commit hash from the store path first + if (HostService.isNixOS) { + var shellDir = Quickshell.shellDir || ""; + Logger.d("VersionSubTab", "Component.onCompleted - NixOS detected, shellDir:", shellDir); + if (shellDir) { + // Extract commit hash from path like: /nix/store/...-noctalia-shell-2025-11-30_225e6d3/share/noctalia-shell + // Pattern matches: noctalia-shell-YYYY-MM-DD_ + var match = shellDir.match(/noctalia-shell-\d{4}-\d{2}-\d{2}_([0-9a-f]{7,})/i); + if (match && match[1]) { + // Use first 7 characters of the commit hash + root.commitInfo = match[1].substring(0, 7); + Logger.d("VersionSubTab", "Component.onCompleted - Extracted commit from NixOS path:", root.commitInfo); + return; + } else { + Logger.d("VersionSubTab", "Component.onCompleted - Could not extract commit from NixOS path, trying fallback"); + } + } + } + fetchGitCommit(); + } + } + + function fetchGitCommit() { + var shellDir = Quickshell.shellDir || ""; + Logger.d("VersionSubTab", "fetchGitCommit - shellDir:", shellDir); + if (!shellDir) { + Logger.d("VersionSubTab", "fetchGitCommit - Cannot determine shell directory, skipping git commit fetch"); + return; + } + + gitProcess.workingDirectory = shellDir; + gitProcess.running = true; + } + + Process { + id: gitProcess + command: ["git", "rev-parse", "--short", "HEAD"] + running: false + + onExited: function (exitCode) { + Logger.d("VersionSubTab", "gitProcess - Process exited with code:", exitCode); + if (exitCode === 0) { + var gitOutput = stdout.text.trim(); + Logger.d("VersionSubTab", "gitProcess - gitOutput:", gitOutput); + if (gitOutput) { + root.commitInfo = gitOutput; + Logger.d("VersionSubTab", "gitProcess - Set commitInfo to:", root.commitInfo); + } + } else { + Logger.d("VersionSubTab", "gitProcess - Git command failed. Exit code:", exitCode); + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + Process { + id: qsVersionProcess + command: ["qs", "--version"] + running: false + + onExited: function (exitCode) { + if (exitCode === 0) { + var output = stdout.text.trim(); + // Format (old): "noctalia-qs 0.3.0, revision abc12345, distributed by: ..." + // Format (new): "noctalia-qs 0.0.9 (revision b602b69c81d96a1d7c645328feb7b1e1d4b7b7a4, distributed by Unset)" + // Only set if this is actually noctalia-qs; leave empty for upstream quickshell + var match = output.match(/noctalia-qs\s+(\S+?)[\s,(]+revision\s*([0-9a-f]*)/i); + if (match) { + root.qsVersion = match[1]; + root.qsRevision = match[2] ? match[2].substring(0, 9) : ""; + } + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + // Check if fastfetch is available before attempting to run it + Process { + id: checkFastfetchProcess + command: ["sh", "-c", "command -v fastfetch"] + running: false + + onExited: function (exitCode) { + if (exitCode === 0) { + // fastfetch is available, run it + Logger.d("VersionSubTab", "fastfetch found, running it"); + fastfetchProcess.running = true; + } else { + // fastfetch not found, show error state immediately + Logger.w("VersionSubTab", "fastfetch not found"); + root.systemInfoLoading = false; + root.systemInfoAvailable = false; + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + Process { + id: fastfetchProcess + command: ["fastfetch", "--format", "json", "--config", Quickshell.shellDir + "/Assets/Services/fastfetch/system-info.jsonc"] + running: false + + onExited: function (exitCode) { + root.systemInfoLoading = false; + if (exitCode === 0) { + try { + root.systemInfo = JSON.parse(stdout.text); + root.systemInfoAvailable = true; + } catch (e) { + Logger.w("VersionSubTab", "Failed to parse fastfetch JSON: " + e); + root.systemInfoAvailable = false; + } + } else { + root.systemInfoAvailable = false; + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Style.marginXL + + // Noctalia logo + Image { + source: "../../../../../Assets/noctalia.svg" + width: 96 * Style.uiScaleRatio + height: width + fillMode: Image.PreserveAspectFit + sourceSize.width: width + sourceSize.height: height + mipmap: true + smooth: true + Layout.alignment: Qt.AlignBottom + rotation: Settings.isDebug ? 180 : 0 + + Behavior on rotation { + NumberAnimation { + duration: Style.animationSlowest + easing.type: Easing.OutBack + } + } + + property int debugTapCount: 0 + + Timer { + id: debugTapTimer + interval: 5000 + onTriggered: parent.debugTapCount = 0 + } + + MouseArea { + anchors.fill: parent + onClicked: { + if (parent.debugTapCount === 0) { + debugTapTimer.restart(); + } + parent.debugTapCount++; + if (parent.debugTapCount >= 8) { + parent.debugTapCount = 0; + debugTapTimer.stop(); + Settings.isDebug = !Settings.isDebug; + if (Settings.isDebug) { + ToastService.showNotice("Debug", I18n.tr("panels.about.debug-enabled")); + } else { + ToastService.showNotice("Debug", I18n.tr("panels.about.debug-disabled")); + } + } + } + } + } + + ColumnLayout { + NHeader { + label: "Noctalia Shell" + } + + // Versions + GridLayout { + columns: 2 + rowSpacing: Style.marginXS + columnSpacing: Style.marginM + + // Installed Version (Shell) + NText { + text: "Noctalia Shell:" + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + + RowLayout { + spacing: Style.marginS + + NText { + text: root.currentVersion + color: Color.mOnSurface + font.weight: Style.fontWeightBold + } + + // Git commit in parentheses + NText { + id: commitText + visible: root.isGitVersion + text: "(" + (root.commitInfo || I18n.tr("common.loading")) + ")" + color: commitMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + font.underline: commitMouseArea.containsMouse && root.commitInfo + + MouseArea { + id: commitMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: root.commitInfo ? Qt.PointingHandCursor : Qt.ArrowCursor + onEntered: { + if (root.commitInfo) { + TooltipService.show(commitText, I18n.tr("panels.about.view-commit")); + } + } + onExited: TooltipService.hide() + onClicked: { + if (root.commitInfo) { + Quickshell.execDetached(["xdg-open", "https://github.com/noctalia-dev/noctalia-shell/commit/" + root.commitInfo]); + } + } + } + } + + // Update status indicator + NIcon { + id: upToDateIcon + visible: root.isUpToDate + icon: "circle-check" + pointSize: Style.fontSizeM + color: Color.mPrimary + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(upToDateIcon, I18n.tr("panels.about.up-to-date")) + onExited: TooltipService.hide() + } + } + + NIcon { + id: updateAvailableIcon + visible: root.updateAvailable + icon: "arrow-up-circle" + pointSize: Style.fontSizeS + color: Color.mPrimary + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(updateAvailableIcon, I18n.tr("panels.about.update-available")) + onExited: TooltipService.hide() + } + } + } + + // Latest Version (Shell) + NText { + visible: root.updateAvailable + text: I18n.tr("panels.about.noctalia-available") + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + + NText { + visible: root.updateAvailable + text: root.latestVersion + color: Color.mOnSurface + font.weight: Style.fontWeightBold + } + + // Divider-like spacing + Item { + visible: root.qsUpdateAvailable || root.updateAvailable + Layout.columnSpan: 2 + Layout.preferredHeight: Style.marginXS + } + + // Quickshell Version + NText { + visible: root.qsVersion !== "" + text: "Noctalia QS:" + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + + RowLayout { + visible: root.qsVersion !== "" + spacing: Style.marginS + + NText { + text: root.qsVersion.startsWith("v") ? root.qsVersion : "v" + root.qsVersion + color: Color.mOnSurface + font.weight: Style.fontWeightBold + } + + // Git revision in parentheses + NText { + id: qsRevisionText + visible: root.qsRevision !== "" + text: "(" + root.qsRevision + ")" + color: qsRevisionMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + font.underline: qsRevisionMouseArea.containsMouse + + MouseArea { + id: qsRevisionMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: TooltipService.show(qsRevisionText, I18n.tr("panels.about.view-commit")) + onExited: TooltipService.hide() + onClicked: { + Quickshell.execDetached(["xdg-open", "https://github.com/noctalia-dev/noctalia-qs/commit/" + root.qsRevision]); + } + } + } + + // Update status indicator + NIcon { + id: qsUpToDateIcon + visible: root.qsIsUpToDate + icon: "circle-check" + pointSize: Style.fontSizeM + color: Color.mPrimary + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(qsUpToDateIcon, I18n.tr("panels.about.up-to-date")) + onExited: TooltipService.hide() + } + } + + NIcon { + id: qsUpdateAvailableIcon + visible: root.qsUpdateAvailable + icon: "arrow-up-circle" + pointSize: Style.fontSizeS + color: Color.mPrimary + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(qsUpdateAvailableIcon, I18n.tr("panels.about.update-available")) + onExited: TooltipService.hide() + } + } + } + + // Latest Quickshell Version + NText { + visible: root.qsUpdateAvailable + text: I18n.tr("panels.about.noctalia-available") + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + } + + NText { + visible: root.qsUpdateAvailable + text: GitHubService.latestQSVersion + color: Color.mOnSurface + font.weight: Style.fontWeightBold + } + } + } + } + + GridLayout { + id: actionsGrid + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Style.marginM + Layout.bottomMargin: Style.marginM + rowSpacing: Style.marginM + columnSpacing: Style.marginM + + columns: (changelogBtn.implicitWidth + copyBtn.implicitWidth + supportBtn.implicitWidth + 2 * columnSpacing) < root.width ? 3 : 1 + + NButton { + id: changelogBtn + icon: "sparkles" + text: I18n.tr("panels.about.changelog") + outlined: true + Layout.alignment: Qt.AlignHCenter + onClicked: { + var screen = PanelService.openedPanel?.screen || SettingsPanelService.settingsWindow?.screen || PanelService.findScreenForPanels(); + SettingsPanelService.close(screen); + UpdateService.viewChangelog(screen); + } + } + + NButton { + id: copyBtn + icon: "copy" + text: I18n.tr("panels.about.copy-info") + outlined: true + Layout.alignment: Qt.AlignHCenter + onClicked: root.copyInfoToClipboard() + } + + NButton { + id: supportBtn + icon: "heart" + text: I18n.tr("panels.about.support") + outlined: true + Layout.alignment: Qt.AlignHCenter + onClicked: { + Quickshell.execDetached(["xdg-open", "https://buymeacoffee.com/noctalia"]); + ToastService.showNotice(I18n.tr("panels.about.support"), I18n.tr("toast.donation-opened")); + } + } + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.about.changelog-on-startup") + description: I18n.tr("panels.about.changelog-on-startup-desc") + checked: Settings.data.general.showChangelogOnStartup + onToggled: checked => Settings.data.general.showChangelogOnStartup = checked + } + + // System Information Section + NDivider { + Layout.fillWidth: true + } + + NHeader { + label: I18n.tr("panels.about.system-title") + } + + // Error state (fastfetch not installed) + ColumnLayout { + visible: !root.systemInfoAvailable + Layout.fillWidth: true + spacing: Style.marginS + + NText { + text: I18n.tr("panels.about.system-not-installed") + color: Color.mOnSurfaceVariant + } + + NText { + text: I18n.tr("panels.about.system-install-hint") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + } + } + + // System info grid + GridLayout { + id: sysInfo + readonly property real textSize: Style.fontSizeS + + visible: root.systemInfoAvailable && root.systemInfo + Layout.fillWidth: true + columns: 2 + rowSpacing: Style.marginXS + columnSpacing: Style.marginM + + // OS + NText { + text: I18n.tr("panels.about.system-os") + color: Color.mOnSurfaceVariant + pointSize: sysInfo.textSize + } + NText { + text: { + const os = root.getModule("OS"); + return os?.result?.prettyName || "N/A"; + } + color: Color.mOnSurface + pointSize: sysInfo.textSize + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + // Kernel + NText { + text: I18n.tr("panels.about.system-kernel") + color: Color.mOnSurfaceVariant + pointSize: sysInfo.textSize + } + NText { + text: { + const kernel = root.getModule("Kernel"); + return kernel?.result?.release || "N/A"; + } + color: Color.mOnSurface + pointSize: sysInfo.textSize + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + // Host + NText { + text: I18n.tr("panels.about.system-host") + color: Color.mOnSurfaceVariant + pointSize: sysInfo.textSize + } + NText { + text: { + const title = root.getModule("Title"); + return title?.result?.hostName || "N/A"; + } + color: Color.mOnSurface + pointSize: sysInfo.textSize + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + // Product name + NText { + text: I18n.tr("panels.about.system-product") + color: Color.mOnSurfaceVariant + pointSize: sysInfo.textSize + } + NText { + text: { + const title = root.getModule("Host"); + return title?.result?.name || "N/A"; + } + color: Color.mOnSurface + pointSize: sysInfo.textSize + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + // Board name + NText { + text: I18n.tr("panels.about.system-board") + color: Color.mOnSurfaceVariant + pointSize: sysInfo.textSize + } + NText { + text: { + const title = root.getModule("Board"); + return title?.result?.name || "N/A"; + } + color: Color.mOnSurface + pointSize: sysInfo.textSize + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + // Uptime + NText { + text: I18n.tr("panels.about.system-uptime") + color: Color.mOnSurfaceVariant + pointSize: sysInfo.textSize + } + NText { + text: { + const value = root.getModule("Uptime")?.result?.uptime; + return value ? Time.formatVagueHumanReadableDuration(value / 1000) : "-"; + } + color: Color.mOnSurface + pointSize: sysInfo.textSize + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + // CPU + NText { + text: I18n.tr("panels.about.system-cpu") + color: Color.mOnSurfaceVariant + pointSize: sysInfo.textSize + } + NText { + text: { + const cpu = root.getModule("CPU"); + if (!cpu?.result) + return "N/A"; + let cpuText = cpu.result.cpu || "N/A"; + const cores = cpu.result.cores; + if (cores?.logical) { + cpuText += " (" + cores.logical + " threads)"; + } + return cpuText; + } + color: Color.mOnSurface + pointSize: sysInfo.textSize + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + // GPU + NText { + text: I18n.tr("panels.about.system-gpu") + color: Color.mOnSurfaceVariant + pointSize: sysInfo.textSize + } + NText { + text: { + const gpu = root.getModule("GPU"); + if (!gpu?.result || !Array.isArray(gpu.result) || gpu.result.length === 0) + return "N/A"; + return gpu.result.map(g => g.name || "Unknown").join(", "); + } + color: Color.mOnSurface + pointSize: sysInfo.textSize + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + // Memory + NText { + text: I18n.tr("panels.about.system-memory") + color: Color.mOnSurfaceVariant + pointSize: sysInfo.textSize + } + NText { + text: { + const mem = root.getModule("Memory"); + if (!mem?.result) + return "N/A"; + const used = (mem.result.used / root.gigaB).toFixed(1); + const total = (mem.result.total / root.gigaB).toFixed(1); + return used + " GiB / " + total + " GiB"; + } + color: Color.mOnSurface + pointSize: sysInfo.textSize + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + // Disk + NText { + text: I18n.tr("panels.about.system-disk") + color: Color.mOnSurfaceVariant + pointSize: sysInfo.textSize + } + NText { + text: { + const disk = root.getModule("Disk"); + if (!disk?.result || !Array.isArray(disk.result) || disk.result.length === 0) + return "N/A"; + const rootDisk = disk.result.find(d => d.mountpoint === "/"); + if (!rootDisk?.bytes) + return "N/A"; + const used = (rootDisk.bytes.used / root.gigaD).toFixed(1); + const total = (rootDisk.bytes.total / root.gigaD).toFixed(1); + return used + " GB / " + total + " GB" + " (" + rootDisk.filesystem + ")"; + } + color: Color.mOnSurface + pointSize: sysInfo.textSize + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + // WM + NText { + text: I18n.tr("panels.about.system-wm") + color: Color.mOnSurfaceVariant + pointSize: sysInfo.textSize + } + NText { + text: { + const wm = root.getModule("WM"); + if (!wm?.result) + return "N/A"; + let wmText = wm.result.prettyName || wm.result.processName || "N/A"; + if (wm.result.protocolName) { + wmText += " (" + wm.result.protocolName + ")"; + } + return wmText; + } + color: Color.mOnSurface + pointSize: sysInfo.textSize + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + // Packages + NText { + text: I18n.tr("panels.about.system-packages") + color: Color.mOnSurfaceVariant + pointSize: sysInfo.textSize + } + NText { + text: { + const pkg = root.getModule("Packages"); + if (!pkg?.result) + return "N/A"; + const result = pkg.result; + if (result.all) { + const managers = []; + if (result.rpm > 0) + managers.push("rpm: " + result.rpm); + if (result.pacman > 0) + managers.push("pacman: " + result.pacman); + if (result.dpkg > 0) + managers.push("dpkg: " + result.dpkg); + if (result.flatpakSystem > 0 || result.flatpakUser > 0) { + const flatpak = (result.flatpakSystem || 0) + (result.flatpakUser || 0); + managers.push("flatpak: " + flatpak); + } + if (result.snap > 0) + managers.push("snap: " + result.snap); + if (result.nixSystem > 0 || result.nixUser > 0 || result.nixDefault > 0) { + const nix = (result.nixSystem || 0) + (result.nixUser || 0) + (result.nixDefault || 0); + managers.push("nix: " + nix); + } + if (result.brew > 0) + managers.push("brew: " + result.brew); + if (managers.length > 0) { + return result.all + " (" + managers.join(", ") + ")"; + } + return result.all.toString(); + } + return "N/A"; + } + color: Color.mOnSurface + pointSize: sysInfo.textSize + Layout.fillWidth: true + wrapMode: Text.Wrap + } + + // Monitors (2 items per screen: label + value) + Repeater { + model: Quickshell.screens.length * 2 + + NText { + readonly property int screenIndex: Math.floor(index / 2) + readonly property bool isLabel: index % 2 === 0 + readonly property var screen: Quickshell.screens[screenIndex] + + text: { + if (isLabel) + return I18n.tr("panels.about.system-monitor"); + const name = screen?.name || "Unknown"; + const scales = CompositorService.displayScales || {}; + const scaleData = scales[name]; + const scaleValue = (typeof scaleData === "object" && scaleData !== null) ? (scaleData.scale || 1.0) : (scaleData || 1.0); + return name + ": " + (screen?.width || 0) + "x" + (screen?.height || 0) + " @ " + scaleValue + "x"; + } + color: isLabel ? Color.mOnSurfaceVariant : Color.mOnSurface + pointSize: sysInfo.textSize + Layout.fillWidth: !isLabel + wrapMode: Text.Wrap + } + } + } + + // Telemetry Section + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginL + } + + NHeader { + label: I18n.tr("panels.about.telemetry-title") + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.about.telemetry-enabled") + description: I18n.tr("panels.about.telemetry-desc") + checked: Settings.data.general.telemetryEnabled + onToggled: checked => Settings.data.general.telemetryEnabled = checked + } + + RowLayout { + spacing: Style.marginM + + NButton { + icon: "eye" + text: I18n.tr("panels.about.telemetry-show-data") + outlined: true + onClicked: root.copyTelemetryData() + } + + NButton { + icon: "shield-lock" + text: I18n.tr("panels.about.privacy-policy") + outlined: true + onClicked: Quickshell.execDetached(["xdg-open", "https://noctalia.dev/privacy"]) + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Audio/AudioTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Audio/AudioTab.qml new file mode 100644 index 0000000..8be4512 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Audio/AudioTab.qml @@ -0,0 +1,54 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.volumes") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.devices") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + NTabButton { + text: I18n.tr("common.media") + tabIndex: 2 + checked: subTabBar.currentIndex === 2 + } + NTabButton { + text: I18n.tr("common.visualizer") + tabIndex: 3 + checked: subTabBar.currentIndex === 3 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + VolumesSubTab {} + DevicesSubTab {} + MediaSubTab {} + VisualizerSubTab {} + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Audio/DevicesSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Audio/DevicesSubTab.qml new file mode 100644 index 0000000..2df10a9 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Audio/DevicesSubTab.qml @@ -0,0 +1,70 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Services.Pipewire +import qs.Commons +import qs.Services.Media +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + // Output Devices + ButtonGroup { + id: sinks + } + + ColumnLayout { + spacing: Style.marginXS + Layout.fillWidth: true + Layout.bottomMargin: Style.marginL + + NLabel { + label: I18n.tr("panels.audio.devices-output-device-label") + description: I18n.tr("panels.audio.devices-output-device-description") + } + + Repeater { + model: AudioService.sinks + NRadioButton { + ButtonGroup.group: sinks + required property PwNode modelData + text: modelData.description + checked: AudioService.sink?.id === modelData.id + onClicked: { + AudioService.setAudioSink(modelData); + } + Layout.fillWidth: true + } + } + } + + // Input Devices + ButtonGroup { + id: sources + } + + ColumnLayout { + spacing: Style.marginXS + Layout.fillWidth: true + + NLabel { + label: I18n.tr("panels.audio.devices-input-device-label") + description: I18n.tr("panels.audio.devices-input-device-description") + } + + Repeater { + model: AudioService.sources + NRadioButton { + ButtonGroup.group: sources + required property PwNode modelData + text: modelData.description + checked: AudioService.source?.id === modelData.id + onClicked: AudioService.setAudioSource(modelData) + Layout.fillWidth: true + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Audio/MediaSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Audio/MediaSubTab.qml new file mode 100644 index 0000000..8740bc5 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Audio/MediaSubTab.qml @@ -0,0 +1,104 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.Media +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + // Preferred player + NTextInput { + label: I18n.tr("panels.audio.media-primary-player-label") + description: I18n.tr("panels.audio.media-primary-player-description") + placeholderText: I18n.tr("panels.audio.media-primary-player-placeholder") + text: Settings.data.audio.preferredPlayer + defaultValue: Settings.getDefaultValue("audio.preferredPlayer") + onTextChanged: { + Settings.data.audio.preferredPlayer = text; + MediaService.updateCurrentPlayer(); + } + } + + // Blacklist editor + ColumnLayout { + spacing: Style.marginS + Layout.fillWidth: true + + NTextInputButton { + id: blacklistInput + label: I18n.tr("panels.audio.media-excluded-player-label") + description: I18n.tr("panels.audio.media-excluded-player-description") + placeholderText: I18n.tr("panels.audio.media-excluded-player-placeholder") + buttonIcon: "add" + Layout.fillWidth: true + onButtonClicked: { + const val = (blacklistInput.text || "").trim(); + if (val !== "") { + const arr = (Settings.data.audio.mprisBlacklist || []); + if (!arr.find(x => String(x).toLowerCase() === val.toLowerCase())) { + Settings.data.audio.mprisBlacklist = [...arr, val]; + blacklistInput.text = ""; + MediaService.updateCurrentPlayer(); + } + } + } + } + + // Current blacklist entries + Flow { + Layout.fillWidth: true + Layout.leftMargin: Style.marginS + spacing: Style.marginS + + Repeater { + model: Settings.data.audio.mprisBlacklist + delegate: Rectangle { + required property string modelData + property real pad: Style.marginS + color: Qt.alpha(Color.mOnSurface, 0.125) + border.color: Qt.alpha(Color.mOnSurface, Style.opacityLight) + border.width: Style.borderS + + RowLayout { + id: chipRow + spacing: Style.marginXS + anchors.fill: parent + anchors.margins: pad + + NText { + text: modelData + color: Color.mOnSurface + pointSize: Style.fontSizeS + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: Style.marginS + } + + NIconButton { + icon: "close" + baseSize: Style.baseWidgetSize * 0.8 + Layout.alignment: Qt.AlignVCenter + Layout.rightMargin: Style.marginXS + onClicked: { + const arr = (Settings.data.audio.mprisBlacklist || []); + const idx = arr.findIndex(x => String(x) === modelData); + if (idx >= 0) { + arr.splice(idx, 1); + Settings.data.audio.mprisBlacklist = arr; + MediaService.updateCurrentPlayer(); + } + } + } + } + + implicitWidth: chipRow.implicitWidth + pad * 2 + implicitHeight: Math.max(chipRow.implicitHeight + pad * 2, Style.baseWidgetSize * 0.8) + radius: Style.radiusM + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Audio/VisualizerSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Audio/VisualizerSubTab.qml new file mode 100644 index 0000000..e1ebf70 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Audio/VisualizerSubTab.qml @@ -0,0 +1,97 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + NComboBox { + label: I18n.tr("panels.audio.visualizer-type-label") + description: I18n.tr("panels.audio.visualizer-type-description") + model: [ + { + "key": "none", + "name": I18n.tr("common.none") + }, + { + "key": "linear", + "name": I18n.tr("options.visualizer-types.linear") + }, + { + "key": "mirrored", + "name": I18n.tr("options.visualizer-types.mirrored") + }, + { + "key": "wave", + "name": I18n.tr("options.visualizer-types.wave") + } + ] + currentKey: Settings.data.audio.visualizerType + defaultValue: Settings.getDefaultValue("audio.visualizerType") + onSelected: key => Settings.data.audio.visualizerType = key + } + + NToggle { + label: I18n.tr("panels.audio.spectrum-mirrored-label") + description: I18n.tr("panels.audio.spectrum-mirrored-description") + checked: Settings.data.audio.spectrumMirrored + defaultValue: Settings.getDefaultValue("audio.spectrumMirrored") + onToggled: Settings.data.audio.spectrumMirrored = checked + } + + NComboBox { + label: I18n.tr("panels.audio.media-frame-rate-label") + description: I18n.tr("panels.audio.media-frame-rate-description") + model: [ + { + "key": "30", + "name": I18n.tr("options.frame-rates-fps", { + "fps": "30" + }) + }, + { + "key": "60", + "name": I18n.tr("options.frame-rates-fps", { + "fps": "60" + }) + }, + { + "key": "100", + "name": I18n.tr("options.frame-rates-fps", { + "fps": "100" + }) + }, + { + "key": "120", + "name": I18n.tr("options.frame-rates-fps", { + "fps": "120" + }) + }, + { + "key": "144", + "name": I18n.tr("options.frame-rates-fps", { + "fps": "144" + }) + }, + { + "key": "165", + "name": I18n.tr("options.frame-rates-fps", { + "fps": "165" + }) + }, + { + "key": "240", + "name": I18n.tr("options.frame-rates-fps", { + "fps": "240" + }) + } + ] + currentKey: Settings.data.audio.spectrumFrameRate + defaultValue: Settings.getDefaultValue("audio.spectrumFrameRate") + onSelected: key => Settings.data.audio.spectrumFrameRate = key + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Audio/VolumesSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Audio/VolumesSubTab.qml new file mode 100644 index 0000000..ed0ef37 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Audio/VolumesSubTab.qml @@ -0,0 +1,196 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Media +import qs.Services.System +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + property real localVolume: AudioService.volume + + Connections { + target: AudioService + function onSinkChanged() { + localVolume = AudioService.volume; + } + function onVolumeChanged() { + localVolume = AudioService.volume; + } + } + + Connections { + target: AudioService.sink?.audio ? AudioService.sink?.audio : null + function onVolumeChanged() { + localVolume = AudioService.volume; + } + } + + // Output Volume + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.osd.types-volume-label") + description: I18n.tr("panels.audio.volumes-output-volume-description") + from: 0 + to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0 + value: localVolume + stepSize: 0.01 + text: Math.round(AudioService.volume * 100) + "%" + onMoved: value => localVolume = value + } + + Timer { + interval: 100 + running: true + repeat: true + onTriggered: { + if (!AudioService.isSwitchingSink && Math.abs(localVolume - AudioService.volume) >= 0.01) { + AudioService.setVolume(localVolume); + } + } + } + } + + // Mute Toggle + ColumnLayout { + spacing: Style.marginS + Layout.fillWidth: true + + NToggle { + label: I18n.tr("panels.audio.volumes-mute-output-label") + description: I18n.tr("panels.audio.volumes-mute-output-description") + checked: AudioService.muted + onToggled: checked => AudioService.setOutputMuted(checked) + } + } + + // Volume Feedback sound Toggle + ColumnLayout { + spacing: Style.marginS + Layout.fillWidth: true + + NToggle { + label: I18n.tr("panels.audio.volumes-volume-feedback-label") + description: I18n.tr("panels.audio.volumes-volume-feedback-description") + checked: Settings.data.audio.volumeFeedback + defaultValue: Settings.getDefaultValue("audio.volumeFeedback") + onToggled: checked => Settings.data.audio.volumeFeedback = checked + } + + ColumnLayout { + enabled: SoundService.multimediaAvailable && Settings.data.audio.volumeFeedback + spacing: Style.marginXXS + Layout.fillWidth: true + + NLabel { + label: I18n.tr("panels.audio.volumes-feedback-sound-file-label") + description: I18n.tr("panels.audio.volumes-feedback-sound-file-description") + } + + NTextInputButton { + enabled: parent.enabled + Layout.fillWidth: true + placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder") + text: Settings.data.audio.volumeFeedbackSoundFile ?? "" + buttonIcon: "folder-open" + buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file") + onInputTextChanged: text => Settings.data.audio.volumeFeedbackSoundFile = text + onButtonClicked: volumeFeedbackFilePicker.open() + } + } + } + + NDivider { + Layout.fillWidth: true + } + + // Input Volume + ColumnLayout { + spacing: Style.marginXS + Layout.fillWidth: true + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.osd.types-input-volume-label") + description: I18n.tr("panels.audio.volumes-input-volume-description") + from: 0 + to: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0 + value: AudioService.inputVolume + stepSize: 0.01 + text: Math.round(AudioService.inputVolume * 100) + "%" + onMoved: value => AudioService.setInputVolume(value) + } + } + + // Input Mute Toggle + ColumnLayout { + spacing: Style.marginS + Layout.fillWidth: true + + NToggle { + label: I18n.tr("panels.audio.volumes-mute-input-label") + description: I18n.tr("panels.audio.volumes-mute-input-description") + checked: AudioService.inputMuted + onToggled: checked => AudioService.setInputMuted(checked) + } + } + + // Volume Step Size + ColumnLayout { + spacing: Style.marginS + Layout.fillWidth: true + + NSpinBox { + Layout.fillWidth: true + label: I18n.tr("panels.audio.volumes-step-size-label") + description: I18n.tr("panels.audio.volumes-step-size-description") + minimum: 1 + maximum: 25 + value: Settings.data.audio.volumeStep + stepSize: 1 + suffix: "%" + defaultValue: Settings.getDefaultValue("audio.volumeStep") + onValueChanged: Settings.data.audio.volumeStep = value + } + } + + NDivider { + Layout.fillWidth: true + } + + // Raise maximum volume above 100% + ColumnLayout { + spacing: Style.marginS + Layout.fillWidth: true + + NToggle { + label: I18n.tr("panels.audio.volumes-volume-overdrive-label") + description: I18n.tr("panels.audio.volumes-volume-overdrive-description") + checked: Settings.data.audio.volumeOverdrive + defaultValue: Settings.getDefaultValue("audio.volumeOverdrive") + onToggled: checked => Settings.data.audio.volumeOverdrive = checked + } + } + + NFilePicker { + id: volumeFeedbackFilePicker + title: I18n.tr("panels.audio.volumes-feedback-sound-file-select-title") + selectionMode: "files" + initialPath: Quickshell.env("HOME") + nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"] + onAccepted: paths => { + if (paths.length > 0) { + Settings.data.audio.volumeFeedbackSoundFile = paths[0]; + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Bar/AppearanceSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Bar/AppearanceSubTab.qml new file mode 100644 index 0000000..61ae9f6 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Bar/AppearanceSubTab.qml @@ -0,0 +1,379 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.Compositor +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-position-label") + description: I18n.tr("panels.bar.appearance-position-description") + model: [ + { + "key": "top", + "name": I18n.tr("positions.top") + }, + { + "key": "bottom", + "name": I18n.tr("positions.bottom") + }, + { + "key": "left", + "name": I18n.tr("positions.left") + }, + { + "key": "right", + "name": I18n.tr("positions.right") + } + ] + currentKey: Settings.data.bar.position + defaultValue: Settings.getDefaultValue("bar.position") + onSelected: key => Settings.data.bar.position = key + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-density-label") + description: I18n.tr("panels.bar.appearance-density-description") + model: [ + { + "key": "mini", + "name": I18n.tr("options.bar.density-mini") + }, + { + "key": "compact", + "name": I18n.tr("options.bar.density-compact") + }, + { + "key": "default", + "name": I18n.tr("options.bar.density-default") + }, + { + "key": "comfortable", + "name": I18n.tr("options.bar.density-comfortable") + }, + { + "key": "spacious", + "name": I18n.tr("options.bar.density-spacious") + } + ] + currentKey: Settings.data.bar.density + defaultValue: Settings.getDefaultValue("bar.density") + onSelected: key => Settings.data.bar.density = key + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-type-label") + description: I18n.tr("panels.bar.appearance-type-description") + model: [ + { + "key": "simple", + "name": I18n.tr("options.bar.type-simple") + }, + { + "key": "floating", + "name": I18n.tr("options.bar.type-floating") + }, + { + "key": "framed", + "name": I18n.tr("options.bar.type-framed") + } + ] + currentKey: Settings.data.bar.barType + defaultValue: Settings.getDefaultValue("bar.barType") + onSelected: key => { + Settings.data.bar.barType = key; + } + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("common.display-mode") + description: I18n.tr("panels.bar.appearance-display-mode-description") + model: [ + { + "key": "always_visible", + "name": I18n.tr("hide-modes.visible") + }, + { + "key": "non_exclusive", + "name": I18n.tr("hide-modes.non-exclusive") + }, + { + "key": "auto_hide", + "name": I18n.tr("hide-modes.auto-hide") + } + ] + currentKey: Settings.data.bar.displayMode + defaultValue: Settings.getDefaultValue("bar.displayMode") + onSelected: key => Settings.data.bar.displayMode = key + } + + NToggle { + label: I18n.tr("panels.bar.appearance-use-separate-opacity-label") + description: I18n.tr("panels.bar.appearance-use-separate-opacity-description") + checked: Settings.data.bar.useSeparateOpacity + defaultValue: Settings.getDefaultValue("bar.useSeparateOpacity") + onToggled: checked => Settings.data.bar.useSeparateOpacity = checked + } + + NValueSlider { + Layout.fillWidth: true + visible: Settings.data.bar.useSeparateOpacity + label: I18n.tr("panels.bar.appearance-background-opacity-label") + description: I18n.tr("panels.bar.appearance-background-opacity-description") + from: 0 + to: 1 + stepSize: 0.01 + showReset: true + value: Settings.data.bar.backgroundOpacity + defaultValue: Settings.getDefaultValue("bar.backgroundOpacity") + onMoved: value => Settings.data.bar.backgroundOpacity = value + text: Math.floor(Settings.data.bar.backgroundOpacity * 100) + "%" + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-font-scale-label") + description: I18n.tr("panels.bar.appearance-font-scale-description") + from: 0.5 + to: 2.0 + stepSize: 0.01 + showReset: true + value: Settings.data.bar.fontScale + defaultValue: Settings.getDefaultValue("bar.fontScale") + onMoved: value => Settings.data.bar.fontScale = value + text: Math.floor(Settings.data.bar.fontScale * 100) + "%" + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-widget-spacing-label") + description: I18n.tr("panels.bar.appearance-widget-spacing-description") + from: 0 + to: 30 + stepSize: 1 + showReset: true + value: Settings.data.bar.widgetSpacing + defaultValue: Settings.getDefaultValue("bar.widgetSpacing") + onMoved: value => Settings.data.bar.widgetSpacing = value + text: Settings.data.bar.widgetSpacing + "px" + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-content-padding-label") + description: I18n.tr("panels.bar.appearance-content-padding-description") + from: 0 + to: 30 + stepSize: 1 + showReset: true + value: Settings.data.bar.contentPadding + defaultValue: Settings.getDefaultValue("bar.contentPadding") + onMoved: value => Settings.data.bar.contentPadding = value + text: Settings.data.bar.contentPadding + "px" + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-show-outline-label") + description: I18n.tr("panels.bar.appearance-show-outline-description") + checked: Settings.data.bar.showOutline + defaultValue: Settings.getDefaultValue("bar.showOutline") + onToggled: checked => Settings.data.bar.showOutline = checked + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-show-capsule-label") + description: I18n.tr("panels.bar.appearance-show-capsule-description") + checked: Settings.data.bar.showCapsule + defaultValue: Settings.getDefaultValue("bar.showCapsule") + onToggled: checked => Settings.data.bar.showCapsule = checked + } + + NColorChoice { + Layout.fillWidth: true + visible: Settings.data.bar.showCapsule + label: I18n.tr("panels.bar.appearance-capsule-color-label") + description: I18n.tr("panels.bar.appearance-capsule-color-description") + noneColor: Color.mSurfaceVariant + noneOnColor: Color.mOnSurfaceVariant + currentKey: Settings.data.bar.capsuleColorKey + onSelected: key => Settings.data.bar.capsuleColorKey = key + } + + NValueSlider { + Layout.fillWidth: true + visible: Settings.data.bar.showCapsule + label: I18n.tr("panels.bar.appearance-capsule-opacity-label") + description: I18n.tr("panels.bar.appearance-capsule-opacity-description") + from: 0 + to: 1 + stepSize: 0.01 + showReset: true + value: Settings.data.bar.capsuleOpacity + defaultValue: Settings.getDefaultValue("bar.capsuleOpacity") + onMoved: value => Settings.data.bar.capsuleOpacity = value + text: Math.floor(Settings.data.bar.capsuleOpacity * 100) + "%" + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-enable-exclusion-zone-inset-label") + description: I18n.tr("panels.bar.appearance-enable-exclusion-zone-inset-description") + checked: Settings.data.bar.enableExclusionZoneInset + defaultValue: Settings.getDefaultValue("bar.enableExclusionZoneInset") + onToggled: checked => Settings.data.bar.enableExclusionZoneInset = checked + } + + NToggle { + Layout.fillWidth: true + visible: CompositorService.isNiri + label: I18n.tr("panels.bar.appearance-hide-on-overview-label") + description: I18n.tr("panels.bar.appearance-hide-on-overview-description") + checked: Settings.data.bar.hideOnOverview + defaultValue: Settings.getDefaultValue("bar.hideOnOverview") + onToggled: checked => Settings.data.bar.hideOnOverview = checked + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-outer-corners-label") + description: I18n.tr("panels.bar.appearance-outer-corners-description") + checked: Settings.data.bar.outerCorners + visible: Settings.data.bar.barType === "simple" + defaultValue: Settings.getDefaultValue("bar.outerCorners") + onToggled: checked => Settings.data.bar.outerCorners = checked + } + + ColumnLayout { + visible: Settings.data.bar.barType === "framed" + spacing: Style.marginS + Layout.fillWidth: true + + NLabel { + label: I18n.tr("panels.bar.appearance-frame-settings-label") + description: I18n.tr("panels.bar.appearance-frame-settings-description") + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginL + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-frame-thickness") + from: 4 + to: 24 + stepSize: 1 + showReset: true + value: Settings.data.bar.frameThickness + defaultValue: Settings.getDefaultValue("bar.frameThickness") + onMoved: value => Settings.data.bar.frameThickness = value + text: Settings.data.bar.frameThickness + "px" + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-frame-radius") + from: 4 + to: 24 + stepSize: 1 + showReset: true + value: Settings.data.bar.frameRadius + defaultValue: Settings.getDefaultValue("bar.frameRadius") + onMoved: value => Settings.data.bar.frameRadius = value + text: Settings.data.bar.frameRadius + "px" + } + } + } + + ColumnLayout { + visible: Settings.data.bar.barType === "floating" + spacing: Style.marginL + Layout.fillWidth: true + + NDivider { + Layout.fillWidth: true + } + + NSpinBox { + label: I18n.tr("panels.bar.appearance-margins-vertical") + description: I18n.tr("panels.bar.appearance-margins-description") + from: 0 + to: 500 + suffix: "px" + value: Settings.data.bar.marginVertical + defaultValue: Settings.getDefaultValue("bar.marginVertical") + onValueChanged: Settings.data.bar.marginVertical = value + } + + NSpinBox { + label: I18n.tr("panels.bar.appearance-margins-horizontal") + description: I18n.tr("panels.bar.appearance-margins-description") + from: 0 + to: 500 + suffix: "px" + value: Settings.data.bar.marginHorizontal + defaultValue: Settings.getDefaultValue("bar.marginHorizontal") + onValueChanged: Settings.data.bar.marginHorizontal = value + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginS + visible: Settings.data.bar.displayMode === "auto_hide" + } + + ColumnLayout { + visible: Settings.data.bar.displayMode === "auto_hide" + spacing: Style.marginS + Layout.fillWidth: true + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-auto-hide-delay-label") + description: I18n.tr("panels.bar.appearance-auto-hide-delay-description") + from: 100 + to: 2000 + stepSize: 100 + showReset: true + value: Settings.data.bar.autoHideDelay + defaultValue: Settings.getDefaultValue("bar.autoHideDelay") + onMoved: value => Settings.data.bar.autoHideDelay = value + text: Settings.data.bar.autoHideDelay + "ms" + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-auto-show-delay-label") + description: I18n.tr("panels.bar.appearance-auto-show-delay-description") + from: 0 + to: 500 + stepSize: 50 + showReset: true + value: Settings.data.bar.autoShowDelay + defaultValue: Settings.getDefaultValue("bar.autoShowDelay") + onMoved: value => Settings.data.bar.autoShowDelay = value + text: Settings.data.bar.autoShowDelay + "ms" + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-show-on-workspace-switch-label") + description: I18n.tr("panels.bar.appearance-show-on-workspace-switch-description") + checked: Settings.data.bar.showOnWorkspaceSwitch + defaultValue: Settings.getDefaultValue("bar.showOnWorkspaceSwitch") + onToggled: checked => Settings.data.bar.showOnWorkspaceSwitch = checked + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Bar/BarTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Bar/BarTab.qml new file mode 100644 index 0000000..3cdf719 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Bar/BarTab.qml @@ -0,0 +1,234 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Noctalia +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + // Helper functions to update arrays immutably + function addMonitor(list, name) { + const arr = (list || []).slice(); + if (!arr.includes(name)) + arr.push(name); + return arr; + } + function removeMonitor(list, name) { + return (list || []).filter(function (n) { + return n !== name; + }); + } + + // Signal functions for widgets sub-tab (global widgets only). + // These intentionally edit Settings.data.bar.widgets (global defaults), + // not per-screen overrides. Per-screen editing is handled by MonitorWidgetsConfig.qml. + function _addWidgetToSection(widgetId, section) { + var newWidget = { + "id": widgetId + }; + if (BarWidgetRegistry.widgetHasUserSettings(widgetId)) { + var metadata = BarWidgetRegistry.widgetMetadata[widgetId]; + if (metadata) { + Object.keys(metadata).forEach(function (key) { + newWidget[key] = metadata[key]; + }); + } + } + Settings.data.bar.widgets[section].push(newWidget); + BarService.widgetsRevision++; + } + + function _removeWidgetFromSection(section, index) { + var widgets = Settings.data.bar.widgets; + if (index >= 0 && index < widgets[section].length) { + var newArray = widgets[section].slice(); + var removedWidgets = newArray.splice(index, 1); + widgets[section] = newArray; + BarService.widgetsRevision++; + + if (removedWidgets[0].id === "ControlCenter" && BarService.lookupWidget("ControlCenter") === undefined) { + ToastService.showWarning(I18n.tr("toast.missing-control-center.label"), I18n.tr("toast.missing-control-center.description"), 6000); + } + } + } + + function _reorderWidgetInSection(section, fromIndex, toIndex) { + var widgets = Settings.data.bar.widgets; + if (fromIndex >= 0 && fromIndex < widgets[section].length && toIndex >= 0 && toIndex < widgets[section].length) { + var newArray = widgets[section].slice(); + var item = newArray[fromIndex]; + newArray.splice(fromIndex, 1); + newArray.splice(toIndex, 0, item); + widgets[section] = newArray; + BarService.widgetsRevision++; + } + } + + // Note: _updateWidgetSettingsInSection does NOT increment revision + // because it only changes settings, not widget structure + function _updateWidgetSettingsInSection(section, index, settings) { + Settings.data.bar.widgets[section][index] = settings; + } + + function _moveWidgetBetweenSections(fromSection, index, toSection) { + var widgets = Settings.data.bar.widgets; + if (index >= 0 && index < widgets[fromSection].length) { + var widget = widgets[fromSection][index]; + var sourceArray = widgets[fromSection].slice(); + sourceArray.splice(index, 1); + widgets[fromSection] = sourceArray; + var targetArray = widgets[toSection].slice(); + targetArray.push(widget); + widgets[toSection] = targetArray; + BarService.widgetsRevision++; + Logger.d("BarTab", "_moveWidgetBetweenSections: revision now", BarService.widgetsRevision); + } + } + + function getWidgetLocations(widgetId) { + if (!BarService) + return []; + const instances = BarService.getAllRegisteredWidgets(); + const locations = {}; + for (var i = 0; i < instances.length; i++) { + if (instances[i].widgetId === widgetId) { + const section = instances[i].section; + if (section === "left") + locations["arrow-bar-to-left"] = true; + else if (section === "center") + locations["layout-columns"] = true; + else if (section === "right") + locations["arrow-bar-to-right"] = true; + } + } + return Object.keys(locations); + } + + function createBadges(isPlugin, locations) { + const badges = []; + if (isPlugin) { + badges.push({ + "icon": "plugin", + "color": Color.mSecondary + }); + } + locations.forEach(function (location) { + badges.push({ + "icon": location, + "color": Color.mOnSurfaceVariant + }); + }); + return badges; + } + + function updateAvailableWidgetsModel() { + availableWidgets.clear(); + const widgets = BarWidgetRegistry.getAvailableWidgets(); + widgets.forEach(entry => { + const isPlugin = BarWidgetRegistry.isPluginWidget(entry); + let displayName = entry; + if (isPlugin) { + const pluginId = entry.replace("plugin:", ""); + const manifest = PluginRegistry.getPluginManifest(pluginId); + if (manifest && manifest.name) { + displayName = manifest.name; + } else { + displayName = pluginId; + } + } + availableWidgets.append({ + "key": entry, + "name": displayName, + "badges": createBadges(isPlugin, getWidgetLocations(entry)) + }); + }); + } + + ListModel { + id: availableWidgets + } + + Component.onCompleted: { + Qt.callLater(updateAvailableWidgetsModel); + } + + Connections { + target: BarService + function onActiveWidgetsChanged() { + updateAvailableWidgetsModel(); + } + } + + Connections { + target: BarWidgetRegistry + function onPluginWidgetRegistryUpdated() { + updateAvailableWidgetsModel(); + } + } + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.appearance") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.widgets") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + NTabButton { + text: I18n.tr("common.behavior") + tabIndex: 2 + checked: subTabBar.currentIndex === 2 + } + NTabButton { + text: I18n.tr("common.monitors") + tabIndex: 3 + checked: subTabBar.currentIndex === 3 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginS + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + AppearanceSubTab {} + WidgetsSubTab { + availableWidgets: availableWidgets + addWidgetToSection: root._addWidgetToSection + removeWidgetFromSection: root._removeWidgetFromSection + reorderWidgetInSection: root._reorderWidgetInSection + updateWidgetSettingsInSection: root._updateWidgetSettingsInSection + moveWidgetBetweenSections: root._moveWidgetBetweenSections + onOpenPluginSettings: manifest => pluginSettingsDialog.openPluginSettings(manifest) + } + BehaviorSubTab {} + MonitorsSubTab { + addMonitor: root.addMonitor + removeMonitor: root.removeMonitor + } + } + + NPluginSettingsPopup { + id: pluginSettingsDialog + parent: Overlay.overlay + showToastOnSave: false + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Bar/BehaviorSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Bar/BehaviorSubTab.qml new file mode 100644 index 0000000..326ae71 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Bar/BehaviorSubTab.qml @@ -0,0 +1,172 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.Compositor +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + readonly property string effectiveWheelAction: Settings.data.bar.mouseWheelAction || "none" + readonly property string effectiveMiddleClickAction: Settings.data.bar.middleClickAction || "none" + readonly property string effectiveRightClickAction: Settings.data.bar.rightClickAction || "controlCenter" + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.bar.behavior-workspace-scroll-label") + description: I18n.tr("panels.bar.behavior-workspace-scroll-description") + model: { + var items = [ + { + "key": "none", + "name": I18n.tr("common.none") + }, + { + "key": "volume", + "name": I18n.tr("common.volume") + }, + { + "key": "workspace", + "name": I18n.tr("panels.bar.behavior-workspace-scroll-option-workspace") + } + ]; + if (CompositorService.isNiri) { + items.push({ + "key": "content", + "name": I18n.tr("panels.bar.behavior-workspace-scroll-option-content") + }); + } + return items; + } + currentKey: root.effectiveWheelAction + defaultValue: Settings.getDefaultValue("bar.mouseWheelAction") + onSelected: key => Settings.data.bar.mouseWheelAction = key + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.general.reverse-scrolling-label") + description: I18n.tr("panels.general.reverse-scrolling-description") + checked: Settings.data.bar.reverseScroll + defaultValue: Settings.getDefaultValue("bar.reverseScroll") + onToggled: checked => Settings.data.bar.reverseScroll = checked + visible: Settings.data.bar.mouseWheelAction !== "none" + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.bar.behavior-wheel-wrap-label") + description: I18n.tr("panels.bar.behavior-wheel-wrap-description") + checked: Settings.data.bar.mouseWheelWrap + defaultValue: Settings.getDefaultValue("bar.mouseWheelWrap") + onToggled: checked => Settings.data.bar.mouseWheelWrap = checked + visible: Settings.data.bar.mouseWheelAction === "workspace" + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.bar.behavior-middle-click-label") + description: I18n.tr("panels.bar.behavior-middle-click-description") + model: [ + { + "key": "none", + "name": I18n.tr("common.none") + }, + { + "key": "controlCenter", + "name": I18n.tr("tooltips.open-control-center") + }, + { + "key": "settings", + "name": I18n.tr("tooltips.open-settings") + }, + { + "key": "launcherPanel", + "name": I18n.tr("actions.open-launcher") + }, + { + "key": "command", + "name": I18n.tr("actions.run-custom-command") + } + ] + currentKey: root.effectiveMiddleClickAction + defaultValue: Settings.getDefaultValue("bar.middleClickAction") + onSelected: key => Settings.data.bar.middleClickAction = key + } + + NTextInput { + Layout.fillWidth: true + label: I18n.tr("panels.bar.behavior-middle-click-command-label") + description: I18n.tr("panels.bar.behavior-middle-click-command-description") + placeholderText: I18n.tr("panels.bar.behavior-middle-click-command-placeholder") + text: Settings.data.bar.middleClickCommand + fontFamily: Settings.data.ui.fontFixed + onTextChanged: Settings.data.bar.middleClickCommand = text + visible: Settings.data.bar.middleClickAction === "command" + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.bar.behavior-middle-click-follow-mouse-label") + description: I18n.tr("panels.bar.behavior-middle-click-follow-mouse-description") + checked: Settings.data.bar.middleClickFollowMouse + defaultValue: Settings.getDefaultValue("bar.middleClickFollowMouse") + onToggled: checked => Settings.data.bar.middleClickFollowMouse = checked + visible: Settings.data.bar.middleClickAction !== "none" && Settings.data.bar.middleClickAction !== "command" && !(Settings.data.bar.middleClickAction === "settings" && Settings.data.ui.settingsPanelMode === "window") + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.bar.behavior-right-click-label") + description: I18n.tr("panels.bar.behavior-right-click-description") + model: [ + { + "key": "none", + "name": I18n.tr("common.none") + }, + { + "key": "controlCenter", + "name": I18n.tr("tooltips.open-control-center") + }, + { + "key": "settings", + "name": I18n.tr("tooltips.open-settings") + }, + { + "key": "launcherPanel", + "name": I18n.tr("actions.open-launcher") + }, + { + "key": "command", + "name": I18n.tr("actions.run-custom-command") + } + ] + currentKey: root.effectiveRightClickAction + defaultValue: Settings.getDefaultValue("bar.rightClickAction") + onSelected: key => Settings.data.bar.rightClickAction = key + } + + NTextInput { + Layout.fillWidth: true + label: I18n.tr("panels.bar.behavior-right-click-command-label") + description: I18n.tr("panels.bar.behavior-right-click-command-description") + placeholderText: I18n.tr("panels.bar.behavior-right-click-command-placeholder") + text: Settings.data.bar.rightClickCommand + fontFamily: Settings.data.ui.fontFixed + onTextChanged: Settings.data.bar.rightClickCommand = text + visible: Settings.data.bar.rightClickAction === "command" + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.bar.behavior-right-click-follow-mouse-label") + description: I18n.tr("panels.bar.behavior-right-click-follow-mouse-description") + checked: Settings.data.bar.rightClickFollowMouse + defaultValue: Settings.getDefaultValue("bar.rightClickFollowMouse") + onToggled: checked => Settings.data.bar.rightClickFollowMouse = checked + visible: Settings.data.bar.rightClickAction !== "none" && Settings.data.bar.rightClickAction !== "command" && !(Settings.data.bar.rightClickAction === "settings" && Settings.data.ui.settingsPanelMode === "window") + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Bar/MonitorsSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Bar/MonitorsSubTab.qml new file mode 100644 index 0000000..c6ad380 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Bar/MonitorsSubTab.qml @@ -0,0 +1,276 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import "../../Bar" as BarSettings +import qs.Commons +import qs.Services.Compositor +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + property var addMonitor + property var removeMonitor + + NText { + text: I18n.tr("panels.bar.monitors-desc-new") + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + // Monitor cards + Repeater { + model: Quickshell.screens || [] + delegate: NBox { + id: monitorCard + Layout.fillWidth: true + implicitHeight: cardContent.implicitHeight + Style.margin2L + color: Color.mSurface + + required property var modelData + readonly property string screenName: modelData.name || "Unknown" + readonly property real compositorScale: { + const info = CompositorService.displayScales[screenName]; + return (info && info.scale) ? info.scale : 1.0; + } + readonly property bool barEnabled: (Settings.data.bar.monitors || []).indexOf(screenName) !== -1 + readonly property bool hasOverride: Settings.hasScreenOverride(screenName) + + // Track if override is enabled (controls both visibility AND whether overrides are applied) + readonly property bool overrideEnabled: Settings.isScreenOverrideEnabled(screenName) + + // Get effective values for this screen + readonly property string effectivePosition: Settings.getBarPositionForScreen(screenName) + readonly property string effectiveDensity: Settings.getBarDensityForScreen(screenName) + + ColumnLayout { + id: cardContent + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + RowLayout { + Layout.fillWidth: true + + // Header: Monitor name and specs + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXXS + + NText { + Layout.fillWidth: true + text: monitorCard.screenName + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: { + return I18n.tr("system.monitor-description", { + "model": monitorCard.modelData.model || I18n.tr("common.unknown"), + "width": Math.round(monitorCard.modelData.width * monitorCard.compositorScale), + "height": Math.round(monitorCard.modelData.height * monitorCard.compositorScale), + "scale": monitorCard.compositorScale + }); + } + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + + // Enable bar toggle + NToggle { + Layout.fillWidth: true + checked: monitorCard.barEnabled + onToggled: checked => { + if (checked) { + Settings.data.bar.monitors = root.addMonitor(Settings.data.bar.monitors, monitorCard.screenName); + } else { + Settings.data.bar.monitors = root.removeMonitor(Settings.data.bar.monitors, monitorCard.screenName); + } + } + } + } + + NDivider { + Layout.fillWidth: true + visible: Settings.data.bar.monitors.includes(monitorCard.screenName) + } + + // Override section (only visible when bar is enabled) + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + visible: monitorCard.barEnabled + + // Override toggle + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.bar.monitor-override-settings") + description: I18n.tr("panels.bar.monitor-override-settings-description") + checked: monitorCard.overrideEnabled + onToggled: checked => { + Settings.setScreenOverride(monitorCard.screenName, "enabled", checked); + BarService.widgetsRevision++; + } + } + + // Override controls (only visible when override toggle is on) + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + visible: monitorCard.overrideEnabled + + // Position override + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-position-label") + description: I18n.tr("panels.bar.appearance-position-description") + model: [ + { + "key": "top", + "name": I18n.tr("positions.top") + }, + { + "key": "bottom", + "name": I18n.tr("positions.bottom") + }, + { + "key": "left", + "name": I18n.tr("positions.left") + }, + { + "key": "right", + "name": I18n.tr("positions.right") + } + ] + currentKey: monitorCard.effectivePosition + onSelected: key => Settings.setScreenOverride(monitorCard.screenName, "position", key) + } + } + + // Density override + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.bar.appearance-density-label") + description: I18n.tr("panels.bar.appearance-density-description") + model: [ + { + "key": "mini", + "name": I18n.tr("options.bar.density-mini") + }, + { + "key": "compact", + "name": I18n.tr("options.bar.density-compact") + }, + { + "key": "default", + "name": I18n.tr("options.bar.density-default") + }, + { + "key": "comfortable", + "name": I18n.tr("options.bar.density-comfortable") + }, + { + "key": "spacious", + "name": I18n.tr("options.bar.density-spacious") + } + ] + currentKey: monitorCard.effectiveDensity + onSelected: key => Settings.setScreenOverride(monitorCard.screenName, "density", key) + } + } + + // DisplayMode override + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("common.display-mode") + description: I18n.tr("panels.bar.appearance-display-mode-description") + model: [ + { + "key": "always_visible", + "name": I18n.tr("hide-modes.visible") + }, + { + "key": "non_exclusive", + "name": I18n.tr("hide-modes.non-exclusive") + }, + { + "key": "auto_hide", + "name": I18n.tr("hide-modes.auto-hide") + } + ] + currentKey: Settings.getBarDisplayModeForScreen(monitorCard.screenName) + onSelected: key => Settings.setScreenOverride(monitorCard.screenName, "displayMode", key) + } + } + + // Widgets configuration button and Reset all + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NButton { + id: widgetConfigButton + property bool expanded: false + Layout.fillWidth: true + fontSize: Style.fontSizeS + text: I18n.tr("panels.bar.monitor-configure-widgets") + icon: expanded ? "chevron-up" : "layout-grid" + onClicked: expanded = !expanded + } + + NButton { + visible: Settings.hasScreenOverride(monitorCard.screenName, "widgets") + Layout.fillWidth: true + fontSize: Style.fontSizeS + text: I18n.tr("panels.bar.use-global-widgets") + icon: "refresh" + onClicked: { + Settings.clearScreenOverride(monitorCard.screenName, "widgets"); + BarService.widgetsRevision++; + } + } + + NButton { + Layout.fillWidth: true + fontSize: Style.fontSizeS + text: I18n.tr("panels.bar.monitor-reset-all") + icon: "restore" + onClicked: { + Settings.clearScreenOverride(monitorCard.screenName); + BarService.widgetsRevision++; + } + } + } + + // Inline widget configuration + BarSettings.MonitorWidgetsConfig { + visible: widgetConfigButton.expanded + screen: monitorCard.modelData + Layout.fillWidth: true + Layout.topMargin: Style.marginS + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Bar/WidgetsSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Bar/WidgetsSubTab.qml new file mode 100644 index 0000000..5eaf510 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Bar/WidgetsSubTab.qml @@ -0,0 +1,97 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + property var availableWidgets + property var addWidgetToSection + property var removeWidgetFromSection + property var reorderWidgetInSection + property var updateWidgetSettingsInSection + property var moveWidgetBetweenSections + + signal openPluginSettings(var manifest) + + // This sub-tab edits the global default widget configuration (Settings.data.bar.widgets). + // Per-screen widget overrides are edited in MonitorWidgetsConfig.qml (Monitors sub-tab). + + // determine bar orientation + readonly property string barPosition: Settings.data.bar.position + readonly property bool barIsVertical: barPosition === "left" || barPosition === "right" + + function getSectionIcons() { + return { + "left": "arrow-bar-to-up", + "center": "layout-distribute-horizontal", + "right": "arrow-bar-to-down" + }; + } + + NText { + text: I18n.tr("panels.bar.widgets-desc") + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + // Left Section + NSectionEditor { + sectionName: root.barIsVertical ? I18n.tr("positions.top") : I18n.tr("positions.left") + sectionId: "left" + barIsVertical: root.barIsVertical + settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml") + widgetRegistry: BarWidgetRegistry + widgetModel: Settings.data.bar.widgets.left + sectionIcons: root.getSectionIcons() + availableWidgets: root.availableWidgets + onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section) + onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index) + onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex) + onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings) + onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection) + onOpenPluginSettingsRequested: manifest => root.openPluginSettings(manifest) + } + + // Center Section + NSectionEditor { + sectionName: I18n.tr("positions.center") + sectionId: "center" + barIsVertical: root.barIsVertical + settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml") + widgetRegistry: BarWidgetRegistry + widgetModel: Settings.data.bar.widgets.center + sectionIcons: root.getSectionIcons() + availableWidgets: root.availableWidgets + onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section) + onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index) + onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex) + onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings) + onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection) + onOpenPluginSettingsRequested: manifest => root.openPluginSettings(manifest) + } + + // Right Section + NSectionEditor { + sectionName: root.barIsVertical ? I18n.tr("positions.bottom") : I18n.tr("positions.right") + sectionId: "right" + barIsVertical: root.barIsVertical + settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml") + widgetRegistry: BarWidgetRegistry + widgetModel: Settings.data.bar.widgets.right + sectionIcons: root.getSectionIcons() + availableWidgets: root.availableWidgets + onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section) + onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index) + onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex) + onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings) + onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection) + onOpenPluginSettingsRequested: manifest => root.openPluginSettings(manifest) + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ColorScheme/ColorSchemeTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ColorScheme/ColorSchemeTab.qml new file mode 100644 index 0000000..3bd4543 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ColorScheme/ColorSchemeTab.qml @@ -0,0 +1,104 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import "." +import qs.Commons +import qs.Services.System +import qs.Services.Theming +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + // Time dropdown options (00:00 .. 23:30) + ListModel { + id: timeOptions + } + + property var screen + + function populateTimeOptions() { + for (var h = 0; h < 24; h++) { + for (var m = 0; m < 60; m += 30) { + var hh = ("0" + h).slice(-2); + var mm = ("0" + m).slice(-2); + var key = hh + ":" + mm; + timeOptions.append({ + "key": key, + "name": key + }); + } + } + } + + Component.onCompleted: { + Qt.callLater(populateTimeOptions); + } + + // Download popup + Loader { + id: downloadPopupLoader + active: false + sourceComponent: SchemeDownloader { + parent: Overlay.overlay + } + + property bool pendingOpen: false + + function open() { + pendingOpen = true; + active = true; + if (item) { + item.open(); + pendingOpen = false; + } + } + + onItemChanged: { + if (item && pendingOpen) { + item.open(); + pendingOpen = false; + } + } + } + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.colors") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.templates") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + ColorsSubTab { + screen: root.screen + timeOptions: timeOptions + onOpenDownloadPopup: downloadPopupLoader.open() + } + TemplatesSubTab {} + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ColorScheme/ColorsSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ColorScheme/ColorsSubTab.qml new file mode 100644 index 0000000..37891d1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ColorScheme/ColorsSubTab.qml @@ -0,0 +1,458 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.System +import qs.Services.Theming +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + property var timeOptions + property var schemeColorsCache: ({}) + property int cacheVersion: 0 + property var screen + + signal openDownloadPopup + + function extractSchemeName(schemePath) { + var pathParts = schemePath.split("/"); + var filename = pathParts[pathParts.length - 1]; + var schemeName = filename.replace(".json", ""); + + if (schemeName === "Noctalia-default") { + schemeName = "Noctalia (default)"; + } else if (schemeName === "Noctalia-legacy") { + schemeName = "Noctalia (legacy)"; + } else if (schemeName === "Tokyo-Night") { + schemeName = "Tokyo Night"; + } else if (schemeName === "Rosepine") { + schemeName = "Rose Pine"; + } + + return schemeName; + } + + function getSchemeColor(schemeName, colorKey) { + var _ = cacheVersion; + + if (schemeColorsCache[schemeName]) { + var entry = schemeColorsCache[schemeName]; + var variant = entry; + + if (entry.dark || entry.light) { + variant = Settings.data.colorSchemes.darkMode ? (entry.dark || entry.light) : (entry.light || entry.dark); + } + + if (variant && variant[colorKey]) { + return variant[colorKey]; + } + } + + if (colorKey === "mSurface") + return Color.mSurfaceVariant; + if (colorKey === "mPrimary") + return Color.mPrimary; + if (colorKey === "mSecondary") + return Color.mSecondary; + if (colorKey === "mTertiary") + return Color.mTertiary; + if (colorKey === "mError") + return Color.mError; + return Color.mOnSurfaceVariant; + } + + function schemeLoaded(schemeName, jsonData) { + var value = jsonData || {}; + schemeColorsCache[schemeName] = value; + cacheVersion++; + } + + Connections { + target: ColorSchemeService + function onSchemesChanged() { + root.schemeColorsCache = {}; + root.cacheVersion++; + } + } + + Item { + id: fileLoaders + visible: false + + Repeater { + model: ColorSchemeService.schemes + delegate: Item { + FileView { + path: modelData + blockLoading: false + onLoaded: { + var schemeName = root.extractSchemeName(path); + + try { + var jsonData = JSON.parse(text()); + root.schemeLoaded(schemeName, jsonData); + } catch (e) { + Logger.w("ColorSchemeTab", "Failed to parse JSON for scheme:", schemeName, e); + root.schemeLoaded(schemeName, null); + } + } + } + } + } + } + + NToggle { + label: I18n.tr("tooltips.switch-to-dark-mode") + description: I18n.tr("panels.color-scheme.dark-mode-switch-description") + checked: Settings.data.colorSchemes.darkMode + onToggled: checked => { + Settings.data.colorSchemes.darkMode = checked; + root.cacheVersion++; + } + } + + NToggle { + label: I18n.tr("panels.color-scheme.sync-gsettings-label") + description: I18n.tr("panels.color-scheme.sync-gsettings-description") + checked: Settings.data.colorSchemes.syncGsettings + onToggled: checked => { + Settings.data.colorSchemes.syncGsettings = checked; + if (checked) + ColorSchemeService.pushSystemColorScheme(); + } + } + + NComboBox { + label: I18n.tr("panels.color-scheme.dark-mode-mode-label") + description: I18n.tr("panels.color-scheme.dark-mode-mode-description") + + model: [ + { + "name": I18n.tr("panels.color-scheme.dark-mode-mode-off"), + "key": "off" + }, + { + "name": I18n.tr("panels.color-scheme.dark-mode-mode-manual"), + "key": "manual" + }, + { + "name": I18n.tr("common.location"), + "key": "location" + } + ] + + currentKey: Settings.data.colorSchemes.schedulingMode + + onSelected: key => { + Settings.data.colorSchemes.schedulingMode = key; + AppThemeService.generate(); + } + } + + ColumnLayout { + spacing: Style.marginS + visible: Settings.data.colorSchemes.schedulingMode === "manual" + + NLabel { + label: I18n.tr("panels.display.night-light-manual-schedule-label") + description: I18n.tr("panels.display.night-light-manual-schedule-description") + } + + RowLayout { + Layout.fillWidth: false + spacing: Style.marginS + + NText { + text: I18n.tr("panels.display.night-light-manual-schedule-sunrise") + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + + NComboBox { + model: root.timeOptions + currentKey: Settings.data.colorSchemes.manualSunrise + placeholder: I18n.tr("panels.display.night-light-manual-schedule-select-start") + onSelected: key => Settings.data.colorSchemes.manualSunrise = key + minimumWidth: 120 + } + + Item { + Layout.preferredWidth: 20 + } + + NText { + text: I18n.tr("panels.display.night-light-manual-schedule-sunset") + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + + NComboBox { + model: root.timeOptions + currentKey: Settings.data.colorSchemes.manualSunset + placeholder: I18n.tr("panels.display.night-light-manual-schedule-select-stop") + onSelected: key => Settings.data.colorSchemes.manualSunset = key + minimumWidth: 120 + } + } + } + + NDivider { + Layout.fillWidth: true + } + + NToggle { + label: I18n.tr("panels.color-scheme.color-source-use-wallpaper-colors-label") + description: I18n.tr("panels.color-scheme.color-source-use-wallpaper-colors-description") + checked: Settings.data.colorSchemes.useWallpaperColors + onToggled: checked => { + Settings.data.colorSchemes.useWallpaperColors = checked; + if (checked) { + AppThemeService.generate(); + } else { + ToastService.showNotice(I18n.tr("toast.wallpaper-colors.label"), I18n.tr("toast.wallpaper-colors.disabled"), "settings-color-scheme"); + if (Settings.data.colorSchemes.predefinedScheme) { + ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme); + } + } + } + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.color-scheme.wallpaper-monitor-source-label") + description: I18n.tr("panels.color-scheme.wallpaper-monitor-source-description") + enabled: Settings.data.colorSchemes.useWallpaperColors + model: { + var m = []; + if (Quickshell.screens) { + for (var i = 0; i < Quickshell.screens.length; i++) { + var screen = Quickshell.screens[i]; + var name = screen.name; + var displayName = name + " (" + screen.width + "x" + screen.height + ")"; + m.push({ + "key": name, + "name": displayName + }); + } + } + return m; + } + currentKey: Settings.data.colorSchemes.monitorForColors || (screen ? screen.name : "") + onSelected: key => { + Settings.data.colorSchemes.monitorForColors = key; + AppThemeService.generate(); + } + defaultValue: "" + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.color-scheme.wallpaper-method-label") + description: I18n.tr("panels.color-scheme.wallpaper-method-description") + enabled: Settings.data.colorSchemes.useWallpaperColors + model: TemplateProcessor.schemeTypes + currentKey: Settings.data.colorSchemes.generationMethod + onSelected: key => { + Settings.data.colorSchemes.generationMethod = key; + AppThemeService.generate(); + } + } + + NBox { + visible: Settings.data.colorSchemes.useWallpaperColors + Layout.fillWidth: true + implicitHeight: descriptionColumn.implicitHeight + Style.margin2L + color: Color.mSurface + + Column { + id: descriptionColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginL + spacing: Style.marginM + + NText { + width: parent.width + wrapMode: Text.WordWrap + text: I18n.tr("panels.color-scheme.method-description." + Settings.data.colorSchemes.generationMethod) + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + + Row { + id: colorPreviewRow + spacing: Style.marginS + + property int diameter: 16 * Style.uiScaleRatio + + Repeater { + model: [Color.mPrimary, Color.mSecondary, Color.mTertiary, Color.mError] + + Rectangle { + width: colorPreviewRow.diameter + height: colorPreviewRow.diameter + radius: width * 0.5 + color: modelData + } + } + } + } + } + + NDivider { + Layout.fillWidth: true + } + + ColumnLayout { + spacing: Style.marginM + Layout.fillWidth: true + enabled: !Settings.data.colorSchemes.useWallpaperColors + + NHeader { + label: I18n.tr("panels.color-scheme.predefined-title") + description: I18n.tr("panels.color-scheme.predefined-desc") + Layout.fillWidth: true + } + + GridLayout { + columns: 2 + rowSpacing: Style.marginM + columnSpacing: Style.marginM + Layout.fillWidth: true + + Repeater { + model: ColorSchemeService.schemes + + Rectangle { + id: schemeItem + + property string schemePath: modelData + property string schemeName: root.extractSchemeName(modelData) + + opacity: enabled ? 1.0 : 0.6 + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + height: 50 * Style.uiScaleRatio + radius: Style.radiusS + color: root.getSchemeColor(schemeName, "mSurface") + border.width: Style.borderL + border.color: { + if ((Settings.data.colorSchemes.predefinedScheme === schemeName) && schemeItem.enabled) { + return Color.mSecondary; + } + if (itemMouseArea.containsMouse) { + return Color.mHover; + } + return Color.mOutline; + } + + RowLayout { + id: scheme + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginS + + NText { + text: schemeItem.schemeName + pointSize: Style.fontSizeS + color: Color.mOnSurface + Layout.fillWidth: true + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + wrapMode: Text.WordWrap + maximumLineCount: 1 + } + + property int diameter: 16 * Style.uiScaleRatio + + Rectangle { + width: scheme.diameter + height: scheme.diameter + radius: scheme.diameter * 0.5 + color: root.getSchemeColor(schemeItem.schemeName, "mPrimary") + } + + Rectangle { + width: scheme.diameter + height: scheme.diameter + radius: scheme.diameter * 0.5 + color: root.getSchemeColor(schemeItem.schemeName, "mSecondary") + } + + Rectangle { + width: scheme.diameter + height: scheme.diameter + radius: scheme.diameter * 0.5 + color: root.getSchemeColor(schemeItem.schemeName, "mTertiary") + } + + Rectangle { + width: scheme.diameter + height: scheme.diameter + radius: scheme.diameter * 0.5 + color: root.getSchemeColor(schemeItem.schemeName, "mError") + } + } + + MouseArea { + id: itemMouseArea + anchors.fill: parent + enabled: schemeItem.enabled + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.data.colorSchemes.useWallpaperColors = false; + Logger.i("ColorSchemeTab", "Disabled wallpaper colors"); + + Settings.data.colorSchemes.predefinedScheme = schemeItem.schemeName; + ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme); + } + } + + Rectangle { + visible: (Settings.data.colorSchemes.predefinedScheme === schemeItem.schemeName) && schemeItem.enabled + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: 0 + anchors.topMargin: -3 + width: 20 + height: 20 + radius: Math.min(Style.radiusL, width / 2) + color: Color.mSecondary + border.width: Style.borderS + border.color: Color.mOnSecondary + + NIcon { + icon: "check" + pointSize: Style.fontSizeXS + color: Color.mOnSecondary + anchors.centerIn: parent + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationNormal + } + } + } + } + } + + NButton { + text: I18n.tr("panels.color-scheme.download-button") + icon: "download" + onClicked: root.openDownloadPopup() + Layout.alignment: Qt.AlignRight + Layout.topMargin: Style.marginS + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ColorScheme/SchemeDownloader.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ColorScheme/SchemeDownloader.qml new file mode 100644 index 0000000..33defbd --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ColorScheme/SchemeDownloader.qml @@ -0,0 +1,972 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.Theming +import qs.Services.UI +import qs.Widgets + +Popup { + id: root + + property var availableSchemes: [] + property bool fetching: false + property bool downloading: false + property bool hasInitialData: false // Track if we've loaded data at least once + property string downloadError: "" + property string downloadingScheme: "" + property string pendingApplyScheme: "" // Scheme name to apply after reload + property string lastStderrOutput: "" // Store stderr from download process + property real lastApiFetchTime: 0 // Track when we last fetched from API to prevent rapid calls + property int minApiFetchInterval: 60 // Minimum seconds between API fetches (1 minute) + + // Cache for remote scheme colors + property var schemeColorsCache: ({}) + property int cacheVersion: 0 + + // Cache for available schemes list (uses ShellState singleton) + property int schemesCacheUpdateFrequency: 2 * 60 * 60 // 2 hours in seconds + + // Cache for repo branch info (to reduce API calls during downloads) + property string cachedBranch: "main" + property string cachedBranchSha: "" + + width: Math.max(500, contentColumn.implicitWidth + Style.margin2XL) + height: Math.min(800, contentColumn.implicitHeight + Style.margin2XL) + padding: Style.marginXL + modal: true + closePolicy: Popup.NoAutoClose + dim: false + anchors.centerIn: parent + + // Helper function to get color from cached scheme data + function getSchemeColor(schemeName, colorKey) { + var _ = cacheVersion; // Create dependency + + if (schemeColorsCache[schemeName]) { + var entry = schemeColorsCache[schemeName]; + var variant = entry; + + // Check if scheme has dark/light variants + if (entry.dark || entry.light) { + variant = Settings.data.colorSchemes.darkMode ? (entry.dark || entry.light) : (entry.light || entry.dark); + } + + if (variant && variant[colorKey]) { + return variant[colorKey]; + } + } + + // Return visible defaults while loading + var defaults = { + "mSurface": Color.mSurfaceVariant, + "mPrimary": Color.mPrimary, + "mSecondary": Color.mSecondary, + "mTertiary": Color.mTertiary, + "mError": Color.mError, + "mOnSurface": Color.mOnSurfaceVariant + }; + return defaults[colorKey] || Color.mOnSurfaceVariant; + } + + // Colors are now provided directly in the registry, no need to fetch individual files + function fetchSchemeColors(scheme) { + return; + } + + background: Rectangle { + color: Color.mSurface + radius: Style.radiusL + border.color: Color.mPrimary + border.width: Style.borderM + } + + function loadSchemesFromCache() { + try { + const now = Time.timestamp; + const cacheData = ShellState.getColorSchemesList(); + const cachedSchemes = cacheData.schemes || []; + const cachedTimestamp = cacheData.timestamp || 0; + + // Check if cache is expired or missing + if (!cachedTimestamp || (now >= cachedTimestamp + schemesCacheUpdateFrequency)) { + // Migration is now handled in Settings.qml + + // Only fetch from API if we haven't fetched recently (prevent rapid repeated calls) + const timeSinceLastFetch = now - lastApiFetchTime; + if (timeSinceLastFetch >= minApiFetchInterval) { + Logger.d("ColorSchemeDownload", "Cache expired or missing, fetching new schemes"); + fetchAvailableSchemesFromAPI(); + return; + } else { + // Use cached data even if expired, to avoid rate limits + Logger.d("ColorSchemeDownload", "Cache expired but recent API call detected, using cached data"); + if (cachedSchemes.length > 0) { + availableSchemes = cachedSchemes; + hasInitialData = true; + fetching = false; + return; + } + } + } + + const ageMinutes = Math.round((now - cachedTimestamp) / 60); + Logger.d("ColorSchemeDownload", "Loading cached schemes from ShellState (age:", ageMinutes, "minutes)"); + + if (cachedSchemes.length > 0) { + availableSchemes = cachedSchemes; + // Restore color cache from cached schemes + for (var i = 0; i < cachedSchemes.length; i++) { + var cachedScheme = cachedSchemes[i]; + if (cachedScheme.dark || cachedScheme.light) { + schemeColorsCache[cachedScheme.name] = { + "dark": cachedScheme.dark || null, + "light": cachedScheme.light || null + }; + } + } + hasInitialData = true; + cacheVersion++; + fetching = false; + } else { + // Cache is empty, only fetch if we haven't fetched recently + const timeSinceLastFetch = now - lastApiFetchTime; + if (timeSinceLastFetch >= minApiFetchInterval) { + fetchAvailableSchemesFromAPI(); + } else { + Logger.d("ColorSchemeDownload", "Cache empty but recent API call detected, skipping fetch"); + fetching = false; + } + } + } catch (error) { + Logger.e("ColorSchemeDownload", "Failed to load schemes from cache:", error); + fetching = false; + } + } + + function saveSchemesToCache() { + try { + ShellState.setColorSchemesList({ + schemes: availableSchemes, + timestamp: Time.timestamp + }); + Logger.d("ColorSchemeDownload", "Schemes list saved to ShellState"); + } catch (error) { + Logger.e("ColorSchemeDownload", "Failed to save schemes to cache:", error); + } + } + + function fetchAvailableSchemes() { + if (fetching) { + return; + } + + // Try to load from ShellState cache first + if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { + loadSchemesFromCache(); + } else { + // ShellState not ready, fetch directly from API + fetchAvailableSchemesFromAPI(); + } + } + + function fetchAvailableSchemesFromAPI() { + if (fetching) { + return; + } + + // Check if we've fetched recently to prevent rapid repeated calls + const now = Time.timestamp; + const timeSinceLastFetch = now - lastApiFetchTime; + if (timeSinceLastFetch < minApiFetchInterval) { + Logger.d("ColorSchemeDownload", "Skipping API fetch - too soon since last fetch (", Math.round(timeSinceLastFetch), "s ago)"); + return; + } + + fetching = true; + lastApiFetchTime = now; + // Don't clear availableSchemes immediately to prevent flicker - keep showing old list while fetching + // availableSchemes = []; + downloadError = ""; + + // Fetch registry.json + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + fetching = false; + if (xhr.status === 200) { + try { + var registry = JSON.parse(xhr.responseText); + if (registry && registry.themes && Array.isArray(registry.themes)) { + var schemes = []; + // Process themes + for (var i = 0; i < registry.themes.length; i++) { + var theme = registry.themes[i]; + schemes.push({ + "name": theme.name, + "path": theme.path, + "dark": theme.dark || null, + "light": theme.light || null + }); + schemeColorsCache[theme.name] = { + "dark": theme.dark || null, + "light": theme.light || null + }; + } + availableSchemes = schemes; + hasInitialData = true; + cacheVersion++; + Logger.d("ColorSchemeDownload", "Fetched", schemes.length, "available schemes from registry.json"); + // Save to cache + saveSchemesToCache(); + } else { + downloadError = I18n.tr("panels.color-scheme.download-error-invalid-response"); + Logger.e("ColorSchemeDownload", downloadError); + } + } catch (e) { + downloadError = I18n.tr("panels.color-scheme.download-error-parse-failed", { + "error": e.toString() + }); + Logger.e("ColorSchemeDownload", downloadError); + } + } else if (xhr.status === 403) { + // Rate limit hit - try to use cache if available + downloadError = I18n.tr("panels.color-scheme.download-error-rate-limit"); + Logger.w("ColorSchemeDownload", downloadError); + if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { + const cacheData = ShellState.getColorSchemesList(); + const cachedSchemes = cacheData.schemes || []; + if (cachedSchemes.length > 0) { + availableSchemes = cachedSchemes; + // Restore color cache from cached schemes + for (var j = 0; j < cachedSchemes.length; j++) { + var cachedScheme = cachedSchemes[j]; + if (cachedScheme.dark || cachedScheme.light) { + schemeColorsCache[cachedScheme.name] = { + "dark": cachedScheme.dark || null, + "light": cachedScheme.light || null + }; + } + } + hasInitialData = true; + cacheVersion++; + Logger.i("ColorSchemeDownload", "Using cached schemes due to rate limit"); + } + } + } else { + downloadError = I18n.tr("panels.color-scheme.download-error-api-error", { + "status": xhr.status + }); + Logger.e("ColorSchemeDownload", downloadError); + } + } + }; + + xhr.open("GET", "https://raw.githubusercontent.com/noctalia-dev/noctalia-colorschemes/main/registry.json"); + xhr.send(); + } + + function downloadScheme(scheme) { + if (downloading) { + return; + } + + downloading = true; + downloadingScheme = scheme.name; + downloadError = ""; + + Logger.i("ColorSchemeDownload", "Downloading scheme:", scheme.name); + + // Use cached branch/SHA if available, otherwise fetch + if (cachedBranchSha) { + // Use cached SHA directly + getSchemeTreeWithSha(scheme, cachedBranch, cachedBranchSha); + } else if (cachedBranch) { + // We have branch name, just need SHA + getSchemeTree(scheme, cachedBranch); + } else { + // Need to fetch branch info first + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + try { + var repoInfo = JSON.parse(xhr.responseText); + var defaultBranch = repoInfo.default_branch || "main"; + cachedBranch = defaultBranch; + // Now get the tree for the scheme directory + getSchemeTree(scheme, defaultBranch); + } catch (e) { + // Fallback: try to get files directly + getSchemeFilesDirect(scheme); + } + } else { + // Fallback: try to get files directly + getSchemeFilesDirect(scheme); + } + } + }; + xhr.open("GET", "https://api.github.com/repos/noctalia-dev/noctalia-colorschemes"); + xhr.send(); + } + } + + function getSchemeTree(scheme, branch) { + // First get the SHA of the branch + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + try { + var refResponse = JSON.parse(xhr.responseText); + var sha = refResponse.object ? refResponse.object.sha : null; + if (sha) { + // Cache the SHA for future downloads + cachedBranchSha = sha; + // Now get the tree + getSchemeTreeWithSha(scheme, branch, sha); + } else { + // Fallback to direct method + getSchemeFilesDirect(scheme); + } + } catch (e) { + // Fallback to direct method + getSchemeFilesDirect(scheme); + } + } else { + // Fallback to direct method + getSchemeFilesDirect(scheme); + } + } + }; + xhr.open("GET", "https://api.github.com/repos/noctalia-dev/noctalia-colorschemes/git/refs/heads/" + branch); + xhr.send(); + } + + function getSchemeTreeWithSha(scheme, branch, sha) { + // Use git trees API to get all files recursively + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + try { + var response = JSON.parse(xhr.responseText); + if (response.tree && Array.isArray(response.tree)) { + // Filter files that belong to this scheme + var files = []; + for (var i = 0; i < response.tree.length; i++) { + var item = response.tree[i]; + if (item.type === "blob" && item.path.startsWith(scheme.path + "/")) { + files.push({ + "path": item.path, + "url": "https://raw.githubusercontent.com/noctalia-dev/noctalia-colorschemes/" + branch + "/" + item.path, + "name": item.path.split("/").pop() + }); + } + } + downloadSchemeFiles(scheme.name, files); + } else { + // Fallback to direct method + getSchemeFilesDirect(scheme); + } + } catch (e) { + downloadError = I18n.tr("panels.color-scheme.download-error-parse-failed", { + "error": e.toString() + }); + downloading = false; + downloadingScheme = ""; + Logger.e("ColorSchemeDownload", downloadError); + } + } else { + // Fallback to direct method + getSchemeFilesDirect(scheme); + } + } + }; + xhr.open("GET", "https://api.github.com/repos/noctalia-dev/noctalia-colorschemes/git/trees/" + sha + "?recursive=1"); + xhr.send(); + } + + function getSchemeFilesDirect(scheme) { + // Fallback: get files directly using contents API (non-recursive, but works) + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + try { + var response = JSON.parse(xhr.responseText); + if (Array.isArray(response)) { + // Recursively get all files + getAllFilesRecursive(scheme, response, []); + } else { + downloadError = I18n.tr("panels.color-scheme.download-error-invalid-response"); + downloading = false; + downloadingScheme = ""; + Logger.e("ColorSchemeDownload", downloadError); + } + } catch (e) { + downloadError = I18n.tr("panels.color-scheme.download-error-parse-failed", { + "error": e.toString() + }); + downloading = false; + downloadingScheme = ""; + Logger.e("ColorSchemeDownload", downloadError); + } + } else { + downloadError = I18n.tr("panels.color-scheme.download-error-api-error", { + "status": xhr.status + }); + downloading = false; + downloadingScheme = ""; + Logger.e("ColorSchemeDownload", downloadError); + } + } + }; + xhr.open("GET", "https://api.github.com/repos/noctalia-dev/noctalia-colorschemes/contents/" + scheme.path); + xhr.send(); + } + + function getAllFilesRecursive(scheme, items, allFiles, callback) { + if (!callback) { + callback = function () { + downloadSchemeFiles(scheme.name, allFiles); + }; + } + + if (items.length === 0) { + callback(); + return; + } + + var pending = 0; + var completed = 0; + + function checkComplete() { + completed++; + if (completed === items.length && pending === 0) { + callback(); + } + } + + for (var i = 0; i < items.length; i++) { + var item = items[i]; + if (item.type === "file") { + allFiles.push({ + "path": item.path, + "url": item.download_url, + "name": item.name + }); + checkComplete(); + } else if (item.type === "dir") { + pending++; + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function (dirItem) { + return function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + pending--; + if (xhr.status === 200) { + try { + var dirResponse = JSON.parse(xhr.responseText); + if (Array.isArray(dirResponse)) { + getAllFilesRecursive(scheme, dirResponse, allFiles, function () { + checkComplete(); + }); + } else { + checkComplete(); + } + } catch (e) { + Logger.e("ColorSchemeDownload", "Failed to parse directory response:", e); + checkComplete(); + } + } else { + checkComplete(); + } + } + }; + }(item); + xhr.open("GET", item.url); + xhr.send(); + } else { + checkComplete(); + } + } + } + + function downloadSchemeFiles(schemeName, files) { + if (files.length === 0) { + downloadError = I18n.tr("panels.color-scheme.download-error-no-files"); + downloading = false; + downloadingScheme = ""; + Logger.e("ColorSchemeDownload", downloadError); + return; + } + + var targetDir = ColorSchemeService.downloadedSchemesDirectory + "/" + schemeName; + var downloadScript = "mkdir -p '" + targetDir + "'\n"; + + // Build download script for all files + for (var i = 0; i < files.length; i++) { + var file = files[i]; + var filePath = file.path; + // Remove scheme name and leading / from path + var relativePath = filePath; + if (filePath.startsWith(schemeName + "/")) { + relativePath = filePath.substring(schemeName.length + 1); + } else if (filePath.startsWith("/" + schemeName + "/")) { + relativePath = filePath.substring(schemeName.length + 2); + } + var localPath = targetDir + "/" + relativePath; + var localDir = localPath.substring(0, localPath.lastIndexOf('/')); + + downloadScript += "mkdir -p '" + localDir + "'\n"; + var downloadUrl = file.url || file.download_url; + if (downloadUrl) { + downloadScript += "curl -L -s -o '" + localPath + "' '" + downloadUrl + "' || wget -q -O '" + localPath + "' '" + downloadUrl + "'\n"; + } + } + + Logger.d("ColorSchemeDownload", "Downloading", files.length, "files for scheme", schemeName); + + // Execute download script + var stderrOutput = ""; + var downloadProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + import qs.Commons + Process { + id: downloadProcess + command: ["sh", "-c", ` + JSON.stringify(downloadScript) + `] + stderr: StdioCollector { + onStreamFinished: { + if (text && text.trim()) { + Logger.e("ColorSchemeDownload", "Download stderr:", text); + root.lastStderrOutput = text.trim(); + } + } + } + } + `, root, "DownloadProcess_" + schemeName); + + downloadProcess.exited.connect(function (exitCode) { + if (exitCode === 0) { + Logger.i("ColorSchemeDownload", "Scheme downloaded successfully:", schemeName); + ToastService.showNotice(I18n.tr("panels.color-scheme.download-success-title"), I18n.tr("panels.color-scheme.download-success-description", { + "scheme": schemeName + }), "settings-color-scheme"); + // Set pending scheme to apply after reload + pendingApplyScheme = schemeName; + // Reload color schemes + ColorSchemeService.loadColorSchemes(); + downloading = false; + downloadingScheme = ""; + } else { + var errorDetails = "Exit code: " + exitCode; + if (root.lastStderrOutput) { + errorDetails += " - " + root.lastStderrOutput; + } + downloadError = I18n.tr("panels.color-scheme.download-error-download-failed", { + "code": exitCode + }) + "\n" + errorDetails; + Logger.e("ColorSchemeDownload", downloadError); + ToastService.showError(I18n.tr("panels.color-scheme.download-error-title"), I18n.tr("panels.color-scheme.download-error-description", { + "scheme": schemeName + }) + "\n" + errorDetails); + // Clean up the partially downloaded directory on failure + var cleanupScript = "rm -rf '" + targetDir + "'"; + var cleanupProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + id: cleanupProcess + command: ["sh", "-c", ` + JSON.stringify(cleanupScript) + `] + } + `, root, "CleanupProcess_" + schemeName); + + cleanupProcess.exited.connect(function (cleanupExitCode) { + if (cleanupExitCode === 0) { + Logger.d("ColorSchemeDownload", "Partially downloaded scheme directory cleaned up:", targetDir); + } else { + Logger.w("ColorSchemeDownload", "Failed to clean up partially downloaded scheme directory:", targetDir); + } + downloading = false; + downloadingScheme = ""; + cleanupProcess.destroy(); + }); + + cleanupProcess.running = true; + } + root.lastStderrOutput = ""; + downloadProcess.destroy(); + }); + + downloadProcess.running = true; + } + + function isSchemeInstalled(schemeName) { + // Check if scheme already exists in ColorSchemeService + for (var i = 0; i < ColorSchemeService.schemes.length; i++) { + var path = ColorSchemeService.schemes[i]; + if (path.indexOf("/" + schemeName + "/") !== -1 || path.indexOf("/" + schemeName + ".json") !== -1) { + return true; + } + } + return false; + } + + function isSchemeDownloaded(schemeName) { + // Check if scheme is in the downloaded directory (not preinstalled) + for (var i = 0; i < ColorSchemeService.schemes.length; i++) { + var path = ColorSchemeService.schemes[i]; + if ((path.indexOf("/" + schemeName + "/") !== -1 || path.indexOf("/" + schemeName + ".json") !== -1) && path.indexOf(ColorSchemeService.downloadedSchemesDirectory) !== -1) { + return true; + } + } + return false; + } + + function deleteScheme(schemeName) { + if (downloading) { + return; + } + + Logger.i("ColorSchemeDownload", "Deleting scheme:", schemeName); + + // Check if the deleted scheme is the currently active one + var currentScheme = Settings.data.colorSchemes.predefinedScheme || ""; + var deletedSchemeDisplayName = ColorSchemeService.getBasename(schemeName); + var needsReset = (currentScheme === deletedSchemeDisplayName); + + // Only allow deleting downloaded schemes, not preinstalled ones + var targetDir = ColorSchemeService.downloadedSchemesDirectory + "/" + schemeName; + var deleteScript = "rm -rf '" + targetDir + "'"; + + var deleteProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + id: deleteProcess + command: ["sh", "-c", ` + JSON.stringify(deleteScript) + `] + } + `, root, "DeleteProcess_" + schemeName); + + deleteProcess.exited.connect(function (exitCode) { + if (exitCode === 0) { + Logger.i("ColorSchemeDownload", "Scheme deleted successfully:", schemeName); + ToastService.showNotice(I18n.tr("panels.color-scheme.delete-success-title"), I18n.tr("panels.color-scheme.delete-success-description", { + "scheme": schemeName + }), "settings-color-scheme"); + + // If the deleted scheme was the active one, reset to default BEFORE reloading + if (needsReset) { + Logger.i("ColorSchemeDownload", "Deleted scheme was active, resetting to Noctalia (default)"); + // Clear the setting immediately so ColorSchemeService won't try to apply the deleted scheme + Settings.data.colorSchemes.predefinedScheme = "Noctalia (default)"; + // Apply the default scheme immediately + ColorSchemeService.setPredefinedScheme("Noctalia (default)"); + } + + // Reload color schemes + ColorSchemeService.loadColorSchemes(); + } else { + Logger.e("ColorSchemeDownload", "Delete failed with exit code:", exitCode); + ToastService.showError(I18n.tr("panels.color-scheme.delete-error-title"), I18n.tr("panels.color-scheme.delete-error-description", { + "scheme": schemeName + })); + } + deleteProcess.destroy(); + }); + + deleteProcess.running = true; + } + + Connections { + target: ColorSchemeService + function onScanningChanged() { + // When scanning completes and we have a pending scheme, apply it + if (!ColorSchemeService.scanning && pendingApplyScheme !== "") { + var schemeToApply = pendingApplyScheme; + pendingApplyScheme = ""; // Clear pending before applying + + // Wait a tiny bit to ensure schemes array is updated + applyTimer.schemeName = schemeToApply; + applyTimer.restart(); + } + } + } + + Timer { + id: applyTimer + property string schemeName: "" + interval: 100 // Small delay to ensure schemes array is populated + onTriggered: { + if (schemeName !== "") { + Logger.i("ColorSchemeDownload", "Auto-applying downloaded scheme:", schemeName); + // Use setPredefinedScheme which will apply and set it as the current scheme + ColorSchemeService.setPredefinedScheme(schemeName); + schemeName = ""; + } + } + } + + onOpened: { + fetchAvailableSchemes(); + } + + function preFetchSchemeColors() { + if (availableSchemes.length > 0 && visible) { + Qt.callLater(function () { + // Just trigger a cache version update to ensure UI refreshes + cacheVersion++; + }); + } + } + + onAvailableSchemesChanged: preFetchSchemeColors() + onVisibleChanged: { + preFetchSchemeColors(); + + // Load schemes from ShellState when popup becomes visible + if (visible) { + if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { + loadSchemesFromCache(); + } + } + } + + Connections { + target: typeof ShellState !== 'undefined' ? ShellState : null + function onIsLoadedChanged() { + if (root.visible && ShellState.isLoaded) { + loadSchemesFromCache(); + } + } + } + + contentItem: ColumnLayout { + id: contentColumn + width: parent.width + spacing: Style.marginL + + // Header + RowLayout { + Layout.fillWidth: true + + NText { + text: I18n.tr("panels.color-scheme.download-title") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + } + + NIconButton { + icon: "refresh" + tooltipText: I18n.tr("common.refresh") + enabled: !fetching && !downloading + onClicked: { + // Force refresh by clearing cache timestamp and fetching directly from API + if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { + ShellState.setColorSchemesList({ + schemes: [], + timestamp: 0 + }); + } + // Fetch directly from API to avoid cache check delay + fetchAvailableSchemesFromAPI(); + } + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + onClicked: root.close() + } + } + + // Separator + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + } + + // Error message + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: errorText.implicitHeight + Style.marginM + visible: downloadError !== "" + color: Color.mError + radius: Style.radiusS + + NText { + id: errorText + anchors.fill: parent + anchors.margins: Style.marginM + text: downloadError + pointSize: Style.fontSizeS + color: Color.mOnError + wrapMode: Text.WordWrap + } + } + + // Loading indicator - only show on initial load, not during refresh + RowLayout { + Layout.fillWidth: true + visible: fetching && !hasInitialData + spacing: Style.marginM + + NBusyIndicator { + Layout.preferredWidth: 20 + Layout.preferredHeight: 20 + } + + NText { + text: I18n.tr("panels.color-scheme.download-fetching") + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + } + + // Schemes list - keep visible during refresh to prevent flicker + NScrollView { + id: schemesScrollView + Layout.fillWidth: true + Layout.preferredHeight: 400 + visible: hasInitialData && availableSchemes.length > 0 + verticalPolicy: ScrollBar.AsNeeded + horizontalPolicy: ScrollBar.AlwaysOff + gradientColor: Color.mSurface + + ColumnLayout { + width: schemesScrollView.availableWidth + spacing: Style.marginS + + Repeater { + model: availableSchemes + + Rectangle { + id: schemeItem + Layout.fillWidth: true + Layout.preferredHeight: 50 * Style.uiScaleRatio + radius: Style.radiusS + property string schemeName: modelData.name + color: root.getSchemeColor(schemeName, "mSurface") + border.width: Style.borderL + border.color: hoverHandler.hovered ? root.getSchemeColor(schemeName, "mPrimary") : Color.mOutline + + HoverHandler { + id: hoverHandler + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutCubic + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutCubic + } + } + + Component.onCompleted: { + if (root.visible && !root.schemeColorsCache[schemeName]) { + root.fetchSchemeColors(modelData); + } + } + + RowLayout { + id: schemeRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Style.marginL + anchors.rightMargin: Style.marginL + spacing: Style.marginS + + property string schemeName: modelData.name + property int diameter: 16 * Style.uiScaleRatio + property var colorKeys: ["mPrimary", "mSecondary", "mTertiary", "mError"] + + NText { + text: schemeRow.schemeName + pointSize: Style.fontSizeS + color: Color.mOnSurface + Layout.fillWidth: true + elide: Text.ElideRight + Layout.alignment: Qt.AlignVCenter + } + + // Color swatches + Repeater { + model: schemeRow.colorKeys + Rectangle { + width: schemeRow.diameter + height: schemeRow.diameter + radius: schemeRow.diameter * 0.5 + color: root.getSchemeColor(schemeRow.schemeName, modelData) + Layout.alignment: Qt.AlignVCenter + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutCubic + } + } + } + } + + // Download/Delete button + NIconButton { + property bool isDownloading: downloading && downloadingScheme === schemeRow.schemeName + property bool isInstalled: root.isSchemeInstalled(schemeRow.schemeName) + property bool isDownloaded: root.isSchemeDownloaded(schemeRow.schemeName) + + icon: isDownloading ? "" : (isDownloaded ? "trash" : "download") + tooltipText: isDownloading ? I18n.tr("panels.color-scheme.download-downloading") : (isDownloaded ? I18n.tr("common.delete") : I18n.tr("common.download")) + enabled: !downloading + Layout.alignment: Qt.AlignVCenter + visible: !isInstalled || isDownloaded // Show button only if not installed (can download) or if downloaded (can delete) + onClicked: isDownloaded ? root.deleteScheme(schemeRow.schemeName) : root.downloadScheme(modelData) + + NBusyIndicator { + anchors.centerIn: parent + width: 16 * Style.uiScaleRatio + height: 16 * Style.uiScaleRatio + visible: parent.isDownloading + } + } + } + } + } + } + } + + // Empty state + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + spacing: Style.marginM + visible: !fetching && availableSchemes.length === 0 && downloadError === "" + + NIcon { + icon: "package" + pointSize: 48 + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: I18n.tr("panels.color-scheme.download-empty") + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ColorScheme/TemplatesSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ColorScheme/TemplatesSubTab.qml new file mode 100644 index 0000000..127787f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ColorScheme/TemplatesSubTab.qml @@ -0,0 +1,318 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.System +import qs.Services.Theming +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + // Helper to format path description + function getDesc(fallbackPath) { + return I18n.tr("panels.color-scheme.templates-write-path", { + "filepath": fallbackPath + }); + } + + // Build a combined list of all available templates from TemplateRegistry, sorted alphabetically + readonly property var allTemplates: { + var templates = []; + + // Add terminals with category "terminal" + for (var i = 0; i < TemplateRegistry.terminals.length; i++) { + var t = TemplateRegistry.terminals[i]; + templates.push({ + "id": t.id, + "name": t.name, + "category": "terminal", + "tooltip": getDesc(t.outputPath) + }); + } + + // Add applications + for (var j = 0; j < TemplateRegistry.applications.length; j++) { + var app = TemplateRegistry.applications[j]; + var path = ""; + + // Determine path to show + if (app.outputs && app.outputs.length > 0) { + var paths = []; + for (var k = 0; k < app.outputs.length; k++) { + paths.push(app.outputs[k].path); + } + path = paths.join("\n"); + } else if (app.id === "emacs") { + // Emacs clients are detected dynamically by ProgramCheckerService + var emacsClients = ProgramCheckerService.availableEmacsClients; + if (emacsClients && emacsClients.length > 0) { + var emacsPaths = []; + for (var k = 0; k < emacsClients.length; k++) { + emacsPaths.push(emacsClients[k].path); + } + path = emacsPaths.join("\n"); + } else { + path = I18n.tr("panels.color-scheme.templates-none-detected"); + } + } else if (app.clients && app.clients.length > 0) { + var validClients = []; + for (var k = 0; k < app.clients.length; k++) { + var client = app.clients[k]; + var include = true; + + if (app.id === "discord") { + include = TemplateProcessor.isDiscordClientEnabled(client.name); + } else if (app.id === "code") { + // For code clients, resolve all theme paths dynamically (version-independent) + if (TemplateProcessor.isCodeClientEnabled(client.name)) { + var resolvedPaths = TemplateRegistry.resolvedCodeClientPaths(client.name); + for (var p = 0; p < resolvedPaths.length; p++) { + validClients.push(resolvedPaths[p]); + } + } + continue; + } + + if (include) { + if (client.path) + validClients.push(client.path); + } + } + + if (validClients.length > 0) { + path = validClients.join("\n"); + } else { + path = I18n.tr("panels.color-scheme.templates-none-detected"); + } + } + + templates.push({ + "id": app.id, + "name": app.name, + "category": app.category || "misc", + "tooltip": getDesc(path) + }); + } + + // Sort alphabetically by name + templates.sort((a, b) => a.name.localeCompare(b.name)); + + return templates; + } + + // Category filter + property string selectedCategory: "" + + // Build available categories dynamically + readonly property var availableCategories: { + var cats = {}; + for (var i = 0; i < allTemplates.length; i++) { + cats[allTemplates[i].category] = true; + } + return Object.keys(cats).sort(); + } + + // Filter toggle + property bool showOnlyActive: false + + // Filtered templates based on category, search, and toggle + property string searchText: "" + readonly property var filteredTemplates: { + var result = allTemplates; + + // Filter by category first (unless searching) + if (selectedCategory !== "" && searchText.trim() === "") { + result = result.filter(t => t.category === selectedCategory); + } + + // Search overrides category filter + if (searchText.trim() !== "") { + var query = searchText.toLowerCase().trim(); + result = result.filter(t => t.name.toLowerCase().includes(query)); + } + + // Filter by active if enabled (and not searching) + if (showOnlyActive && searchText.trim() === "") { + result = result.filter(t => isTemplateActive(t.id)); + } + + return result; + } + + // Check if a template is active + function isTemplateActive(templateId) { + for (var i = 0; i < Settings.data.templates.activeTemplates.length; i++) { + if (Settings.data.templates.activeTemplates[i].id === templateId) { + return true; + } + } + return false; + } + + // Toggle a template on/off + function toggleTemplate(templateId) { + var current = Settings.data.templates.activeTemplates.slice(); + var existingIndex = -1; + + for (var i = 0; i < current.length; i++) { + if (current[i].id === templateId) { + existingIndex = i; + break; + } + } + + if (existingIndex >= 0) { + // Remove it + current.splice(existingIndex, 1); + } else { + // Add it + current.push({ + "id": templateId, + "enabled": true + }); + } + + Settings.data.templates.activeTemplates = current; + AppThemeService.generate(); + + // Clear search context on interaction to return to filtered view + if (searchText !== "") { + searchText = ""; + } + } + + NText { + text: I18n.tr("panels.color-scheme.templates-desc") + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + // Category filter chips + NTagFilter { + tags: root.availableCategories + selectedTag: root.selectedCategory + onSelectedTagChanged: root.selectedCategory = selectedTag + label: I18n.tr("panels.color-scheme.templates-filter-label") + description: I18n.tr("panels.color-scheme.templates-filter-description") + expanded: true + } + + // Search/filter input row + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NTextInput { + Layout.fillWidth: true + placeholderText: I18n.tr("placeholders.search") + text: root.searchText + onTextChanged: root.searchText = text + } + + NIconButton { + icon: "filter" + tooltipText: root.showOnlyActive ? I18n.tr("actions.show-all") : I18n.tr("actions.show-active-only") + + colorBg: root.showOnlyActive ? Color.mPrimary : Color.mSurface + colorFg: root.showOnlyActive ? Color.mOnPrimary : Color.mOnSurface + + onClicked: root.showOnlyActive = !root.showOnlyActive + } + } + + // Chip grid - uniform columns + GridLayout { + Layout.fillWidth: true + columns: 4 + columnSpacing: Style.marginS + rowSpacing: Style.marginS + + Repeater { + model: filteredTemplates + + Rectangle { + id: chip + Layout.fillWidth: true + Layout.preferredHeight: Math.round(Style.baseWidgetSize * 0.9) + radius: Style.iRadiusM + color: chipMouse.containsMouse ? Color.mHover : (isActive ? Color.mPrimary : Color.mSurface) + border.color: isActive ? Color.mPrimary : Color.mOutline + border.width: Style.borderS + + required property int index + required property var modelData + readonly property bool isActive: root.isTemplateActive(modelData.id) + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NText { + id: chipText + anchors.centerIn: parent + width: parent.width - Style.margin2L + text: chip.modelData.name + pointSize: Style.fontSizeS + color: chipMouse.containsMouse ? Color.mOnHover : (isActive ? Color.mOnPrimary : Color.mOnSurface) + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + MouseArea { + id: chipMouse + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: root.toggleTemplate(chip.modelData.id) + onEntered: { + if (chip.modelData.tooltip) { + TooltipService.show(chip, chip.modelData.tooltip, "bottom"); + } + } + onExited: { + TooltipService.hide(); + } + } + } + } + } + + // No results message + NText { + visible: filteredTemplates.length === 0 && searchText.trim() !== "" + text: I18n.tr("common.no-results") + color: Color.mOnSurfaceVariant + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginM + } + + // User templates checkbox + NCheckbox { + label: I18n.tr("panels.color-scheme.templates-misc-user-templates-label") + description: I18n.tr("panels.color-scheme.templates-misc-user-templates-description") + checked: Settings.data.templates.enableUserTheming + onToggled: checked => { + Settings.data.templates.enableUserTheming = checked; + if (checked) { + TemplateRegistry.writeUserTemplatesToml(); + } + AppThemeService.generate(); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml new file mode 100644 index 0000000..c424a52 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Connections/BluetoothSubTab.qml @@ -0,0 +1,786 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import Quickshell +import Quickshell.Bluetooth + +import qs.Commons +import qs.Services.Hardware +import qs.Services.Networking +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +Item { + id: root + Layout.fillWidth: true + implicitHeight: mainLayout.implicitHeight + + // Configuration for shared use (e.g. by BluetoothPanel) + property bool showOnlyLists: false + + readonly property bool isScanningActive: BluetoothService.scanningActive + readonly property bool isDiscoverable: BluetoothService.discoverable + + // Device lists with local filtering logic + readonly property var connectedDevices: { + if (!BluetoothService.adapter || !BluetoothService.adapter.devices) + return []; + var filtered = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && dev.connected); + filtered = BluetoothService.dedupeDevices(filtered); + return BluetoothService.sortDevices(filtered); + } + + readonly property var pairedDevices: { + if (!BluetoothService.adapter || !BluetoothService.adapter.devices) + return []; + var filtered = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted)); + filtered = BluetoothService.dedupeDevices(filtered); + return BluetoothService.sortDevices(filtered); + } + + readonly property var unnamedAvailableDevices: { + if (!BluetoothService.adapter || !BluetoothService.adapter.devices) + return []; + return BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && !dev.paired && !dev.trusted); + } + + readonly property var availableDevices: { + var list = root.unnamedAvailableDevices; + + if (Settings.data.network.bluetoothHideUnnamedDevices) { + list = list.filter(function (dev) { + var dn = dev.name || dev.deviceName || ""; + var s = String(dn).trim(); + if (s.length === 0) + return false; + var lower = s.toLowerCase(); + if (lower === "unknown" || lower === "unnamed" || lower === "n/a" || lower === "na") + return false; + var addr = dev.address || dev.bdaddr || dev.mac || ""; + if (addr.length > 0) { + var normName = s.toLowerCase().replace(/[^0-9a-z]/g, ""); + var normAddr = String(addr).toLowerCase().replace(/[^0-9a-z]/g, ""); + if (normName.length > 0 && normName === normAddr) + return false; + } + var macRegexComb = /^(([0-9A-Fa-f]{2}[:\-]){5}[0-9A-Fa-f]{2}|([0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4}|[0-9A-Fa-f]{12})$/; + if (macRegexComb.test(s)) { + return false; + } + return true; + }); + } + list = BluetoothService.dedupeDevices(list); + return BluetoothService.sortDevices(list); + } + + // For managing expanded device details + property string expandedDeviceKey: "" + property bool detailsGrid: (Settings.data.network.bluetoothDetailsViewMode === "grid") + + // Combined visibility check: tab must be visible AND the window must be visible + readonly property bool effectivelyVisible: root.visible && Window.window && Window.window.visible + + Connections { + target: BluetoothService + function onEnabledChanged() { + stateChangeDebouncer.restart(); + } + function onDiscoverableChanged() { + stateChangeDebouncer.restart(); + } + } + + onEffectivelyVisibleChanged: stateChangeDebouncer.restart() + + Timer { + id: stateChangeDebouncer + interval: 100 // 100ms debounce + repeat: false + onTriggered: root._updateScanningState() + } + + function _updateScanningState() { + if (effectivelyVisible && BluetoothService.enabled && !showOnlyLists) { + Logger.d("BluetoothPrefs", "Panel/tab active"); + if (!isScanningActive) { + BluetoothService.setScanActive(true); + } + if (!Settings.data.network.disableDiscoverability && !isDiscoverable) { + BluetoothService.setDiscoverable(true); + } + } else { + Logger.d("BluetoothPrefs", "Panel/tab inactive"); + if (isScanningActive && !showOnlyLists) { + BluetoothService.setScanActive(false); + } + if (isDiscoverable && !showOnlyLists) { + BluetoothService.setDiscoverable(false); + } + } + } + + Component.onDestruction: { + // Ensure scanning is stopped when component is closed + if (isScanningActive && !showOnlyLists) { + BluetoothService.setScanActive(false); + } + // Ensure discoverable is disabled when component is closed + if (isDiscoverable && !showOnlyLists) { + BluetoothService.setDiscoverable(false); + } + Logger.d("BluetoothPrefs", "Panel closed"); + } + + ColumnLayout { + id: mainLayout + anchors.left: parent.left + anchors.right: parent.right + spacing: Style.marginL + + // Master Control Section + NBox { + visible: !root.showOnlyLists + Layout.fillWidth: true + Layout.preferredHeight: masterControlCol.implicitHeight + Style.margin2L + color: Color.mSurface + + ColumnLayout { + id: masterControlCol + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NToggle { + label: I18n.tr("common.bluetooth") + icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off" + checked: BluetoothService.enabled + enabled: !NetworkService.airplaneModeEnabled && BluetoothService.bluetoothAvailable + onToggled: checked => BluetoothService.setBluetoothEnabled(checked) + Layout.alignment: Qt.AlignVCenter + } + } + + NDivider { + Layout.fillWidth: true + visible: BluetoothService.enabled && isDiscoverable + } + + NText { + visible: BluetoothService.enabled && isDiscoverable + Layout.fillWidth: true + text: I18n.tr("panels.connections.bluetooth-discoverable", { + hostName: HostService.hostName + }) + color: Color.mOnSurfaceVariant + richTextEnabled: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + } + + Item { + visible: !showOnlyLists + Layout.fillWidth: true + } + + // Device List [1] (Connected) + NBox { + id: connectedDevicesBox + visible: root.connectedDevices.length > 0 && BluetoothService.enabled + Layout.fillWidth: true + Layout.preferredHeight: connectedDevicesCol.implicitHeight + Style.margin2M + border.color: showOnlyLists ? Style.boxBorderColor : "transparent" + color: showOnlyLists ? Color.mSurfaceVariant : "transparent" + + ColumnLayout { + id: connectedDevicesCol + anchors.fill: parent + anchors.topMargin: Style.marginM + anchors.bottomMargin: Style.marginM + anchors.leftMargin: showOnlyLists ? Style.marginL : 0 + anchors.rightMargin: showOnlyLists ? Style.marginL : 0 + spacing: Style.marginM + + NLabel { + label: I18n.tr("bluetooth.panel.connected-devices") + Layout.fillWidth: true + Layout.leftMargin: Style.marginS + } + + Repeater { + model: root.connectedDevices + delegate: nboxDelegate + } + } + } + + // Devices List [2] (Paired) + NBox { + id: pairedDevicesBox + visible: root.pairedDevices.length > 0 && BluetoothService.enabled + Layout.fillWidth: true + Layout.preferredHeight: pairedDevicesCol.implicitHeight + Style.margin2M + border.color: showOnlyLists ? Style.boxBorderColor : "transparent" + color: showOnlyLists ? Color.mSurfaceVariant : "transparent" + + ColumnLayout { + id: pairedDevicesCol + anchors.fill: parent + anchors.topMargin: Style.marginM + anchors.bottomMargin: Style.marginM + anchors.leftMargin: showOnlyLists ? Style.marginL : 0 + anchors.rightMargin: showOnlyLists ? Style.marginL : 0 + spacing: Style.marginM + + NLabel { + label: I18n.tr("bluetooth.panel.paired-devices") + Layout.fillWidth: true + Layout.leftMargin: Style.marginS + } + + Repeater { + model: root.pairedDevices + delegate: nboxDelegate + } + } + } + + // Device List [3] (Available) + NBox { + id: availableDevicesBox + visible: !root.showOnlyLists && root.unnamedAvailableDevices.length > 0 && BluetoothService.enabled + Layout.fillWidth: true + Layout.preferredHeight: availableDevicesCol.implicitHeight + Style.margin2M + border.color: "transparent" + color: showOnlyLists ? Color.mSurfaceVariant : "transparent" + + ColumnLayout { + id: availableDevicesCol + anchors.fill: parent + anchors.topMargin: Style.marginM + anchors.bottomMargin: Style.marginM + spacing: Style.marginM + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: Style.marginS + spacing: Style.marginS + + NLabel { + label: I18n.tr("bluetooth.panel.available-devices") + description: BluetoothService.scanningActive ? I18n.tr("bluetooth.panel.scanning") : "" + Layout.fillWidth: true + } + } + + Repeater { + model: root.availableDevices + delegate: nboxDelegate + } + + NText { + visible: root.availableDevices.length === 0 && root.unnamedAvailableDevices.length > 0 + text: I18n.tr("panels.connections.bluetooth-devices-unnamed") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + Layout.margins: Style.marginL + } + } + } + + Item { + visible: !showOnlyLists + Layout.fillWidth: true + } + + NBox { + id: miscSettingsBox + visible: !root.showOnlyLists && BluetoothService.enabled + Layout.fillWidth: true + Layout.preferredHeight: miscSettingsCol.implicitHeight + Style.margin2XL + color: Color.mSurface + + ColumnLayout { + id: miscSettingsCol + anchors.fill: parent + anchors.margins: Style.marginXL + spacing: Style.marginM + + NToggle { + label: I18n.tr("panels.connections.bluetooth-auto-connect-label") + description: I18n.tr("panels.connections.bluetooth-auto-connect-description") + checked: Settings.data.network.bluetoothAutoConnect + onToggled: checked => Settings.data.network.bluetoothAutoConnect = checked + } + + NToggle { + label: I18n.tr("panels.connections.hide-unnamed-devices-label") + description: I18n.tr("panels.connections.hide-unnamed-devices-description") + checked: Settings.data.network.bluetoothHideUnnamedDevices + onToggled: checked => Settings.data.network.bluetoothHideUnnamedDevices = checked + } + + NToggle { + label: I18n.tr("panels.connections.disable-discoverability-label") + description: I18n.tr("panels.connections.disable-discoverability-description") + checked: Settings.data.network.disableDiscoverability + onToggled: checked => { + Settings.data.network.disableDiscoverability = checked; + BluetoothService.setDiscoverable(!checked); + } + } + + // RSSI Polling + NToggle { + label: I18n.tr("panels.connections.bluetooth-rssi-polling-label") + description: I18n.tr("panels.connections.bluetooth-rssi-polling-description") + checked: Settings.data.network.bluetoothRssiPollingEnabled + onToggled: checked => Settings.data.network.bluetoothRssiPollingEnabled = checked + } + NSpinBox { + label: I18n.tr("panels.connections.bluetooth-rssi-polling-interval-label") + description: I18n.tr("panels.connections.bluetooth-rssi-polling-interval-description") + from: 10000 + to: 120000 + stepSize: 1000 + value: Settings.data.network.bluetoothRssiPollIntervalMs + defaultValue: Settings.getDefaultValue("network.bluetoothRssiPollIntervalMs") + onValueChanged: Settings.data.network.bluetoothRssiPollIntervalMs = value + suffix: " ms" + Layout.alignment: Qt.AlignVCenter + visible: Settings.data.network.bluetoothRssiPollingEnabled + } + } + } + } + + // Shared Delegate + Component { + id: nboxDelegate + NBox { + id: device + + readonly property bool canConnect: BluetoothService.canConnect(modelData) + readonly property bool canDisconnect: BluetoothService.canDisconnect(modelData) + readonly property bool canPair: BluetoothService.canPair(modelData) + readonly property bool isBusy: BluetoothService.isDeviceBusy(modelData) + readonly property bool isExpanded: root.expandedDeviceKey === BluetoothService.deviceKey(modelData) + + function getContentColors(defaultColors = [Color.mSurface, Color.mOnSurface]) { + if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) { + return [Color.mPrimary, Color.mOnPrimary]; + } + if (modelData.connected && modelData.state !== BluetoothDeviceState.Disconnecting) { + return [Color.mPrimary, Color.mOnPrimary]; + } + if (modelData.blocked || modelData.state === BluetoothDeviceState.Disconnecting) { + return [Color.mError, Color.mOnError]; + } + return defaultColors; + } + + Layout.fillWidth: true + Layout.preferredHeight: deviceColumn.implicitHeight + (Style.marginXL) + radius: Style.radiusM + clip: true + forceOpaque: true + color: device.getContentColors()[0] + + ColumnLayout { + id: deviceColumn + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + RowLayout { + id: deviceLayout + Layout.fillWidth: true + spacing: Style.marginM + Layout.alignment: Qt.AlignVCenter + + NIcon { + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + horizontalAlignment: Text.AlignLeft + icon: BluetoothService.getDeviceIcon(modelData) + pointSize: Style.fontSizeXXL + color: device.getContentColors()[1] + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXXS + + NText { + text: modelData.name || modelData.deviceName + pointSize: Style.fontSizeM + font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium + elide: Text.ElideRight + color: device.getContentColors()[1] + Layout.fillWidth: true + } + + NText { + text: { + const k = BluetoothService.getStatusKey(modelData); + if (k === "pairing") + return I18n.tr("common.pairing"); + if (k === "blocked") + return I18n.tr("bluetooth.panel.blocked"); + if (k === "connecting") + return I18n.tr("common.connecting"); + if (k === "disconnecting") + return I18n.tr("common.disconnecting"); + return ""; + } + visible: text !== "" + pointSize: Style.fontSizeXS + color: Qt.alpha(device.getContentColors([Color.mSurfaceVariant, Color.mOnSurfaceVariant])[1], Style.opacityHeavy) + } + + RowLayout { + visible: modelData.batteryAvailable + spacing: Style.marginS + NIcon { + icon: { + var b = BluetoothService.getBatteryPercent(modelData); + return BatteryService.getIcon(b !== null ? b : 0, false, false, b !== null); + } + pointSize: Style.fontSizeXS + color: Qt.alpha(device.getContentColors()[1], Style.opacityHeavy) + } + NText { + text: { + var b = BluetoothService.getBatteryPercent(modelData); + return b === null ? "-" : (b + "%"); + } + pointSize: Style.fontSizeXS + color: Qt.alpha(device.getContentColors([Color.mSurfaceVariant, Color.mOnSurfaceVariant])[1], Style.opacityHeavy) + } + } + } + + Item { + Layout.fillWidth: true + } + + RowLayout { + spacing: Style.marginS + + NBusyIndicator { + visible: isBusy + running: visible && root.effectivelyVisible + color: device.getContentColors()[1] + size: Style.baseWidgetSize * 0.5 + } + + NIconButton { + visible: modelData.connected && modelData.state !== BluetoothDeviceState.Disconnecting + icon: "info" + tooltipText: I18n.tr("common.info") + baseSize: Style.baseWidgetSize * 0.75 + colorBg: Color.mSurfaceVariant + colorFg: Color.mOnSurface + colorBorder: "transparent" + colorBorderHover: "transparent" + onClicked: { + const key = BluetoothService.deviceKey(modelData); + root.expandedDeviceKey = (root.expandedDeviceKey === key) ? "" : key; + } + } + + NIconButton { + visible: !root.showOnlyLists && (modelData.paired || modelData.trusted) && !modelData.connected && !isBusy && !modelData.blocked + icon: "trash" + tooltipText: I18n.tr("common.unpair") + baseSize: Style.baseWidgetSize * 0.75 + colorBg: Color.mPrimary + colorFg: Color.mOnPrimary + colorBorder: "transparent" + colorBorderHover: "transparent" + onClicked: BluetoothService.unpairDevice(modelData) + } + + NButton { + id: button + visible: modelData.state !== BluetoothDeviceState.Connecting && modelData.state !== BluetoothDeviceState.Disconnecting + enabled: (canConnect || canDisconnect || (root.showOnlyLists ? false : canPair)) && !isBusy + fontSize: Style.fontSizeS + backgroundColor: modelData.connected ? Color.mSurfaceVariant : Color.mPrimary + textColor: modelData.connected ? Color.mOnSurface : Color.mOnPrimary + text: { + if (modelData.pairing) + return I18n.tr("common.pairing"); + if (modelData.blocked) + return I18n.tr("bluetooth.panel.blocked"); + if (modelData.connected) + return I18n.tr("common.disconnect"); + if (!root.showOnlyLists && device.canPair) + return I18n.tr("common.pair"); + return I18n.tr("common.connect"); + } + onClicked: { + if (modelData.connected) { + BluetoothService.disconnectDevice(modelData); + } else { + if (!root.showOnlyLists && device.canPair) { + BluetoothService.pairDevice(modelData); + } else { + BluetoothService.connectDeviceWithTrust(modelData); + } + } + } + } + } + } + + // Expanded info section + Rectangle { + visible: device.isExpanded + Layout.fillWidth: true + implicitHeight: infoColumn.implicitHeight + Style.margin2S + radius: Style.radiusXS + color: Color.mSurfaceVariant + border.width: Style.borderS + border.color: Style.boxBorderColor + clip: true + + NIconButton { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginS + icon: root.detailsGrid ? "layout-list" : "layout-grid" + tooltipText: root.detailsGrid ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view") + baseSize: Style.baseWidgetSize * 0.65 + onClicked: { + root.detailsGrid = !root.detailsGrid; + Settings.data.network.bluetoothDetailsViewMode = root.detailsGrid ? "grid" : "list"; + } + z: 1 + } + + GridLayout { + id: infoColumn + anchors.fill: parent + anchors.margins: Style.marginS + flow: root.detailsGrid ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: root.detailsGrid ? 3 : 6 + columns: root.detailsGrid ? 2 : 1 + columnSpacing: Style.marginM + rowSpacing: Style.marginXS + + // --- Item 1: Signal Strength --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: BluetoothService.getSignalIcon(modelData) + pointSize: Style.fontSizeXS + color: Color.mOnSurface + } + NText { + text: BluetoothService.getSignalStrength(modelData) + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + } + } + + // --- Item 2: Battery --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: { + var b = BluetoothService.getBatteryPercent(modelData); + return BatteryService.getIcon(b !== null ? b : 0, false, false, b !== null); + } + pointSize: Style.fontSizeXS + color: Color.mOnSurface + } + NText { + text: { + var b = BluetoothService.getBatteryPercent(modelData); + return b === null ? "-" : (b + "%"); + } + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + } + } + // --- Item 3: Pair state --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "link" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + } + NText { + text: modelData.paired ? I18n.tr("common.yes") : I18n.tr("common.no") + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + } + } + // --- Item 4: Trust state --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "shield-check" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + } + NText { + text: modelData.trusted ? I18n.tr("common.yes") : I18n.tr("common.no") + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + } + } + // --- Item 5: Address --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "hash" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + } + NText { + text: modelData.address || "-" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + } + } + // --- Item 6: Auto-connect --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + Layout.topMargin: -Style.marginXXS + spacing: Style.marginXS + visible: Settings.data.network.bluetoothAutoConnect + NIcon { + icon: BluetoothService.getDeviceAutoConnect(modelData) ? "repeat" : "repeat-off" + pointSize: Style.fontSizeXS + } + NCheckbox { + label: I18n.tr("common.auto-connect") + labelSize: Style.fontSizeXS + baseSize: Style.baseWidgetSize * 0.5 + checked: BluetoothService.getDeviceAutoConnect(modelData) + onToggled: checked => BluetoothService.setDeviceAutoConnect(modelData, checked) + } + } + } + } + } + } + } + + // PIN Authentication Overlay (This part needs some love :P) + Rectangle { + id: pinOverlay + visible: !root.showOnlyLists && BluetoothService.pinRequired + anchors.centerIn: parent + width: Math.min(parent.width * 0.9, 400) + height: pinCol.implicitHeight + Style.margin2L + color: Color.mSurface + radius: Style.radiusM + border.color: Style.boxBorderColor + border.width: Style.borderS + z: 1000 + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.AllButtons + onClicked: mouse => mouse.accepted = true + onWheel: wheel => wheel.accepted = true + } + + ColumnLayout { + id: pinCol + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginL + + NIcon { + icon: "lock" + pointSize: 48 + color: Color.mPrimary + Layout.alignment: Qt.AlignHCenter + } + NText { + text: I18n.tr("panels.connections.authentication-required") + pointSize: Style.fontSizeXL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + } + NText { + text: I18n.tr("panels.connections.pin-instructions") + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + Layout.fillWidth: true + } + NTextInput { + id: pinInput + Layout.fillWidth: true + placeholderText: "123456" + inputIconName: "key" + onVisibleChanged: { + if (visible) { + text = ""; + inputItem.forceActiveFocus(); + } + } + inputItem.onEditingFinished: { + if (text.length > 0) { + BluetoothService.submitPin(text); + text = ""; + } + } + } + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Style.marginM + NButton { + text: I18n.tr("common.cancel") + icon: "x" + onClicked: BluetoothService.cancelPairing() + } + NButton { + text: I18n.tr("common.confirm") + icon: "check" + backgroundColor: Color.mPrimary + textColor: Color.mOnPrimary + enabled: pinInput.text.length > 0 + onClicked: { + BluetoothService.submitPin(pinInput.text); + pinInput.text = ""; + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Connections/ConnectionsTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Connections/ConnectionsTab.qml new file mode 100644 index 0000000..de2f7c8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Connections/ConnectionsTab.qml @@ -0,0 +1,43 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.Networking +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.wifi") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.bluetooth") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + WifiSubTab {} + BluetoothSubTab {} + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml new file mode 100644 index 0000000..092ac77 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Connections/WifiSubTab.qml @@ -0,0 +1,1462 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import Quickshell +import Quickshell.Bluetooth + +import qs.Commons +import qs.Services.Networking +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +Item { + id: root + Layout.fillWidth: true + implicitHeight: mainLayout.implicitHeight + + // Configuration for shared use (e.g. by NetworkPanel) + property bool showOnlyLists: false + + // State properties + property string passwordSsid: "" + property string identity: "" + property string enterpriseEap: "peap" + property string enterprisePhase2: "mschapv2" + property string enterpriseAnonIdentity: "" + property string enterpriseCaCert: "" + property string expandedSsid: "" + property string infoSsid: "" + property int ipVersion: 4 + property bool detailsGrid: (Settings.data && Settings.data.network && Settings.data.network.wifiDetailsViewMode === "grid") + + // Freezing models for password entry + property var cachedNetworks: ({}) + + onPasswordSsidChanged: { + if (passwordSsid && passwordSsid.length > 0) { + try { + cachedNetworks = JSON.parse(JSON.stringify(NetworkService.networks)); + } catch (e) { + cachedNetworks = Object.assign({}, NetworkService.networks); + } + } else { + cachedNetworks = ({}); + } + } + + readonly property var activeNetworks: (passwordSsid && passwordSsid.length > 0) ? Object.values(cachedNetworks) : Object.values(NetworkService.networks) + + readonly property var connectedNetworks: { + if (!NetworkService.wifiEnabled) { + return []; + } + return activeNetworks.filter(n => n.connected).sort((a, b) => b.signal - a.signal); + } + + readonly property var savedNetworks: { + if (!NetworkService.wifiEnabled) { + return []; + } + return activeNetworks.filter(n => !n.connected && n.existing).sort((a, b) => b.signal - a.signal); + } + + readonly property var availableNetworks: { + if (!NetworkService.wifiEnabled) { + return []; + } + return activeNetworks.filter(n => !n.connected && !n.existing).sort((a, b) => b.signal - a.signal); + } + + // Combined visibility check: tab must be visible AND the window must be visible + readonly property bool effectivelyVisible: root.visible && Window.window && Window.window.visible + + onEffectivelyVisibleChanged: { + if (effectivelyVisible) { + SystemStatService.registerComponent("wifi-subtab"); + if (NetworkService.wifiEnabled && !NetworkService.scanningActive && !showOnlyLists) { + NetworkService.scan(); + NetworkService.refreshActiveWifiDetails(); + } + } else { + SystemStatService.unregisterComponent("wifi-subtab"); + } + } + + Component.onDestruction: { + SystemStatService.unregisterComponent("wifi-subtab"); + } + + // Actions + function requestPassword(ssid) { + passwordSsid = ssid; + identity = ""; + enterpriseEap = "peap"; + enterprisePhase2 = "mschapv2"; + enterpriseAnonIdentity = ""; + enterpriseCaCert = ""; + expandedSsid = ""; + } + function submitPassword(ssid, password, identity = "") { + NetworkService.connect(ssid, password, false, identity, { + eap: enterpriseEap, + phase2: enterprisePhase2, + anonIdentity: enterpriseAnonIdentity, + caCert: enterpriseCaCert + }); + passwordSsid = ""; + } + function cancelPassword() { + passwordSsid = ""; + } + function requestForget(ssid) { + expandedSsid = (expandedSsid === ssid) ? "" : ssid; + } + function confirmForget(ssid) { + NetworkService.forget(ssid); + expandedSsid = ""; + } + function cancelForget() { + expandedSsid = ""; + } + + ColumnLayout { + id: mainLayout + anchors.left: parent.left + anchors.right: parent.right + spacing: Style.marginL + + // Master Control Section + NBox { + visible: !root.showOnlyLists + Layout.fillWidth: true + Layout.preferredHeight: masterControlCol.implicitHeight + Style.margin2L + color: Color.mSurface + + ColumnLayout { + id: masterControlCol + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NToggle { + label: I18n.tr("common.wifi") + icon: NetworkService.wifiEnabled ? "wifi" : "wifi-off" + checked: NetworkService.wifiEnabled + onToggled: checked => NetworkService.setWifiEnabled(checked) + enabled: !NetworkService.airplaneModeEnabled && NetworkService.wifiAvailable + Layout.alignment: Qt.AlignVCenter + } + } + + NDivider { + Layout.fillWidth: true + visible: NetworkService.wifiEnabled + } + + NText { + visible: !root.showOnlyLists && NetworkService.wifiEnabled + Layout.fillWidth: true + text: I18n.tr("panels.connections.wifi-header-text") + color: Color.mOnSurfaceVariant + richTextEnabled: true + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + } + } + + Item { + visible: !showOnlyLists + Layout.fillWidth: true + } + + // Network List [1] (Connected) + NBox { + id: connectedBox + visible: root.connectedNetworks.length > 0 && NetworkService.wifiEnabled + Layout.fillWidth: true + Layout.preferredHeight: connectedCol.implicitHeight + Style.margin2M + border.color: showOnlyLists ? Style.boxBorderColor : "transparent" + color: showOnlyLists ? Color.mSurfaceVariant : "transparent" + + ColumnLayout { + id: connectedCol + anchors.fill: parent + anchors.topMargin: Style.marginM + anchors.bottomMargin: Style.marginM + anchors.leftMargin: showOnlyLists ? Style.marginL : 0 + anchors.rightMargin: showOnlyLists ? Style.marginL : 0 + spacing: Style.marginM + + NLabel { + label: I18n.tr("common.connected") + Layout.fillWidth: true + Layout.leftMargin: Style.marginS + } + + Repeater { + model: root.connectedNetworks + delegate: nboxDelegate + } + } + } + + // Network List [2] (Saved) + NBox { + id: savedBox + visible: root.savedNetworks.length > 0 && NetworkService.wifiEnabled + Layout.fillWidth: true + Layout.preferredHeight: savedCol.implicitHeight + Style.margin2M + border.color: showOnlyLists ? Style.boxBorderColor : "transparent" + color: showOnlyLists ? Color.mSurfaceVariant : "transparent" + + ColumnLayout { + id: savedCol + anchors.fill: parent + anchors.topMargin: Style.marginM + anchors.bottomMargin: Style.marginM + anchors.leftMargin: showOnlyLists ? Style.marginL : 0 + anchors.rightMargin: showOnlyLists ? Style.marginL : 0 + spacing: Style.marginM + + NLabel { + label: I18n.tr("wifi.panel.known-networks") + Layout.fillWidth: true + Layout.leftMargin: Style.marginS + } + + Repeater { + model: root.savedNetworks + delegate: nboxDelegate + } + } + } + + // Network List [3] (Available) + NBox { + id: availableBox + visible: root.availableNetworks.length > 0 && NetworkService.wifiEnabled + Layout.fillWidth: true + Layout.preferredHeight: availableCol.implicitHeight + Style.margin2M + border.color: showOnlyLists ? Style.boxBorderColor : "transparent" + color: showOnlyLists ? Color.mSurfaceVariant : "transparent" + + ColumnLayout { + id: availableCol + anchors.fill: parent + anchors.topMargin: Style.marginM + anchors.bottomMargin: Style.marginM + anchors.leftMargin: showOnlyLists ? Style.marginL : 0 + anchors.rightMargin: showOnlyLists ? Style.marginL : 0 + spacing: Style.marginM + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: Style.marginS + spacing: Style.marginS + + NLabel { + label: I18n.tr("wifi.panel.available-networks") + Layout.fillWidth: true + } + } + + Repeater { + model: root.availableNetworks + delegate: nboxDelegate + } + + // Add hidden network button + NBox { + visible: !root.showOnlyLists + Layout.fillWidth: true + Layout.preferredHeight: addHiddenContent.implicitHeight + Style.margin2M + color: addHiddenMouseArea.containsMouse ? Color.mSurfaceVariant : Color.mSurface + radius: Style.radiusM + + RowLayout { + id: addHiddenContent + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + NIcon { + icon: "plus" + pointSize: Style.fontSizeXXL + color: Color.mOnSurfaceVariant + } + + NText { + text: I18n.tr("wifi.panel.add-network") + pointSize: Style.fontSizeM + color: Color.mOnSurface + Layout.fillWidth: true + } + } + + MouseArea { + id: addHiddenMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + addNetworkPopup.customSsid = ""; + addNetworkPopup.customPassword = ""; + addNetworkPopup.customSecurityKey = "wpa2-psk"; + addNetworkPopup.open(); + } + } + } + } + } + + Item { + visible: !showOnlyLists && NetworkService.wifiEnabled + Layout.fillWidth: true + } + + // Airplane Mode + NBox { + id: miscSettingsBox + visible: !root.showOnlyLists && miscSettingsCol.visibleChildren.length > 0 + Layout.fillWidth: true + Layout.preferredHeight: miscSettingsCol.implicitHeight + Style.margin2XL + color: Color.mSurface + + ColumnLayout { + id: miscSettingsCol + anchors.fill: parent + anchors.margins: Style.marginXL + spacing: Style.marginM + + NToggle { + visible: NetworkService.wifiAvailable && BluetoothService.bluetoothAvailable + label: I18n.tr("toast.airplane-mode.title") + description: I18n.tr("toast.airplane-mode.description") + icon: NetworkService.airplaneModeEnabled ? "plane" : "plane-off" + checked: NetworkService.airplaneModeEnabled + onToggled: checked => NetworkService.setAirplaneMode(checked) + } + } + } + } + + // Add Hidden Network Popup + Popup { + id: addNetworkPopup + visible: false + anchors.centerIn: parent + width: Math.min(parent.width * 0.9, 400 * Style.uiScaleRatio) + height: addNetworkContent.implicitHeight + Style.margin2L + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + property string customSsid: "" + property string customPassword: "" + property string customIdentity: "" + property string customSecurityKey: "wpa2-psk" + property string customEnterpriseEap: "peap" + property string customEnterprisePhase2: "mschapv2" + property string customEnterpriseAnonIdentity: "" + property string customEnterpriseCaCert: "" + property bool customShowPassword: false + property bool customIsHidden: false + + onOpened: { + customSsidInput.inputItem.forceActiveFocus(); + } + + // Make background transparent so we can use NDropShadow + background: Item {} + + // Shadow effect (behind background) + NDropShadow { + anchors.fill: customPopupBg + source: customPopupBg + autoPaddingEnabled: true + z: -1 + } + + Rectangle { + id: customPopupBg + anchors.fill: parent + radius: Style.radiusL + color: Qt.alpha(Color.mSurface, 0.95) + border.color: Color.mOutline + border.width: Style.borderS + } + + ColumnLayout { + id: addNetworkContent + anchors.centerIn: parent + width: parent.width - (Style.marginL * 2) + spacing: Style.marginM + + // Header with Icon + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NImageRounded { + Layout.preferredWidth: Style.fontSizeXXL * 2 + Layout.preferredHeight: Style.fontSizeXXL * 2 + fallbackIcon: "wifi" + borderWidth: 0 + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NText { + text: I18n.tr("wifi.panel.add-network") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + wrapMode: Text.Wrap + Layout.fillWidth: true + } + } + } + + // Input Fields + NTextInput { + id: customSsidInput + Layout.fillWidth: true + inputIconName: "wifi" + placeholderText: I18n.tr("wifi.panel.network-name-ssid") + label: I18n.tr("wifi.panel.network-name-ssid") + text: addNetworkPopup.customSsid + onTextChanged: addNetworkPopup.customSsid = text + onEditingFinished: { + if (addNetworkPopup.customSsid.length > 0 && (addNetworkPopup.customSecurityKey === "open" || addNetworkPopup.customPassword.length > 0)) { + NetworkService.connect(addNetworkPopup.customSsid, addNetworkPopup.customPassword, addNetworkPopup.customIsHidden, addNetworkPopup.customSecurityKey, addNetworkPopup.customIdentity, { + eap: addNetworkPopup.customEnterpriseEap, + phase2: addNetworkPopup.customEnterprisePhase2, + anonIdentity: addNetworkPopup.customEnterpriseAnonIdentity, + caCert: addNetworkPopup.customEnterpriseCaCert + }); + addNetworkPopup.close(); + } + } + } + + NComboBox { + Layout.fillWidth: true + model: NetworkService.supportedSecurityTypes + currentKey: addNetworkPopup.customSecurityKey + onSelected: key => { + addNetworkPopup.customSecurityKey = key; + } + } + + ColumnLayout { + visible: addNetworkPopup.customSecurityKey.indexOf("-eap") !== -1 + Layout.fillWidth: true + spacing: Style.marginM + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("wifi.enterprise.eap-method") + model: [ + { + key: "peap", + name: "PEAP" + }, + { + key: "ttls", + name: "TTLS" + } + ] + currentKey: addNetworkPopup.customEnterpriseEap + onSelected: key => addNetworkPopup.customEnterpriseEap = key + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("wifi.enterprise.phase2-auth") + model: [ + { + key: "mschapv2", + name: "MSCHAPv2" + }, + { + key: "pap", + name: "PAP" + }, + { + key: "mschap", + name: "MSCHAP" + }, + { + key: "chap", + name: "CHAP" + } + ] + currentKey: addNetworkPopup.customEnterprisePhase2 + onSelected: key => addNetworkPopup.customEnterprisePhase2 = key + } + } + + NTextInput { + id: customAnonIdentityInput + Layout.fillWidth: true + inputIconName: "user-question" + visible: addNetworkPopup.customSecurityKey.indexOf("-eap") !== -1 + placeholderText: I18n.tr("wifi.enterprise.anonymous-identity") + label: I18n.tr("wifi.enterprise.anonymous-identity") + text: addNetworkPopup.customEnterpriseAnonIdentity + onTextChanged: addNetworkPopup.customEnterpriseAnonIdentity = text + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + visible: addNetworkPopup.customSecurityKey.indexOf("-eap") !== -1 + + NTextInput { + id: customCaCertInput + Layout.fillWidth: true + inputIconName: "certificate" + placeholderText: I18n.tr("wifi.enterprise.ca-cert") + label: I18n.tr("wifi.enterprise.ca-cert") + text: addNetworkPopup.customEnterpriseCaCert + onTextChanged: addNetworkPopup.customEnterpriseCaCert = text + } + + NIconButton { + icon: "folder-open" + Layout.alignment: Qt.AlignBottom + onClicked: caCertPicker.openForAddNetwork() + } + } + + NTextInput { + id: customIdentityInput + Layout.fillWidth: true + inputIconName: "user" + visible: addNetworkPopup.customSecurityKey.indexOf("-eap") !== -1 + placeholderText: I18n.tr("wifi.enterprise.username") + label: I18n.tr("wifi.enterprise.username") + text: addNetworkPopup.customIdentity + onTextChanged: addNetworkPopup.customIdentity = text + } + + NTextInput { + id: customPasswordInput + Layout.fillWidth: true + inputIconName: "key" + visible: addNetworkPopup.customSecurityKey !== "open" + placeholderText: I18n.tr("common.password") + label: I18n.tr("common.password") + text: addNetworkPopup.customPassword + onTextChanged: addNetworkPopup.customPassword = text + inputItem.echoMode: addNetworkPopup.customShowPassword ? TextInput.Normal : TextInput.Password + onEditingFinished: { + if (addNetworkPopup.customSsid.length > 0 && addNetworkPopup.customPassword.length > 0) { + NetworkService.connect(addNetworkPopup.customSsid, addNetworkPopup.customPassword, addNetworkPopup.customIsHidden, addNetworkPopup.customSecurityKey, addNetworkPopup.customIdentity, { + eap: addNetworkPopup.customEnterpriseEap, + phase2: addNetworkPopup.customEnterprisePhase2, + anonIdentity: addNetworkPopup.customEnterpriseAnonIdentity, + caCert: addNetworkPopup.customEnterpriseCaCert + }); + addNetworkPopup.close(); + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NCheckbox { + Layout.fillWidth: true + label: I18n.tr("wifi.panel.show-password") + checked: addNetworkPopup.customShowPassword + onToggled: checked => addNetworkPopup.customShowPassword = checked + visible: addNetworkPopup.customSecurityKey !== "open" + } + + NCheckbox { + Layout.fillWidth: true + label: I18n.tr("wifi.panel.hidden-network") + checked: addNetworkPopup.customIsHidden + onToggled: checked => addNetworkPopup.customIsHidden = checked + } + } + + // Actions + RowLayout { + Layout.fillWidth: true + Layout.topMargin: Style.marginS + spacing: Style.marginM + + Item { + Layout.fillWidth: true + } // Spacer + + NButton { + text: I18n.tr("common.cancel") + backgroundColor: Color.mSurfaceVariant + textColor: Color.mOnSurfaceVariant + outlined: false + onClicked: addNetworkPopup.close() + } + + NButton { + text: I18n.tr("common.connect") + backgroundColor: Color.mPrimary + textColor: Color.mOnPrimary + enabled: addNetworkPopup.customSsid.length > 0 && (addNetworkPopup.customSecurityKey === "open" || addNetworkPopup.customPassword.length > 0) && (addNetworkPopup.customSecurityKey.indexOf("-eap") === -1 || addNetworkPopup.customIdentity.length > 0) + onClicked: { + NetworkService.connect(addNetworkPopup.customSsid, addNetworkPopup.customPassword, addNetworkPopup.customIsHidden, addNetworkPopup.customSecurityKey, addNetworkPopup.customIdentity, { + eap: addNetworkPopup.customEnterpriseEap, + phase2: addNetworkPopup.customEnterprisePhase2, + anonIdentity: addNetworkPopup.customEnterpriseAnonIdentity, + caCert: addNetworkPopup.customEnterpriseCaCert + }); + addNetworkPopup.close(); + } + } + } + } + } + + // Shared Delegate + Component { + id: nboxDelegate + NBox { + id: networkItem + + readonly property bool isBusy: NetworkService.connectingTo === modelData.ssid || NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid + readonly property bool isExpanded: root.infoSsid === modelData.ssid + readonly property bool isEnterprise: NetworkService.isEnterprise(modelData.security) + + function getContentColors(defaultColors = [Color.mSurface, Color.mOnSurface]) { + if (root.passwordSsid === modelData.ssid || NetworkService.connectingTo === modelData.ssid) { + return [Color.mPrimary, Color.mOnPrimary]; + } + if (modelData.connected && NetworkService.internetConnectivity && NetworkService.disconnectingFrom !== modelData.ssid) { + return [Color.mPrimary, Color.mOnPrimary]; + } + if (NetworkService.disconnectingFrom === modelData.ssid || NetworkService.forgettingNetwork === modelData.ssid) { + return [Color.mError, Color.mOnError]; + } + if (modelData.connected && !NetworkService.internetConnectivity) { + return [Color.mError, Color.mOnError]; + } + return defaultColors; + } + + Layout.fillWidth: true + Layout.preferredHeight: deviceColumn.implicitHeight + (Style.marginXL) + radius: Style.radiusM + clip: true + forceOpaque: true + color: networkItem.getContentColors()[0] + + ColumnLayout { + id: deviceColumn + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + RowLayout { + id: deviceLayout + Layout.fillWidth: true + spacing: Style.marginM + Layout.alignment: Qt.AlignVCenter + + NIcon { + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + horizontalAlignment: Text.AlignLeft + icon: NetworkService.getSignalInfo(modelData.signal, modelData.connected).icon + pointSize: Style.fontSizeXXL + color: networkItem.getContentColors()[1] + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, NetworkService.getSignalInfo(modelData.signal, modelData.connected).label + " (" + modelData.signal + "%)") + onExited: TooltipService.hide() + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXXS + + NText { + text: modelData.ssid + pointSize: Style.fontSizeM + font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium + elide: Text.ElideRight + color: networkItem.getContentColors()[1] + Layout.fillWidth: true + } + + RowLayout { + spacing: Style.marginXS + + NIcon { + icon: NetworkService.isSecured(modelData.security) ? "lock" : "lock-open" + pointSize: Style.fontSizeXXS + color: Qt.alpha(networkItem.getContentColors()[1], Style.opacityHeavy) + visible: !modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid && NetworkService.forgettingNetwork !== modelData.ssid + } + + NText { + text: { + if (NetworkService.disconnectingFrom === modelData.ssid) { + return I18n.tr("wifi.panel.disconnecting"); + } + if (NetworkService.forgettingNetwork === modelData.ssid) { + return I18n.tr("wifi.panel.forgetting"); + } + if (modelData.connected) { + switch (NetworkService.networkConnectivity) { + case "full": + return I18n.tr("common.connected"); + case "limited": + return I18n.tr("wifi.panel.internet-limited"); + case "portal": + return I18n.tr("wifi.panel.action-required"); + default: + return NetworkService.networkConnectivity; + } + } + return NetworkService.isSecured(modelData.security) ? modelData.security : I18n.tr("wifi.panel.security-open"); + } + pointSize: Style.fontSizeXXS + color: Qt.alpha(networkItem.getContentColors()[1], Style.opacityHeavy) + } + + // Network speed indicators (visible when connected and speed > 0) + RowLayout { + visible: (modelData.connected && NetworkService.networkConnectivity === "full") && (SystemStatService.rxSpeed > 0 || SystemStatService.txSpeed > 0) + spacing: 2 + Layout.leftMargin: Style.marginXS + Layout.fillWidth: false + + NIcon { + visible: SystemStatService.rxSpeed > 0 + icon: "arrow-down" + pointSize: Style.fontSizeXXS + color: Qt.alpha(networkItem.getContentColors()[1], Style.opacityHeavy) + } + + NText { + visible: SystemStatService.rxSpeed > 0 + text: SystemStatService.formatSpeed(SystemStatService.rxSpeed) + pointSize: Style.fontSizeXXS + color: Qt.alpha(networkItem.getContentColors()[1], Style.opacityHeavy) + elide: Text.ElideNone + } + + Item { + visible: SystemStatService.rxSpeed > 0 && SystemStatService.txSpeed > 0 + width: Style.marginXS + height: 1 + } + + NIcon { + visible: SystemStatService.txSpeed > 0 + icon: "arrow-up" + pointSize: Style.fontSizeXXS + color: Qt.alpha(networkItem.getContentColors()[1], Style.opacityHeavy) + } + + NText { + visible: SystemStatService.txSpeed > 0 + text: SystemStatService.formatSpeed(SystemStatService.txSpeed) + pointSize: Style.fontSizeXXS + color: Qt.alpha(networkItem.getContentColors()[1], Style.opacityHeavy) + elide: Text.ElideNone + } + } + } + } + + Item { + Layout.fillWidth: true + } + + RowLayout { + spacing: Style.marginS + + NBusyIndicator { + visible: networkItem.isBusy + running: visible && root.effectivelyVisible + color: networkItem.getContentColors()[1] + size: Style.baseWidgetSize * 0.5 + } + + NIconButton { + visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid + icon: "info" + tooltipText: I18n.tr("common.info") + baseSize: Style.baseWidgetSize * 0.75 + colorBg: Color.mSurfaceVariant + colorFg: Color.mOnSurface + colorBorder: "transparent" + colorBorderHover: "transparent" + onClicked: { + if (root.infoSsid === modelData.ssid) { + root.infoSsid = ""; + } else { + root.infoSsid = modelData.ssid; + NetworkService.refreshActiveWifiDetails(); + } + } + } + + NIconButton { + visible: !root.showOnlyLists && modelData.existing && !modelData.connected && !networkItem.isBusy + icon: "trash" + tooltipText: I18n.tr("tooltips.forget-network") + baseSize: Style.baseWidgetSize * 0.75 + colorBg: Color.mPrimary + colorFg: Color.mOnPrimary + colorBorder: "transparent" + colorBorderHover: "transparent" + onClicked: root.requestForget(modelData.ssid) + } + + NButton { + id: button + visible: !modelData.connected && NetworkService.connectingTo !== modelData.ssid && root.passwordSsid !== modelData.ssid + enabled: !NetworkService.connecting && !networkItem.isBusy + fontSize: Style.fontSizeS + backgroundColor: Color.mPrimary + textColor: Color.mOnPrimary + text: I18n.tr("common.connect") + onClicked: { + if (modelData.existing || !NetworkService.isSecured(modelData.security)) { + NetworkService.connect(modelData.ssid); + } else { + root.requestPassword(modelData.ssid); + } + } + } + + NButton { + id: disconnectButton + visible: modelData.connected && NetworkService.disconnectingFrom !== modelData.ssid + text: I18n.tr("common.disconnect") + fontSize: Style.fontSizeS + backgroundColor: Color.mSurfaceVariant + textColor: Color.mOnSurface + onClicked: NetworkService.disconnect(modelData.ssid) + } + } + } + + // Connection info details + Rectangle { + visible: networkItem.isExpanded + Layout.fillWidth: true + implicitHeight: infoColumn.implicitHeight + Style.margin2S + radius: Style.radiusXS + color: Color.mSurfaceVariant + border.width: Style.borderS + border.color: Style.boxBorderColor + clip: true + + onVisibleChanged: { + if (visible && infoColumn && infoColumn.forceLayout) { + Qt.callLater(function () { + infoColumn.forceLayout(); + }); + } + } + + NIconButton { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginS + icon: root.detailsGrid ? "layout-list" : "layout-grid" + tooltipText: root.detailsGrid ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view") + baseSize: Style.baseWidgetSize * 0.65 + onClicked: { + root.detailsGrid = !root.detailsGrid; + Settings.data.network.wifiDetailsViewMode = root.detailsGrid ? "grid" : "list"; + } + z: 1 + } + + GridLayout { + id: infoColumn + anchors.fill: parent + anchors.margins: Style.marginS + flow: root.detailsGrid ? GridLayout.TopToBottom : GridLayout.LeftToRight + rows: root.detailsGrid ? 3 : 6 + columns: root.detailsGrid ? 2 : 1 + columnSpacing: Style.marginM + rowSpacing: Style.marginXS + onColumnsChanged: { + if (infoColumn.forceLayout) { + Qt.callLater(function () { + infoColumn.forceLayout(); + }); + } + } + + // --- Item 1: Interface --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "network" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, I18n.tr("wifi.panel.interface")) + onExited: TooltipService.hide() + } + } + NText { + text: NetworkService.activeWifiIf || "-" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + wrapMode: root.detailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere + elide: root.detailsGrid ? Text.ElideRight : Text.ElideNone + maximumLineCount: root.detailsGrid ? 1 : 6 + clip: true + + MouseArea { + anchors.fill: parent + enabled: (NetworkService.activeWifiIf && NetworkService.activeWifiIf.length > 0) + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) + onExited: TooltipService.hide() + onClicked: { + const value = NetworkService.activeWifiIf || ""; + if (value.length > 0) { + Quickshell.execDetached(["wl-copy", value]); + ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("common.copied-to-clipboard"), "wifi"); + } + } + } + } + } + // --- Item 2: Band & Channel & Width of channel --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "router" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, I18n.tr("common.frequency")) + onExited: TooltipService.hide() + } + } + NText { + text: NetworkService.activeWifiDetails.band || "-" + pointSize: Style.fontSizeXS + Layout.fillWidth: true + } + } + // --- Item 3: Link Speed --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "gauge" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, I18n.tr("wifi.panel.link-speed")) + onExited: TooltipService.hide() + } + } + NText { + text: (NetworkService.activeWifiDetails.rateShort && NetworkService.activeWifiDetails.rateShort.length > 0) ? NetworkService.activeWifiDetails.rateShort : ((NetworkService.activeWifiDetails.rate && NetworkService.activeWifiDetails.rate.length > 0) ? NetworkService.activeWifiDetails.rate : "-") + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + } + } + // --- Item 4: IPv4 || IPv6 --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "network" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("wifi.panel.ipv4") : I18n.tr("wifi.panel.ipv6")) + onExited: TooltipService.hide() + onClicked: { + root.ipVersion = root.ipVersion === 4 ? 6 : 4; + TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("wifi.panel.ipv4") : I18n.tr("wifi.panel.ipv6")); + } + } + } + NText { + text: root.ipVersion === 4 ? (NetworkService.activeWifiDetails.ipv4 || "-") : ((NetworkService.activeWifiDetails.ipv6 || []).join(", ") || "-") + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + + MouseArea { + anchors.fill: parent + enabled: root.ipVersion === 4 ? !!(NetworkService.activeWifiDetails.ipv4 && NetworkService.activeWifiDetails.ipv4.length > 0) : !!(NetworkService.activeWifiDetails.ipv6 && NetworkService.activeWifiDetails.ipv6.length > 0) + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) + onExited: TooltipService.hide() + onClicked: { + const value = root.ipVersion === 4 ? (NetworkService.activeWifiDetails.ipv4 || "") : ((NetworkService.activeWifiDetails.ipv6 || []).join(", ") || ""); + if (value.length > 0) { + Quickshell.execDetached(["wl-copy", value]); + ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("common.copied-to-clipboard"), "wifi"); + } + } + } + } + } + // --- Item 5: DNS --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "world" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("wifi.panel.dns") + " (" + I18n.tr("wifi.panel.ipv4") + ")" : I18n.tr("wifi.panel.dns") + " (" + I18n.tr("wifi.panel.ipv6") + ")") + onExited: TooltipService.hide() + onClicked: { + root.ipVersion = root.ipVersion === 4 ? 6 : 4; + TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("wifi.panel.dns") + " (" + I18n.tr("wifi.panel.ipv4") + ")" : I18n.tr("wifi.panel.dns") + " (" + I18n.tr("wifi.panel.ipv6") + ")"); + } + } + } + NText { + text: root.ipVersion === 4 ? ((NetworkService.activeWifiDetails.dns4 || []).join(", ") || "-") : ((NetworkService.activeWifiDetails.dns6 || []).join(", ") || "-") + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + + MouseArea { + anchors.fill: parent + enabled: root.ipVersion === 4 ? !!(NetworkService.activeWifiDetails.dns4 && NetworkService.activeWifiDetails.dns4.length > 0) : !!(NetworkService.activeWifiDetails.dns6 && NetworkService.activeWifiDetails.dns6.length > 0) + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) + onExited: TooltipService.hide() + onClicked: { + const value = root.ipVersion === 4 ? ((NetworkService.activeWifiDetails.dns4 || []).join(", ") || "") : ((NetworkService.activeWifiDetails.dns6 || []).join(", ") || ""); + if (value.length > 0) { + Quickshell.execDetached(["wl-copy", value]); + ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("common.copied-to-clipboard"), "wifi"); + } + } + } + } + } + // --- Item 6: Gateway --- + RowLayout { + Layout.fillWidth: true + Layout.preferredWidth: 1 + spacing: Style.marginXS + NIcon { + icon: "router" + pointSize: Style.fontSizeXS + color: Color.mOnSurface + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("common.gateway") + " (" + I18n.tr("wifi.panel.ipv4") + ")" : I18n.tr("common.gateway") + " (" + I18n.tr("wifi.panel.ipv6") + ")") + onExited: TooltipService.hide() + onClicked: { + root.ipVersion = root.ipVersion === 4 ? 6 : 4; + TooltipService.show(parent, root.ipVersion === 4 ? I18n.tr("common.gateway") + " (" + I18n.tr("wifi.panel.ipv4") + ")" : I18n.tr("common.gateway") + " (" + I18n.tr("wifi.panel.ipv6") + ")"); + } + } + } + NText { + text: root.ipVersion === 4 ? (NetworkService.activeWifiDetails.gateway4 || "-") : ((NetworkService.activeWifiDetails.gateway6 || []).join(", ") || "-") + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + + MouseArea { + anchors.fill: parent + enabled: root.ipVersion === 4 ? !!(NetworkService.activeWifiDetails.gateway4 && NetworkService.activeWifiDetails.gateway4.length > 0) : !!(NetworkService.activeWifiDetails.gateway6 && NetworkService.activeWifiDetails.gateway6.length > 0) + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address")) + onExited: TooltipService.hide() + onClicked: { + const value = root.ipVersion === 4 ? (NetworkService.activeWifiDetails.gateway4 || "") : ((NetworkService.activeWifiDetails.gateway6 || []).join(", ") || ""); + if (value.length > 0) { + Quickshell.execDetached(["wl-copy", value]); + ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("common.copied-to-clipboard"), "wifi"); + } + } + } + } + } + } + } + + // Password input overlay-style within card + Rectangle { + visible: root.passwordSsid === modelData.ssid && !networkItem.isBusy + Layout.fillWidth: true + height: passwordLayout.implicitHeight + Style.margin2S + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Style.borderS + radius: Style.iRadiusXS + + ColumnLayout { + id: passwordLayout + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginS + + // Inputs Container + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + + // Enterprise Configuration + ColumnLayout { + visible: networkItem.isEnterprise + Layout.fillWidth: true + spacing: Style.marginS + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("wifi.enterprise.eap-method") + model: [ + { + key: "peap", + name: "PEAP" + }, + { + key: "ttls", + name: "TTLS" + } + ] + currentKey: root.enterpriseEap + onSelected: key => root.enterpriseEap = key + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("wifi.enterprise.phase2-auth") + model: [ + { + key: "mschapv2", + name: "MSCHAPv2" + }, + { + key: "pap", + name: "PAP" + }, + { + key: "mschap", + name: "MSCHAP" + }, + { + key: "chap", + name: "CHAP" + } + ] + currentKey: root.enterprisePhase2 + onSelected: key => root.enterprisePhase2 = key + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize * 0.9 + radius: Style.iRadiusXS + color: Color.mSurface + border.color: caCertInput.activeFocus ? Color.mSecondary : Color.mOutline + border.width: Style.borderS + + TextInput { + id: caCertInput + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Style.marginS + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS + color: Color.mOnSurface + selectByMouse: true + text: root.enterpriseCaCert + onTextChanged: root.enterpriseCaCert = text + + NText { + visible: parent.text.length === 0 + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("wifi.enterprise.ca-cert") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + } + } + } + + NIconButton { + icon: "folder-open" + baseSize: Style.baseWidgetSize * 0.75 + onClicked: caCertPicker.openForInline() + } + } + } + + // Anonymous Identity field (Enterprise only) + Rectangle { + visible: networkItem.isEnterprise + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize * 0.9 + radius: Style.iRadiusXS + color: Color.mSurface + border.color: anonIdentityInput.activeFocus ? Color.mSecondary : Color.mOutline + border.width: Style.borderS + + TextInput { + id: anonIdentityInput + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Style.marginS + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS + color: Color.mOnSurface + selectByMouse: true + text: root.enterpriseAnonIdentity + onTextChanged: root.enterpriseAnonIdentity = text + onEditingFinished: identityInput.forceActiveFocus() + + NText { + visible: parent.text.length === 0 + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("wifi.enterprise.anonymous-identity") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + } + } + } + + // Identity field (Enterprise only) + Rectangle { + visible: networkItem.isEnterprise + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize * 0.9 + radius: Style.iRadiusXS + color: Color.mSurface + border.color: identityInput.activeFocus ? Color.mSecondary : Color.mOutline + border.width: Style.borderS + + TextInput { + id: identityInput + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Style.marginS + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS + color: Color.mOnSurface + selectByMouse: true + onVisibleChanged: { + if (visible) { + forceActiveFocus(); + } + } + onEditingFinished: pwdInput.forceActiveFocus() + + NText { + visible: parent.text.length === 0 + anchors.verticalCenter: parent.verticalCenter + text: I18n.tr("wifi.enterprise.username") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + } + } + } + + // Password field + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: Style.baseWidgetSize * 0.9 + radius: Style.iRadiusXS + color: Color.mSurface + border.color: pwdInput.activeFocus ? Color.mSecondary : Color.mOutline + border.width: Style.borderS + + TextInput { + id: pwdInput + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Style.marginS + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeS + color: Color.mOnSurface + echoMode: TextInput.Password + selectByMouse: true + passwordCharacter: "●" + onVisibleChanged: { + if (visible && !networkItem.isEnterprise) { + forceActiveFocus(); + } + } + onEditingFinished: { + if (text && !NetworkService.connecting) { + if (!networkItem.isEnterprise || identityInput.text.length > 0) { + root.submitPassword(modelData.ssid, text, identityInput.text); + } + } + } + + NText { + visible: parent.text.length === 0 + anchors.verticalCenter: parent.verticalCenter + text: networkItem.isEnterprise ? I18n.tr("wifi.enterprise.password") : I18n.tr("wifi.panel.enter-password") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + Item { + Layout.fillWidth: true + } + + NButton { + text: I18n.tr("common.connect") + fontSize: Style.fontSizeS + enabled: pwdInput.text.length > 0 && (!networkItem.isEnterprise || identityInput.text.length > 0) && !NetworkService.connecting + outlined: true + onClicked: root.submitPassword(modelData.ssid, pwdInput.text, identityInput.text) + } + + NIconButton { + icon: "close" + baseSize: Style.baseWidgetSize * 0.75 + onClicked: root.cancelPassword() + } + } + } + } + + // Forget network confirmation within card + Rectangle { + visible: root.expandedSsid === modelData.ssid && !networkItem.isBusy + Layout.fillWidth: true + height: forgetRow.implicitHeight + Style.margin2S + color: Color.mSurfaceVariant + radius: Style.radiusS + border.width: Style.borderS + border.color: Color.mOutline + + RowLayout { + id: forgetRow + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginM + + RowLayout { + NIcon { + icon: "trash" + pointSize: Style.fontSizeL + color: Color.mError + } + + NText { + text: I18n.tr("wifi.panel.forget-network") + pointSize: Style.fontSizeS + color: Color.mError + Layout.fillWidth: true + } + } + + NButton { + id: forgetButton + text: I18n.tr("wifi.panel.forget") + fontSize: Style.fontSizeXXS + backgroundColor: Color.mError + outlined: !forgetButton.hovered + onClicked: root.confirmForget(modelData.ssid) + } + + NIconButton { + icon: "close" + baseSize: Style.baseWidgetSize * 0.75 + onClicked: root.cancelForget() + } + } + } + } + } + } + + NFilePicker { + id: caCertPicker + title: I18n.tr("wifi.enterprise.ca-cert") + nameFilters: ["*.pem", "*.crt", "*.cer", "*.der", "*"] + + property bool isForAddNetwork: false + + function openForInline() { + isForAddNetwork = false; + openFilePicker(); + } + + function openForAddNetwork() { + isForAddNetwork = true; + openFilePicker(); + } + + onAccepted: paths => { + if (paths.length > 0) { + if (isForAddNetwork) { + addNetworkPopup.customEnterpriseCaCert = paths[0]; + } else { + root.enterpriseCaCert = paths[0]; + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ControlCenter/AppearanceSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ControlCenter/AppearanceSubTab.qml new file mode 100644 index 0000000..279d0d1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ControlCenter/AppearanceSubTab.qml @@ -0,0 +1,87 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + spacing: Style.marginL + Layout.fillWidth: true + + NComboBox { + id: controlCenterPosition + label: I18n.tr("common.position") + description: I18n.tr("panels.control-center.position-description") + Layout.fillWidth: true + model: [ + { + "key": "close_to_bar_button", + "name": I18n.tr("positions.close-to-bar") + }, + { + "key": "center", + "name": I18n.tr("positions.center") + }, + { + "key": "top_center", + "name": I18n.tr("positions.top-center") + }, + { + "key": "top_left", + "name": I18n.tr("positions.top-left") + }, + { + "key": "top_right", + "name": I18n.tr("positions.top-right") + }, + { + "key": "bottom_center", + "name": I18n.tr("positions.bottom-center") + }, + { + "key": "bottom_left", + "name": I18n.tr("positions.bottom-left") + }, + { + "key": "bottom_right", + "name": I18n.tr("positions.bottom-right") + } + ] + currentKey: Settings.data.controlCenter.position + onSelected: function (key) { + Settings.data.controlCenter.position = key; + } + defaultValue: Settings.getDefaultValue("controlCenter.position") + } + + NComboBox { + id: diskPathComboBox + Layout.fillWidth: true + label: I18n.tr("panels.control-center.system-monitor-disk-path-label") + description: I18n.tr("panels.control-center.system-monitor-disk-path-description") + model: { + const paths = Object.keys(SystemStatService.diskPercents).sort(); + return paths.map(path => ({ + key: path, + name: path + })); + } + currentKey: Settings.data.controlCenter.diskPath || "/" + onSelected: key => Settings.data.controlCenter.diskPath = key + defaultValue: Settings.getDefaultValue("controlCenter.diskPath") || "/" + } + } + + Rectangle { + Layout.fillHeight: true + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ControlCenter/CardsSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ControlCenter/CardsSubTab.qml new file mode 100644 index 0000000..aae1ea3 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ControlCenter/CardsSubTab.qml @@ -0,0 +1,79 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + Layout.fillHeight: true + + required property var cardsModel + required property list cardsDefault + + function saveCards() { + var toSave = []; + for (var i = 0; i < cardsModel.length; i++) { + toSave.push({ + "id": cardsModel[i].id, + "enabled": cardsModel[i].enabled + }); + } + Settings.data.controlCenter.cards = toSave; + } + + NText { + text: I18n.tr("panels.control-center.cards-desc") + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + + Connections { + target: Settings.data.location + function onWeatherEnabledChanged() { + // Auto-disable weather card when weather is disabled + var newModel = root.cardsModel.slice(); + for (var i = 0; i < newModel.length; i++) { + if (newModel[i].id === "weather-card") { + newModel[i] = Object.assign({}, newModel[i], { + "enabled": Settings.data.location.weatherEnabled + }); + root.cardsModel = newModel; + saveCards(); + break; + } + } + } + } + + NReorderCheckboxes { + Layout.fillWidth: true + model: root.cardsModel + disabledIds: Settings.data.location.weatherEnabled ? [] : ["weather-card"] + onItemToggled: function (index, enabled) { + var newModel = root.cardsModel.slice(); + newModel[index] = Object.assign({}, newModel[index], { + "enabled": enabled + }); + root.cardsModel = newModel; + saveCards(); + } + onItemsReordered: function (fromIndex, toIndex) { + var newModel = root.cardsModel.slice(); + var item = newModel.splice(fromIndex, 1)[0]; + newModel.splice(toIndex, 0, item); + root.cardsModel = newModel; + saveCards(); + } + } + } + } diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ControlCenter/ControlCenterTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ControlCenter/ControlCenterTab.qml new file mode 100644 index 0000000..9f5502c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ControlCenter/ControlCenterTab.qml @@ -0,0 +1,252 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Noctalia +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + property list cardsModel: [] + property list cardsDefault: [ + { + "id": "profile-card", + "text": "Profile", + "enabled": true, + "required": true + }, + { + "id": "shortcuts-card", + "text": "Shortcuts", + "enabled": true, + "required": false + }, + { + "id": "audio-card", + "text": "Audio Sliders", + "enabled": true, + "required": false + }, + { + "id": "brightness-card", + "text": "Brightness", + "enabled": false, + "required": false + }, + { + "id": "weather-card", + "text": "Weather", + "enabled": true, + "required": false + }, + { + "id": "media-sysmon-card", + "text": "Media and System Monitor", + "enabled": true, + "required": false + } + ] + + Component.onCompleted: { + // Fill out availableWidgets ListModel + availableWidgets.clear(); + var sortedEntries = ControlCenterWidgetRegistry.getAvailableWidgets().slice().sort(); + sortedEntries.forEach(entry => { + const isPlugin = ControlCenterWidgetRegistry.isPluginWidget(entry); + let displayName = entry; + let badges = []; + if (isPlugin) { + const pluginId = entry.replace("plugin:", ""); + const manifest = PluginRegistry.getPluginManifest(pluginId); + if (manifest && manifest.name) { + displayName = manifest.name; + } else { + displayName = pluginId; + } + badges.push({ + "icon": "plugin", + "color": Color.mSecondary + }); + } + availableWidgets.append({ + "key": entry, + "name": displayName, + "badges": badges + }); + }); + // Starts empty + cardsModel = []; + + // Add the cards available in settings + for (var i = 0; i < Settings.data.controlCenter.cards.length; i++) { + const settingCard = Settings.data.controlCenter.cards[i]; + + for (var j = 0; j < cardsDefault.length; j++) { + if (settingCard.id === cardsDefault[j].id) { + var card = cardsDefault[j]; + card.enabled = settingCard.enabled; + // Auto-disable weather card if weather is disabled + if (card.id === "weather-card" && !Settings.data.location.weatherEnabled) { + card.enabled = false; + } + cardsModel.push(card); + } + } + } + + // Add any missing cards from default + for (var i = 0; i < cardsDefault.length; i++) { + var found = false; + for (var j = 0; j < cardsModel.length; j++) { + if (cardsModel[j].id === cardsDefault[i].id) { + found = true; + break; + } + } + + if (!found) { + var card = cardsDefault[i]; + // Auto-disable weather card if weather is disabled + if (card.id === "weather-card" && !Settings.data.location.weatherEnabled) { + card.enabled = false; + } + cardsModel.push(card); + } + } + } + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.appearance") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.cards") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + NTabButton { + text: I18n.tr("common.shortcuts") + tabIndex: 2 + checked: subTabBar.currentIndex === 2 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + Layout.fillWidth: true + Layout.fillHeight: true + + AppearanceSubTab {} + + CardsSubTab { + cardsModel: root.cardsModel + cardsDefault: root.cardsDefault + } + + ShortcutsSubTab { + availableWidgets: availableWidgets + onAddWidgetToSection: (widgetId, section) => _addWidgetToSection(widgetId, section) + onRemoveWidgetFromSection: (section, index) => _removeWidgetFromSection(section, index) + onReorderWidgetInSection: (section, fromIndex, toIndex) => _reorderWidgetInSection(section, fromIndex, toIndex) + onUpdateWidgetSettingsInSection: (section, index, settings) => _updateWidgetSettingsInSection(section, index, settings) + onMoveWidgetBetweenSections: (fromSection, index, toSection) => _moveWidgetBetweenSections(fromSection, index, toSection) + onOpenPluginSettingsRequested: manifest => pluginSettingsDialog.openPluginSettings(manifest) + } + } + + // --------------------------------- + // Signal functions + // --------------------------------- + function _addWidgetToSection(widgetId, section) { + var newWidget = { + "id": widgetId + }; + if (ControlCenterWidgetRegistry.widgetHasUserSettings(widgetId)) { + var metadata = ControlCenterWidgetRegistry.widgetMetadata[widgetId]; + if (metadata) { + Object.keys(metadata).forEach(function (key) { + newWidget[key] = metadata[key]; + }); + } + } + Settings.data.controlCenter.shortcuts[section].push(newWidget); + } + + function _removeWidgetFromSection(section, index) { + if (index >= 0 && index < Settings.data.controlCenter.shortcuts[section].length) { + var newArray = Settings.data.controlCenter.shortcuts[section].slice(); + var removedWidgets = newArray.splice(index, 1); + Settings.data.controlCenter.shortcuts[section] = newArray; + } + } + + function _reorderWidgetInSection(section, fromIndex, toIndex) { + if (fromIndex >= 0 && fromIndex < Settings.data.controlCenter.shortcuts[section].length && toIndex >= 0 && toIndex < Settings.data.controlCenter.shortcuts[section].length) { + + // Create a new array to avoid modifying the original + var newArray = Settings.data.controlCenter.shortcuts[section].slice(); + var item = newArray[fromIndex]; + newArray.splice(fromIndex, 1); + newArray.splice(toIndex, 0, item); + + Settings.data.controlCenter.shortcuts[section] = newArray; + } + } + + function _moveWidgetBetweenSections(fromSection, index, toSection) { + // Get the widget from the source section + if (index >= 0 && index < Settings.data.controlCenter.shortcuts[fromSection].length) { + var widget = Settings.data.controlCenter.shortcuts[fromSection][index]; + + // Remove from source section + var sourceArray = Settings.data.controlCenter.shortcuts[fromSection].slice(); + sourceArray.splice(index, 1); + Settings.data.controlCenter.shortcuts[fromSection] = sourceArray; + + // Add to target section + var targetArray = Settings.data.controlCenter.shortcuts[toSection].slice(); + targetArray.push(widget); + Settings.data.controlCenter.shortcuts[toSection] = targetArray; + } + } + + function _updateWidgetSettingsInSection(section, index, settings) { + // Create a new array to trigger QML's change detection for persistence. + // This is crucial for Settings.data to detect the change and persist it. + var newSectionArray = Settings.data.controlCenter.shortcuts[section].slice(); + newSectionArray[index] = settings; + Settings.data.controlCenter.shortcuts[section] = newSectionArray; + Settings.saveImmediate(); + } + + // Base list model for all combo boxes + ListModel { + id: availableWidgets + } + + // Shared Plugin Settings Popup + NPluginSettingsPopup { + id: pluginSettingsDialog + parent: Overlay.overlay + showToastOnSave: false + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ControlCenter/ShortcutsSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ControlCenter/ShortcutsSubTab.qml new file mode 100644 index 0000000..d93c9b2 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/ControlCenter/ShortcutsSubTab.qml @@ -0,0 +1,87 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + Layout.fillHeight: true + + required property var availableWidgets + + signal addWidgetToSection(string widgetId, string section) + signal removeWidgetFromSection(string section, int index) + signal reorderWidgetInSection(string section, int fromIndex, int toIndex) + signal updateWidgetSettingsInSection(string section, int index, var settings) + signal moveWidgetBetweenSections(string fromSection, int index, string toSection) + signal openPluginSettingsRequested(var manifest) + + function getSectionIcons() { + return { + "left": "arrow-bar-to-up", + "right": "arrow-bar-to-down" + }; + } + + // Widgets Management Section + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + + // Sections + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: Style.marginM + spacing: Style.marginM + + // Left + NSectionEditor { + sectionName: I18n.tr("positions.left") + sectionId: "left" + settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml") + maxWidgets: Settings.data.controlCenter.shortcuts["right"].length > 5 ? 0 : (Settings.data.controlCenter.shortcuts["right"].length > 0 ? 5 : 10) + widgetRegistry: ControlCenterWidgetRegistry + widgetModel: Settings.data.controlCenter.shortcuts["left"] + sectionIcons: root.getSectionIcons() + availableWidgets: root.availableWidgets + availableSections: ["left", "right"] + onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section) + onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index) + onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex) + onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings) + onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection) + onOpenPluginSettingsRequested: manifest => root.openPluginSettingsRequested(manifest) + } + + // Right + NSectionEditor { + sectionName: I18n.tr("positions.right") + sectionId: "right" + settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/ControlCenter/ControlCenterWidgetSettingsDialog.qml") + maxWidgets: Settings.data.controlCenter.shortcuts["left"].length > 5 ? 0 : (Settings.data.controlCenter.shortcuts["left"].length > 0 ? 5 : 10) + widgetRegistry: ControlCenterWidgetRegistry + widgetModel: Settings.data.controlCenter.shortcuts["right"] + sectionIcons: root.getSectionIcons() + availableWidgets: root.availableWidgets + availableSections: ["left", "right"] + onAddWidget: (widgetId, section) => root.addWidgetToSection(widgetId, section) + onRemoveWidget: (section, index) => root.removeWidgetFromSection(section, index) + onReorderWidget: (section, fromIndex, toIndex) => root.reorderWidgetInSection(section, fromIndex, toIndex) + onUpdateWidgetSettings: (section, index, settings) => root.updateWidgetSettingsInSection(section, index, settings) + onMoveWidget: (fromSection, index, toSection) => root.moveWidgetBetweenSections(fromSection, index, toSection) + onOpenPluginSettingsRequested: manifest => root.openPluginSettingsRequested(manifest) + } + } + } + + Rectangle { + Layout.fillHeight: true + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/DesktopWidgetsTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/DesktopWidgetsTab.qml new file mode 100644 index 0000000..9707524 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/DesktopWidgetsTab.qml @@ -0,0 +1,332 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Compositor +import qs.Services.Noctalia +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + + spacing: Style.marginL + + // Available widgets model - declared early so Repeater delegates can access it + property alias availableWidgetsModel: availableWidgets + ListModel { + id: availableWidgets + } + + // Listen for plugin widget registration/unregistration + Connections { + target: DesktopWidgetRegistry + function onPluginWidgetRegistryUpdated() { + updateAvailableWidgetsModel(); + } + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.desktop-widgets.enabled-label") + description: I18n.tr("panels.desktop-widgets.enabled-description") + checked: Settings.data.desktopWidgets.enabled + defaultValue: Settings.getDefaultValue("desktopWidgets.enabled") + onToggled: checked => Settings.data.desktopWidgets.enabled = checked + } + + NToggle { + Layout.fillWidth: true + enabled: Settings.data.desktopWidgets.enabled + label: I18n.tr("panels.desktop-widgets.overview-enabled-label") + description: I18n.tr("panels.desktop-widgets.overview-enabled-description") + checked: Settings.data.desktopWidgets.overviewEnabled + defaultValue: Settings.getDefaultValue("desktopWidgets.overviewEnabled") + onToggled: checked => Settings.data.desktopWidgets.overviewEnabled = checked + } + + ColumnLayout { + enabled: Settings.data.desktopWidgets.enabled + + NLabel { + description: I18n.tr("panels.desktop-widgets.cpu-intensive-note") + } + + NButton { + Layout.fillWidth: true + Layout.topMargin: Style.marginM + Layout.bottomMargin: Style.marginM + text: DesktopWidgetRegistry.editMode ? I18n.tr("panels.desktop-widgets.edit-mode-exit-button") : I18n.tr("panels.desktop-widgets.edit-mode-button-label") + icon: "edit" + onClicked: { + DesktopWidgetRegistry.editMode = !DesktopWidgetRegistry.editMode; + if (DesktopWidgetRegistry.editMode && Settings.data.ui.settingsPanelMode !== "window") { + var item = root.parent; + while (item) { + if (item.closeRequested !== undefined) { + item.closeRequested(); + break; + } + item = item.parent; + } + } + } + } + + // One NSectionEditor per monitor + Repeater { + model: Quickshell.screens + + NSectionEditor { + required property var modelData + readonly property real compositorScale: { + const info = CompositorService.displayScales[modelData.name]; + return (info && info.scale) ? info.scale : 1.0; + } + + Layout.fillWidth: true + sectionName: modelData.name + sectionSubtitle: { + // Format scale to 2 decimal places to prevent overly long text + var formattedScale = compositorScale.toFixed(2); + return "(" + modelData.width + "x" + modelData.height + " @ " + formattedScale + "x)"; + } + + sectionId: modelData.name + screen: modelData + settingsDialogComponent: Qt.resolvedUrl(Quickshell.shellDir + "/Modules/Panels/Settings/DesktopWidgets/DesktopWidgetSettingsDialog.qml") + widgetRegistry: DesktopWidgetRegistry + widgetModel: getWidgetsForMonitor(modelData.name) + availableWidgets: root.availableWidgetsModel + availableSections: root.getScreenNames() + sectionLabels: root.getScreenLabels() + sectionIcons: root.getScreenIcons() + draggable: false // Desktop widgets are positioned by X,Y, not list order + crossSectionDraggable: true + maxWidgets: -1 + onAddWidget: (widgetId, section) => _addWidgetToMonitor(modelData.name, widgetId) + onRemoveWidget: (section, index) => _removeWidgetFromMonitor(modelData.name, index) + onMoveWidget: (fromSection, index, toSection) => _moveWidgetToMonitor(fromSection, index, toSection) + onUpdateWidgetSettings: (section, index, settings) => _updateWidgetSettingsForMonitor(modelData.name, index, settings) + pluginSettingsEntryPoints: ["desktopWidgetSettings", "settings"] + onOpenPluginSettingsRequested: (manifest, entryPoint) => pluginSettingsDialog.openPluginSettings(manifest, entryPoint) + } + } + } + + // Shared Plugin Settings Popup + NPluginSettingsPopup { + id: pluginSettingsDialog + parent: Overlay.overlay + showToastOnSave: false + } + + Component.onCompleted: { + // Use Qt.callLater to ensure DesktopWidgetRegistry is ready + Qt.callLater(updateAvailableWidgetsModel); + } + + // Helper to get screen names array + function getScreenNames() { + var names = []; + for (var i = 0; i < Quickshell.screens.length; i++) { + names.push(Quickshell.screens[i].name); + } + return names; + } + + // Helper to get screen labels map (just screen names, NSectionEditor adds "Move to" prefix) + function getScreenLabels() { + var labels = {}; + for (var i = 0; i < Quickshell.screens.length; i++) { + var screen = Quickshell.screens[i]; + labels[screen.name] = screen.name; + } + return labels; + } + + // Helper to get screen icons map + function getScreenIcons() { + var icons = {}; + for (var i = 0; i < Quickshell.screens.length; i++) { + icons[Quickshell.screens[i].name] = "device-desktop"; + } + return icons; + } + + function updateAvailableWidgetsModel() { + availableWidgets.clear(); + try { + if (typeof DesktopWidgetRegistry === "undefined" || !DesktopWidgetRegistry) { + Logger.e("DesktopWidgetsTab", "DesktopWidgetRegistry is not available"); + // Retry after a short delay + Qt.callLater(function () { + if (typeof DesktopWidgetRegistry !== "undefined" && DesktopWidgetRegistry) { + updateAvailableWidgetsModel(); + } + }); + return; + } + var widgetIds = DesktopWidgetRegistry.getAvailableWidgets(); + Logger.d("DesktopWidgetsTab", "Found widgets:", widgetIds, "count:", widgetIds ? widgetIds.length : 0); + if (!widgetIds || widgetIds.length === 0) { + Logger.w("DesktopWidgetsTab", "No widgets found in registry"); + return; + } + for (var i = 0; i < widgetIds.length; i++) { + var widgetId = widgetIds[i]; + var displayName = widgetId; + + // Get plugin name for plugin widgets + var isPlugin = false; + if (DesktopWidgetRegistry.isPluginWidget(widgetId)) { + isPlugin = true; + var pluginId = widgetId.replace("plugin:", ""); + var manifest = PluginRegistry.getPluginManifest(pluginId); + if (manifest && manifest.name) { + displayName = manifest.name; + } + } + + // Add plugin badge first (with custom color) + const badges = []; + if (isPlugin) { + badges.push({ + "icon": "plugin", + "color": Color.mSecondary + }); + } + if (DesktopWidgetRegistry.isCpuIntensive(widgetId)) { + badges.push({ + "icon": "cpu-intensive", + "color": Color.mSecondary + }); + } + + availableWidgets.append({ + "key": widgetId, + "name": displayName, + "badges": badges + }); + } + Logger.d("DesktopWidgetsTab", "Available widgets model count:", availableWidgets.count); + } catch (e) { + Logger.e("DesktopWidgetsTab", "Error updating available widgets:", e, e.stack); + } + } + + // Get widgets for a specific monitor + function getWidgetsForMonitor(monitorName) { + var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || []; + for (var i = 0; i < monitorWidgets.length; i++) { + if (monitorWidgets[i].name === monitorName) { + return monitorWidgets[i].widgets || []; + } + } + return []; + } + + // Set widgets for a specific monitor + function setWidgetsForMonitor(monitorName, widgets) { + var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || []; + var newMonitorWidgets = monitorWidgets.slice(); + var found = false; + for (var i = 0; i < newMonitorWidgets.length; i++) { + if (newMonitorWidgets[i].name === monitorName) { + newMonitorWidgets[i] = { + "name": monitorName, + "widgets": widgets + }; + found = true; + break; + } + } + if (!found) { + newMonitorWidgets.push({ + "name": monitorName, + "widgets": widgets + }); + } + Settings.data.desktopWidgets.monitorWidgets = newMonitorWidgets; + } + + function _addWidgetToMonitor(monitorName, widgetId) { + var newWidget = { + "id": widgetId + }; + if (DesktopWidgetRegistry.widgetHasUserSettings(widgetId)) { + var metadata = DesktopWidgetRegistry.widgetMetadata[widgetId]; + if (metadata) { + Object.keys(metadata).forEach(function (key) { + newWidget[key] = metadata[key]; + }); + } + } + // Set default positions + if (widgetId === "Clock") { + newWidget.x = 50; + newWidget.y = 50; + } else if (widgetId === "MediaPlayer") { + newWidget.x = 100; + newWidget.y = 200; + } else if (widgetId === "AudioVisualizer") { + newWidget.x = 120; + newWidget.y = 280; + } else if (widgetId === "Weather") { + newWidget.x = 100; + newWidget.y = 300; + } else { + // Default position for plugin widgets + newWidget.x = 150; + newWidget.y = 150; + } + var widgets = getWidgetsForMonitor(monitorName).slice(); + widgets.push(newWidget); + setWidgetsForMonitor(monitorName, widgets); + } + + function _removeWidgetFromMonitor(monitorName, index) { + var widgets = getWidgetsForMonitor(monitorName); + if (index >= 0 && index < widgets.length) { + var newArray = widgets.slice(); + newArray.splice(index, 1); + setWidgetsForMonitor(monitorName, newArray); + } + } + + function _updateWidgetSettingsForMonitor(monitorName, index, settings) { + var widgets = getWidgetsForMonitor(monitorName); + if (index >= 0 && index < widgets.length) { + var newArray = widgets.slice(); + newArray[index] = Object.assign({}, newArray[index], settings); + setWidgetsForMonitor(monitorName, newArray); + } + } + + function _moveWidgetToMonitor(fromMonitor, index, toMonitor) { + Logger.i("DesktopWidgetsTab", "Moving widget from", fromMonitor, "index", index, "to", toMonitor); + var sourceWidgets = getWidgetsForMonitor(fromMonitor); + if (index < 0 || index >= sourceWidgets.length) { + Logger.e("DesktopWidgetsTab", "Invalid index", index, "for monitor", fromMonitor); + return; + } + + // Get the widget to move + var newSourceWidgets = sourceWidgets.slice(); + var widget = Object.assign({}, newSourceWidgets.splice(index, 1)[0]); + + // Reset position and scale to ensure widget is accessible on new screen + widget.x = 100; + widget.y = 100; + widget.scale = 1.0; + + // Add to destination monitor + var destWidgets = getWidgetsForMonitor(toMonitor).slice(); + destWidgets.push(widget); + + // Update both monitors + setWidgetsForMonitor(fromMonitor, newSourceWidgets); + setWidgetsForMonitor(toMonitor, destWidgets); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Display/BrightnessSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Display/BrightnessSubTab.qml new file mode 100644 index 0000000..63e6725 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Display/BrightnessSubTab.qml @@ -0,0 +1,249 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Compositor +import qs.Services.Hardware +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + ColumnLayout { + spacing: Style.marginL + + Repeater { + model: Quickshell.screens || [] + delegate: NBox { + Layout.fillWidth: true + implicitHeight: Math.round(contentCol.implicitHeight + Style.margin2L) + color: Color.mSurface + + property var brightnessMonitor: BrightnessService.getMonitorForScreen(modelData) + property real localBrightness: 0.5 + property bool localBrightnessChanging: false + readonly property string automaticOptionLabel: { + var baseLabel = I18n.tr("panels.display.monitors-backlight-device-auto-option"); + var autoDevicePath = (BrightnessService.availableBacklightDevices && BrightnessService.availableBacklightDevices.length > 0) ? BrightnessService.availableBacklightDevices[0] : ""; + if (autoDevicePath === "") + return baseLabel; + + var autoDeviceName = BrightnessService.getBacklightDeviceName(autoDevicePath) || autoDevicePath; + return baseLabel + "(" + autoDeviceName + ")"; + } + readonly property var backlightDeviceOptions: { + var options = [ + { + "key": "", + "name": automaticOptionLabel + } + ]; + + var devices = BrightnessService.availableBacklightDevices || []; + for (var i = 0; i < devices.length; i++) { + var devicePath = devices[i]; + var deviceName = BrightnessService.getBacklightDeviceName(devicePath) || devicePath; + options.push({ + "key": devicePath, + "name": deviceName + }); + } + return options; + } + + onBrightnessMonitorChanged: { + if (brightnessMonitor && !localBrightnessChanging) + localBrightness = brightnessMonitor.brightness || 0.5; + } + + Connections { + target: BrightnessService + function onMonitorBrightnessChanged(monitor, newBrightness) { + if (monitor === brightnessMonitor && !localBrightnessChanging) { + localBrightness = newBrightness; + } + } + } + Connections { + target: brightnessMonitor + ignoreUnknownSignals: true + function onBrightnessUpdated() { + if (brightnessMonitor && !localBrightnessChanging) { + localBrightness = brightnessMonitor.brightness || 0; + } + } + } + Timer { + id: debounceTimer + interval: 120 + repeat: false + onTriggered: { + if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable && Math.abs(localBrightness - brightnessMonitor.brightness) >= 0.005) { + brightnessMonitor.setBrightness(localBrightness); + } + } + } + + ColumnLayout { + id: contentCol + width: parent.width - 2 * Style.marginL + x: Style.marginL + y: Style.marginL + spacing: Style.marginXXS + + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignBottom + + NText { + text: modelData.name || "Unknown" + pointSize: Style.fontSizeL + font.weight: Style.fontWeightSemiBold + Layout.alignment: Qt.AlignBottom + } + + NText { + Layout.fillWidth: true + readonly property real compositorScale: { + const info = CompositorService.displayScales[modelData.name]; + return (info && info.scale) ? info.scale : 1.0; + } + text: { + I18n.tr("system.monitor-description", { + "model": modelData.model, + "width": modelData.width * compositorScale, + "height": modelData.height * compositorScale, + "scale": compositorScale + }); + } + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignRight + Layout.alignment: Qt.AlignBottom + } + } + + ColumnLayout { + spacing: Style.marginS + Layout.fillWidth: true + visible: brightnessMonitor !== undefined && brightnessMonitor !== null + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginL + + NText { + text: I18n.tr("common.brightness") + Layout.preferredWidth: 90 + Layout.alignment: Qt.AlignVCenter + } + + NValueSlider { + id: brightnessSlider + from: 0 + to: 1 + value: localBrightness + stepSize: 0.01 + enabled: brightnessMonitor ? brightnessMonitor.brightnessControlAvailable : false + onMoved: value => { + if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable) { + localBrightness = value; + debounceTimer.restart(); + } + } + onPressedChanged: (pressed, value) => { + localBrightnessChanging = pressed; + if (brightnessMonitor && brightnessMonitor.brightnessControlAvailable) { + if (pressed) { + localBrightness = value; + debounceTimer.restart(); + } else { + localBrightness = value; + debounceTimer.restart(); + } + } + } + Layout.fillWidth: true + } + + NText { + text: brightnessMonitor ? Math.round(localBrightness * 100) + "%" : "N/A" + Layout.preferredWidth: 55 + horizontalAlignment: Text.AlignRight + Layout.alignment: Qt.AlignVCenter + opacity: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable ? 0.5 : 1.0 + } + + Item { + Layout.preferredWidth: 30 + Layout.fillHeight: true + NIcon { + icon: brightnessMonitor && brightnessMonitor.method == "internal" ? "device-laptop" : "device-desktop" + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + opacity: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable ? 0.5 : 1.0 + } + } + } + + NText { + visible: brightnessMonitor && !brightnessMonitor.brightnessControlAvailable && !(brightnessMonitor.method === "internal" && brightnessMonitor.initInProgress) + text: !Settings.data.brightness.enableDdcSupport ? I18n.tr("panels.display.monitors-brightness-unavailable-ddc-disabled") : I18n.tr("panels.display.monitors-brightness-unavailable-generic") + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + Layout.fillWidth: true + wrapMode: Text.WordWrap + } + + NComboBox { + Layout.fillWidth: true + visible: brightnessMonitor && brightnessMonitor.method === "internal" + label: I18n.tr("panels.display.monitors-backlight-device-label") + description: I18n.tr("panels.display.monitors-backlight-device-description") + model: backlightDeviceOptions + currentKey: BrightnessService.getMappedBacklightDevice(modelData.name) || "" + onSelected: key => BrightnessService.setMappedBacklightDevice(modelData.name, key) + } + } + } + } + } + + NSpinBox { + Layout.fillWidth: true + label: I18n.tr("panels.display.monitors-brightness-step-label") + description: I18n.tr("panels.display.monitors-brightness-step-description") + minimum: 1 + maximum: 50 + value: Settings.data.brightness.brightnessStep + stepSize: 1 + suffix: "%" + onValueChanged: Settings.data.brightness.brightnessStep = value + defaultValue: Settings.getDefaultValue("brightness.brightnessStep") + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.display.monitors-enforce-minimum-label") + description: I18n.tr("panels.display.monitors-enforce-minimum-description") + checked: Settings.data.brightness.enforceMinimum + onToggled: checked => Settings.data.brightness.enforceMinimum = checked + defaultValue: Settings.getDefaultValue("brightness.enforceMinimum") + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.display.monitors-external-brightness-label") + description: I18n.tr("panels.display.monitors-external-brightness-description") + checked: Settings.data.brightness.enableDdcSupport + onToggled: checked => { + Settings.data.brightness.enableDdcSupport = checked; + } + defaultValue: Settings.getDefaultValue("brightness.enableDdcSupport") + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Display/DisplayTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Display/DisplayTab.qml new file mode 100644 index 0000000..8d2d990 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Display/DisplayTab.qml @@ -0,0 +1,92 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Io +import qs.Commons +import qs.Services.Location +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + // Time dropdown options (00:00 .. 23:30) + ListModel { + id: timeOptions + } + + function populateTimeOptions() { + for (var h = 0; h < 24; h++) { + for (var m = 0; m < 60; m += 30) { + var hh = ("0" + h).slice(-2); + var mm = ("0" + m).slice(-2); + var key = hh + ":" + mm; + timeOptions.append({ + "key": key, + "name": key + }); + } + } + } + + Component.onCompleted: { + Qt.callLater(populateTimeOptions); + } + + // Check for wlsunset availability when enabling Night Light + Process { + id: wlsunsetCheck + command: ["sh", "-c", "command -v wlsunset"] + running: false + + onExited: function (exitCode) { + if (exitCode === 0) { + Settings.data.nightLight.enabled = true; + NightLightService.apply(); + ToastService.showNotice(I18n.tr("common.night-light"), I18n.tr("common.enabled"), "nightlight-on"); + } else { + Settings.data.nightLight.enabled = false; + ToastService.showWarning(I18n.tr("common.night-light"), I18n.tr("toast.night-light.not-installed")); + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.brightness") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.night-light") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + BrightnessSubTab {} + NightLightSubTab { + timeOptions: timeOptions + onCheckWlsunset: wlsunsetCheck.running = true + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Display/NightLightSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Display/NightLightSubTab.qml new file mode 100644 index 0000000..982910e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Display/NightLightSubTab.qml @@ -0,0 +1,219 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.Location +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + property var timeOptions + + signal checkWlsunset + + NToggle { + label: I18n.tr("panels.display.night-light-enable-label") + description: I18n.tr("panels.display.night-light-enable-description") + checked: Settings.data.nightLight.enabled + onToggled: checked => { + if (checked) { + root.checkWlsunset(); + } else { + Settings.data.nightLight.enabled = false; + Settings.data.nightLight.forced = false; + NightLightService.apply(); + ToastService.showNotice(I18n.tr("common.night-light"), I18n.tr("common.disabled"), "nightlight-off"); + } + } + } + + ColumnLayout { + enabled: Settings.data.nightLight.enabled + spacing: Style.marginL + Layout.fillWidth: true + + NLabel { + label: I18n.tr("panels.display.night-light-temperature-night") + description: I18n.tr("panels.display.night-light-temperature-night-description") + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NSlider { + id: nightSlider + Layout.fillWidth: true + from: 1000 + to: 6500 + value: Settings.data.nightLight.nightTemp + + onValueChanged: { + var dayTemp = parseInt(Settings.data.nightLight.dayTemp); + var v = Math.round(value); + if (!isNaN(dayTemp)) { + var maxNight = dayTemp - 500; + v = Math.min(maxNight, Math.max(1000, v)); + } else { + v = Math.max(1000, v); + } + if (v !== value) + value = v; + } + + onPressedChanged: { + if (!pressed) { + var dayTemp = parseInt(Settings.data.nightLight.dayTemp); + var v = Math.round(value); + if (!isNaN(dayTemp)) { + var maxNight = dayTemp - 500; + v = Math.min(maxNight, Math.max(1000, v)); + } else { + v = Math.max(1000, v); + } + Settings.data.nightLight.nightTemp = v; + } + } + } + + NText { + text: nightSlider.value + "K" + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignVCenter + } + } + + NLabel { + label: I18n.tr("panels.display.night-light-temperature-day") + description: I18n.tr("panels.display.night-light-temperature-day-description") + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NSlider { + id: daySlider + Layout.fillWidth: true + from: 1000 + to: 6500 + value: Settings.data.nightLight.dayTemp + + onValueChanged: { + var nightTemp = parseInt(Settings.data.nightLight.nightTemp); + var v = Math.round(value); + if (!isNaN(nightTemp)) { + var minDay = nightTemp + 500; + v = Math.max(minDay, Math.min(6500, v)); + } else { + v = Math.min(6500, v); + } + if (v !== value) + value = v; + } + + onPressedChanged: { + if (!pressed) { + var nightTemp = parseInt(Settings.data.nightLight.nightTemp); + var v = Math.round(value); + if (!isNaN(nightTemp)) { + var minDay = nightTemp + 500; + v = Math.max(minDay, Math.min(6500, v)); + } else { + v = Math.min(6500, v); + } + Settings.data.nightLight.dayTemp = v; + } + } + } + + NText { + text: daySlider.value + "K" + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignVCenter + } + } + + NToggle { + label: I18n.tr("panels.display.night-light-auto-schedule-label") + description: I18n.tr("panels.display.night-light-auto-schedule-description", { + "location": LocationService.stableName + }) + checked: Settings.data.nightLight.autoSchedule + onToggled: checked => Settings.data.nightLight.autoSchedule = checked + } + + ColumnLayout { + spacing: Style.marginS + Layout.fillWidth: true + visible: !Settings.data.nightLight.autoSchedule && !Settings.data.nightLight.forced + + NLabel { + label: I18n.tr("panels.display.night-light-manual-schedule-label") + description: I18n.tr("panels.display.night-light-manual-schedule-description") + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NText { + text: I18n.tr("panels.display.night-light-manual-schedule-sunrise") + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignVCenter + } + + NComboBox { + model: root.timeOptions + currentKey: Settings.data.nightLight.manualSunrise + placeholder: I18n.tr("panels.display.night-light-manual-schedule-select-start") + onSelected: key => Settings.data.nightLight.manualSunrise = key + Layout.fillWidth: true + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NText { + text: I18n.tr("panels.display.night-light-manual-schedule-sunset") + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignVCenter + } + + NComboBox { + model: root.timeOptions + currentKey: Settings.data.nightLight.manualSunset + placeholder: I18n.tr("panels.display.night-light-manual-schedule-select-stop") + onSelected: key => Settings.data.nightLight.manualSunset = key + Layout.fillWidth: true + } + } + } + + NToggle { + label: I18n.tr("panels.display.night-light-force-activation-label") + description: I18n.tr("panels.display.night-light-force-activation-description") + checked: Settings.data.nightLight.forced + onToggled: checked => { + Settings.data.nightLight.forced = checked; + if (checked && !Settings.data.nightLight.enabled) { + root.checkWlsunset(); + } else { + NightLightService.apply(); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Dock/AppearanceSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Dock/AppearanceSubTab.qml new file mode 100644 index 0000000..01887a0 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Dock/AppearanceSubTab.qml @@ -0,0 +1,433 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.System +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + readonly property color launcherPreviewColor: Color.resolveColorKey((Settings.data.dock.launcherIconColor !== undefined) ? Settings.data.dock.launcherIconColor : "none") + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.dock.enabled-label") + description: I18n.tr("panels.dock.enabled-description") + checked: Settings.data.dock.enabled + defaultValue: Settings.getDefaultValue("dock.enabled") + onToggled: checked => Settings.data.dock.enabled = checked + } + + ColumnLayout { + spacing: Style.marginL + enabled: Settings.data.dock.enabled + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.dock.appearance-position-label") + description: I18n.tr("panels.dock.appearance-position-description") + model: [ + { + "key": "top", + "name": I18n.tr("positions.top") + }, + { + "key": "bottom", + "name": I18n.tr("positions.bottom") + }, + { + "key": "left", + "name": I18n.tr("positions.left") + }, + { + "key": "right", + "name": I18n.tr("positions.right") + } + ] + currentKey: Settings.data.dock.position + defaultValue: Settings.getDefaultValue("dock.position") + onSelected: key => Settings.data.dock.position = key + } + + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.dock.appearance-type-label") + description: I18n.tr("panels.dock.appearance-type-description") + model: [ + { + "key": "floating", + "name": I18n.tr("panels.dock.appearance-type-floating") + }, + { + "key": "attached", + "name": I18n.tr("panels.dock.appearance-type-attached") + } + ] + currentKey: Settings.data.dock.dockType + defaultValue: Settings.getDefaultValue("dock.dockType") + onSelected: key => Settings.data.dock.dockType = key + } + + NComboBox { + visible: Settings.data.dock.dockType === "floating" + Layout.fillWidth: true + label: I18n.tr("panels.display.title") + description: I18n.tr("panels.dock.appearance-display-description") + model: [ + { + "key": "always_visible", + "name": I18n.tr("hide-modes.visible") + }, + { + "key": "auto_hide", + "name": I18n.tr("panels.dock.appearance-display-auto-hide") + }, + { + "key": "exclusive", + "name": I18n.tr("panels.dock.appearance-display-exclusive") + } + ] + currentKey: Settings.data.dock.displayMode + defaultValue: Settings.getDefaultValue("dock.displayMode") + onSelected: key => { + Settings.data.dock.displayMode = key; + } + } + + NToggle { + Layout.fillWidth: true + visible: Settings.data.dock.dockType === "attached" && Settings.data.bar.barType === "framed" + label: I18n.tr("panels.dock.appearance-sit-on-frame-label") + description: I18n.tr("panels.dock.appearance-sit-on-frame-description") + checked: Settings.data.dock.sitOnFrame + defaultValue: Settings.getDefaultValue("dock.sitOnFrame") + onToggled: checked => Settings.data.dock.sitOnFrame = checked + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.dock.appearance-dock-indicator-label") + description: I18n.tr("panels.dock.appearance-dock-indicator-description") + checked: Settings.data.dock.showDockIndicator + defaultValue: Settings.getDefaultValue("dock.showDockIndicator") + onToggled: checked => Settings.data.dock.showDockIndicator = checked + } + + NToggle { + Layout.fillWidth: true + visible: Settings.data.dock.showDockIndicator + label: I18n.tr("panels.dock.appearance-indicator-thickness-label") + description: I18n.tr("panels.dock.appearance-indicator-thickness-description") + checked: (Settings.data.dock.indicatorThickness || 3) >= 6 + defaultValue: (Settings.getDefaultValue("dock.indicatorThickness") || 3) >= 6 + onToggled: checked => Settings.data.dock.indicatorThickness = checked ? 6 : 3 + } + + NColorChoice { + Layout.fillWidth: true + visible: Settings.data.dock.showDockIndicator + label: I18n.tr("panels.dock.appearance-indicator-color-label") + description: I18n.tr("panels.dock.appearance-indicator-color-description") + currentKey: Settings.data.dock.indicatorColor || "primary" + defaultValue: Settings.getDefaultValue("dock.indicatorColor") + onSelected: key => Settings.data.dock.indicatorColor = key + } + + NValueSlider { + Layout.fillWidth: true + visible: Settings.data.dock.showDockIndicator + label: I18n.tr("panels.dock.appearance-indicator-opacity-label") + description: I18n.tr("panels.dock.appearance-indicator-opacity-description") + from: 0.1 + to: 1 + stepSize: 0.01 + showReset: true + value: Settings.data.dock.indicatorOpacity + defaultValue: Settings.getDefaultValue("dock.indicatorOpacity") + onMoved: value => Settings.data.dock.indicatorOpacity = value + text: Math.floor(Settings.data.dock.indicatorOpacity * 100) + "%" + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.osd.background-opacity-label") + description: I18n.tr("panels.dock.appearance-background-opacity-description") + from: 0 + to: 1 + stepSize: 0.01 + showReset: true + value: Settings.data.dock.backgroundOpacity + defaultValue: Settings.getDefaultValue("dock.backgroundOpacity") + onMoved: value => Settings.data.dock.backgroundOpacity = value + text: Math.floor(Settings.data.dock.backgroundOpacity * 100) + "%" + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.dock.appearance-dead-opacity-label") + description: I18n.tr("panels.dock.appearance-dead-opacity-description") + from: 0 + to: 1 + stepSize: 0.01 + showReset: true + value: Settings.data.dock.deadOpacity + defaultValue: Settings.getDefaultValue("dock.deadOpacity") + onMoved: value => Settings.data.dock.deadOpacity = value + text: Math.floor(Settings.data.dock.deadOpacity * 100) + "%" + } + + NValueSlider { + Layout.fillWidth: true + visible: Settings.data.dock.dockType === "floating" + label: I18n.tr("panels.dock.appearance-floating-distance-label") + description: I18n.tr("panels.dock.appearance-floating-distance-description") + from: 0 + to: 4 + stepSize: 0.01 + showReset: true + value: Settings.data.dock.floatingRatio + defaultValue: Settings.getDefaultValue("dock.floatingRatio") + onMoved: value => Settings.data.dock.floatingRatio = value + text: Math.floor(Settings.data.dock.floatingRatio * 100) + "%" + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.dock.appearance-icon-size-label") + description: I18n.tr("panels.dock.appearance-icon-size-description") + from: 0 + to: 2 + stepSize: 0.01 + showReset: true + value: Settings.data.dock.size + defaultValue: Settings.getDefaultValue("dock.size") + onMoved: value => Settings.data.dock.size = value + text: Math.floor(Settings.data.dock.size * 100) + "%" + } + + NValueSlider { + visible: Settings.data.dock.dockType === "floating" && Settings.data.dock.displayMode === "auto_hide" + Layout.fillWidth: true + label: I18n.tr("panels.dock.appearance-hide-show-speed-label") + description: I18n.tr("panels.dock.appearance-hide-show-speed-description") + from: 0.1 + to: 2.0 + stepSize: 0.01 + showReset: true + value: Settings.data.dock.animationSpeed + defaultValue: Settings.getDefaultValue("dock.animationSpeed") + onMoved: value => Settings.data.dock.animationSpeed = value + text: (Settings.data.dock.animationSpeed * 100).toFixed(0) + "%" + } + + NToggle { + label: I18n.tr("panels.dock.appearance-inactive-indicators-label") + description: I18n.tr("panels.dock.appearance-inactive-indicators-description") + checked: Settings.data.dock.inactiveIndicators + defaultValue: Settings.getDefaultValue("dock.inactiveIndicators") + onToggled: checked => Settings.data.dock.inactiveIndicators = checked + } + + NToggle { + label: I18n.tr("panels.dock.appearance-pinned-static-label") + description: I18n.tr("panels.dock.appearance-pinned-static-description") + checked: Settings.data.dock.pinnedStatic + defaultValue: Settings.getDefaultValue("dock.pinnedStatic") + onToggled: checked => Settings.data.dock.pinnedStatic = checked + } + + NToggle { + label: I18n.tr("panels.dock.appearance-group-apps-label") + description: I18n.tr("panels.dock.appearance-group-apps-description") + checked: Settings.data.dock.groupApps + defaultValue: Settings.getDefaultValue("dock.groupApps") + onToggled: checked => Settings.data.dock.groupApps = checked + } + + NComboBox { + Layout.fillWidth: true + visible: Settings.data.dock.groupApps + label: I18n.tr("panels.dock.appearance-group-click-action-label") + description: I18n.tr("panels.dock.appearance-group-click-action-description") + model: [ + { + "key": "cycle", + "name": I18n.tr("panels.dock.appearance-group-click-action-cycle") + }, + { + "key": "list", + "name": I18n.tr("panels.dock.appearance-group-click-action-list") + } + ] + currentKey: Settings.data.dock.groupClickAction + defaultValue: Settings.getDefaultValue("dock.groupClickAction") + onSelected: key => Settings.data.dock.groupClickAction = key + } + + NComboBox { + Layout.fillWidth: true + visible: Settings.data.dock.groupApps + label: I18n.tr("panels.dock.appearance-group-context-menu-mode-label") + description: I18n.tr("panels.dock.appearance-group-context-menu-mode-description") + model: [ + { + "key": "list", + "name": I18n.tr("panels.dock.appearance-group-context-menu-mode-list") + }, + { + "key": "extended", + "name": I18n.tr("panels.dock.appearance-group-context-menu-mode-extended") + } + ] + currentKey: Settings.data.dock.groupContextMenuMode + defaultValue: Settings.getDefaultValue("dock.groupContextMenuMode") + onSelected: key => Settings.data.dock.groupContextMenuMode = key + } + + NComboBox { + Layout.fillWidth: true + visible: Settings.data.dock.groupApps + label: I18n.tr("panels.dock.appearance-group-indicator-style-label") + description: I18n.tr("panels.dock.appearance-group-indicator-style-description") + model: [ + { + "key": "number", + "name": I18n.tr("panels.dock.appearance-group-indicator-style-number") + }, + { + "key": "dots", + "name": I18n.tr("panels.dock.appearance-group-indicator-style-dots") + } + ] + currentKey: Settings.data.dock.groupIndicatorStyle + defaultValue: Settings.getDefaultValue("dock.groupIndicatorStyle") + onSelected: key => Settings.data.dock.groupIndicatorStyle = key + } + + NToggle { + label: I18n.tr("panels.dock.monitors-only-same-monitor-label") + description: I18n.tr("panels.dock.monitors-only-same-monitor-description") + checked: Settings.data.dock.onlySameOutput + defaultValue: Settings.getDefaultValue("dock.onlySameOutput") + onToggled: checked => Settings.data.dock.onlySameOutput = checked + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.dock.appearance-colorize-icons-label") + description: I18n.tr("panels.dock.appearance-colorize-icons-description") + checked: Settings.data.dock.colorizeIcons + defaultValue: Settings.getDefaultValue("dock.colorizeIcons") + onToggled: checked => Settings.data.dock.colorizeIcons = checked + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.dock.appearance-show-launcher-icon-label") + description: I18n.tr("panels.dock.appearance-show-launcher-icon-description") + checked: Settings.data.dock.showLauncherIcon + defaultValue: Settings.getDefaultValue("dock.showLauncherIcon") + onToggled: checked => Settings.data.dock.showLauncherIcon = checked + } + + NComboBox { + Layout.fillWidth: true + visible: Settings.data.dock.showLauncherIcon + label: I18n.tr("panels.dock.appearance-launcher-position-label") + description: I18n.tr("panels.dock.appearance-launcher-position-description") + model: [ + { + "key": "start", + "name": I18n.tr("panels.dock.appearance-launcher-position-start") + }, + { + "key": "end", + "name": I18n.tr("panels.dock.appearance-launcher-position-end") + } + ] + currentKey: Settings.data.dock.launcherPosition + defaultValue: Settings.getDefaultValue("dock.launcherPosition") + onSelected: key => Settings.data.dock.launcherPosition = key + } + + NToggle { + Layout.fillWidth: true + visible: Settings.data.dock.showLauncherIcon + label: I18n.tr("panels.dock.appearance-launcher-use-distro-logo-label") + description: I18n.tr("panels.dock.appearance-launcher-use-distro-logo-description") + checked: Settings.data.dock.launcherUseDistroLogo + defaultValue: Settings.getDefaultValue("dock.launcherUseDistroLogo") + onToggled: checked => Settings.data.dock.launcherUseDistroLogo = checked + } + + RowLayout { + visible: Settings.data.dock.showLauncherIcon + Layout.fillWidth: true + + NLabel { + Layout.fillWidth: true + label: I18n.tr("panels.dock.appearance-launcher-icon-label") + description: I18n.tr("panels.dock.appearance-launcher-icon-description") + } + + NIconButton { + visible: !Settings.data.dock.launcherUseDistroLogo + enabled: !Settings.data.dock.launcherUseDistroLogo + icon: (Settings.data.dock.launcherIcon && Settings.data.dock.launcherIcon !== "") ? Settings.data.dock.launcherIcon : "search" + colorFg: root.launcherPreviewColor + colorFgHover: root.launcherPreviewColor + tooltipText: I18n.tr("bar.control-center.browse-library") + onClicked: launcherIconPicker.open() + } + + Rectangle { + visible: Settings.data.dock.launcherUseDistroLogo + width: Style.toOdd(Style.baseWidgetSize * Style.uiScaleRatio) + height: width + radius: Math.min(Style.iRadiusL, width / 2) + color: Color.smartAlpha(Color.mSurfaceVariant) + border.color: Color.mOutline + border.width: Style.borderS + + Image { + anchors.centerIn: parent + width: parent.width * 0.62 + height: width + source: HostService.osLogo + fillMode: Image.PreserveAspectFit + smooth: true + asynchronous: true + layer.enabled: visible + layer.effect: ShaderEffect { + property color targetColor: root.launcherPreviewColor + property real colorizeMode: 2.0 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") + } + } + } + } + + NIconPicker { + id: launcherIconPicker + initialIcon: (Settings.data.dock.launcherIcon && Settings.data.dock.launcherIcon !== "") ? Settings.data.dock.launcherIcon : "search" + onIconSelected: iconName => { + Settings.data.dock.launcherIcon = iconName; + Settings.saveImmediate(); + } + } + + NColorChoice { + Layout.fillWidth: true + visible: Settings.data.dock.showLauncherIcon + label: I18n.tr("common.select-icon-color") + currentKey: Settings.data.dock.launcherIconColor + defaultValue: Settings.getDefaultValue("dock.launcherIconColor") + onSelected: key => Settings.data.dock.launcherIconColor = key + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Dock/DockTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Dock/DockTab.qml new file mode 100644 index 0000000..497f256 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Dock/DockTab.qml @@ -0,0 +1,42 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.appearance") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.monitors") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + AppearanceSubTab {} + MonitorsSubTab {} + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Dock/MonitorsSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Dock/MonitorsSubTab.qml new file mode 100644 index 0000000..a0e08d9 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Dock/MonitorsSubTab.qml @@ -0,0 +1,61 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Compositor +import qs.Widgets + +ColumnLayout { + id: root + enabled: Settings.data.dock.enabled + spacing: Style.marginL + Layout.fillWidth: true + + // Helper functions to update arrays immutably + function addMonitor(list, name) { + const arr = (list || []).slice(); + if (!arr.includes(name)) + arr.push(name); + return arr; + } + function removeMonitor(list, name) { + return (list || []).filter(function (n) { + return n !== name; + }); + } + + NText { + text: I18n.tr("panels.dock.monitors-desc") + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Repeater { + model: Quickshell.screens || [] + delegate: NCheckbox { + Layout.fillWidth: true + readonly property real compositorScale: { + const info = CompositorService.displayScales[modelData.name]; + return (info && info.scale) ? info.scale : 1.0; + } + label: modelData.name || "Unknown" + description: { + I18n.tr("system.monitor-description", { + "model": modelData.model, + "width": modelData.width * compositorScale, + "height": modelData.height * compositorScale, + "scale": compositorScale + }); + } + checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1 + onToggled: checked => { + if (checked) { + Settings.data.dock.monitors = root.addMonitor(Settings.data.dock.monitors, modelData.name); + } else { + Settings.data.dock.monitors = root.removeMonitor(Settings.data.dock.monitors, modelData.name); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/General/BasicsSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/General/BasicsSubTab.qml new file mode 100644 index 0000000..f459e79 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/General/BasicsSubTab.qml @@ -0,0 +1,221 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import "../../../../../Helpers/QtObj2JS.js" as QtObj2JS +import qs.Commons +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + + // Profile section + RowLayout { + Layout.fillWidth: true + spacing: Style.marginL + + // Avatar preview + NImageRounded { + Layout.preferredWidth: 128 * Style.uiScaleRatio + Layout.preferredHeight: width + radius: width / 2 + imagePath: Settings.preprocessPath(Settings.data.general.avatarImage) + fallbackIcon: "person" + borderColor: Color.mPrimary + borderWidth: Style.borderM + Layout.alignment: Qt.AlignTop + } + + ColumnLayout { + NText { + text: HostService.displayName + pointSize: Style.fontSizeM + color: Color.mPrimary + } + + NTextInputButton { + label: I18n.tr("panels.general.profile-picture-label") + description: I18n.tr("panels.general.profile-picture-description") + text: Settings.data.general.avatarImage + placeholderText: '~/.face' // don't translate path + buttonIcon: "photo" + buttonTooltip: I18n.tr("panels.general.profile-tooltip") + onInputTextChanged: text => Settings.data.general.avatarImage = text + onButtonClicked: { + avatarPicker.openFilePicker(); + } + } + } + } + + NFilePicker { + id: avatarPicker + title: I18n.tr("panels.general.profile-select-avatar") + selectionMode: "files" + initialPath: Settings.preprocessPath(Settings.data.general.avatarImage).substr(0, Settings.preprocessPath(Settings.data.general.avatarImage).lastIndexOf("/")) || Quickshell.env("HOME") + nameFilters: ImageCacheService.basicImageFilters + onAccepted: paths => { + if (paths.length > 0) { + Settings.data.general.avatarImage = paths[0]; + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginM + Layout.bottomMargin: Style.marginM + } + + // Fonts + ColumnLayout { + spacing: Style.marginL + Layout.fillWidth: true + + // Font configuration section + ColumnLayout { + spacing: Style.marginL + Layout.fillWidth: true + + NSearchableComboBox { + label: I18n.tr("panels.general.fonts-default-label") + description: I18n.tr("panels.general.fonts-default-description") + model: FontService.availableFonts + currentKey: Settings.data.ui.fontDefault + placeholder: I18n.tr("panels.general.fonts-default-placeholder") + searchPlaceholder: I18n.tr("panels.general.fonts-default-search-placeholder") + popupHeight: 420 + defaultValue: Settings.getDefaultValue("ui.fontDefault") + settingsPath: "ui.fontDefault" + onSelected: key => Settings.data.ui.fontDefault = key + } + + NSearchableComboBox { + label: I18n.tr("panels.general.fonts-monospace-label") + description: I18n.tr("panels.general.fonts-monospace-description") + model: FontService.monospaceFonts + currentKey: Settings.data.ui.fontFixed + placeholder: I18n.tr("panels.general.fonts-monospace-placeholder") + searchPlaceholder: I18n.tr("panels.general.fonts-monospace-search-placeholder") + popupHeight: 320 + defaultValue: Settings.getDefaultValue("ui.fontFixed") + settingsPath: "ui.fontFixed" + onSelected: key => Settings.data.ui.fontFixed = key + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.general.fonts-default-scale-label") + description: I18n.tr("panels.general.fonts-default-scale-description") + from: 0.75 + to: 1.25 + stepSize: 0.01 + showReset: true + value: Settings.data.ui.fontDefaultScale + defaultValue: Settings.getDefaultValue("ui.fontDefaultScale") + onMoved: value => Settings.data.ui.fontDefaultScale = value + text: Math.floor(Settings.data.ui.fontDefaultScale * 100) + "%" + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.general.fonts-monospace-scale-label") + description: I18n.tr("panels.general.fonts-monospace-scale-description") + from: 0.75 + to: 1.25 + stepSize: 0.01 + showReset: true + value: Settings.data.ui.fontFixedScale + defaultValue: Settings.getDefaultValue("ui.fontFixedScale") + onMoved: value => Settings.data.ui.fontFixedScale = value + text: Math.floor(Settings.data.ui.fontFixedScale * 100) + "%" + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginM + Layout.bottomMargin: Style.marginM + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.general.reverse-scrolling-label") + description: I18n.tr("panels.general.reverse-scrolling-description") + checked: Settings.data.general.reverseScroll + defaultValue: Settings.getDefaultValue("general.reverseScroll") + onToggled: checked => Settings.data.general.reverseScroll = checked + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.general.smooth-scrolling-label") + description: I18n.tr("panels.general.smooth-scrolling-description") + checked: Settings.data.general.smoothScrollEnabled + defaultValue: Settings.getDefaultValue("general.smoothScrollEnabled") + onToggled: checked => Settings.data.general.smoothScrollEnabled = checked + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginM + Layout.bottomMargin: Style.marginM + } + + RowLayout { + spacing: Style.marginL + Layout.fillWidth: true + + NButton { + icon: "wand" + text: I18n.tr("panels.general.launch-setup-wizard") + outlined: true + Layout.fillWidth: true + onClicked: { + var targetScreen = PanelService.openedPanel ? PanelService.openedPanel.screen : (Quickshell.screens.length > 0 ? Quickshell.screens[0] : null); + if (!targetScreen) { + return; + } + var setupPanel = PanelService.getPanel("setupWizardPanel", targetScreen); + if (setupPanel) { + setupPanel.telemetryOnlyMode = false; + setupPanel.open(); + } else { + Qt.callLater(() => { + var sp = PanelService.getPanel("setupWizardPanel", targetScreen); + if (sp) { + sp.telemetryOnlyMode = false; + sp.open(); + } + }); + } + } + } + + NButton { + icon: "external-link" + text: I18n.tr("common.documentation") + outlined: true + Layout.fillWidth: true + onClicked: { + Qt.openUrlExternally("https://docs.noctalia.dev"); + } + } + + NButton { + icon: "json" + text: I18n.tr("panels.general.copy-settings") + outlined: true + Layout.fillWidth: true + onClicked: { + var plainData = QtObj2JS.qtObjectToPlainObject(Settings.data); + var json = JSON.stringify(plainData, null, 2); + Quickshell.execDetached(["wl-copy", json]); + ToastService.showNotice(I18n.tr("panels.general.settings-copied")); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/General/KeybindsSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/General/KeybindsSubTab.qml new file mode 100644 index 0000000..661589c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/General/KeybindsSubTab.qml @@ -0,0 +1,81 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + width: parent.width + + // Keybinds section + NLabel { + label: I18n.tr("panels.general.keybinds-title") + description: I18n.tr("panels.general.keybinds-description") + Layout.fillWidth: true + } + + NKeybindRecorder { + Layout.fillWidth: true + label: I18n.tr("panels.general.keybinds-up") + currentKeybinds: Settings.data.general.keybinds.keyUp + defaultKeybind: "Up" + settingsPath: "general.keybinds.keyUp" + onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyUp = newKeybinds + } + + NKeybindRecorder { + Layout.fillWidth: true + label: I18n.tr("panels.general.keybinds-down") + currentKeybinds: Settings.data.general.keybinds.keyDown + defaultKeybind: "Down" + settingsPath: "general.keybinds.keyDown" + onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyDown = newKeybinds + } + + NKeybindRecorder { + Layout.fillWidth: true + label: I18n.tr("panels.general.keybinds-left") + currentKeybinds: Settings.data.general.keybinds.keyLeft + defaultKeybind: "Left" + settingsPath: "general.keybinds.keyLeft" + onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyLeft = newKeybinds + } + + NKeybindRecorder { + Layout.fillWidth: true + label: I18n.tr("panels.general.keybinds-right") + currentKeybinds: Settings.data.general.keybinds.keyRight + defaultKeybind: "Right" + settingsPath: "general.keybinds.keyRight" + onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyRight = newKeybinds + } + + NKeybindRecorder { + Layout.fillWidth: true + label: I18n.tr("panels.general.keybinds-enter") + currentKeybinds: Settings.data.general.keybinds.keyEnter + defaultKeybind: "Return" + settingsPath: "general.keybinds.keyEnter" + onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyEnter = newKeybinds + } + + NKeybindRecorder { + Layout.fillWidth: true + label: I18n.tr("panels.general.keybinds-escape") + currentKeybinds: Settings.data.general.keybinds.keyEscape + defaultKeybind: "Esc" + settingsPath: "general.keybinds.keyEscape" + onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyEscape = newKeybinds + } + + NKeybindRecorder { + Layout.fillWidth: true + label: I18n.tr("panels.general.keybinds-remove") + currentKeybinds: Settings.data.general.keybinds.keyRemove + defaultKeybind: "Del" + settingsPath: "general.keybinds.keyRemove" + onKeybindsChanged: newKeybinds => Settings.data.general.keybinds.keyRemove = newKeybinds + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/GeneralTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/GeneralTab.qml new file mode 100644 index 0000000..17d0450 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/GeneralTab.qml @@ -0,0 +1,46 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import "../../../../Helpers/QtObj2JS.js" as QtObj2JS +import "General" +import qs.Commons +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("panels.general.tab-basics") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("panels.general.tab-keybinds") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + BasicsSubTab {} + KeybindsSubTab {} + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Hooks/GeneralSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Hooks/GeneralSubTab.qml new file mode 100644 index 0000000..9ec09c4 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Hooks/GeneralSubTab.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + width: parent.width + + // Enable/Disable Toggle + NToggle { + label: I18n.tr("panels.hooks.system-hooks-enable-label") + description: I18n.tr("panels.hooks.system-hooks-enable-description") + checked: Settings.data.hooks.enabled + onToggled: checked => Settings.data.hooks.enabled = checked + } + + NDivider { + Layout.fillWidth: true + } + + // Info section + ColumnLayout { + spacing: Style.marginM + Layout.fillWidth: true + + NLabel { + label: I18n.tr("panels.hooks.info-parameters-label") + description: I18n.tr("panels.hooks.info-parameters-description") + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Hooks/HookEditPopup.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Hooks/HookEditPopup.qml new file mode 100644 index 0000000..31cf3ec --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Hooks/HookEditPopup.qml @@ -0,0 +1,118 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +Popup { + id: root + modal: true + closePolicy: Popup.NoAutoClose + dim: true + anchors.centerIn: parent + + // Use a reasonable width/height relative to parent or fixed + width: Math.min(600 * Style.uiScaleRatio, parent.width * 0.9) + height: Math.min(contentLayout.implicitHeight + padding * 2, parent.height * 0.9) + padding: Style.marginL + + property string hookLabel: "" + property string hookDescription: "" + property string hookPlaceholder: "" + property string initialValue: "" + + signal saved(string newValue) + signal test(string value) + + property var _savedSlot: null + property var _testSlot: null + + background: Rectangle { + color: Color.mSurface + radius: Style.radiusL + border.color: Color.mOutline + border.width: Style.borderS + } + + onOpened: { + commandInput.text = initialValue; + commandInput.forceActiveFocus(); + } + + contentItem: ColumnLayout { + id: contentLayout + spacing: Style.marginL + + // Header + RowLayout { + Layout.fillWidth: true + NText { + text: root.hookLabel + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeL + Layout.fillWidth: true + } + NIconButton { + icon: "close" + onClicked: root.close() + } + } + + // Description/Help + NText { + text: root.hookDescription + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + // Input Area + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NTextInput { + id: commandInput + Layout.fillWidth: true + placeholderText: root.hookPlaceholder + // Allow multiline? Hooks are usually oneline commands but can be scripts. + // NTextInput is likely single line. Let's assume single line for now as per previous implementation. + } + } + + // Action Buttons + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NButton { + text: I18n.tr("common.test") + icon: "caret-right" + onClicked: root.test(commandInput.text) + // Disable test if empty? Or maybe allow testing defined script. + enabled: commandInput.text !== "" + } + + Item { + Layout.fillWidth: true + } // Spacer + + NButton { + text: I18n.tr("common.cancel") + outlined: true + onClicked: root.close() + } + + NButton { + text: I18n.tr("common.save") + icon: "check" + backgroundColor: Color.mPrimary + textColor: Color.mOnPrimary + onClicked: { + root.saved(commandInput.text); + root.close(); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Hooks/HookRow.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Hooks/HookRow.qml new file mode 100644 index 0000000..6ebc5a1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Hooks/HookRow.qml @@ -0,0 +1,28 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +RowLayout { + id: root + + property string label: "" + property string description: "" + property string value: "" + + signal editClicked + + spacing: Style.marginM + + NLabel { + label: root.label + description: root.description + labelColor: root.value ? Color.mPrimary : Color.mOnSurface + } + + NIconButton { + icon: "settings" + onClicked: root.editClicked() + tooltipText: I18n.tr("common.edit") + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Hooks/HooksListSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Hooks/HooksListSubTab.qml new file mode 100644 index 0000000..9e434c0 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Hooks/HooksListSubTab.qml @@ -0,0 +1,172 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Control +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + enabled: Settings.data.hooks.enabled + spacing: Style.marginL + width: parent.width + + // Shared Edit Popup + HookEditPopup { + id: editPopup + parent: Overlay.overlay + } + + // Helper to open popup + function openEdit(label, description, placeholder, value, onSave, onTest) { + editPopup.hookLabel = label; + editPopup.hookDescription = description; + editPopup.hookPlaceholder = placeholder; + editPopup.initialValue = value; + + // Disconnect previous signals + try { + editPopup.saved.disconnect(editPopup._savedSlot); + } catch (e) {} + try { + editPopup.test.disconnect(editPopup._testSlot); + } catch (e) {} + + // Define slots + editPopup._savedSlot = onSave; + editPopup._testSlot = onTest; + + // Connect new signals + editPopup.saved.connect(editPopup._savedSlot); + editPopup.test.connect(editPopup._testSlot); + + editPopup.open(); + } + + // Startup Hook + HookRow { + label: I18n.tr("panels.hooks.noctalia-started-label") + description: I18n.tr("panels.hooks.noctalia-started-description") + value: Settings.data.hooks.startup + onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.noctalia-started-placeholder"), value, newValue => { + Settings.data.hooks.startup = newValue; + Settings.saveImmediate(); + }, val => { + HooksService.executeStartupHook(); + }) + } + + // Wallpaper Hook + HookRow { + label: I18n.tr("panels.hooks.wallpaper-changed-label") + description: I18n.tr("panels.hooks.wallpaper-changed-description") + value: Settings.data.hooks.wallpaperChange + onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.wallpaper-changed-placeholder"), value, newValue => { + Settings.data.hooks.wallpaperChange = newValue; + Settings.saveImmediate(); + }, val => { + if (val) + Quickshell.execDetached(["sh", "-lc", val.replace("$1", "test_wallpaper_path").replace("$2", "test_screen").replace("$3", "dark")]); + }) + } + + // Color Generation Hook + HookRow { + label: I18n.tr("panels.hooks.color-generation-label") + description: I18n.tr("panels.hooks.color-generation-description") + value: Settings.data.hooks.colorGeneration + onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.color-generation-placeholder"), value, newValue => { + Settings.data.hooks.colorGeneration = newValue; + Settings.saveImmediate(); + }, val => { + if (val) + Quickshell.execDetached(["sh", "-lc", val.replace("$1", "dark")]); + }) + } + + // Theme Hook + HookRow { + label: I18n.tr("panels.hooks.theme-changed-label") + description: I18n.tr("panels.hooks.theme-changed-description") + value: Settings.data.hooks.darkModeChange + onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.theme-changed-placeholder"), value, newValue => { + Settings.data.hooks.darkModeChange = newValue; + Settings.saveImmediate(); + }, val => { + if (val) + Quickshell.execDetached(["sh", "-lc", val.replace("$1", "true")]); + }) + } + + // Screen Lock Hook + HookRow { + label: I18n.tr("panels.hooks.screen-lock-label") + description: I18n.tr("panels.hooks.screen-lock-description") + value: Settings.data.hooks.screenLock + onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.screen-lock-placeholder"), value, newValue => { + Settings.data.hooks.screenLock = newValue; + Settings.saveImmediate(); + }, val => { + if (val) + Quickshell.execDetached(["sh", "-lc", val]); + }) + } + + // Screen Unlock Hook + HookRow { + label: I18n.tr("panels.hooks.screen-unlock-label") + description: I18n.tr("panels.hooks.screen-unlock-description") + value: Settings.data.hooks.screenUnlock + onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.screen-unlock-placeholder"), value, newValue => { + Settings.data.hooks.screenUnlock = newValue; + Settings.saveImmediate(); + }, val => { + if (val) + Quickshell.execDetached(["sh", "-lc", val]); + }) + } + + // Performance Mode Enabled Hook + HookRow { + label: I18n.tr("panels.hooks.performance-mode-enabled-label") + description: I18n.tr("panels.hooks.performance-mode-enabled-description") + value: Settings.data.hooks.performanceModeEnabled + onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.performance-mode-enabled-placeholder"), value, newValue => { + Settings.data.hooks.performanceModeEnabled = newValue; + Settings.saveImmediate(); + }, val => { + if (val) + Quickshell.execDetached(["sh", "-lc", val]); + }) + } + + // Performance Mode Disabled Hook + HookRow { + label: I18n.tr("panels.hooks.performance-mode-disabled-label") + description: I18n.tr("panels.hooks.performance-mode-disabled-description") + value: Settings.data.hooks.performanceModeDisabled + onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.performance-mode-disabled-placeholder"), value, newValue => { + Settings.data.hooks.performanceModeDisabled = newValue; + Settings.saveImmediate(); + }, val => { + if (val) + Quickshell.execDetached(["sh", "-lc", val]); + }) + } + + // Session Hook + HookRow { + label: I18n.tr("panels.hooks.session-label") + description: I18n.tr("panels.hooks.session-description") + value: Settings.data.hooks.session + onEditClicked: openEdit(label, description, I18n.tr("panels.hooks.session-placeholder"), value, newValue => { + Settings.data.hooks.session = newValue; + Settings.saveImmediate(); + }, val => { + if (val) + Quickshell.execDetached(["sh", "-lc", val + " test"]); + }) + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Hooks/HooksTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Hooks/HooksTab.qml new file mode 100644 index 0000000..436a325 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Hooks/HooksTab.qml @@ -0,0 +1,43 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.general") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("panels.hooks.title") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginS + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + GeneralSubTab {} + HooksListSubTab {} + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Idle/BehaviorSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Idle/BehaviorSubTab.qml new file mode 100644 index 0000000..c26bb55 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Idle/BehaviorSubTab.qml @@ -0,0 +1,199 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.Power +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + // Master enable + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.idle.enable-label") + description: I18n.tr("panels.idle.enable-description") + checked: Settings.data.idle.enabled + defaultValue: Settings.getDefaultValue("idle.enabled") + onToggled: checked => Settings.data.idle.enabled = checked + } + + // Live idle status + RowLayout { + Layout.fillWidth: true + enabled: Settings.data.idle.enabled + visible: IdleService.nativeIdleMonitorAvailable + + NLabel { + label: I18n.tr("panels.idle.status-label") + description: I18n.tr("panels.idle.status-description") + } + + Item { + Layout.fillWidth: true + } + + NText { + Layout.alignment: Qt.AlignBottom | Qt.AlignRight + text: IdleService.idleSeconds > 0 ? I18n.trp("common.second", IdleService.idleSeconds) : I18n.tr("common.active") + family: Settings.data.ui.fontFixed + pointSize: Style.fontSizeM + color: IdleService.idleSeconds > 0 ? Color.mPrimary : Color.mOnSurfaceVariant + } + } + + NLabel { + visible: !IdleService.nativeIdleMonitorAvailable + description: I18n.tr("panels.idle.unavailable") + } + + NDivider { + Layout.fillWidth: true + } + + IdleCommandEditPopup { + id: editPopup + parent: Overlay.overlay + } + + function openEdit(actionName, cmdVal, resumeCmdVal, onSaveCmd, onSaveResume) { + editPopup.editIndex = -1; + editPopup.showCommand = true; + editPopup.showTimeout = false; + editPopup.titleText = I18n.tr("common.edit") + " " + actionName; + editPopup.timeoutValue = 0; + editPopup.commandValue = cmdVal; + editPopup.resumeCommandValue = resumeCmdVal; + + try { + editPopup.saved.disconnect(editPopup._savedSlot); + } catch (e) {} + + editPopup._savedSlot = function (timeout, cmd, resumeCmd, name) { + onSaveCmd(cmd); + onSaveResume(resumeCmd); + }; + + editPopup.saved.connect(editPopup._savedSlot); + editPopup.open(); + } + + // Timeout spinboxes and resume commands + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginL + enabled: Settings.data.idle.enabled + + NLabel { + label: I18n.tr("panels.idle.timeouts-label") + description: I18n.tr("panels.idle.timeouts-description") + } + + DefaultActionRow { + actionName: I18n.tr("panels.idle.screen-off-label") + actionDescription: I18n.tr("panels.idle.screen-off-description") + timeoutValue: Settings.data.idle.screenOffTimeout + defaultValue: Settings.getDefaultValue("idle.screenOffTimeout") + command: Settings.data.idle.screenOffCommand + resumeCommand: Settings.data.idle.resumeScreenOffCommand + onActionTimeoutChanged: val => Settings.data.idle.screenOffTimeout = val + onActionCommandChanged: cmd => { + Settings.data.idle.screenOffCommand = cmd; + Settings.saveImmediate(); + } + onActionResumeCommandChanged: cmd => { + Settings.data.idle.resumeScreenOffCommand = cmd; + Settings.saveImmediate(); + } + } + + DefaultActionRow { + actionName: I18n.tr("panels.idle.lock-label") + actionDescription: I18n.tr("panels.idle.lock-description") + timeoutValue: Settings.data.idle.lockTimeout + defaultValue: Settings.getDefaultValue("idle.lockTimeout") + command: Settings.data.idle.lockCommand + resumeCommand: Settings.data.idle.resumeLockCommand + onActionTimeoutChanged: val => Settings.data.idle.lockTimeout = val + onActionCommandChanged: cmd => { + Settings.data.idle.lockCommand = cmd; + Settings.saveImmediate(); + } + onActionResumeCommandChanged: cmd => { + Settings.data.idle.resumeLockCommand = cmd; + Settings.saveImmediate(); + } + } + + DefaultActionRow { + actionName: I18n.tr("common.suspend") + actionDescription: I18n.tr("panels.idle.suspend-description") + timeoutValue: Settings.data.idle.suspendTimeout + defaultValue: Settings.getDefaultValue("idle.suspendTimeout") + command: Settings.data.idle.suspendCommand + resumeCommand: Settings.data.idle.resumeSuspendCommand + onActionTimeoutChanged: val => Settings.data.idle.suspendTimeout = val + onActionCommandChanged: cmd => { + Settings.data.idle.suspendCommand = cmd; + Settings.saveImmediate(); + } + onActionResumeCommandChanged: cmd => { + Settings.data.idle.resumeSuspendCommand = cmd; + Settings.saveImmediate(); + } + } + + NDivider { + Layout.fillWidth: true + } + + NSpinBox { + label: I18n.tr("panels.idle.fade-duration-label") + description: I18n.tr("panels.idle.fade-duration-description") + from: 1 + to: 60 + suffix: "s" + value: Settings.data.idle.fadeDuration + defaultValue: Settings.getDefaultValue("idle.fadeDuration") + onValueChanged: Settings.data.idle.fadeDuration = value + } + } + + component DefaultActionRow: RowLayout { + id: rowRoot + Layout.fillWidth: true + spacing: Style.marginM + + property string actionName + property string actionDescription + property alias timeoutValue: spinBox.value + property int defaultValue + property string command + property string resumeCommand + + signal actionTimeoutChanged(int newValue) + signal actionCommandChanged(string newCmd) + signal actionResumeCommandChanged(string newCmd) + + NSpinBox { + id: spinBox + Layout.fillWidth: true + label: rowRoot.actionName + description: rowRoot.actionDescription + from: 0 + to: 86400 + suffix: "s" + defaultValue: rowRoot.defaultValue + onValueChanged: rowRoot.actionTimeoutChanged(value) + } + + NIconButton { + Layout.alignment: Qt.AlignVCenter + icon: "settings" + tooltipText: I18n.tr("common.edit") + onClicked: root.openEdit(rowRoot.actionName, rowRoot.command, rowRoot.resumeCommand, rowRoot.actionCommandChanged, rowRoot.actionResumeCommandChanged) + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Idle/CustomSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Idle/CustomSubTab.qml new file mode 100644 index 0000000..211519f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Idle/CustomSubTab.qml @@ -0,0 +1,163 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.Power +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + enabled: Settings.data.idle.enabled + + property bool _saving: false + + ListModel { + id: entriesModel + } + + function _loadToModel() { + if (_saving) + return; + entriesModel.clear(); + var entries = []; + try { + entries = JSON.parse(Settings.data.idle.customCommands); + } catch (e) { + Logger.w("CustomSubTab", "Failed to parse customCommands:", e); + } + for (var i = 0; i < entries.length; i++) { + entriesModel.append({ + "name": String(entries[i].name || ""), + "timeout": parseInt(entries[i].timeout) || 60, + "command": String(entries[i].command || ""), + "resumeCommand": String(entries[i].resumeCommand || "") + }); + } + } + + function _saveFromModel() { + _saving = true; + var arr = []; + for (var i = 0; i < entriesModel.count; i++) { + var item = entriesModel.get(i); + arr.push({ + "name": item.name, + "timeout": item.timeout, + "command": item.command, + "resumeCommand": item.resumeCommand + }); + } + Settings.data.idle.customCommands = JSON.stringify(arr); + _saving = false; + } + + function _removeEntry(index) { + entriesModel.remove(index, 1); + _saveFromModel(); + } + + Component.onCompleted: Qt.callLater(_loadToModel) + + Connections { + target: Settings.data.idle + function onCustomCommandsChanged() { + root._loadToModel(); + } + } + + // Shared Edit Popup + IdleCommandEditPopup { + id: editPopup + parent: Overlay.overlay + } + + function openEdit(index, nameVal, timeoutVal, cmdVal, resumeCmdVal) { + editPopup.editIndex = index; + editPopup.nameValue = nameVal; + editPopup.timeoutValue = timeoutVal; + editPopup.commandValue = cmdVal; + editPopup.resumeCommandValue = resumeCmdVal; + editPopup.showName = true; + + try { + editPopup.saved.disconnect(editPopup._savedSlot); + } catch (e) {} + + editPopup._savedSlot = function (timeout, cmd, resumeCmd, name) { + if (index >= 0 && index < entriesModel.count) { + entriesModel.setProperty(index, "name", name); + entriesModel.setProperty(index, "timeout", timeout); + entriesModel.setProperty(index, "command", cmd); + entriesModel.setProperty(index, "resumeCommand", resumeCmd); + } else { + entriesModel.append({ + "name": name, + "timeout": timeout, + "command": cmd, + "resumeCommand": resumeCmd + }); + } + root._saveFromModel(); + }; + + editPopup.saved.connect(editPopup._savedSlot); + editPopup.open(); + } + + NLabel { + label: I18n.tr("panels.idle.custom-label") + description: I18n.tr("panels.idle.custom-description") + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginS + Layout.bottomMargin: Style.marginS + } + + Repeater { + model: entriesModel + + delegate: RowLayout { + id: entryDelegate + required property int index + required property string name + required property int timeout + required property string command + required property string resumeCommand + + spacing: Style.marginM + Layout.fillWidth: true + + NLabel { + Layout.fillWidth: true + label: entryDelegate.name || I18n.tr("panels.idle.custom-entry-unnamed") + description: I18n.trp("common.second", entryDelegate.timeout) + labelColor: (entryDelegate.command || entryDelegate.resumeCommand) ? Color.mPrimary : Color.mOnSurface + } + + NIconButton { + icon: "settings" + tooltipText: I18n.tr("common.edit") + onClicked: root.openEdit(entryDelegate.index, entryDelegate.name, entryDelegate.timeout, entryDelegate.command, entryDelegate.resumeCommand) + } + + NIconButton { + icon: "trash" + tooltipText: I18n.tr("panels.idle.custom-entry-delete") + onClicked: root._removeEntry(entryDelegate.index) + } + } + } + + NButton { + text: I18n.tr("panels.idle.custom-add") + icon: "add" + enabled: Settings.data.idle.enabled + onClicked: { + root.openEdit(-1, "", 60, "", ""); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Idle/IdleCommandEditPopup.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Idle/IdleCommandEditPopup.qml new file mode 100644 index 0000000..374c4aa --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Idle/IdleCommandEditPopup.qml @@ -0,0 +1,138 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +Popup { + id: root + modal: true + closePolicy: Popup.NoAutoClose + dim: true + anchors.centerIn: parent + + width: Math.min(600 * Style.uiScaleRatio, parent.width * 0.9) + height: Math.min(contentLayout.implicitHeight + padding * 2, parent.height * 0.9) + padding: Style.marginL + + property int editIndex: -1 + property int timeoutValue: 60 + property string commandValue: "" + property string resumeCommandValue: "" + property string nameValue: "" + property bool showCommand: true + property bool showTimeout: true + property bool showName: false + property string titleText: root.editIndex >= 0 ? I18n.tr("panels.idle.custom-entry-edit") : I18n.tr("panels.idle.custom-entry-new") + + signal saved(int timeout, string command, string resumeCommand, string name) + + property var _savedSlot: null + + background: Rectangle { + color: Color.mSurface + radius: Style.radiusL + border.color: Color.mOutline + border.width: Style.borderS + } + + onOpened: { + nameInput.text = nameValue; + timeoutSpinBox.value = timeoutValue; + commandInput.text = commandValue; + resumeCommandInput.text = resumeCommandValue; + if (showName) { + nameInput.forceActiveFocus(); + } else { + timeoutSpinBox.forceActiveFocus(); + } + } + + contentItem: ColumnLayout { + id: contentLayout + spacing: Style.marginL + + // Header + RowLayout { + Layout.fillWidth: true + NText { + text: root.titleText + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeL + Layout.fillWidth: true + } + NIconButton { + icon: "close" + onClicked: root.close() + } + } + + // Input Area + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NTextInput { + id: nameInput + visible: root.showName + Layout.fillWidth: true + label: I18n.tr("panels.idle.custom-entry-name") + placeholderText: I18n.tr("panels.idle.custom-entry-name-placeholder") + } + + NSpinBox { + id: timeoutSpinBox + visible: root.showTimeout + Layout.fillWidth: true + label: I18n.tr("panels.idle.custom-entry-timeout") + from: 0 + to: 86400 + suffix: "s" + } + + NTextInput { + id: commandInput + visible: root.showCommand + Layout.fillWidth: true + label: I18n.tr("panels.idle.custom-entry-command") + placeholderText: "notify-send \"Idle\"" + fontFamily: Settings.data.ui.fontFixed + } + + NTextInput { + id: resumeCommandInput + Layout.fillWidth: true + label: I18n.tr("panels.idle.resume-command-label") + placeholderText: "notify-send \"Welcome back!\"" + fontFamily: Settings.data.ui.fontFixed + } + } + + // Action Buttons + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + Item { + Layout.fillWidth: true + } // Spacer + + NButton { + text: I18n.tr("common.cancel") + outlined: true + onClicked: root.close() + } + + NButton { + text: I18n.tr("common.save") + icon: "check" + backgroundColor: Color.mPrimary + textColor: Color.mOnPrimary + onClicked: { + root.saved(timeoutSpinBox.value, commandInput.text, resumeCommandInput.text, nameInput.text); + root.close(); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Idle/IdleTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Idle/IdleTab.qml new file mode 100644 index 0000000..c16156b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Idle/IdleTab.qml @@ -0,0 +1,42 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("panels.idle.tab-behavior") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("panels.idle.tab-custom") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + BehaviorSubTab {} + CustomSubTab {} + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml new file mode 100644 index 0000000..991ef00 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Launcher/ClipboardSubTab.qml @@ -0,0 +1,90 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.System +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + NToggle { + label: I18n.tr("panels.launcher.settings-clipboard-history-label") + description: I18n.tr("panels.launcher.settings-clipboard-history-description") + checked: Settings.data.appLauncher.enableClipboardHistory + onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardHistory") + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-preview-label") + description: I18n.tr("panels.launcher.settings-clip-preview-description") + checked: Settings.data.appLauncher.enableClipPreview + onToggled: checked => Settings.data.appLauncher.enableClipPreview = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipPreview") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-wrap-text-label") + description: I18n.tr("panels.launcher.settings-clip-wrap-text-description") + checked: Settings.data.appLauncher.clipboardWrapText + onToggled: checked => Settings.data.appLauncher.clipboardWrapText = checked + defaultValue: Settings.getDefaultValue("appLauncher.clipboardWrapText") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NToggle { + label: I18n.tr("panels.launcher.settings-auto-paste-label") + description: I18n.tr("panels.launcher.settings-auto-paste-description") + checked: Settings.data.appLauncher.autoPasteClipboard + onToggled: checked => Settings.data.appLauncher.autoPasteClipboard = checked + defaultValue: Settings.getDefaultValue("appLauncher.autoPasteClipboard") + enabled: Settings.data.appLauncher.enableClipboardHistory && ProgramCheckerService.wtypeAvailable + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-smart-icons-label") + description: I18n.tr("panels.launcher.settings-clip-smart-icons-description") + checked: Settings.data.appLauncher.enableClipboardSmartIcons + onToggled: checked => Settings.data.appLauncher.enableClipboardSmartIcons = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardSmartIcons") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NToggle { + label: I18n.tr("panels.launcher.settings-clip-chips-label") + description: I18n.tr("panels.launcher.settings-clip-chips-description") + checked: Settings.data.appLauncher.enableClipboardChips + onToggled: checked => Settings.data.appLauncher.enableClipboardChips = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableClipboardChips") + enabled: Settings.data.appLauncher.enableClipboardHistory + } + + NDivider { + Layout.fillWidth: true + visible: Settings.data.appLauncher.enableClipboardHistory + } + + NTextInput { + label: I18n.tr("panels.launcher.settings-clipboard-watch-text-label") + description: I18n.tr("panels.launcher.settings-clipboard-watch-text-description") + Layout.fillWidth: true + text: Settings.data.appLauncher.clipboardWatchTextCommand + onTextChanged: Settings.data.appLauncher.clipboardWatchTextCommand = text + enabled: Settings.data.appLauncher.enableClipboardHistory + visible: Settings.data.appLauncher.enableClipboardHistory + } + + NTextInput { + label: I18n.tr("panels.launcher.settings-clipboard-watch-image-label") + description: I18n.tr("panels.launcher.settings-clipboard-watch-image-description") + Layout.fillWidth: true + text: Settings.data.appLauncher.clipboardWatchImageCommand + onTextChanged: Settings.data.appLauncher.clipboardWatchImageCommand = text + enabled: Settings.data.appLauncher.enableClipboardHistory + visible: Settings.data.appLauncher.enableClipboardHistory + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Launcher/ExecuteSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Launcher/ExecuteSubTab.qml new file mode 100644 index 0000000..07fab76 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Launcher/ExecuteSubTab.qml @@ -0,0 +1,49 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.System +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + NTextInput { + label: I18n.tr("panels.launcher.settings-terminal-command-label") + description: I18n.tr("panels.launcher.settings-terminal-command-description") + Layout.fillWidth: true + text: Settings.data.appLauncher.terminalCommand + onTextChanged: { + Settings.data.appLauncher.terminalCommand = text; + } + } + + NToggle { + label: I18n.tr("panels.launcher.settings-custom-launch-prefix-enabled-label") + description: I18n.tr("panels.launcher.settings-custom-launch-prefix-enabled-description") + checked: Settings.data.appLauncher.customLaunchPrefixEnabled + onToggled: checked => Settings.data.appLauncher.customLaunchPrefixEnabled = checked + defaultValue: Settings.getDefaultValue("appLauncher.customLaunchPrefixEnabled") + } + + NTextInput { + label: I18n.tr("panels.launcher.settings-custom-launch-prefix-label") + description: I18n.tr("panels.launcher.settings-custom-launch-prefix-description") + Layout.fillWidth: true + text: Settings.data.appLauncher.customLaunchPrefix + enabled: Settings.data.appLauncher.customLaunchPrefixEnabled + visible: Settings.data.appLauncher.customLaunchPrefixEnabled + onTextChanged: Settings.data.appLauncher.customLaunchPrefix = text + } + + NTextInput { + label: I18n.tr("panels.launcher.settings-annotation-tool-label") + description: I18n.tr("panels.launcher.settings-annotation-tool-description") + Layout.fillWidth: true + text: Settings.data.appLauncher.screenshotAnnotationTool + placeholderText: I18n.tr("panels.launcher.settings-annotation-tool-placeholder") + onTextChanged: Settings.data.appLauncher.screenshotAnnotationTool = text + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Launcher/GeneralSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Launcher/GeneralSubTab.qml new file mode 100644 index 0000000..e6dc189 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Launcher/GeneralSubTab.qml @@ -0,0 +1,179 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + NComboBox { + label: I18n.tr("common.position") + description: I18n.tr("panels.launcher.settings-position-description") + Layout.fillWidth: true + model: [ + { + "key": "follow_bar", + "name": I18n.tr("positions.follow-bar") + }, + { + "key": "center", + "name": I18n.tr("positions.center") + }, + { + "key": "top_center", + "name": I18n.tr("positions.top-center") + }, + { + "key": "top_left", + "name": I18n.tr("positions.top-left") + }, + { + "key": "top_right", + "name": I18n.tr("positions.top-right") + }, + { + "key": "bottom_left", + "name": I18n.tr("positions.bottom-left") + }, + { + "key": "bottom_right", + "name": I18n.tr("positions.bottom-right") + }, + { + "key": "bottom_center", + "name": I18n.tr("positions.bottom-center") + } + ] + currentKey: Settings.data.appLauncher.position + onSelected: function (key) { + Settings.data.appLauncher.position = key; + } + + defaultValue: Settings.getDefaultValue("appLauncher.position") + } + + NToggle { + label: I18n.tr("panels.launcher.settings-overlay-layer-label") + description: I18n.tr("panels.launcher.settings-overlay-layer-description") + checked: Settings.data.appLauncher.overviewLayer + onToggled: checked => Settings.data.appLauncher.overviewLayer = checked + defaultValue: Settings.getDefaultValue("appLauncher.overviewLayer") + } + + NComboBox { + label: I18n.tr("panels.launcher.settings-view-mode-label") + description: I18n.tr("panels.launcher.settings-view-mode-description") + Layout.fillWidth: true + model: [ + { + "key": "list", + "name": I18n.tr("options.launcher-view-mode.list") + }, + { + "key": "grid", + "name": I18n.tr("options.launcher-view-mode.grid") + } + ] + currentKey: Settings.data.appLauncher.viewMode + onSelected: function (key) { + Settings.data.appLauncher.viewMode = key; + } + defaultValue: Settings.getDefaultValue("appLauncher.viewMode") + } + + NComboBox { + label: I18n.tr("panels.launcher.settings-density-label") + description: I18n.tr("panels.launcher.settings-density-description") + Layout.fillWidth: true + model: [ + { + "key": "compact", + "name": I18n.tr("options.launcher-density.compact") + }, + { + "key": "default", + "name": I18n.tr("options.launcher-density.default") + }, + { + "key": "comfortable", + "name": I18n.tr("options.launcher-density.comfortable") + } + ] + currentKey: Settings.data.appLauncher.density || "compact" + onSelected: function (key) { + Settings.data.appLauncher.density = key; + } + defaultValue: Settings.getDefaultValue("appLauncher.density") + } + + NToggle { + label: I18n.tr("panels.launcher.settings-show-categories-label") + description: I18n.tr("panels.launcher.settings-show-categories-description") + checked: Settings.data.appLauncher.showCategories + onToggled: checked => Settings.data.appLauncher.showCategories = checked + defaultValue: Settings.getDefaultValue("appLauncher.showCategories") + } + + NToggle { + label: I18n.tr("panels.launcher.settings-icon-mode-label") + description: I18n.tr("panels.launcher.settings-icon-mode-description") + checked: Settings.data.appLauncher.iconMode === "native" + onToggled: checked => Settings.data.appLauncher.iconMode = checked ? "native" : "tabler" + defaultValue: Settings.getDefaultValue("appLauncher.iconMode") === "native" + } + + NToggle { + label: I18n.tr("panels.launcher.settings-show-icon-background-label") + description: I18n.tr("panels.launcher.settings-show-icon-background-description") + checked: Settings.data.appLauncher.showIconBackground + onToggled: checked => Settings.data.appLauncher.showIconBackground = checked + defaultValue: Settings.getDefaultValue("appLauncher.showIconBackground") + } + + NDivider { + Layout.fillWidth: true + } + + NToggle { + label: I18n.tr("panels.launcher.settings-sort-by-usage-label") + description: I18n.tr("panels.launcher.settings-sort-by-usage-description") + checked: Settings.data.appLauncher.sortByMostUsed + onToggled: checked => Settings.data.appLauncher.sortByMostUsed = checked + defaultValue: Settings.getDefaultValue("appLauncher.sortByMostUsed") + } + + NToggle { + label: I18n.tr("panels.launcher.settings-enable-settings-search-label") + description: I18n.tr("panels.launcher.settings-enable-settings-search-description") + checked: Settings.data.appLauncher.enableSettingsSearch + onToggled: checked => Settings.data.appLauncher.enableSettingsSearch = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableSettingsSearch") + } + + NToggle { + label: I18n.tr("panels.launcher.settings-enable-windows-search-label") + description: I18n.tr("panels.launcher.settings-enable-windows-search-description") + checked: Settings.data.appLauncher.enableWindowsSearch + onToggled: checked => Settings.data.appLauncher.enableWindowsSearch = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableWindowsSearch") + } + + NToggle { + label: I18n.tr("panels.launcher.settings-enable-session-search-label") + description: I18n.tr("panels.launcher.settings-enable-session-search-description") + checked: Settings.data.appLauncher.enableSessionSearch + onToggled: checked => Settings.data.appLauncher.enableSessionSearch = checked + defaultValue: Settings.getDefaultValue("appLauncher.enableSessionSearch") + } + + NToggle { + label: I18n.tr("panels.launcher.settings-ignore-mouse-input-label") + description: I18n.tr("panels.launcher.settings-ignore-mouse-input-description") + checked: Settings.data.appLauncher.ignoreMouseInput + onToggled: checked => Settings.data.appLauncher.ignoreMouseInput = checked + defaultValue: Settings.getDefaultValue("appLauncher.ignoreMouseInput") + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Launcher/LauncherTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Launcher/LauncherTab.qml new file mode 100644 index 0000000..46f2c2b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Launcher/LauncherTab.qml @@ -0,0 +1,48 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.general") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.clipboard") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + NTabButton { + text: I18n.tr("common.execute") + tabIndex: 2 + checked: subTabBar.currentIndex === 2 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + GeneralSubTab {} + ClipboardSubTab {} + ExecuteSubTab {} + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/LockScreen/AppearanceSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/LockScreen/AppearanceSubTab.qml new file mode 100644 index 0000000..545b047 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/LockScreen/AppearanceSubTab.qml @@ -0,0 +1,123 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + function insertToken(token) { + if (formatInput.inputItem) { + var input = formatInput.inputItem; + var cursorPos = input.cursorPosition; + var currentText = input.text; + var newText = currentText.substring(0, cursorPos) + token + currentText.substring(cursorPos); + input.text = newText + " "; + input.cursorPosition = cursorPos + token.length + 1; + input.forceActiveFocus(); + } + } + + NComboBox { + label: I18n.tr("panels.lock-screen.clock-style-label") + description: I18n.tr("panels.lock-screen.clock-style-description") + model: [ + { + "key": "analog", + "name": I18n.tr("panels.lock-screen.clock-style-analog") + }, + { + "key": "digital", + "name": I18n.tr("panels.lock-screen.clock-style-digital") + }, + { + "key": "custom", + "name": I18n.tr("panels.lock-screen.clock-style-custom") + } + ] + currentKey: Settings.data.general.clockStyle + onSelected: key => Settings.data.general.clockStyle = key + defaultValue: Settings.getDefaultValue("general.clockStyle") + z: 10 + } + + NTextInput { + id: formatInput + label: I18n.tr("panels.lock-screen.clock-format-label") + description: I18n.tr("panels.lock-screen.clock-format-description") + text: Settings.data.general.clockFormat + onTextChanged: Settings.data.general.clockFormat = text + visible: Settings.data.general.clockStyle === "custom" + defaultValue: Settings.getDefaultValue("general.clockFormat") + } + + NDateTimeTokens { + Layout.fillWidth: true + Layout.preferredHeight: 300 + visible: Settings.data.general.clockStyle === "custom" + onTokenClicked: token => root.insertToken(token) + } + + NToggle { + label: I18n.tr("panels.lock-screen.password-chars-label") + description: I18n.tr("panels.lock-screen.password-chars-description") + checked: Settings.data.general.passwordChars + onToggled: checked => Settings.data.general.passwordChars = checked + defaultValue: Settings.getDefaultValue("general.passwordChars") + } + + NToggle { + label: I18n.tr("panels.lock-screen.compact-lockscreen-label") + description: I18n.tr("panels.lock-screen.compact-lockscreen-description") + checked: Settings.data.general.compactLockScreen + onToggled: checked => Settings.data.general.compactLockScreen = checked + defaultValue: Settings.getDefaultValue("general.compactLockScreen") + } + + NToggle { + label: I18n.tr("panels.lock-screen.enable-lockscreen-media-controls-label") + description: I18n.tr("panels.lock-screen.enable-lockscreen-media-controls-description") + checked: Settings.data.general.enableLockScreenMediaControls + onToggled: checked => Settings.data.general.enableLockScreenMediaControls = checked + visible: !Settings.data.general.compactLockScreen + defaultValue: Settings.getDefaultValue("general.enableLockScreenMediaControls") + } + + NToggle { + label: I18n.tr("panels.lock-screen.lock-screen-animations-label") + description: I18n.tr("panels.lock-screen.lock-screen-animations-description") + checked: Settings.data.general.lockScreenAnimations + onToggled: checked => Settings.data.general.lockScreenAnimations = checked + defaultValue: Settings.getDefaultValue("general.lockScreenAnimations") + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.lock-screen.lock-screen-blur-strength-label") + description: I18n.tr("panels.lock-screen.lock-screen-blur-strength-description") + from: 0.0 + to: 1.0 + stepSize: 0.01 + showReset: true + value: Settings.data.general.lockScreenBlur + onMoved: value => Settings.data.general.lockScreenBlur = value + text: ((Settings.data.general.lockScreenBlur) * 100).toFixed(0) + "%" + defaultValue: Settings.getDefaultValue("general.lockScreenBlur") + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.lock-screen.lock-screen-tint-strength-label") + description: I18n.tr("panels.lock-screen.lock-screen-tint-strength-description") + from: 0.0 + to: 1.0 + stepSize: 0.01 + showReset: true + value: Settings.data.general.lockScreenTint + onMoved: value => Settings.data.general.lockScreenTint = value + text: ((Settings.data.general.lockScreenTint) * 100).toFixed(0) + "%" + defaultValue: Settings.getDefaultValue("general.lockScreenTint") + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/LockScreen/BehaviorSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/LockScreen/BehaviorSubTab.qml new file mode 100644 index 0000000..e3a464f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/LockScreen/BehaviorSubTab.qml @@ -0,0 +1,75 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + + NToggle { + label: I18n.tr("panels.lock-screen.lock-on-suspend-label") + description: I18n.tr("panels.lock-screen.lock-on-suspend-description") + checked: Settings.data.general.lockOnSuspend + onToggled: checked => Settings.data.general.lockOnSuspend = checked + defaultValue: Settings.getDefaultValue("general.lockOnSuspend") + } + + NToggle { + label: I18n.tr("panels.lock-screen.auto-start-auth-label") + description: I18n.tr("panels.lock-screen.auto-start-auth-description") + checked: Settings.data.general.autoStartAuth + onToggled: checked => Settings.data.general.autoStartAuth = checked + defaultValue: Settings.getDefaultValue("general.autoStartAuth") + } + + NToggle { + label: I18n.tr("panels.lock-screen.allow-password-with-fprintd-label") + description: I18n.tr("panels.lock-screen.allow-password-with-fprintd-description") + checked: Settings.data.general.allowPasswordWithFprintd + onToggled: checked => Settings.data.general.allowPasswordWithFprintd = checked + defaultValue: Settings.getDefaultValue("general.allowPasswordWithFprintd") + } + + NToggle { + label: I18n.tr("panels.lock-screen.show-session-buttons-label") + description: I18n.tr("panels.lock-screen.show-session-buttons-description") + checked: Settings.data.general.showSessionButtonsOnLockScreen + onToggled: checked => Settings.data.general.showSessionButtonsOnLockScreen = checked + defaultValue: Settings.getDefaultValue("general.showSessionButtonsOnLockScreen") + } + + NToggle { + label: I18n.tr("panels.lock-screen.show-hibernate-label") + description: I18n.tr("panels.lock-screen.show-hibernate-description") + checked: Settings.data.general.showHibernateOnLockScreen + onToggled: checked => Settings.data.general.showHibernateOnLockScreen = checked + visible: Settings.data.general.showSessionButtonsOnLockScreen + defaultValue: Settings.getDefaultValue("general.showSessionButtonsOnLockScreen") + } + + NToggle { + label: I18n.tr("panels.session-menu.enable-countdown-label") + description: I18n.tr("panels.session-menu.enable-countdown-description") + checked: Settings.data.general.enableLockScreenCountdown + onToggled: checked => Settings.data.general.enableLockScreenCountdown = checked + visible: Settings.data.general.showSessionButtonsOnLockScreen + defaultValue: Settings.getDefaultValue("general.enableLockScreenCountdown") + } + + NValueSlider { + visible: Settings.data.general.showSessionButtonsOnLockScreen && Settings.data.general.enableLockScreenCountdown + Layout.fillWidth: true + label: I18n.tr("panels.session-menu.countdown-duration-label") + description: I18n.tr("panels.session-menu.countdown-duration-description") + from: 1000 + to: 30000 + stepSize: 1000 + showReset: true + value: Settings.data.general.lockScreenCountdownDuration + onMoved: value => Settings.data.general.lockScreenCountdownDuration = value + text: Math.round(Settings.data.general.lockScreenCountdownDuration / 1000) + "s" + defaultValue: Settings.getDefaultValue("general.lockScreenCountdownDuration") + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/LockScreen/LockScreenTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/LockScreen/LockScreenTab.qml new file mode 100644 index 0000000..b6a1320 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/LockScreen/LockScreenTab.qml @@ -0,0 +1,48 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.appearance") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.behavior") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + NTabButton { + text: I18n.tr("common.monitors") + tabIndex: 2 + checked: subTabBar.currentIndex === 2 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + AppearanceSubTab {} + BehaviorSubTab {} + MonitorsSubTab {} + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/LockScreen/MonitorsSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/LockScreen/MonitorsSubTab.qml new file mode 100644 index 0000000..13239ab --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/LockScreen/MonitorsSubTab.qml @@ -0,0 +1,61 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Compositor +import qs.Widgets + +ColumnLayout { + id: root + enabled: true + spacing: Style.marginL + Layout.fillWidth: true + + // Helper functions to update arrays immutably + function addMonitor(list, name) { + const arr = (list || []).slice(); + if (!arr.includes(name)) + arr.push(name); + return arr; + } + function removeMonitor(list, name) { + return (list || []).filter(function (n) { + return n !== name; + }); + } + + NText { + text: I18n.tr("panels.lock-screen.monitors-desc") + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Repeater { + model: Quickshell.screens || [] + delegate: NCheckbox { + Layout.fillWidth: true + readonly property real compositorScale: { + const info = CompositorService.displayScales[modelData.name]; + return (info && info.scale) ? info.scale : 1.0; + } + label: modelData.name || "Unknown" + description: { + I18n.tr("system.monitor-description", { + "model": modelData.model, + "width": modelData.width * compositorScale, + "height": modelData.height * compositorScale, + "scale": compositorScale + }); + } + checked: (Settings.data.general.lockScreenMonitors || []).indexOf(modelData.name) !== -1 + onToggled: checked => { + if (checked) { + Settings.data.general.lockScreenMonitors = root.addMonitor(Settings.data.general.lockScreenMonitors, modelData.name); + } else { + Settings.data.general.lockScreenMonitors = root.removeMonitor(Settings.data.general.lockScreenMonitors, modelData.name); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/DurationSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/DurationSubTab.qml new file mode 100644 index 0000000..708e272 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/DurationSubTab.qml @@ -0,0 +1,62 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + enabled: Settings.data.notifications.enabled + + NToggle { + label: I18n.tr("panels.notifications.duration-respect-expire-label") + description: I18n.tr("panels.notifications.duration-respect-expire-description") + checked: Settings.data.notifications.respectExpireTimeout + onToggled: checked => Settings.data.notifications.respectExpireTimeout = checked + defaultValue: Settings.getDefaultValue("notifications.respectExpireTimeout") + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.notifications.duration-low-urgency-label") + description: I18n.tr("panels.notifications.duration-low-urgency-description") + from: 1 + to: 30 + stepSize: 1 + showReset: true + value: Settings.data.notifications.lowUrgencyDuration + onMoved: value => Settings.data.notifications.lowUrgencyDuration = value + text: Settings.data.notifications.lowUrgencyDuration + "s" + defaultValue: Settings.getDefaultValue("notifications.lowUrgencyDuration") + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.notifications.duration-normal-urgency-label") + description: I18n.tr("panels.notifications.duration-normal-urgency-description") + from: 1 + to: 30 + stepSize: 1 + showReset: true + value: Settings.data.notifications.normalUrgencyDuration + onMoved: value => Settings.data.notifications.normalUrgencyDuration = value + text: Settings.data.notifications.normalUrgencyDuration + "s" + defaultValue: Settings.getDefaultValue("notifications.normalUrgencyDuration") + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.notifications.duration-critical-urgency-label") + description: I18n.tr("panels.notifications.duration-critical-urgency-description") + from: 1 + to: 30 + stepSize: 1 + showReset: true + value: Settings.data.notifications.criticalUrgencyDuration + onMoved: value => Settings.data.notifications.criticalUrgencyDuration = value + text: Settings.data.notifications.criticalUrgencyDuration + "s" + defaultValue: Settings.getDefaultValue("notifications.criticalUrgencyDuration") + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/GeneralSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/GeneralSubTab.qml new file mode 100644 index 0000000..bad998b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/GeneralSubTab.qml @@ -0,0 +1,149 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Compositor +import qs.Services.System +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + property var addMonitor + property var removeMonitor + + NToggle { + label: I18n.tr("panels.notifications.settings-enabled-label") + description: I18n.tr("panels.notifications.settings-enabled-description") + checked: Settings.data.notifications.enabled !== false + onToggled: checked => Settings.data.notifications.enabled = checked + defaultValue: Settings.getDefaultValue("notifications.enabled") + } + + ColumnLayout { + spacing: Style.marginL + enabled: Settings.data.notifications.enabled + + NComboBox { + label: I18n.tr("panels.notifications.settings-density-label") + description: I18n.tr("panels.notifications.settings-density-description") + model: [ + { + "key": "default", + "name": I18n.tr("options.notification-density.default") + }, + { + "key": "compact", + "name": I18n.tr("options.notification-density.compact") + } + ] + currentKey: Settings.data.notifications.density || "default" + onSelected: key => Settings.data.notifications.density = key + defaultValue: Settings.getDefaultValue("notifications.density") + } + + NToggle { + label: I18n.tr("tooltips.do-not-disturb-enabled") + description: I18n.tr("panels.notifications.settings-do-not-disturb-description") + checked: NotificationService.doNotDisturb + onToggled: checked => NotificationService.doNotDisturb = checked + } + + NComboBox { + label: I18n.tr("common.position") + description: I18n.tr("panels.notifications.settings-location-description") + model: [ + { + "key": "top", + "name": I18n.tr("positions.top-center") + }, + { + "key": "top_left", + "name": I18n.tr("positions.top-left") + }, + { + "key": "top_right", + "name": I18n.tr("positions.top-right") + }, + { + "key": "bottom", + "name": I18n.tr("positions.bottom-center") + }, + { + "key": "bottom_left", + "name": I18n.tr("positions.bottom-left") + }, + { + "key": "bottom_right", + "name": I18n.tr("positions.bottom-right") + } + ] + currentKey: Settings.data.notifications.location || "top_right" + onSelected: key => Settings.data.notifications.location = key + defaultValue: Settings.getDefaultValue("notifications.location") + } + + NToggle { + label: I18n.tr("panels.osd.always-on-top-label") + description: I18n.tr("panels.notifications.settings-always-on-top-description") + checked: Settings.data.notifications.overlayLayer + onToggled: checked => Settings.data.notifications.overlayLayer = checked + defaultValue: Settings.getDefaultValue("notifications.overlayLayer") + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.osd.background-opacity-label") + description: I18n.tr("panels.notifications.settings-background-opacity-description") + from: 0 + to: 1 + stepSize: 0.01 + showReset: true + value: Settings.data.notifications.backgroundOpacity + onMoved: value => Settings.data.notifications.backgroundOpacity = value + text: Math.round(Settings.data.notifications.backgroundOpacity * 100) + "%" + defaultValue: Settings.getDefaultValue("notifications.backgroundOpacity") + } + + NDivider { + Layout.fillWidth: true + } + + NText { + text: I18n.tr("panels.notifications.monitors-desc") + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Repeater { + model: Quickshell.screens || [] + delegate: NCheckbox { + Layout.fillWidth: true + readonly property real compositorScale: { + const info = CompositorService.displayScales[modelData.name]; + return (info && info.scale) ? info.scale : 1.0; + } + label: modelData.name || I18n.tr("common.unknown") + description: { + I18n.tr("system.monitor-description", { + "model": modelData.model, + "width": modelData.width * compositorScale, + "height": modelData.height * compositorScale, + "scale": compositorScale + }); + } + checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1 + onToggled: checked => { + if (checked) { + Settings.data.notifications.monitors = root.addMonitor(Settings.data.notifications.monitors, modelData.name); + } else { + Settings.data.notifications.monitors = root.removeMonitor(Settings.data.notifications.monitors, modelData.name); + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/HistorySubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/HistorySubTab.qml new file mode 100644 index 0000000..929de49 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/HistorySubTab.qml @@ -0,0 +1,52 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + enabled: Settings.data.notifications.enabled + + NToggle { + label: I18n.tr("panels.notifications.history-clear-dismiss-label") + description: I18n.tr("panels.notifications.history-clear-dismiss-description") + checked: Settings.data.notifications.clearDismissed + onToggled: checked => Settings.data.notifications.clearDismissed = checked + defaultValue: Settings.getDefaultValue("notifications.clearDismissed") + } + + NToggle { + label: I18n.tr("panels.notifications.settings-markdown-label") + description: I18n.tr("panels.notifications.settings-markdown-description") + checked: Settings.data.notifications.enableMarkdown + onToggled: checked => Settings.data.notifications.enableMarkdown = checked + defaultValue: Settings.getDefaultValue("notifications.enableMarkdown") + } + + NToggle { + label: I18n.tr("panels.notifications.history-low-urgency-label") + description: I18n.tr("panels.notifications.history-low-urgency-description") + checked: Settings.data.notifications?.saveToHistory?.low !== false + onToggled: checked => Settings.data.notifications.saveToHistory.low = checked + defaultValue: Settings.getDefaultValue("notifications.saveToHistory.low") + } + + NToggle { + label: I18n.tr("panels.notifications.history-normal-urgency-label") + description: I18n.tr("panels.notifications.history-normal-urgency-description") + checked: Settings.data.notifications?.saveToHistory?.normal !== false + onToggled: checked => Settings.data.notifications.saveToHistory.normal = checked + defaultValue: Settings.getDefaultValue("notifications.saveToHistory.normal") + } + + NToggle { + label: I18n.tr("panels.notifications.history-critical-urgency-label") + description: I18n.tr("panels.notifications.history-critical-urgency-description") + checked: Settings.data.notifications?.saveToHistory?.critical !== false + onToggled: checked => Settings.data.notifications.saveToHistory.critical = checked + defaultValue: Settings.getDefaultValue("notifications.saveToHistory.critical") + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/NotificationRuleEditPopup.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/NotificationRuleEditPopup.qml new file mode 100644 index 0000000..8b71f0a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/NotificationRuleEditPopup.qml @@ -0,0 +1,130 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.System +import qs.Widgets + +Popup { + id: root + modal: true + closePolicy: Popup.NoAutoClose + dim: true + anchors.centerIn: parent + + width: Math.min(500 * Style.uiScaleRatio, parent.width * 0.9) + padding: Style.marginL + + property int editIndex: -1 + property string patternValue: "" + property string actionValue: "block" + + signal saved(string pattern, string action) + + property var _savedSlot: null + property string _selectedAction: "block" + + background: Rectangle { + color: Color.mSurface + radius: Style.radiusL + border.color: Color.mOutline + border.width: Style.borderS + } + + onOpened: { + patternInput.text = patternValue; + actionCombo.currentKey = actionValue; + _selectedAction = actionValue; + patternInput.forceActiveFocus(); + } + + contentItem: ColumnLayout { + id: contentLayout + spacing: Style.marginL + + RowLayout { + Layout.fillWidth: true + NText { + text: editIndex >= 0 ? I18n.tr("panels.notifications.rules-edit") : I18n.tr("panels.notifications.rules-add") + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeL + Layout.fillWidth: true + } + NIconButton { + icon: "close" + onClicked: root.close() + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NTextInput { + id: patternInput + Layout.fillWidth: true + label: I18n.tr("panels.notifications.rules-pattern-label") + placeholderText: I18n.tr("panels.notifications.rules-pattern-placeholder") + fontFamily: Settings.data.ui.fontFixed + } + + NComboBox { + id: actionCombo + Layout.fillWidth: true + label: I18n.tr("panels.notifications.rules-action-label") + model: [ + { + "key": "block", + "name": I18n.tr("panels.notifications.rules-action-block") + }, + { + "key": "mute", + "name": I18n.tr("panels.notifications.rules-action-mute") + }, + { + "key": "hide", + "name": I18n.tr("panels.notifications.rules-action-hide") + } + ] + currentKey: actionValue + onSelected: key => { + actionValue = key; + _selectedAction = key; + } + } + + NLabel { + Layout.fillWidth: true + label: _selectedAction === "block" ? I18n.tr("panels.notifications.rules-action-block-desc") : (_selectedAction === "mute" ? I18n.tr("panels.notifications.rules-action-mute-desc") : I18n.tr("panels.notifications.rules-action-hide-desc")) + labelColor: Color.mOnSurfaceVariant + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + Item { + Layout.fillWidth: true + } + + NButton { + text: I18n.tr("common.cancel") + outlined: true + onClicked: root.close() + } + + NButton { + text: I18n.tr("common.save") + icon: "check" + backgroundColor: Color.mPrimary + textColor: Color.mOnPrimary + enabled: patternInput.text.trim() !== "" + onClicked: { + root.saved(patternInput.text.trim(), _selectedAction || "block"); + root.close(); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/NotificationsTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/NotificationsTab.qml new file mode 100644 index 0000000..831dbb9 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/NotificationsTab.qml @@ -0,0 +1,158 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + // Helper functions to update arrays immutably + function addMonitor(list, name) { + const arr = (list || []).slice(); + if (!arr.includes(name)) + arr.push(name); + return arr; + } + function removeMonitor(list, name) { + return (list || []).filter(function (n) { + return n !== name; + }); + } + + // File pickers for sound sub-tab + function openUnifiedSoundPicker() { + unifiedSoundFilePicker.open(); + } + function openLowSoundPicker() { + lowSoundFilePicker.open(); + } + function openNormalSoundPicker() { + normalSoundFilePicker.open(); + } + function openCriticalSoundPicker() { + criticalSoundFilePicker.open(); + } + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: false // this is too cramped on this tab to split evenly + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.appearance") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.duration") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + NTabButton { + text: I18n.tr("common.history") + tabIndex: 2 + checked: subTabBar.currentIndex === 2 + } + NTabButton { + text: I18n.tr("common.sound") + tabIndex: 3 + checked: subTabBar.currentIndex === 3 + } + NTabButton { + text: I18n.tr("common.toast") + tabIndex: 4 + checked: subTabBar.currentIndex === 4 + } + NTabButton { + text: I18n.tr("panels.notifications.rules-tab") + tabIndex: 5 + checked: subTabBar.currentIndex === 5 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + GeneralSubTab { + addMonitor: root.addMonitor + removeMonitor: root.removeMonitor + } + DurationSubTab {} + HistorySubTab {} + SoundSubTab { + onOpenUnifiedPicker: root.openUnifiedSoundPicker() + onOpenLowPicker: root.openLowSoundPicker() + onOpenNormalPicker: root.openNormalSoundPicker() + onOpenCriticalPicker: root.openCriticalSoundPicker() + } + ToastSubTab {} + RulesSubTab {} + } + + // File Pickers for Sound Files + NFilePicker { + id: unifiedSoundFilePicker + title: I18n.tr("panels.notifications.sounds-files-unified-select-title") + selectionMode: "files" + initialPath: Quickshell.env("HOME") + nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"] + onAccepted: paths => { + if (paths.length > 0) { + const soundPath = paths[0]; + Settings.data.notifications.sounds.normalSoundFile = soundPath; + Settings.data.notifications.sounds.lowSoundFile = soundPath; + Settings.data.notifications.sounds.criticalSoundFile = soundPath; + } + } + } + + NFilePicker { + id: lowSoundFilePicker + title: I18n.tr("panels.notifications.sounds-files-low-select-title") + selectionMode: "files" + initialPath: Quickshell.env("HOME") + nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"] + onAccepted: paths => { + if (paths.length > 0) { + Settings.data.notifications.sounds.lowSoundFile = paths[0]; + } + } + } + + NFilePicker { + id: normalSoundFilePicker + title: I18n.tr("panels.notifications.sounds-files-normal-select-title") + selectionMode: "files" + initialPath: Quickshell.env("HOME") + nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"] + onAccepted: paths => { + if (paths.length > 0) { + Settings.data.notifications.sounds.normalSoundFile = paths[0]; + } + } + } + + NFilePicker { + id: criticalSoundFilePicker + title: I18n.tr("panels.notifications.sounds-files-critical-select-title") + selectionMode: "files" + initialPath: Quickshell.env("HOME") + nameFilters: ["*.wav", "*.mp3", "*.ogg", "*.flac", "*.m4a", "*.aac"] + onAccepted: paths => { + if (paths.length > 0) { + Settings.data.notifications.sounds.criticalSoundFile = paths[0]; + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/RulesSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/RulesSubTab.qml new file mode 100644 index 0000000..daf9633 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/RulesSubTab.qml @@ -0,0 +1,114 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.System +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + enabled: Settings.data.notifications.enabled + + function _saveToService() { + NotificationRulesService.save(); + } + + function _removeRule(index) { + var arr = (NotificationRulesService.rules || []).slice(); + arr.splice(index, 1); + NotificationRulesService.rules = arr; + _saveToService(); + } + + NotificationRuleEditPopup { + id: editPopup + parent: Overlay.overlay + } + + function openEdit(index, patternVal, actionVal) { + editPopup.editIndex = index; + editPopup.patternValue = patternVal || ""; + editPopup.actionValue = actionVal || "block"; + + try { + editPopup.saved.disconnect(editPopup._savedSlot); + } catch (e) {} + + editPopup._savedSlot = function (pattern, action) { + const trimmed = (pattern || "").trim(); + if (trimmed === "") + return; + var arr = (NotificationRulesService.rules || []).slice(); + var rule = { + "pattern": trimmed, + "action": action + }; + if (index >= 0 && index < arr.length) { + arr[index] = rule; + } else { + arr.push(rule); + } + NotificationRulesService.rules = arr; + _saveToService(); + }; + + editPopup.saved.connect(editPopup._savedSlot); + editPopup.open(); + } + + NLabel { + label: I18n.tr("panels.notifications.rules-label") + description: I18n.tr("panels.notifications.rules-description") + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginS + Layout.bottomMargin: Style.marginS + } + + Repeater { + model: NotificationRulesService.rules || [] + + delegate: RowLayout { + id: entryDelegate + required property int index + required property var modelData + + property string pattern: modelData.pattern || "" + property string action: modelData.action || "block" + property bool isRegex: pattern.length >= 3 && pattern.startsWith("/") && pattern.endsWith("/") + + spacing: Style.marginM + Layout.fillWidth: true + + NLabel { + Layout.fillWidth: true + label: (entryDelegate.isRegex ? "regex: " : "") + entryDelegate.pattern + description: entryDelegate.action === "block" ? I18n.tr("panels.notifications.rules-action-block") : (entryDelegate.action === "mute" ? I18n.tr("panels.notifications.rules-action-mute") : I18n.tr("panels.notifications.rules-action-hide")) + labelColor: entryDelegate.pattern ? Color.mPrimary : Color.mOnSurface + } + + NIconButton { + icon: "settings" + tooltipText: I18n.tr("common.edit") + onClicked: root.openEdit(entryDelegate.index, entryDelegate.pattern, entryDelegate.action) + } + + NIconButton { + icon: "trash" + tooltipText: I18n.tr("panels.notifications.rules-delete") + onClicked: root._removeRule(entryDelegate.index) + } + } + } + + NButton { + text: I18n.tr("panels.notifications.rules-add") + icon: "add" + enabled: Settings.data.notifications.enabled + onClicked: root.openEdit(-1, "", "block") + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/SoundSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/SoundSubTab.qml new file mode 100644 index 0000000..2a69bdd --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/SoundSubTab.qml @@ -0,0 +1,210 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.System +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + enabled: Settings.data.notifications.enabled + + signal openUnifiedPicker + signal openLowPicker + signal openNormalPicker + signal openCriticalPicker + + // QtMultimedia unavailable message + NBox { + Layout.fillWidth: true + visible: !SoundService.multimediaAvailable + implicitHeight: unavailableContent.implicitHeight + Style.margin2L + + RowLayout { + id: unavailableContent + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + NIcon { + icon: "warning" + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeXL + Layout.alignment: Qt.AlignVCenter + } + + NLabel { + Layout.fillWidth: true + label: I18n.tr("panels.notifications.sounds-unavailable-label") + description: I18n.tr("panels.notifications.sounds-unavailable-description") + } + } + } + + NToggle { + enabled: SoundService.multimediaAvailable + label: I18n.tr("panels.notifications.sounds-enabled-label") + description: I18n.tr("panels.notifications.sounds-enabled-description") + checked: Settings.data.notifications?.sounds?.enabled ?? false + onToggled: checked => Settings.data.notifications.sounds.enabled = checked + defaultValue: Settings.getDefaultValue("notifications.sounds.enabled") + } + + // Sound Volume + NValueSlider { + enabled: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) + Layout.fillWidth: true + label: I18n.tr("panels.notifications.sounds-volume-label") + description: I18n.tr("panels.notifications.sounds-volume-description") + from: 0 + to: 1 + stepSize: 0.01 + showReset: true + value: Settings.data.notifications?.sounds?.volume ?? 0.5 + onMoved: value => Settings.data.notifications.sounds.volume = value + text: Math.round((Settings.data.notifications?.sounds?.volume ?? 0.5) * 100) + "%" + defaultValue: Settings.getDefaultValue("notifications.sounds.volume") + } + + // Separate Sounds Toggle + NToggle { + enabled: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) + Layout.fillWidth: true + label: I18n.tr("panels.notifications.sounds-separate-label") + description: I18n.tr("panels.notifications.sounds-separate-description") + checked: Settings.data.notifications?.sounds?.separateSounds ?? false + onToggled: checked => Settings.data.notifications.sounds.separateSounds = checked + defaultValue: Settings.getDefaultValue("notifications.sounds.separateSounds") + } + + // Unified Sound File (shown when separateSounds is false) + ColumnLayout { + enabled: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) + visible: !(Settings.data.notifications?.sounds?.separateSounds ?? false) + spacing: Style.marginXXS + Layout.fillWidth: true + + NLabel { + label: I18n.tr("panels.notifications.sounds-files-unified-label") + description: I18n.tr("panels.notifications.sounds-files-unified-description") + } + + NTextInputButton { + enabled: parent.enabled + Layout.fillWidth: true + placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder") + text: Settings.data.notifications?.sounds?.normalSoundFile ?? "" + buttonIcon: "folder-open" + buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file") + onInputTextChanged: text => { + // When separate sounds are enabled, this row is hidden but still + // bound to normalSoundFile; user edits to normal still update that + // key and would re-trigger this handler and wipe low/critical. + if (Settings.data.notifications.sounds.separateSounds) { + return; + } + const soundPath = text; + Settings.data.notifications.sounds.normalSoundFile = soundPath; + Settings.data.notifications.sounds.lowSoundFile = soundPath; + Settings.data.notifications.sounds.criticalSoundFile = soundPath; + } + onButtonClicked: root.openUnifiedPicker() + } + } + + // Separate Sound Files (shown when separateSounds is true) + ColumnLayout { + visible: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) && (Settings.data.notifications?.sounds?.separateSounds ?? false) + spacing: Style.marginXXS + Layout.fillWidth: true + + // Low Urgency Sound File + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + + NLabel { + label: I18n.tr("panels.notifications.sounds-files-low-label") + description: I18n.tr("panels.notifications.sounds-files-low-description") + } + + NTextInputButton { + enabled: parent.enabled + Layout.fillWidth: true + placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder") + text: Settings.data.notifications?.sounds?.lowSoundFile ?? "" + buttonIcon: "folder-open" + buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file") + onInputTextChanged: text => Settings.data.notifications.sounds.lowSoundFile = text + onButtonClicked: root.openLowPicker() + } + } + + // Normal Urgency Sound File + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + + NLabel { + label: I18n.tr("panels.notifications.sounds-files-normal-label") + description: I18n.tr("panels.notifications.sounds-files-normal-description") + } + + NTextInputButton { + enabled: parent.enabled + Layout.fillWidth: true + placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder") + text: Settings.data.notifications?.sounds?.normalSoundFile ?? "" + buttonIcon: "folder-open" + buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file") + onInputTextChanged: text => Settings.data.notifications.sounds.normalSoundFile = text + onButtonClicked: root.openNormalPicker() + } + } + + // Critical Urgency Sound File + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + + NLabel { + label: I18n.tr("panels.notifications.sounds-files-critical-label") + description: I18n.tr("panels.notifications.sounds-files-critical-description") + } + + NTextInputButton { + enabled: parent.enabled + Layout.fillWidth: true + placeholderText: I18n.tr("panels.notifications.sounds-files-placeholder") + text: Settings.data.notifications?.sounds?.criticalSoundFile ?? "" + buttonIcon: "folder-open" + buttonTooltip: I18n.tr("panels.notifications.sounds-files-select-file") + onInputTextChanged: text => Settings.data.notifications.sounds.criticalSoundFile = text + onButtonClicked: root.openCriticalPicker() + } + } + } + + // Excluded Apps List + ColumnLayout { + enabled: SoundService.multimediaAvailable && (Settings.data.notifications?.sounds?.enabled ?? false) + spacing: Style.marginXXS + Layout.fillWidth: true + + NLabel { + label: I18n.tr("panels.notifications.sounds-excluded-apps-label") + description: I18n.tr("panels.notifications.sounds-excluded-apps-description") + } + + NTextInput { + enabled: parent.enabled + Layout.fillWidth: true + placeholderText: I18n.tr("panels.notifications.sounds-excluded-apps-placeholder") + text: Settings.data.notifications?.sounds?.excludedApps ?? "" + onTextChanged: Settings.data.notifications.sounds.excludedApps = text + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/ToastSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/ToastSubTab.qml new file mode 100644 index 0000000..f6aa029 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Notifications/ToastSubTab.qml @@ -0,0 +1,35 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + NCheckbox { + Layout.fillWidth: true + label: I18n.tr("panels.notifications.toast-media-label") + description: I18n.tr("panels.notifications.toast-media-description") + checked: Settings.data.notifications.enableMediaToast + onToggled: checked => Settings.data.notifications.enableMediaToast = checked + } + + NCheckbox { + Layout.fillWidth: true + label: I18n.tr("panels.notifications.toast-keyboard-label") + description: I18n.tr("panels.notifications.toast-keyboard-description") + checked: Settings.data.notifications.enableKeyboardLayoutToast + onToggled: checked => Settings.data.notifications.enableKeyboardLayoutToast = checked + } + + NCheckbox { + Layout.fillWidth: true + label: I18n.tr("panels.notifications.toast-battery-label") + description: I18n.tr("panels.notifications.toast-battery-description") + checked: Settings.data.notifications.enableBatteryToast + onToggled: checked => Settings.data.notifications.enableBatteryToast = checked + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Osd/EventsSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Osd/EventsSubTab.qml new file mode 100644 index 0000000..c780d7d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Osd/EventsSubTab.qml @@ -0,0 +1,50 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Modules.OSD +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + property var addType + property var removeType + + Repeater { + model: [ + { + type: OSD.Type.Volume, + key: "types-volume" + }, + { + type: OSD.Type.InputVolume, + key: "types-input-volume" + }, + { + type: OSD.Type.Brightness, + key: "types-brightness" + }, + { + type: OSD.Type.LockKey, + key: "types-lockkey" + } + ] + delegate: NCheckbox { + required property var modelData + Layout.fillWidth: true + label: I18n.tr("panels.osd." + modelData.key + "-label") + description: I18n.tr("panels.osd." + modelData.key + "-description") + checked: (Settings.data.osd.enabledTypes || []).includes(modelData.type) + onToggled: checked => { + if (checked) { + Settings.data.osd.enabledTypes = root.addType(Settings.data.osd.enabledTypes, modelData.type); + } else { + Settings.data.osd.enabledTypes = root.removeType(Settings.data.osd.enabledTypes, modelData.type); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Osd/GeneralSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Osd/GeneralSubTab.qml new file mode 100644 index 0000000..3b9ff78 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Osd/GeneralSubTab.qml @@ -0,0 +1,140 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Compositor +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + property var addMonitor + property var removeMonitor + + NComboBox { + label: I18n.tr("common.position") + description: I18n.tr("panels.osd.location-description") + model: [ + { + "key": "top", + "name": I18n.tr("positions.top-center") + }, + { + "key": "top_left", + "name": I18n.tr("positions.top-left") + }, + { + "key": "top_right", + "name": I18n.tr("positions.top-right") + }, + { + "key": "bottom", + "name": I18n.tr("positions.bottom-center") + }, + { + "key": "bottom_left", + "name": I18n.tr("positions.bottom-left") + }, + { + "key": "bottom_right", + "name": I18n.tr("positions.bottom-right") + }, + { + "key": "left", + "name": I18n.tr("positions.center-left") + }, + { + "key": "right", + "name": I18n.tr("positions.center-right") + } + ] + currentKey: Settings.data.osd.location || "top_right" + defaultValue: Settings.getDefaultValue("osd.location") + onSelected: key => Settings.data.osd.location = key + } + + NToggle { + label: I18n.tr("panels.osd.enabled-label") + description: I18n.tr("panels.osd.enabled-description") + checked: Settings.data.osd.enabled + defaultValue: Settings.getDefaultValue("osd.enabled") + onToggled: checked => Settings.data.osd.enabled = checked + } + + NToggle { + label: I18n.tr("panels.osd.always-on-top-label") + description: I18n.tr("panels.osd.always-on-top-description") + checked: Settings.data.osd.overlayLayer + defaultValue: Settings.getDefaultValue("osd.overlayLayer") + onToggled: checked => Settings.data.osd.overlayLayer = checked + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.osd.background-opacity-label") + description: I18n.tr("panels.osd.background-opacity-description") + from: 0 + to: 100 + stepSize: 1 + showReset: true + value: Settings.data.osd.backgroundOpacity * 100 + defaultValue: (Settings.getDefaultValue("osd.backgroundOpacity") || 1) * 100 + onMoved: value => Settings.data.osd.backgroundOpacity = value / 100 + text: Math.round(Settings.data.osd.backgroundOpacity * 100) + "%" + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.osd.duration-auto-hide-label") + description: I18n.tr("panels.osd.duration-auto-hide-description") + from: 500 + to: 5000 + stepSize: 100 + showReset: true + value: Settings.data.osd.autoHideMs + defaultValue: Settings.getDefaultValue("osd.autoHideMs") + onMoved: value => Settings.data.osd.autoHideMs = value + text: Math.round(Settings.data.osd.autoHideMs / 1000 * 10) / 10 + "s" + } + + NDivider { + Layout.fillWidth: true + } + + NText { + text: I18n.tr("panels.osd.monitors-desc") + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + Repeater { + model: Quickshell.screens || [] + delegate: NCheckbox { + Layout.fillWidth: true + readonly property real compositorScale: { + const info = CompositorService.displayScales[modelData.name]; + return (info && info.scale) ? info.scale : 1.0; + } + label: modelData.name || I18n.tr("common.unknown") + description: { + I18n.tr("system.monitor-description", { + "model": modelData.model, + "width": modelData.width * compositorScale, + "height": modelData.height * compositorScale, + "scale": compositorScale + }); + } + checked: (Settings.data.osd.monitors || []).indexOf(modelData.name) !== -1 + onToggled: checked => { + if (checked) { + Settings.data.osd.monitors = root.addMonitor(Settings.data.osd.monitors, modelData.name); + } else { + Settings.data.osd.monitors = root.removeMonitor(Settings.data.osd.monitors, modelData.name); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Osd/OsdTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Osd/OsdTab.qml new file mode 100644 index 0000000..6e5bc61 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Osd/OsdTab.qml @@ -0,0 +1,72 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + // Helper functions to update arrays immutably + function addMonitor(list, name) { + const arr = (list || []).slice(); + if (!arr.includes(name)) + arr.push(name); + return arr; + } + function removeMonitor(list, name) { + return (list || []).filter(function (n) { + return n !== name; + }); + } + function addType(list, type) { + const arr = (list || []).slice(); + if (!arr.includes(type)) + arr.push(type); + return arr; + } + function removeType(list, type) { + return (list || []).filter(function (t) { + return t !== type; + }); + } + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.general") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.events") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + GeneralSubTab { + addMonitor: root.addMonitor + removeMonitor: root.removeMonitor + } + EventsSubTab { + addType: root.addType + removeType: root.removeType + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Plugins/AvailableSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Plugins/AvailableSubTab.qml new file mode 100644 index 0000000..de00411 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Plugins/AvailableSubTab.qml @@ -0,0 +1,393 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Services.Noctalia +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + property string pluginSearchText: "" + property string selectedTag: "" + property int tagsRefreshCounter: 0 + property int availablePluginsRefreshCounter: 0 + + // Pseudo tags for filtering + readonly property var pseudoTags: ["official", "downloaded", "notDownloaded"] + + readonly property var availableTags: { + // Reference counter to force re-evaluation + void (root.tagsRefreshCounter); + var tags = {}; + var plugins = PluginService.availablePlugins || []; + for (var i = 0; i < plugins.length; i++) { + var pluginTags = plugins[i].tags || []; + for (var j = 0; j < pluginTags.length; j++) { + tags[pluginTags[j]] = true; + } + } + return Object.keys(tags).sort(); + } + + function stripAuthorEmail(author) { + if (!author) + return ""; + var lastBracket = author.lastIndexOf("<"); + if (lastBracket >= 0) { + return author.substring(0, lastBracket).trim(); + } + return author; + } + + // Tag filter chips in collapsible + NTagFilter { + tags: root.pseudoTags.concat(root.availableTags) + selectedTag: root.selectedTag + onSelectedTagChanged: root.selectedTag = selectedTag + label: I18n.tr("panels.plugins.filter-tags-label") + description: I18n.tr("panels.plugins.filter-tags-description") + expanded: true + + formatTag: function (tag) { + if (tag === "") + return I18n.tr("launcher.categories.all"); + if (tag === "official") + return I18n.tr("common.official"); + if (tag === "downloaded") + return I18n.tr("panels.plugins.filter-downloaded"); + if (tag === "notDownloaded") + return I18n.tr("panels.plugins.filter-not-downloaded"); + return tag; + } + } + + // Search input with refresh button + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NTextInput { + placeholderText: I18n.tr("placeholders.search") + inputIconName: "search" + text: root.pluginSearchText + onTextChanged: root.pluginSearchText = text + Layout.fillWidth: true + } + + NIconButton { + icon: "refresh" + tooltipText: I18n.tr("panels.plugins.refresh-tooltip") + baseSize: Style.baseWidgetSize * 0.9 + onClicked: { + PluginService.refreshAvailablePlugins(); + checkUpdatesTimer.restart(); + ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.refresh-refreshing")); + } + } + } + + // Available plugins list + ColumnLayout { + spacing: Style.marginM + Layout.fillWidth: true + + Repeater { + id: availablePluginsRepeater + + model: { + // Reference counter to force re-evaluation when plugins are updated + void (root.availablePluginsRefreshCounter); + + var all = PluginService.availablePlugins || []; + var filtered = []; + + // Apply filter based on selectedTag + for (var i = 0; i < all.length; i++) { + var plugin = all[i]; + var downloaded = plugin.downloaded || false; + var pluginTags = plugin.tags || []; + + if (root.selectedTag === "") { + // "All" - no filter + filtered.push(plugin); + } else if (root.selectedTag === "official") { + // Official (team-maintained) pseudo tag + if (plugin.official === true) + filtered.push(plugin); + } else if (root.selectedTag === "downloaded") { + // Downloaded pseudo tag + if (downloaded) + filtered.push(plugin); + } else if (root.selectedTag === "notDownloaded") { + // Not Downloaded pseudo tag + if (!downloaded) + filtered.push(plugin); + } else { + // Actual category tag + if (pluginTags.indexOf(root.selectedTag) >= 0) { + filtered.push(plugin); + } + } + } + + // Then apply fuzzy search if there's search text + var query = root.pluginSearchText.trim(); + if (query !== "") { + var results = FuzzySort.go(query, filtered, { + "keys": ["name", "description"], + "limit": 50 + }); + filtered = []; + for (var j = 0; j < results.length; j++) { + filtered.push(results[j].obj); + } + } else { + // Sort by lastUpdated (most recent first) when not searching + filtered.sort(function (a, b) { + var dateA = a.lastUpdated ? new Date(a.lastUpdated).getTime() : 0; + var dateB = b.lastUpdated ? new Date(b.lastUpdated).getTime() : 0; + return dateB - dateA; + }); + } + + // Move hello-world plugin to the end + var helloWorldIndex = -1; + for (var h = 0; h < filtered.length; h++) { + if (filtered[h].id === "hello-world") { + helloWorldIndex = h; + break; + } + } + if (helloWorldIndex >= 0) { + var helloWorld = filtered.splice(helloWorldIndex, 1)[0]; + filtered.push(helloWorld); + } + + return filtered; + } + + delegate: NBox { + id: pluginBox + + Layout.fillWidth: true + Layout.leftMargin: Style.borderS + Layout.rightMargin: Style.borderS + implicitHeight: Math.round(contentColumn.implicitHeight + Style.margin2L) + color: Color.mSurface + + ColumnLayout { + id: contentColumn + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginS + + RowLayout { + spacing: Style.marginM + Layout.fillWidth: true + + NIcon { + icon: "plugin" + pointSize: Style.fontSizeL + color: Color.mPrimary + } + + NText { + text: modelData.name + color: Color.mPrimary + elide: Text.ElideRight + } + + // Official badge (Noctalia Team maintained) + Rectangle { + visible: modelData.official === true + color: Color.mSecondary + radius: Style.radiusXS + implicitWidth: officialBadgeRow.implicitWidth + Style.margin2S + implicitHeight: officialBadgeRow.implicitHeight + Style.margin2XS + + RowLayout { + id: officialBadgeRow + anchors.centerIn: parent + spacing: Style.marginXS + + NIcon { + icon: "official-plugin" + pointSize: Style.fontSizeXXS + color: Color.mOnSecondary + } + + NText { + text: I18n.tr("common.official") + font.pointSize: Style.fontSizeXXS + font.weight: Style.fontWeightMedium + color: Color.mOnSecondary + } + } + } + + // Spacer + Item { + Layout.fillWidth: true + } + + // Open plugin page button + NIconButton { + icon: "external-link" + baseSize: Style.baseWidgetSize * 0.7 + tooltipText: I18n.tr("panels.plugins.open-plugin-page") + onClicked: { + var sourceUrl = modelData.source?.url || ""; + Qt.openUrlExternally(sourceUrl && !PluginRegistry.isMainSource(sourceUrl) ? sourceUrl : "https://noctalia.dev/plugins/" + modelData.id + "/"); + } + } + + // Downloaded indicator + NIcon { + icon: "circle-check" + pointSize: Style.baseWidgetSize * 0.5 + color: Color.mPrimary + visible: modelData.downloaded === true + } + + // Install button (only shown when not downloaded and not installing) + NIconButton { + visible: modelData.downloaded === false && !PluginService.installingPlugins[modelData.id] + icon: "download" + baseSize: Style.baseWidgetSize * 0.7 + tooltipText: I18n.tr("common.install") + onClicked: installPlugin(modelData) + } + + // Installing spinner + NBusyIndicator { + visible: !modelData.downloaded && (PluginService.installingPlugins[modelData.id] === true) + size: Style.baseWidgetSize * 0.5 + running: visible + } + } + + // Description + NText { + visible: modelData.description + text: modelData.description || "" + font.pointSize: Style.fontSizeXS + color: Color.mOnSurface + wrapMode: Text.WordWrap + maximumLineCount: 2 + elide: Text.ElideRight + Layout.fillWidth: true + } + + // Details row + RowLayout { + spacing: Style.marginS + Layout.fillWidth: true + + NText { + text: "v" + modelData.version + font.pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + NText { + text: "•" + font.pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + NText { + text: stripAuthorEmail(modelData.author) + font.pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + NText { + text: "•" + font.pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + NText { + text: modelData.source ? modelData.source.name : "" + font.pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + NText { + visible: !!modelData.lastUpdated + text: "•" + font.pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + NText { + visible: !!modelData.lastUpdated + text: modelData.lastUpdated ? Time.formatRelativeTime(new Date(modelData.lastUpdated)) : "" + font.pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + } + } + } + } + + NLabel { + visible: availablePluginsRepeater.count === 0 + label: I18n.tr("panels.plugins.available-no-plugins-label") + description: I18n.tr("panels.plugins.available-no-plugins-description") + Layout.fillWidth: true + } + } + + // Timer to check for updates after refresh starts + Timer { + id: checkUpdatesTimer + interval: 100 + onTriggered: { + PluginService.checkForUpdates(); + } + } + + function installPlugin(pluginMetadata) { + ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.installing", { + "plugin": pluginMetadata.name + })); + + PluginService.installPlugin(pluginMetadata, false, function (success, error, registeredKey) { + if (success) { + ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.install-success", { + "plugin": pluginMetadata.name + })); + // Auto-enable the plugin after installation (use registered key which may be composite) + PluginService.enablePlugin(registeredKey); + } else { + ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.install-error", { + "error": error || "Unknown error" + })); + } + }); + } + + // Listen to plugin service signals + Connections { + target: PluginService + + function onAvailablePluginsUpdated() { + // Force tags and plugins model to re-evaluate + root.tagsRefreshCounter++; + root.availablePluginsRefreshCounter++; + + // Manually trigger update check after a small delay to ensure all registries are loaded + Qt.callLater(function () { + PluginService.checkForUpdates(); + }); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Plugins/InstalledSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Plugins/InstalledSubTab.qml new file mode 100644 index 0000000..32de24f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Plugins/InstalledSubTab.qml @@ -0,0 +1,536 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.Noctalia +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + // Track which plugins are currently updating + property var updatingPlugins: ({}) + property int installedPluginsRefreshCounter: 0 + + function stripAuthorEmail(author) { + if (!author) + return ""; + var lastBracket = author.lastIndexOf("<"); + if (lastBracket >= 0) { + return author.substring(0, lastBracket).trim(); + } + return author; + } + + // Check for updates when tab becomes visible + onVisibleChanged: { + if (visible && PluginService.pluginsFullyLoaded) { + PluginService.checkForUpdates(); + } + } + + // Auto-update toggle + NToggle { + label: I18n.tr("panels.plugins.auto-update") + description: I18n.tr("panels.plugins.auto-update-description") + checked: Settings.data.plugins.autoUpdate + onToggled: checked => Settings.data.plugins.autoUpdate = checked + } + + // Update notification toggle + NToggle { + label: I18n.tr("panels.plugins.notify-updates") + description: I18n.tr("panels.plugins.notify-updates-description") + checked: Settings.data.plugins.notifyUpdates + onToggled: checked => Settings.data.plugins.notifyUpdates = checked + } + + // Check for updates button + NButton { + property bool isChecking: Object.keys(PluginService.activeFetches).length > 0 + + text: isChecking ? I18n.tr("panels.plugins.checking-for-updates") : I18n.tr("panels.plugins.check-for-updates") + icon: "refresh" + enabled: !isChecking + visible: Object.keys(PluginService.pluginUpdates).length === 0 + Layout.fillWidth: true + onClicked: PluginService.checkForUpdates() + } + + // Update All button + NButton { + property int updateCount: Object.keys(PluginService.pluginUpdates).length + property bool isUpdating: false + + text: I18n.tr("panels.plugins.update-all", { + "count": updateCount + }) + icon: "download" + visible: (updateCount > 0) + enabled: !isUpdating + backgroundColor: Color.mPrimary + textColor: Color.mOnPrimary + Layout.fillWidth: true + onClicked: { + isUpdating = true; + var pluginIds = Object.keys(PluginService.pluginUpdates); + var currentIndex = 0; + + function updateNext() { + if (currentIndex >= pluginIds.length) { + isUpdating = false; + ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.update-all-success")); + return; + } + + var pluginId = pluginIds[currentIndex]; + currentIndex++; + + PluginService.updatePlugin(pluginId, function (success, error) { + if (!success) { + Logger.w("InstalledSubTab", "Failed to update", pluginId + ":", error); + } + Qt.callLater(updateNext); + }); + } + + updateNext(); + } + } + + // Installed plugins list + ColumnLayout { + spacing: Style.marginM + Layout.fillWidth: true + + Repeater { + id: installedPluginsRepeater + + model: { + // Force refresh when counter changes + var _ = root.installedPluginsRefreshCounter; + + var allIds = PluginRegistry.getAllInstalledPluginIds(); + var plugins = []; + for (var i = 0; i < allIds.length; i++) { + var compositeKey = allIds[i]; + var manifest = PluginRegistry.getPluginManifest(compositeKey); + if (manifest) { + // Create a copy of manifest and include update info, enabled state, and source info + var pluginData = JSON.parse(JSON.stringify(manifest)); + pluginData.compositeKey = compositeKey; + pluginData.updateInfo = PluginService.pluginUpdates[compositeKey]; + pluginData.pendingUpdateInfo = PluginService.pluginUpdatesPending[compositeKey]; + pluginData.enabled = PluginRegistry.isPluginEnabled(compositeKey); + + // Add source info + var parsed = PluginRegistry.parseCompositeKey(compositeKey); + pluginData.isFromOfficialRepo = parsed.isOfficial; + if (!parsed.isOfficial) { + pluginData.sourceName = PluginRegistry.getSourceNameByHash(parsed.sourceHash); + } + + // Look up "official" (team-maintained) status and lastUpdated from available plugins + pluginData.official = false; + pluginData.lastUpdated = null; + var availablePlugins = PluginService.availablePlugins || []; + for (var j = 0; j < availablePlugins.length; j++) { + if (availablePlugins[j].id === manifest.id) { + pluginData.official = availablePlugins[j].official === true; + pluginData.lastUpdated = availablePlugins[j].lastUpdated || null; + break; + } + } + + plugins.push(pluginData); + } + } + return plugins; + } + + delegate: NBox { + Layout.fillWidth: true + Layout.leftMargin: Style.borderS + Layout.rightMargin: Style.borderS + implicitHeight: Math.round(contentColumn.implicitHeight + Style.margin2L) + color: Color.mSurface + + ColumnLayout { + id: contentColumn + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginS + + // Top row: icon, name, badge, spacer, action buttons + RowLayout { + spacing: Style.marginM + Layout.fillWidth: true + + NIcon { + icon: "plugin" + pointSize: Style.fontSizeL + color: PluginService.hasPluginError(modelData.compositeKey) ? Color.mError : Color.mPrimary + } + + NText { + text: modelData.name + color: Color.mPrimary + elide: Text.ElideRight + } + + // Official badge (Noctalia Team maintained) + Rectangle { + visible: modelData.official === true + color: Color.mSecondary + radius: Style.radiusXS + implicitWidth: officialBadgeRow.implicitWidth + Style.margin2S + implicitHeight: officialBadgeRow.implicitHeight + Style.margin2XS + + RowLayout { + id: officialBadgeRow + anchors.centerIn: parent + spacing: Style.marginXS + + NIcon { + icon: "official-plugin" + pointSize: Style.fontSizeXXS + color: Color.mOnSecondary + } + + NText { + text: I18n.tr("common.official") + font.pointSize: Style.fontSizeXXS + font.weight: Style.fontWeightMedium + color: Color.mOnSecondary + } + } + } + + // Spacer + Item { + Layout.fillWidth: true + } + + NIconButtonHot { + icon: "bug" + hot: PluginService.isPluginHotReloadEnabled(modelData.id) + tooltipText: PluginService.isPluginHotReloadEnabled(modelData.id) ? I18n.tr("panels.plugins.development-disable") : I18n.tr("panels.plugins.development-enable") + baseSize: Style.baseWidgetSize * 0.7 + onClicked: PluginService.togglePluginHotReload(modelData.id) + visible: Settings.isDebug + } + + NIconButton { + icon: "settings" + tooltipText: I18n.tr("panels.plugins.settings-tooltip") + baseSize: Style.baseWidgetSize * 0.7 + visible: (modelData.entryPoints?.settings !== undefined) + enabled: modelData.enabled + onClicked: { + pluginSettingsDialog.openPluginSettings(modelData); + } + } + + NIconButton { + icon: "external-link" + tooltipText: I18n.tr("panels.plugins.open-plugin-page") + baseSize: Style.baseWidgetSize * 0.7 + visible: true + onClicked: { + var sourceUrl = PluginRegistry.getPluginSourceUrl(modelData.compositeKey) || ""; + Qt.openUrlExternally(sourceUrl && !PluginRegistry.isMainSource(sourceUrl) ? sourceUrl : "https://noctalia.dev/plugins/" + modelData.id); + } + } + + NIconButton { + icon: "trash" + tooltipText: I18n.tr("common.uninstall") + baseSize: Style.baseWidgetSize * 0.7 + onClicked: { + uninstallDialog.pluginToUninstall = modelData; + uninstallDialog.open(); + } + } + + NButton { + id: updateButton + property string pluginId: modelData.compositeKey + property bool isUpdating: root.updatingPlugins[pluginId] === true + + text: isUpdating ? I18n.tr("panels.plugins.updating") : I18n.tr("common.update") + icon: isUpdating ? "" : "download" + visible: modelData.updateInfo !== undefined + enabled: !isUpdating + backgroundColor: Color.mPrimary + textColor: Color.mOnPrimary + fontSize: Style.fontSizeXXS + fontWeight: Style.fontWeightMedium + onClicked: { + var pid = pluginId; + var pname = modelData.name; + var pversion = modelData.updateInfo?.availableVersion || ""; + var rootRef = root; + var updates = Object.assign({}, rootRef.updatingPlugins); + updates[pid] = true; + rootRef.updatingPlugins = updates; + + PluginService.updatePlugin(pid, function (success, error) { + var updates2 = Object.assign({}, rootRef.updatingPlugins); + updates2[pid] = false; + rootRef.updatingPlugins = updates2; + + if (success) { + ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.update-success", { + "plugin": pname, + "version": pversion + })); + } else { + ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.update-error", { + "plugin": pname, + "error": error || "Unknown error" + })); + } + }); + } + } + + NToggle { + checked: modelData.enabled + baseSize: Style.baseWidgetSize * 0.7 + onToggled: checked => { + if (checked) { + PluginService.enablePlugin(modelData.compositeKey); + } else { + PluginService.disablePlugin(modelData.compositeKey); + } + } + } + } + + // Description + NText { + visible: modelData.description + text: modelData.description || "" + font.pointSize: Style.fontSizeXS + color: Color.mOnSurface + wrapMode: Text.WordWrap + maximumLineCount: 2 + elide: Text.ElideRight + Layout.fillWidth: true + } + + // Details row + RowLayout { + spacing: Style.marginS + Layout.fillWidth: true + + NText { + text: { + if (modelData.updateInfo) { + return I18n.tr("panels.plugins.update-version", { + "current": modelData.version, + "new": modelData.updateInfo.availableVersion + }); + } else if (modelData.pendingUpdateInfo) { + return I18n.tr("panels.plugins.update-pending", { + "current": modelData.version, + "new": modelData.pendingUpdateInfo.availableVersion, + "required": modelData.pendingUpdateInfo.minNoctaliaVersion + }); + } + return "v" + modelData.version; + } + font.pointSize: Style.fontSizeXS + color: modelData.updateInfo ? Color.mPrimary : (modelData.pendingUpdateInfo ? Color.mTertiary : Color.mOnSurfaceVariant) + font.weight: (modelData.updateInfo || modelData.pendingUpdateInfo) ? Style.fontWeightMedium : Style.fontWeightRegular + } + + NText { + text: "•" + font.pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + NText { + text: stripAuthorEmail(modelData.author) + font.pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + // Source indicator for plugins from non-official repos + NText { + visible: !modelData.isFromOfficialRepo + text: "•" + font.pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + NText { + visible: !modelData.isFromOfficialRepo + text: modelData.sourceName || I18n.tr("panels.plugins.source-custom") + font.pointSize: Style.fontSizeXS + color: Color.mTertiary + } + + NText { + visible: !!modelData.lastUpdated + text: "•" + font.pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + NText { + visible: !!modelData.lastUpdated + text: modelData.lastUpdated ? Time.formatRelativeTime(new Date(modelData.lastUpdated)) : "" + font.pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + } + + // Error indicator + RowLayout { + spacing: Style.marginS + visible: PluginService.hasPluginError(modelData.compositeKey) + + NIcon { + icon: "alert-triangle" + pointSize: Style.fontSizeS + color: Color.mError + } + + NText { + property var errorInfo: PluginService.getPluginError(modelData.compositeKey) + text: errorInfo ? errorInfo.error : "" + font.pointSize: Style.fontSizeXXS + color: Color.mError + wrapMode: Text.WordWrap + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 3 + } + } + } + } + } + + NLabel { + visible: PluginRegistry.getAllInstalledPluginIds().length === 0 + label: I18n.tr("panels.plugins.installed-no-plugins-label") + description: I18n.tr("panels.plugins.installed-no-plugins-description") + Layout.fillWidth: true + } + } + + // Uninstall confirmation dialog + Popup { + id: uninstallDialog + parent: Overlay.overlay + modal: true + dim: false + anchors.centerIn: parent + width: 400 * Style.uiScaleRatio + padding: Style.marginL + + property var pluginToUninstall: null + + background: Rectangle { + color: Color.mSurface + radius: Style.radiusS + border.color: Color.mPrimary + border.width: Style.borderM + } + + contentItem: ColumnLayout { + width: parent.width + spacing: Style.marginL + + NHeader { + label: I18n.tr("panels.plugins.uninstall-dialog-title") + description: I18n.tr("panels.plugins.uninstall-dialog-description", { + "plugin": uninstallDialog.pluginToUninstall?.name || "" + }) + } + + RowLayout { + spacing: Style.marginM + Layout.fillWidth: true + + Item { + Layout.fillWidth: true + } + + NButton { + text: I18n.tr("common.cancel") + onClicked: uninstallDialog.close() + } + + NButton { + text: I18n.tr("common.uninstall") + backgroundColor: Color.mPrimary + textColor: Color.mOnPrimary + onClicked: { + if (uninstallDialog.pluginToUninstall) { + root.uninstallPlugin(uninstallDialog.pluginToUninstall.compositeKey); + uninstallDialog.close(); + } + } + } + } + } + } + + // Plugin settings popup + NPluginSettingsPopup { + id: pluginSettingsDialog + parent: Overlay.overlay + showToastOnSave: true + } + + function uninstallPlugin(pluginId) { + var manifest = PluginRegistry.getPluginManifest(pluginId); + var pluginName = manifest?.name || pluginId; + + BarService.widgetsRevision++; + + ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.uninstalling", { + "plugin": pluginName + })); + + PluginService.uninstallPlugin(pluginId, function (success, error) { + if (success) { + ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.uninstall-success", { + "plugin": pluginName + })); + } else { + ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.uninstall-error", { + "error": error || "Unknown error" + })); + } + }); + } + + // Listen to plugin registry changes + Connections { + target: PluginRegistry + + function onPluginsChanged() { + root.installedPluginsRefreshCounter++; + } + } + + // Listen to plugin service signals + Connections { + target: PluginService + + function onPluginUpdatesChanged() { + root.installedPluginsRefreshCounter++; + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Plugins/PluginsTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Plugins/PluginsTab.qml new file mode 100644 index 0000000..aaddc90 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Plugins/PluginsTab.qml @@ -0,0 +1,48 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.installed") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.available") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + NTabButton { + text: I18n.tr("common.sources") + tabIndex: 2 + checked: subTabBar.currentIndex === 2 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + InstalledSubTab {} + AvailableSubTab {} + SourcesSubTab {} + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Plugins/SourcesSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Plugins/SourcesSubTab.qml new file mode 100644 index 0000000..5adeba9 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Plugins/SourcesSubTab.qml @@ -0,0 +1,181 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.Noctalia +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + // List of plugin sources + ColumnLayout { + spacing: Style.marginM + Layout.fillWidth: true + + Repeater { + id: pluginSourcesRepeater + model: PluginRegistry.pluginSources || [] + + delegate: NBox { + Layout.fillWidth: true + implicitHeight: sourceRow.implicitHeight + Style.margin2L + color: Color.mSurface + + RowLayout { + id: sourceRow + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + NIcon { + icon: "brand-github" + pointSize: Style.fontSizeL + } + + ColumnLayout { + spacing: 2 + Layout.fillWidth: true + + NText { + text: modelData.name + color: Color.mOnSurface + Layout.fillWidth: true + } + + NText { + text: modelData.url + font.pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + Layout.fillWidth: true + elide: Text.ElideRight + } + } + + NIconButton { + icon: "trash" + tooltipText: I18n.tr("panels.plugins.sources-remove-tooltip") + visible: index !== 0 // Cannot remove official source + baseSize: Style.baseWidgetSize * 0.7 + onClicked: { + PluginRegistry.removePluginSource(modelData.url); + } + } + + // Enable/Disable a source + NToggle { + checked: modelData.enabled !== false // Default to true if not set + baseSize: Style.baseWidgetSize * 0.7 + onToggled: checked => { + PluginRegistry.setSourceEnabled(modelData.url, checked); + PluginService.refreshAvailablePlugins(); + ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.refresh-refreshing")); + } + } + } + } + } + } + + // Add custom repository + NButton { + text: I18n.tr("panels.plugins.sources-add-custom") + icon: "plus" + onClicked: { + addSourceDialog.open(); + } + Layout.fillWidth: true + } + + // Add source dialog + Popup { + id: addSourceDialog + parent: Overlay.overlay + modal: true + dim: false + anchors.centerIn: parent + width: 500 + padding: Style.marginL + + background: Rectangle { + color: Color.mSurface + radius: Style.radiusS + border.color: Color.mPrimary + border.width: Style.borderM + } + + contentItem: ColumnLayout { + width: parent.width + spacing: Style.marginL + + NHeader { + label: I18n.tr("panels.plugins.sources-add-dialog-title") + description: I18n.tr("panels.plugins.sources-add-dialog-description") + } + + NTextInput { + id: sourceNameInput + label: I18n.tr("panels.plugins.sources-add-dialog-name") + placeholderText: I18n.tr("panels.plugins.sources-add-dialog-name-placeholder") + Layout.fillWidth: true + } + + NTextInput { + id: sourceUrlInput + label: I18n.tr("panels.plugins.sources-add-dialog-url") + placeholderText: "https://github.com/user/repo" + Layout.fillWidth: true + } + + RowLayout { + spacing: Style.marginM + Layout.fillWidth: true + + Item { + Layout.fillWidth: true + } + + NButton { + text: I18n.tr("common.cancel") + onClicked: addSourceDialog.close() + } + + NButton { + text: I18n.tr("common.add") + backgroundColor: Color.mPrimary + textColor: Color.mOnPrimary + enabled: sourceNameInput.text.length > 0 && sourceUrlInput.text.length > 0 + onClicked: { + if (PluginRegistry.addPluginSource(sourceNameInput.text, sourceUrlInput.text)) { + ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.sources-add-dialog-success")); + PluginService.refreshAvailablePlugins(); + addSourceDialog.close(); + sourceNameInput.text = ""; + sourceUrlInput.text = ""; + } else { + ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.sources-add-dialog-error")); + } + } + } + } + } + } + + // Listen to plugin registry changes + Connections { + target: PluginRegistry + + function onPluginsChanged() { + // Force model refresh for plugin sources + pluginSourcesRepeater.model = undefined; + Qt.callLater(function () { + pluginSourcesRepeater.model = Qt.binding(function () { + return PluginRegistry.pluginSources || []; + }); + }); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Region/ClockPanelSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Region/ClockPanelSubTab.qml new file mode 100644 index 0000000..4b7e943 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Region/ClockPanelSubTab.qml @@ -0,0 +1,164 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + property list cardsModel: [] + property list cardsDefault: [ + { + "id": "calendar-header-card", + "text": I18n.tr("panels.location.calendar-header-label"), + "enabled": true, + "required": true + }, + { + "id": "calendar-month-card", + "text": I18n.tr("panels.location.calendar-month-label"), + "enabled": true, + "required": false + }, + { + "id": "weather-card", + "text": I18n.tr("common.weather"), + "enabled": true, + "required": false + } + ] + + function saveCards() { + var toSave = []; + for (var i = 0; i < cardsModel.length; i++) { + toSave.push({ + "id": cardsModel[i].id, + "enabled": cardsModel[i].enabled + }); + } + Settings.data.calendar.cards = toSave; + } + + Component.onCompleted: { + // Starts empty + cardsModel = []; + + // Add the cards available in settings + for (var i = 0; i < Settings.data.calendar.cards.length; i++) { + const settingCard = Settings.data.calendar.cards[i]; + + for (var j = 0; j < cardsDefault.length; j++) { + if (settingCard.id === cardsDefault[j].id) { + var card = cardsDefault[j]; + card.enabled = settingCard.enabled; + // Auto-disable weather card if weather is disabled + if (card.id === "weather-card" && !Settings.data.location.weatherEnabled) { + card.enabled = false; + } + cardsModel.push(card); + } + } + } + + // Add any missing cards from default + for (var i = 0; i < cardsDefault.length; i++) { + var found = false; + for (var j = 0; j < cardsModel.length; j++) { + if (cardsModel[j].id === cardsDefault[i].id) { + found = true; + break; + } + } + + if (!found) { + var card = cardsDefault[i]; + // Auto-disable weather card if weather is disabled + if (card.id === "weather-card" && !Settings.data.location.weatherEnabled) { + card.enabled = false; + } + cardsModel.push(card); + } + } + + saveCards(); + } + + // Clock style section + ColumnLayout { + spacing: Style.marginM + Layout.fillWidth: true + + NToggle { + label: I18n.tr("panels.location.date-time-use-analog-label") + description: I18n.tr("panels.location.date-time-use-analog-description") + checked: Settings.data.location.analogClockInCalendar + onToggled: checked => Settings.data.location.analogClockInCalendar = checked + } + + NToggle { + label: I18n.tr("panels.location.date-time-week-numbers-label") + description: I18n.tr("panels.location.date-time-week-numbers-description") + checked: Settings.data.location.showWeekNumberInCalendar + onToggled: checked => Settings.data.location.showWeekNumberInCalendar = checked + } + + NToggle { + label: I18n.tr("panels.location.date-time-show-events-label") + description: I18n.tr("panels.location.date-time-show-events-description") + checked: Settings.data.location.showCalendarEvents + onToggled: checked => Settings.data.location.showCalendarEvents = checked + } + } + + NDivider { + Layout.fillWidth: true + } + + // Calendar Cards Management Section + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + + Connections { + target: Settings.data.location + function onWeatherEnabledChanged() { + // Auto-disable weather card when weather is disabled + var newModel = cardsModel.slice(); + for (var i = 0; i < newModel.length; i++) { + if (newModel[i].id === "weather-card") { + newModel[i] = Object.assign({}, newModel[i], { + "enabled": Settings.data.location.weatherEnabled + }); + cardsModel = newModel; + saveCards(); + break; + } + } + } + } + + NReorderCheckboxes { + Layout.fillWidth: true + model: cardsModel + disabledIds: Settings.data.location.weatherEnabled ? [] : ["weather-card"] + onItemToggled: function (index, enabled) { + var newModel = cardsModel.slice(); + newModel[index] = Object.assign({}, newModel[index], { + "enabled": enabled + }); + cardsModel = newModel; + saveCards(); + } + onItemsReordered: function (fromIndex, toIndex) { + var newModel = cardsModel.slice(); + var item = newModel.splice(fromIndex, 1)[0]; + newModel.splice(toIndex, 0, item); + cardsModel = newModel; + saveCards(); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Region/DateSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Region/DateSubTab.qml new file mode 100644 index 0000000..82a965b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Region/DateSubTab.qml @@ -0,0 +1,46 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + NToggle { + label: I18n.tr("panels.location.date-time-12hour-format-label") + description: I18n.tr("panels.location.date-time-12hour-format-description") + checked: Settings.data.location.use12hourFormat + onToggled: checked => Settings.data.location.use12hourFormat = checked + } + + NComboBox { + label: I18n.tr("panels.location.date-time-first-day-of-week-label") + description: I18n.tr("panels.location.date-time-first-day-of-week-description") + currentKey: Settings.data.location.firstDayOfWeek.toString() + minimumWidth: 260 * Style.uiScaleRatio + model: [ + { + "key": "-1", + "name": I18n.tr("panels.location.date-time-first-day-of-week-automatic") + }, + { + "key": "6", + "name": I18n.locale.dayName(6, Locale.LongFormat).trim() + } // Saturday + , + { + "key": "0", + "name": I18n.locale.dayName(0, Locale.LongFormat).trim() + } // Sunday + , + { + "key": "1", + "name": I18n.locale.dayName(1, Locale.LongFormat).trim() + } // Monday + ] + onSelected: key => Settings.data.location.firstDayOfWeek = parseInt(key) + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Region/LocationSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Region/LocationSubTab.qml new file mode 100644 index 0000000..7e7c099 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Region/LocationSubTab.qml @@ -0,0 +1,157 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.Location +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + // Language + NComboBox { + Layout.fillWidth: true + label: I18n.tr("panels.general.language-select-label") + description: I18n.tr("panels.general.language-select-description") + defaultValue: Settings.getDefaultValue("general.language") + model: [ + { + "key": "", + "name": I18n.tr("panels.general.language-select-auto-detect") + " (" + I18n.systemDetectedLangCode + ")" + } + ].concat(I18n.availableLanguages.map(function (langCode) { + return { + "key": langCode, + "name": langCode + }; + })) + currentKey: Settings.data.general.language + settingsPath: "general.language" + onSelected: key => { + // Need to change language on next frame using "callLater" or it will pull the rug below our feet: the NComboBox would be rebuilt immediately before it can close properly. + Qt.callLater(() => { + Settings.data.general.language = key; + if (key === "") { + I18n.detectLanguage(); // Re-detect system language if "Automatic" is selected + } else { + I18n.setLanguage(key); // Set specific language + } + }); + } + } + + NDivider { + Layout.fillWidth: true + } + + // Location + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + + // Auto-locate + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.location.auto-locate-label") + description: I18n.tr("panels.location.auto-locate-description") + checked: Settings.data.location.autoLocate + onToggled: checked => Settings.data.location.autoLocate = checked + defaultValue: Settings.getDefaultValue("location.autoLocate") + } + + NButton { + text: I18n.tr("panels.location.geolocate-now-button") + icon: "current-location" + enabled: !LocationService.isFetchingWeather + onClicked: LocationService.geolocateAndApply() + } + } + + NTextInput { + visible: !Settings.data.location.autoLocate + Layout.maximumWidth: root.width / 2 + label: I18n.tr("panels.location.location-search-label") + description: I18n.tr("panels.location.location-search-description") + text: Settings.data.location.name + placeholderText: I18n.tr("panels.location.location-search-placeholder") + onEditingFinished: { + // Verify the location has really changed to avoid extra resets + var newLocation = text.trim(); + if (newLocation != Settings.data.location.name) { + Settings.data.location.name = newLocation; + LocationService.resetWeather(); + } + } + } + + NText { + text: LocationService.coordinatesReady ? I18n.tr("system.location-display", { + "name": LocationService.stableName, + "coordinates": LocationService.displayCoordinates + }) : "" + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + font.italic: true + } + } + + ColumnLayout { + spacing: Style.marginL + Layout.fillWidth: true + + NToggle { + label: I18n.tr("panels.location.weather-enabled-label") + description: I18n.tr("panels.location.weather-enabled-description") + checked: Settings.data.location.weatherEnabled + onToggled: checked => Settings.data.location.weatherEnabled = checked + defaultValue: Settings.getDefaultValue("location.weatherEnabled") + } + + NToggle { + label: I18n.tr("panels.location.weather-fahrenheit-label") + description: I18n.tr("panels.location.weather-fahrenheit-description") + checked: Settings.data.location.useFahrenheit + onToggled: checked => Settings.data.location.useFahrenheit = checked + enabled: Settings.data.location.weatherEnabled + } + + NToggle { + label: I18n.tr("panels.location.weather-show-effects-label") + description: I18n.tr("panels.location.weather-show-effects-description") + checked: Settings.data.location.weatherShowEffects + onToggled: checked => Settings.data.location.weatherShowEffects = checked + enabled: Settings.data.location.weatherEnabled + } + + NToggle { + label: I18n.tr("panels.location.weather-talia-mascot-always-label") + description: I18n.tr("panels.location.weather-talia-mascot-always-description") + checked: Settings.data.location.weatherTaliaMascotAlways + onToggled: checked => Settings.data.location.weatherTaliaMascotAlways = checked + enabled: Settings.data.location.weatherEnabled + defaultValue: Settings.getDefaultValue("location.weatherTaliaMascotAlways") + } + + NToggle { + label: I18n.tr("panels.location.weather-hide-city-label") + description: I18n.tr("panels.location.weather-hide-city-description") + checked: Settings.data.location.hideWeatherCityName + onToggled: checked => Settings.data.location.hideWeatherCityName = checked + enabled: Settings.data.location.weatherEnabled + } + + NToggle { + label: I18n.tr("panels.location.weather-hide-timezone-label") + description: I18n.tr("panels.location.weather-hide-timezone-description") + checked: Settings.data.location.hideWeatherTimezone + onToggled: checked => Settings.data.location.hideWeatherTimezone = checked + enabled: Settings.data.location.weatherEnabled + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Region/RegionTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Region/RegionTab.qml new file mode 100644 index 0000000..b56a9ef --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Region/RegionTab.qml @@ -0,0 +1,48 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.location") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.date") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + NTabButton { + text: I18n.tr("common.calendar-panel") + tabIndex: 2 + checked: subTabBar.currentIndex === 2 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + LocationSubTab {} + DateSubTab {} + ClockPanelSubTab {} + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SessionMenu/ActionsSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SessionMenu/ActionsSubTab.qml new file mode 100644 index 0000000..0318b6b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SessionMenu/ActionsSubTab.qml @@ -0,0 +1,252 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + property var entriesModel: [] + property var updateEntry + property var reorderEntries + property var openEntrySettingsDialog + + // List of items + Item { + Layout.fillWidth: true + implicitHeight: listView.contentHeight + + NListView { + id: listView + anchors.fill: parent + spacing: Style.marginS + interactive: false + reserveScrollbarSpace: false + model: root.entriesModel + + delegate: Item { + id: delegateItem + width: listView.availableWidth + height: contentRow.height + + required property int index + required property var modelData + + property bool dragging: false + property int dragStartY: 0 + property int dragStartIndex: -1 + property int dragTargetIndex: -1 + + Rectangle { + anchors.fill: parent + radius: Style.radiusM + color: delegateItem.dragging ? Color.mSurfaceVariant : "transparent" + border.color: delegateItem.dragging ? Color.mOutline : "transparent" + border.width: Style.borderS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + RowLayout { + id: contentRow + width: parent.width + spacing: Style.marginS + + // Drag handle + Rectangle { + Layout.preferredWidth: Style.baseWidgetSize * 0.7 + Layout.preferredHeight: Style.baseWidgetSize * 0.7 + Layout.alignment: Qt.AlignVCenter + radius: Style.radiusXS + color: dragHandleMouseArea.containsMouse ? Color.mSurfaceVariant : "transparent" + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + ColumnLayout { + anchors.centerIn: parent + spacing: 2 + + Repeater { + model: 3 + Rectangle { + Layout.preferredWidth: Style.baseWidgetSize * 0.28 + Layout.preferredHeight: 2 + radius: 1 + color: Color.mOutline + } + } + } + + MouseArea { + id: dragHandleMouseArea + anchors.fill: parent + cursorShape: Qt.SizeVerCursor + hoverEnabled: true + preventStealing: false + z: 1000 + + onPressed: mouse => { + delegateItem.dragStartIndex = delegateItem.index; + delegateItem.dragTargetIndex = delegateItem.index; + delegateItem.dragStartY = delegateItem.y; + delegateItem.dragging = true; + delegateItem.z = 999; + preventStealing = true; + } + + onPositionChanged: mouse => { + if (delegateItem.dragging) { + var dy = mouse.y - height / 2; + var newY = delegateItem.y + dy; + newY = Math.max(0, Math.min(newY, listView.contentHeight - delegateItem.height)); + delegateItem.y = newY; + var targetIndex = Math.floor((newY + delegateItem.height / 2) / (delegateItem.height + Style.marginS)); + targetIndex = Math.max(0, Math.min(targetIndex, listView.count - 1)); + delegateItem.dragTargetIndex = targetIndex; + } + } + + onReleased: { + preventStealing = false; + if (delegateItem.dragStartIndex !== -1 && delegateItem.dragTargetIndex !== -1 && delegateItem.dragStartIndex !== delegateItem.dragTargetIndex) { + root.reorderEntries(delegateItem.dragStartIndex, delegateItem.dragTargetIndex); + } + delegateItem.dragging = false; + delegateItem.dragStartIndex = -1; + delegateItem.dragTargetIndex = -1; + delegateItem.z = 0; + } + + onCanceled: { + preventStealing = false; + delegateItem.dragging = false; + delegateItem.dragStartIndex = -1; + delegateItem.dragTargetIndex = -1; + delegateItem.z = 0; + } + } + } + + // Enable checkbox + Rectangle { + Layout.preferredWidth: Style.baseWidgetSize * 0.7 + Layout.preferredHeight: Style.baseWidgetSize * 0.7 + Layout.alignment: Qt.AlignVCenter + radius: Style.radiusXS + color: modelData.enabled ? Color.mPrimary : Color.mSurface + border.color: Color.mOutline + border.width: Style.borderS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NIcon { + visible: modelData.enabled + anchors.centerIn: parent + anchors.horizontalCenterOffset: -1 + icon: "check" + color: Color.mOnPrimary + pointSize: Math.max(Style.fontSizeXS, Style.baseWidgetSize * 0.35) + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + root.updateEntry(index, { + "enabled": !modelData.enabled + }); + } + } + } + + // Label + NText { + Layout.fillWidth: true + text: modelData.text + color: Color.mOnSurface + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + // Countdown toggle (only shown when global countdown is enabled) + NIconButtonHot { + visible: Settings.data.sessionMenu.enableCountdown + icon: "clock" + hot: modelData.countdownEnabled !== undefined ? modelData.countdownEnabled : true + baseSize: Style.baseWidgetSize * 0.8 + Layout.alignment: Qt.AlignVCenter + tooltipText: I18n.tr("common.countdown") + onClicked: root.updateEntry(delegateItem.index, { + "countdownEnabled": !(modelData.countdownEnabled !== undefined ? modelData.countdownEnabled : true) + }) + } + + // Settings button (cogwheel) + NIconButton { + icon: "settings" + tooltipText: I18n.tr("panels.session-menu.entry-settings-tooltip") + baseSize: Style.baseWidgetSize * 0.8 + Layout.alignment: Qt.AlignVCenter + onClicked: root.openEntrySettingsDialog(delegateItem.index) + } + } + + // Position binding for non-dragging state + y: { + if (delegateItem.dragging) { + return delegateItem.y; + } + + var draggedIndex = -1; + var targetIndex = -1; + for (var i = 0; i < listView.count; i++) { + var item = listView.itemAtIndex(i); + if (item && item.dragging) { + draggedIndex = item.dragStartIndex; + targetIndex = item.dragTargetIndex; + break; + } + } + + if (draggedIndex !== -1 && targetIndex !== -1 && draggedIndex !== targetIndex) { + var currentIndex = delegateItem.index; + if (draggedIndex < targetIndex) { + if (currentIndex > draggedIndex && currentIndex <= targetIndex) { + return (currentIndex - 1) * (delegateItem.height + Style.marginS); + } + } else { + if (currentIndex >= targetIndex && currentIndex < draggedIndex) { + return (currentIndex + 1) * (delegateItem.height + Style.marginS); + } + } + } + + return delegateItem.index * (delegateItem.height + Style.marginS); + } + + Behavior on y { + enabled: !delegateItem.dragging + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SessionMenu/GeneralSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SessionMenu/GeneralSubTab.qml new file mode 100644 index 0000000..3d52085 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SessionMenu/GeneralSubTab.qml @@ -0,0 +1,122 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.session-menu.large-buttons-style-label") + description: I18n.tr("panels.session-menu.large-buttons-style-description") + checked: Settings.data.sessionMenu.largeButtonsStyle + onToggled: checked => Settings.data.sessionMenu.largeButtonsStyle = checked + } + + NComboBox { + visible: Settings.data.sessionMenu.largeButtonsStyle + Layout.fillWidth: true + label: I18n.tr("panels.session-menu.large-buttons-layout-label") + description: I18n.tr("panels.session-menu.large-buttons-layout-description") + model: [ + { + "key": "grid", + "name": I18n.tr("options.session-menu-grid-layout.grid") + }, + { + "key": "single-row", + "name": I18n.tr("options.session-menu-grid-layout.single-row") + } + ] + currentKey: Settings.data.sessionMenu.largeButtonsLayout + defaultValue: Settings.getDefaultValue("sessionMenu.largeButtonsLayout") + onSelected: key => Settings.data.sessionMenu.largeButtonsLayout = key + } + + NComboBox { + label: I18n.tr("common.position") + description: I18n.tr("panels.session-menu.position-description") + Layout.fillWidth: true + model: [ + { + "key": "center", + "name": I18n.tr("positions.center") + }, + { + "key": "top_center", + "name": I18n.tr("positions.top-center") + }, + { + "key": "top_left", + "name": I18n.tr("positions.top-left") + }, + { + "key": "top_right", + "name": I18n.tr("positions.top-right") + }, + { + "key": "bottom_center", + "name": I18n.tr("positions.bottom-center") + }, + { + "key": "bottom_left", + "name": I18n.tr("positions.bottom-left") + }, + { + "key": "bottom_right", + "name": I18n.tr("positions.bottom-right") + } + ] + currentKey: Settings.data.sessionMenu.position + onSelected: key => Settings.data.sessionMenu.position = key + visible: !Settings.data.sessionMenu.largeButtonsStyle + defaultValue: Settings.getDefaultValue("sessionMenu.position") + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.session-menu.show-header-label") + description: I18n.tr("panels.session-menu.show-header-description") + checked: Settings.data.sessionMenu.showHeader + onToggled: checked => Settings.data.sessionMenu.showHeader = checked + visible: !Settings.data.sessionMenu.largeButtonsStyle + defaultValue: Settings.getDefaultValue("sessionMenu.showHeader") + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.session-menu.show-keybinds-label") + description: I18n.tr("panels.session-menu.show-keybinds-description") + checked: Settings.data.sessionMenu.showKeybinds + onToggled: checked => Settings.data.sessionMenu.showKeybinds = checked + defaultValue: Settings.getDefaultValue("sessionMenu.showKeybinds") + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.session-menu.enable-countdown-label") + description: I18n.tr("panels.session-menu.enable-countdown-description") + checked: Settings.data.sessionMenu.enableCountdown + onToggled: checked => Settings.data.sessionMenu.enableCountdown = checked + defaultValue: Settings.getDefaultValue("sessionMenu.enableCountdown") + } + + NValueSlider { + visible: Settings.data.sessionMenu.enableCountdown + Layout.fillWidth: true + label: I18n.tr("panels.session-menu.countdown-duration-label") + description: I18n.tr("panels.session-menu.countdown-duration-description") + from: 1000 + to: 30000 + stepSize: 1000 + showReset: true + value: Settings.data.sessionMenu.countdownDuration + onMoved: value => Settings.data.sessionMenu.countdownDuration = value + text: Math.round(Settings.data.sessionMenu.countdownDuration / 1000) + "s" + defaultValue: Settings.getDefaultValue("sessionMenu.countdownDuration") + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SessionMenu/SessionMenuEntrySettingsDialog.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SessionMenu/SessionMenuEntrySettingsDialog.qml new file mode 100644 index 0000000..3e4310a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SessionMenu/SessionMenuEntrySettingsDialog.qml @@ -0,0 +1,186 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +// Session Menu Entry Settings Dialog Component +Popup { + id: root + + property int entryIndex: -1 + property var entryData: null + property string entryId: "" + property string entryText: "" + property string keybindInputText: "" + + signal updateEntryProperties(int index, var properties) + + // Default commands mapping + readonly property var defaultCommands: { + "lock": I18n.tr("panels.session-menu.entry-settings-default-command-lock"), + "suspend": "systemctl suspend || loginctl suspend", + "hibernate": "systemctl hibernate || loginctl hibernate", + "reboot": "systemctl reboot || loginctl reboot", + "rebootToUefi": "systemctl reboot --firmware-setup || loginctl reboot --firmware-setup", + "logout": I18n.tr("panels.session-menu.entry-settings-default-command-logout"), + "shutdown": "systemctl poweroff || loginctl poweroff", + "userspaceReboot": "systemctl soft-reboot" + } + + readonly property string defaultCommand: defaultCommands[entryId] || "" + + width: Math.max(content.implicitWidth + padding * 2, 500) + height: content.implicitHeight + padding * 2 + padding: Style.marginXL + modal: true + closePolicy: Popup.NoAutoClose + dim: false + anchors.centerIn: parent + + onOpened: { + // Load command when popup opens + if (entryData) { + commandInput.text = entryData.command || ""; + keybindInputText = entryData.keybind || ""; + } + // Request focus to ensure keyboard input works + forceActiveFocus(); + } + + function save() { + root.updateEntryProperties(root.entryIndex, { + "command": commandInput.text, + "keybind": keybindInputText + }); + } + + background: Rectangle { + id: bgRect + + color: Color.mSurface + radius: Style.radiusL + border.color: Color.mPrimary + border.width: Style.borderM + } + + contentItem: FocusScope { + id: focusScope + focus: true + + ColumnLayout { + id: content + anchors.fill: parent + spacing: Style.marginM + + // Title + RowLayout { + Layout.fillWidth: true + + NText { + text: I18n.tr("panels.session-menu.entry-settings-title", { + "entry": root.entryText + }) + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + onClicked: { + root.save(); + root.close(); + } + } + } + + // Separator + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + } + + // Command input + NTextInput { + id: commandInput + Layout.fillWidth: true + label: I18n.tr("common.command") + description: I18n.tr("panels.session-menu.entry-settings-command-description") + placeholderText: I18n.tr("panels.session-menu.entry-settings-command-placeholder") + onTextChanged: root.save() + } + + // Default command info + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NLabel { + label: I18n.tr("panels.session-menu.entry-settings-default-info-label") + description: I18n.tr("panels.session-menu.entry-settings-default-info-description") + Layout.fillWidth: true + } + + // Default command display + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: defaultCommandText.implicitHeight + Style.margin2M + radius: Style.radiusM + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Style.borderS + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginS + + NIcon { + icon: "info" + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeM + } + + NText { + id: defaultCommandText + Layout.fillWidth: true + text: root.defaultCommand + color: Color.mOnSurfaceVariant + font.family: "monospace" + font.pointSize: Style.fontSizeS + wrapMode: Text.Wrap + } + } + } + } + + NKeybindRecorder { + id: keybindRecorder + Layout.fillWidth: true + label: I18n.tr("common.keybind") + description: I18n.tr("placeholders.keybind-recording") + allowEmpty: true + maxKeybinds: 1 + requireModifierForNormalKeys: false + currentKeybinds: keybindInputText ? [keybindInputText] : [] + settingsPath: "sessionMenu.powerOptions[" + root.entryIndex + "].keybind" + onKeybindsChanged: newKeybinds => { + keybindInputText = newKeybinds.length > 0 ? newKeybinds[0] : ""; + root.save(); + } + } + + // Hidden property to store the text since NKeybindRecorder manages its own state + // but we need to initialize it and read from it + + // Bottom spacer to maintain padding + Item { + Layout.preferredHeight: Style.marginS + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SessionMenu/SessionMenuTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SessionMenu/SessionMenuTab.qml new file mode 100644 index 0000000..89375a7 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SessionMenu/SessionMenuTab.qml @@ -0,0 +1,266 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + property var _activeDialog: null + property list entriesModel: [] + property list entriesDefault: [ + { + "id": "lock", + "text": I18n.tr("common.lock"), + "enabled": true, + "required": false + }, + { + "id": "suspend", + "text": I18n.tr("common.suspend"), + "enabled": true, + "required": false + }, + { + "id": "hibernate", + "text": I18n.tr("common.hibernate"), + "enabled": true, + "required": false + }, + { + "id": "reboot", + "text": I18n.tr("common.reboot"), + "enabled": true, + "required": false + }, + { + "id": "userspaceReboot", + "text": I18n.tr("common.userspace-reboot"), + "enabled": false, + "required": false + }, + { + "id": "rebootToUefi", + "text": I18n.tr("common.reboot-to-uefi"), + "enabled": true, + "required": false + }, + { + "id": "logout", + "text": I18n.tr("common.logout"), + "enabled": true, + "required": false + }, + { + "id": "shutdown", + "text": I18n.tr("common.shutdown"), + "enabled": true, + "required": false + } + ] + + function saveEntries() { + var toSave = []; + var enabledNumber = 1; + + for (var i = 0; i < entriesModel.length; i++) { + var keybind = entriesModel[i].keybind || ""; + + if (entriesModel[i].enabled) { + // For enabled entries with numeric or empty keybinds, assign sequential number + if (keybind === "" || /^\d+$/.test(keybind)) { + keybind = String(enabledNumber); + } + enabledNumber++; + } else { + // For disabled entries with numeric keybinds, clear them + if (/^\d+$/.test(keybind)) { + keybind = ""; + } + } + + toSave.push({ + "action": entriesModel[i].id, + "enabled": entriesModel[i].enabled, + "countdownEnabled": entriesModel[i].countdownEnabled !== undefined ? entriesModel[i].countdownEnabled : true, + "command": entriesModel[i].command || "", + "keybind": keybind + }); + } + + // Update local model with renumbered keybinds + var newModel = []; + for (var i = 0; i < entriesModel.length; i++) { + newModel.push(Object.assign({}, entriesModel[i], { + "keybind": toSave[i].keybind + })); + } + entriesModel = newModel; + + Settings.data.sessionMenu.powerOptions = toSave; + } + + function updateEntry(idx, properties) { + var newModel = entriesModel.slice(); + newModel[idx] = Object.assign({}, newModel[idx], properties); + entriesModel = newModel; + saveEntries(); + } + + function reorderEntries(fromIndex, toIndex) { + var newModel = entriesModel.slice(); + var item = newModel.splice(fromIndex, 1)[0]; + newModel.splice(toIndex, 0, item); + entriesModel = newModel; + saveEntries(); + } + + Component.onDestruction: { + if (_activeDialog && _activeDialog.close) { + var dialog = _activeDialog; + _activeDialog = null; + dialog.close(); + dialog.destroy(); + } + } + + function openEntrySettingsDialog(index) { + if (index < 0 || index >= entriesModel.length) { + return; + } + + var entry = entriesModel[index]; + var component = Qt.createComponent(Quickshell.shellDir + "/Modules/Panels/Settings/Tabs/SessionMenu/SessionMenuEntrySettingsDialog.qml"); + + function instantiateAndOpen() { + if (root._activeDialog) { + root._activeDialog.close(); + root._activeDialog.destroy(); + root._activeDialog = null; + } + + var dialog = component.createObject(Overlay.overlay, { + "entryIndex": index, + "entryData": entry, + "entryId": entry.id, + "entryText": entry.text + }); + + if (dialog) { + root._activeDialog = dialog; + dialog.updateEntryProperties.connect((idx, properties) => { + root.updateEntry(idx, properties); + }); + dialog.closed.connect(() => { + if (root._activeDialog === dialog) { + root._activeDialog = null; + dialog.destroy(); + } + }); + dialog.open(); + } else { + Logger.e("SessionMenuTab", "Failed to create entry settings dialog"); + } + } + + if (component.status === Component.Ready) { + instantiateAndOpen(); + } else if (component.status === Component.Error) { + Logger.e("SessionMenuTab", "Error loading entry settings dialog:", component.errorString()); + } else { + component.statusChanged.connect(function () { + if (component.status === Component.Ready) { + instantiateAndOpen(); + } else if (component.status === Component.Error) { + Logger.e("SessionMenuTab", "Error loading entry settings dialog:", component.errorString()); + } + }); + } + } + + Component.onCompleted: { + entriesModel = []; + + // Add the entries available in settings + for (var i = 0; i < Settings.data.sessionMenu.powerOptions.length; i++) { + const settingEntry = Settings.data.sessionMenu.powerOptions[i]; + + for (var j = 0; j < entriesDefault.length; j++) { + if (settingEntry.action === entriesDefault[j].id) { + var entry = entriesDefault[j]; + entry.enabled = settingEntry.enabled; + // Default countdownEnabled to true for backward compatibility + entry.countdownEnabled = settingEntry.countdownEnabled !== undefined ? settingEntry.countdownEnabled : true; + // Load custom command if defined + entry.command = settingEntry.command || ""; + entry.keybind = settingEntry.keybind || ""; + entriesModel.push(entry); + } + } + } + + // Add any missing entries from default + for (var i = 0; i < entriesDefault.length; i++) { + var found = false; + for (var j = 0; j < entriesModel.length; j++) { + if (entriesModel[j].id === entriesDefault[i].id) { + found = true; + break; + } + } + + if (!found) { + var entry = entriesDefault[i]; + // Default countdownEnabled to true for new entries + entry.countdownEnabled = true; + // Default command to empty string for new entries + entry.command = ""; + entry.keybind = ""; + entriesModel.push(entry); + } + } + + saveEntries(); + } + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.general") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.actions") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + GeneralSubTab {} + ActionsSubTab { + entriesModel: root.entriesModel + updateEntry: root.updateEntry + reorderEntries: root.reorderEntries + openEntrySettingsDialog: root.openEntrySettingsDialog + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SystemMonitor/GeneralSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SystemMonitor/GeneralSubTab.qml new file mode 100644 index 0000000..74af597 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SystemMonitor/GeneralSubTab.qml @@ -0,0 +1,105 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + property var screen + + NToggle { + Layout.fillWidth: true + Layout.topMargin: Style.marginM + label: I18n.tr("panels.system-monitor.enable-dgpu-monitoring-label") + description: I18n.tr("panels.system-monitor.enable-dgpu-monitoring-description") + checked: Settings.data.systemMonitor.enableDgpuMonitoring + defaultValue: Settings.getDefaultValue("systemMonitor.enableDgpuMonitoring") + onToggled: checked => Settings.data.systemMonitor.enableDgpuMonitoring = checked + } + + // Colors Section + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NToggle { + label: I18n.tr("panels.system-monitor.use-custom-highlight-colors-label") + description: I18n.tr("panels.system-monitor.use-custom-highlight-colors-description") + checked: Settings.data.systemMonitor.useCustomColors + defaultValue: Settings.getDefaultValue("systemMonitor.useCustomColors") + onToggled: checked => { + // If enabling custom colors and no custom color is saved, persist current theme colors + if (checked) { + if (!Settings.data.systemMonitor.warningColor || Settings.data.systemMonitor.warningColor === "") { + Settings.data.systemMonitor.warningColor = Color.mTertiary.toString(); + } + if (!Settings.data.systemMonitor.criticalColor || Settings.data.systemMonitor.criticalColor === "") { + Settings.data.systemMonitor.criticalColor = Color.mError.toString(); + } + } + Settings.data.systemMonitor.useCustomColors = checked; + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginXL + visible: Settings.data.systemMonitor.useCustomColors + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NText { + text: I18n.tr("panels.system-monitor.warning-color-label") + pointSize: Style.fontSizeM + } + + NColorPicker { + screen: root.screen + Layout.preferredWidth: Style.sliderWidth + Layout.preferredHeight: Style.baseWidgetSize + enabled: Settings.data.systemMonitor.useCustomColors + selectedColor: Settings.data.systemMonitor.warningColor || Color.mTertiary + onColorSelected: color => Settings.data.systemMonitor.warningColor = color + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NText { + text: I18n.tr("panels.system-monitor.critical-color-label") + pointSize: Style.fontSizeM + } + + NColorPicker { + screen: root.screen + Layout.preferredWidth: Style.sliderWidth + Layout.preferredHeight: Style.baseWidgetSize + enabled: Settings.data.systemMonitor.useCustomColors + selectedColor: Settings.data.systemMonitor.criticalColor || Color.mError + onColorSelected: color => Settings.data.systemMonitor.criticalColor = color + } + } + } + + NDivider { + Layout.fillWidth: true + } + + NTextInput { + label: I18n.tr("panels.system-monitor.external-monitor-label") + description: I18n.tr("panels.system-monitor.external-monitor-description") + placeholderText: I18n.tr("panels.system-monitor.external-monitor-placeholder") + text: Settings.data.systemMonitor.externalMonitor + defaultValue: Settings.getDefaultValue("systemMonitor.externalMonitor") + onTextChanged: Settings.data.systemMonitor.externalMonitor = text + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SystemMonitor/PerformanceSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SystemMonitor/PerformanceSubTab.qml new file mode 100644 index 0000000..b461c6f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SystemMonitor/PerformanceSubTab.qml @@ -0,0 +1,29 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.system.noctalia-performance-disable-wallpaper-label") + description: I18n.tr("panels.system.noctalia-performance-disable-wallpaper-description") + checked: !Settings.data.noctaliaPerformance.disableWallpaper + defaultValue: !Settings.getDefaultValue("noctaliaPerformance.disableWallpaper") + onToggled: checked => Settings.data.noctaliaPerformance.disableWallpaper = !checked + } + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.system.noctalia-performance-disable-desktop-widgets-label") + description: I18n.tr("panels.system.noctalia-performance-disable-desktop-widgets-description") + checked: !Settings.data.noctaliaPerformance.disableDesktopWidgets + defaultValue: !Settings.getDefaultValue("noctaliaPerformance.disableDesktopWidgets") + onToggled: checked => Settings.data.noctaliaPerformance.disableDesktopWidgets = !checked + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SystemMonitor/SystemMonitorTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SystemMonitor/SystemMonitorTab.qml new file mode 100644 index 0000000..e5418ca --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SystemMonitor/SystemMonitorTab.qml @@ -0,0 +1,52 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + property var screen + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("system-monitor.title") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.thresholds") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + NTabButton { + text: I18n.tr("common.performance") + tabIndex: 2 + checked: subTabBar.currentIndex === 2 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + GeneralSubTab { + screen: root.screen + } + ThresholdsSubTab {} + PerformanceSubTab {} + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SystemMonitor/ThresholdsSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SystemMonitor/ThresholdsSubTab.qml new file mode 100644 index 0000000..e91db98 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/SystemMonitor/ThresholdsSubTab.qml @@ -0,0 +1,313 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.System +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + Layout.fillWidth: true + + NLabel { + Layout.fillWidth: true + description: I18n.tr("panels.system-monitor.thresholds-section-description") + } + + GridLayout { + Layout.fillWidth: true + Layout.topMargin: Style.marginM + columns: 3 + columnSpacing: Style.marginM + rowSpacing: Style.marginM + + // Header row + Item { + Layout.fillWidth: true + } + + NText { + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + text: I18n.tr("panels.system-monitor.threshold-warning") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + + NText { + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + text: I18n.tr("panels.system-monitor.threshold-critical") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + + // CPU Usage + NText { + text: I18n.tr("bar.system-monitor.cpu-usage-label") + pointSize: Style.fontSizeM + } + + NSpinBox { + Layout.alignment: Qt.AlignHCenter + from: 0 + to: 100 + stepSize: 5 + value: Settings.data.systemMonitor.cpuWarningThreshold + defaultValue: Settings.getDefaultValue("systemMonitor.cpuWarningThreshold") + suffix: "%" + onValueChanged: { + Settings.data.systemMonitor.cpuWarningThreshold = value; + if (Settings.data.systemMonitor.cpuCriticalThreshold < value) { + Settings.data.systemMonitor.cpuCriticalThreshold = value; + } + } + } + + NSpinBox { + Layout.alignment: Qt.AlignHCenter + from: Settings.data.systemMonitor.cpuWarningThreshold + to: 100 + stepSize: 5 + value: Settings.data.systemMonitor.cpuCriticalThreshold + defaultValue: Settings.getDefaultValue("systemMonitor.cpuCriticalThreshold") + suffix: "%" + onValueChanged: Settings.data.systemMonitor.cpuCriticalThreshold = value + } + + // CPU Temperature + NText { + text: I18n.tr("bar.system-monitor.cpu-temperature-label") + pointSize: Style.fontSizeM + } + + NSpinBox { + Layout.alignment: Qt.AlignHCenter + from: 0 + to: 100 + stepSize: 5 + value: Settings.data.systemMonitor.tempWarningThreshold + defaultValue: Settings.getDefaultValue("systemMonitor.tempWarningThreshold") + suffix: "°C" + onValueChanged: { + Settings.data.systemMonitor.tempWarningThreshold = value; + if (Settings.data.systemMonitor.tempCriticalThreshold < value) { + Settings.data.systemMonitor.tempCriticalThreshold = value; + } + } + } + + NSpinBox { + Layout.alignment: Qt.AlignHCenter + from: Settings.data.systemMonitor.tempWarningThreshold + to: 100 + stepSize: 5 + value: Settings.data.systemMonitor.tempCriticalThreshold + defaultValue: Settings.getDefaultValue("systemMonitor.tempCriticalThreshold") + suffix: "°C" + onValueChanged: Settings.data.systemMonitor.tempCriticalThreshold = value + } + + // GPU Temperature + NText { + visible: SystemStatService.gpuAvailable + text: I18n.tr("panels.system-monitor.gpu-section-label") + pointSize: Style.fontSizeM + } + + NSpinBox { + visible: SystemStatService.gpuAvailable + Layout.alignment: Qt.AlignHCenter + from: 0 + to: 120 + stepSize: 5 + value: Settings.data.systemMonitor.gpuWarningThreshold + defaultValue: Settings.getDefaultValue("systemMonitor.gpuWarningThreshold") + suffix: "°C" + onValueChanged: { + Settings.data.systemMonitor.gpuWarningThreshold = value; + if (Settings.data.systemMonitor.gpuCriticalThreshold < value) { + Settings.data.systemMonitor.gpuCriticalThreshold = value; + } + } + } + + NSpinBox { + visible: SystemStatService.gpuAvailable + Layout.alignment: Qt.AlignHCenter + from: Settings.data.systemMonitor.gpuWarningThreshold + to: 120 + stepSize: 5 + value: Settings.data.systemMonitor.gpuCriticalThreshold + defaultValue: Settings.getDefaultValue("systemMonitor.gpuCriticalThreshold") + suffix: "°C" + onValueChanged: Settings.data.systemMonitor.gpuCriticalThreshold = value + } + + // Memory Usage + NText { + text: I18n.tr("bar.system-monitor.memory-usage-label") + pointSize: Style.fontSizeM + } + + NSpinBox { + Layout.alignment: Qt.AlignHCenter + from: 0 + to: 100 + stepSize: 5 + value: Settings.data.systemMonitor.memWarningThreshold + defaultValue: Settings.getDefaultValue("systemMonitor.memWarningThreshold") + suffix: "%" + onValueChanged: { + Settings.data.systemMonitor.memWarningThreshold = value; + if (Settings.data.systemMonitor.memCriticalThreshold < value) { + Settings.data.systemMonitor.memCriticalThreshold = value; + } + } + } + + NSpinBox { + Layout.alignment: Qt.AlignHCenter + from: Settings.data.systemMonitor.memWarningThreshold + to: 100 + stepSize: 5 + value: Settings.data.systemMonitor.memCriticalThreshold + defaultValue: Settings.getDefaultValue("systemMonitor.memCriticalThreshold") + suffix: "%" + onValueChanged: Settings.data.systemMonitor.memCriticalThreshold = value + } + + // Swap Usage + NText { + text: I18n.tr("bar.system-monitor.swap-usage-label") + pointSize: Style.fontSizeM + } + + NSpinBox { + Layout.alignment: Qt.AlignHCenter + from: 0 + to: 100 + stepSize: 5 + value: Settings.data.systemMonitor.swapWarningThreshold + defaultValue: Settings.getDefaultValue("systemMonitor.swapWarningThreshold") + suffix: "%" + onValueChanged: { + Settings.data.systemMonitor.swapWarningThreshold = value; + if (Settings.data.systemMonitor.swapCriticalThreshold < value) { + Settings.data.systemMonitor.swapCriticalThreshold = value; + } + } + } + + NSpinBox { + Layout.alignment: Qt.AlignHCenter + from: Settings.data.systemMonitor.swapWarningThreshold + to: 100 + stepSize: 5 + value: Settings.data.systemMonitor.swapCriticalThreshold + defaultValue: Settings.getDefaultValue("systemMonitor.swapCriticalThreshold") + suffix: "%" + onValueChanged: Settings.data.systemMonitor.swapCriticalThreshold = value + } + + // Disk Usage + NText { + text: I18n.tr("panels.system-monitor.disk-section-label") + pointSize: Style.fontSizeM + } + + NSpinBox { + Layout.alignment: Qt.AlignHCenter + from: 0 + to: 100 + stepSize: 5 + value: Settings.data.systemMonitor.diskWarningThreshold + defaultValue: Settings.getDefaultValue("systemMonitor.diskWarningThreshold") + suffix: "%" + onValueChanged: { + Settings.data.systemMonitor.diskWarningThreshold = value; + if (Settings.data.systemMonitor.diskCriticalThreshold < value) { + Settings.data.systemMonitor.diskCriticalThreshold = value; + } + } + } + + NSpinBox { + Layout.alignment: Qt.AlignHCenter + from: Settings.data.systemMonitor.diskWarningThreshold + to: 100 + stepSize: 5 + value: Settings.data.systemMonitor.diskCriticalThreshold + defaultValue: Settings.getDefaultValue("systemMonitor.diskCriticalThreshold") + suffix: "%" + onValueChanged: Settings.data.systemMonitor.diskCriticalThreshold = value + } + + // Disk Available + NText { + text: I18n.tr("panels.system-monitor.disk-available-label") + pointSize: Style.fontSizeM + } + + NSpinBox { + Layout.alignment: Qt.AlignHCenter + from: 0 + to: 100 + stepSize: 5 + value: Settings.data.systemMonitor.diskAvailWarningThreshold + defaultValue: Settings.getDefaultValue("systemMonitor.diskAvailWarningThreshold") + suffix: "%" + onValueChanged: { + Settings.data.systemMonitor.diskAvailWarningThreshold = value; + if (Settings.data.systemMonitor.diskAvailCriticalThreshold > value) { + Settings.data.systemMonitor.diskAvailCriticalThreshold = value; + } + } + } + + NSpinBox { + Layout.alignment: Qt.AlignHCenter + from: 0 + to: 20 + stepSize: 5 + value: Settings.data.systemMonitor.diskAvailCriticalThreshold + defaultValue: Settings.getDefaultValue("systemMonitor.diskAvailCriticalThreshold") + suffix: "%" + onValueChanged: Settings.data.systemMonitor.diskAvailCriticalThreshold = value + } + + // Battery + NText { + text: I18n.tr("panels.notifications.toast-battery-label") + pointSize: Style.fontSizeM + } + + NSpinBox { + Layout.alignment: Qt.AlignHCenter + from: 0 + to: 100 + stepSize: 5 + value: Settings.data.systemMonitor.batteryWarningThreshold + defaultValue: Settings.getDefaultValue("systemMonitor.batteryWarningThreshold") + suffix: "%" + onValueChanged: { + Settings.data.systemMonitor.batteryWarningThreshold = value; + if (Settings.data.systemMonitor.batteryCriticalThreshold > value) { + Settings.data.systemMonitor.batteryCriticalThreshold = value; + } + } + } + + NSpinBox { + Layout.alignment: Qt.AlignHCenter + from: 0 + to: Settings.data.systemMonitor.batteryWarningThreshold + stepSize: 5 + value: Settings.data.systemMonitor.batteryCriticalThreshold + defaultValue: Settings.getDefaultValue("systemMonitor.batteryCriticalThreshold") + suffix: "%" + onValueChanged: Settings.data.systemMonitor.batteryCriticalThreshold = value + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/UserInterface/AppearanceSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/UserInterface/AppearanceSubTab.qml new file mode 100644 index 0000000..76f6fa7 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/UserInterface/AppearanceSubTab.qml @@ -0,0 +1,211 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + NToggle { + label: I18n.tr("panels.user-interface.tooltips-label") + description: I18n.tr("panels.user-interface.tooltips-description") + checked: Settings.data.ui.tooltipsEnabled + defaultValue: Settings.getDefaultValue("ui.tooltipsEnabled") + onToggled: checked => Settings.data.ui.tooltipsEnabled = checked + } + + NToggle { + label: I18n.tr("panels.user-interface.box-border-label") + description: I18n.tr("panels.user-interface.box-border-description") + checked: Settings.data.ui.boxBorderEnabled + defaultValue: Settings.getDefaultValue("ui.boxBorderEnabled") + onToggled: checked => Settings.data.ui.boxBorderEnabled = checked + } + + NToggle { + label: I18n.tr("panels.user-interface.scrollbar-always-visible-label") + description: I18n.tr("panels.user-interface.scrollbar-always-visible-description") + checked: Settings.data.ui.scrollbarAlwaysVisible + defaultValue: Settings.getDefaultValue("ui.scrollbarAlwaysVisible") + onToggled: checked => Settings.data.ui.scrollbarAlwaysVisible = checked + } + + NToggle { + label: I18n.tr("panels.user-interface.shadows-label") + description: I18n.tr("panels.user-interface.shadows-description") + checked: Settings.data.general.enableShadows + defaultValue: Settings.getDefaultValue("general.enableShadows") + onToggled: checked => Settings.data.general.enableShadows = checked + } + + NToggle { + label: I18n.tr("panels.user-interface.blur-behind-label") + description: I18n.tr("panels.user-interface.blur-behind-description") + checked: Settings.data.general.enableBlurBehind + defaultValue: Settings.getDefaultValue("general.enableBlurBehind") + onToggled: checked => Settings.data.general.enableBlurBehind = checked + } + + NToggle { + label: I18n.tr("panels.user-interface.translucent-widgets-label") + description: I18n.tr("panels.user-interface.translucent-widgets-description") + checked: Settings.data.ui.translucentWidgets + defaultValue: Settings.getDefaultValue("ui.translucentWidgets") + onToggled: checked => Settings.data.ui.translucentWidgets = checked + } + + NComboBox { + visible: Settings.data.general.enableShadows + label: I18n.tr("panels.user-interface.shadows-direction-label") + description: I18n.tr("panels.user-interface.shadows-direction-description") + Layout.fillWidth: true + + readonly property var shadowOptionsMap: ({ + "top_left": { + "name": I18n.tr("positions.top-left"), + "p": Qt.point(-2, -2) + }, + "top": { + "name": I18n.tr("positions.top"), + "p": Qt.point(0, -3) + }, + "top_right": { + "name": I18n.tr("positions.top-right"), + "p": Qt.point(2, -2) + }, + "left": { + "name": I18n.tr("positions.left"), + "p": Qt.point(-3, 0) + }, + "center": { + "name": I18n.tr("positions.center"), + "p": Qt.point(0, 0) + }, + "right": { + "name": I18n.tr("positions.right"), + "p": Qt.point(3, 0) + }, + "bottom_left": { + "name": I18n.tr("positions.bottom-left"), + "p": Qt.point(-2, 2) + }, + "bottom": { + "name": I18n.tr("positions.bottom"), + "p": Qt.point(0, 3) + }, + "bottom_right": { + "name": I18n.tr("positions.bottom-right"), + "p": Qt.point(2, 3) + } + }) + + model: Object.keys(shadowOptionsMap).map(function (k) { + return { + "key": k, + "name": shadowOptionsMap[k].name + }; + }) + + currentKey: Settings.data.general.shadowDirection + defaultValue: Settings.getDefaultValue("general.shadowDirection") + + onSelected: function (key) { + var opt = shadowOptionsMap[key]; + if (opt) { + Settings.data.general.shadowDirection = key; + Settings.data.general.shadowOffsetX = opt.p.x; + Settings.data.general.shadowOffsetY = opt.p.y; + } + } + } + + NDivider { + Layout.fillWidth: true + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.user-interface.scaling-label") + description: I18n.tr("panels.user-interface.scaling-description") + from: 0.8 + to: 1.2 + stepSize: 0.05 + showReset: true + value: Settings.data.general.scaleRatio + defaultValue: Settings.getDefaultValue("general.scaleRatio") + onMoved: value => Settings.data.general.scaleRatio = value + text: Math.floor(Settings.data.general.scaleRatio * 100) + "%" + } + + NDivider { + Layout.fillWidth: true + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.user-interface.box-border-radius-label") + description: I18n.tr("panels.user-interface.box-border-radius-description") + from: 0 + to: 2 + stepSize: 0.01 + showReset: true + value: Settings.data.general.radiusRatio + defaultValue: Settings.getDefaultValue("general.radiusRatio") + onMoved: value => Settings.data.general.radiusRatio = value + text: Math.floor(Settings.data.general.radiusRatio * 100) + "%" + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.user-interface.control-border-radius-label") + description: I18n.tr("panels.user-interface.control-border-radius-description") + from: 0 + to: 2 + stepSize: 0.01 + showReset: true + value: Settings.data.general.iRadiusRatio + defaultValue: Settings.getDefaultValue("general.iRadiusRatio") + onMoved: value => Settings.data.general.iRadiusRatio = value + text: Math.floor(Settings.data.general.iRadiusRatio * 100) + "%" + } + + NDivider { + Layout.fillWidth: true + } + + ColumnLayout { + spacing: Style.marginL + Layout.fillWidth: true + + NToggle { + label: I18n.tr("panels.user-interface.animation-disable-label") + description: I18n.tr("panels.user-interface.animation-disable-description") + checked: Settings.data.general.animationDisabled + defaultValue: Settings.getDefaultValue("general.animationDisabled") + onToggled: checked => Settings.data.general.animationDisabled = checked + } + + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + visible: !Settings.data.general.animationDisabled + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.user-interface.animation-speed-label") + description: I18n.tr("panels.user-interface.animation-speed-description") + from: 0 + to: 2.0 + stepSize: 0.01 + showReset: true + value: Settings.data.general.animationSpeed + defaultValue: Settings.getDefaultValue("general.animationSpeed") + onMoved: value => Settings.data.general.animationSpeed = Math.max(value, 0.05) + text: Math.round(Settings.data.general.animationSpeed * 100) + "%" + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/UserInterface/PanelsSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/UserInterface/PanelsSubTab.qml new file mode 100644 index 0000000..7ee185e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/UserInterface/PanelsSubTab.qml @@ -0,0 +1,113 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.Panels.Settings // For SettingsPanel +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + NToggle { + label: I18n.tr("panels.user-interface.panels-attached-to-bar-label") + description: I18n.tr("panels.user-interface.panels-attached-to-bar-description") + checked: Settings.data.ui.panelsAttachedToBar + defaultValue: Settings.getDefaultValue("ui.panelsAttachedToBar") + onToggled: checked => Settings.data.ui.panelsAttachedToBar = checked + } + + NToggle { + visible: (Quickshell.screens.length > 1) + label: I18n.tr("panels.user-interface.allow-panels-without-bar-label") + description: I18n.tr("panels.user-interface.allow-panels-without-bar-description") + checked: Settings.data.general.allowPanelsOnScreenWithoutBar + defaultValue: Settings.getDefaultValue("general.allowPanelsOnScreenWithoutBar") + onToggled: checked => Settings.data.general.allowPanelsOnScreenWithoutBar = checked + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.user-interface.panel-background-opacity-label") + description: I18n.tr("panels.user-interface.panel-background-opacity-description") + from: 0 + to: 1 + stepSize: 0.01 + showReset: true + value: Settings.data.ui.panelBackgroundOpacity + defaultValue: Settings.getDefaultValue("ui.panelBackgroundOpacity") + onMoved: value => Settings.data.ui.panelBackgroundOpacity = value + text: Math.floor(Settings.data.ui.panelBackgroundOpacity * 100) + "%" + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.user-interface.dimmer-opacity-label") + description: I18n.tr("panels.user-interface.dimmer-opacity-description") + from: 0 + to: 1 + stepSize: 0.01 + showReset: true + value: Settings.data.general.dimmerOpacity + defaultValue: Settings.getDefaultValue("general.dimmerOpacity") + onMoved: value => Settings.data.general.dimmerOpacity = value + text: Math.floor(Settings.data.general.dimmerOpacity * 100) + "%" + } + + NDivider { + Layout.fillWidth: true + } + + NHeader { + label: I18n.tr("panels.user-interface.settings-panel-header") + } + + NComboBox { + label: I18n.tr("panels.user-interface.settings-panel-mode-label") + description: I18n.tr("panels.user-interface.settings-panel-mode-description") + Layout.fillWidth: true + minimumWidth: 220 * Style.uiScaleRatio + model: [ + { + "key": "attached", + "name": I18n.tr("options.settings-panel-mode.attached") + }, + { + "key": "centered", + "name": I18n.tr("options.settings-panel-mode.centered") + }, + { + "key": "window", + "name": I18n.tr("options.settings-panel-mode.window") + } + ] + currentKey: Settings.data.ui.settingsPanelMode + defaultValue: Settings.getDefaultValue("ui.settingsPanelMode") + onSelected: key => { + // Defer setup to next update so close can do its work properly + Qt.callLater(() => { + Settings.data.ui.settingsPanelMode = key; + }); + if (Settings.data.ui.settingsPanelMode === "window" || key === "window") { + // Just switched from/to window, need to close panel + var screen = PanelService.openedPanel?.screen || SettingsPanelService.settingsWindow?.screen || PanelService.findScreenForPanels(); + SettingsPanelService.close(screen); + + Qt.callLater(() => { + SettingsPanelService.openToTab(SettingsPanel.Tab.UserInterface, 1, screen); + }); + } + } + } + + NToggle { + label: I18n.tr("panels.user-interface.settings-panel-sidebar-card-style-label") + description: I18n.tr("panels.user-interface.settings-panel-sidebar-card-style-description") + checked: Settings.data.ui.settingsPanelSideBarCardStyle + defaultValue: Settings.getDefaultValue("ui.settingsPanelSideBarCardStyle") + onToggled: checked => Settings.data.ui.settingsPanelSideBarCardStyle = checked + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/UserInterface/ScreenCornersSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/UserInterface/ScreenCornersSubTab.qml new file mode 100644 index 0000000..1607abc --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/UserInterface/ScreenCornersSubTab.qml @@ -0,0 +1,46 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + NToggle { + label: I18n.tr("panels.general.screen-corners-show-corners-label") + description: I18n.tr("panels.general.screen-corners-show-corners-description") + checked: Settings.data.general.showScreenCorners + defaultValue: Settings.getDefaultValue("general.showScreenCorners") + onToggled: checked => Settings.data.general.showScreenCorners = checked + } + + NToggle { + label: I18n.tr("panels.general.screen-corners-solid-black-label") + description: I18n.tr("panels.general.screen-corners-solid-black-description") + checked: Settings.data.general.forceBlackScreenCorners + defaultValue: Settings.getDefaultValue("general.forceBlackScreenCorners") + onToggled: checked => Settings.data.general.forceBlackScreenCorners = checked + } + + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.general.screen-corners-radius-label") + description: I18n.tr("panels.general.screen-corners-radius-description") + from: 0 + to: 2 + stepSize: 0.01 + showReset: true + value: Settings.data.general.screenRadiusRatio + defaultValue: Settings.getDefaultValue("general.screenRadiusRatio") + onMoved: value => Settings.data.general.screenRadiusRatio = value + text: Math.floor(Settings.data.general.screenRadiusRatio * 100) + "%" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/UserInterface/UserInterfaceTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/UserInterface/UserInterfaceTab.qml new file mode 100644 index 0000000..8df9994 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/UserInterface/UserInterfaceTab.qml @@ -0,0 +1,48 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.appearance") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.panels") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + NTabButton { + text: I18n.tr("common.screen-corners") + tabIndex: 2 + checked: subTabBar.currentIndex === 2 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + AppearanceSubTab {} + PanelsSubTab {} + ScreenCornersSubTab {} + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Wallpaper/AutomationSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Wallpaper/AutomationSubTab.qml new file mode 100644 index 0000000..69d7429 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Wallpaper/AutomationSubTab.qml @@ -0,0 +1,65 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + enabled: Settings.data.wallpaper.enabled + + NToggle { + label: I18n.tr("panels.wallpaper.automation-scheduled-change-label") + description: I18n.tr("panels.wallpaper.automation-scheduled-change-description") + checked: Settings.data.wallpaper.automationEnabled + onToggled: checked => Settings.data.wallpaper.automationEnabled = checked + } + + ColumnLayout { + enabled: Settings.data.wallpaper.automationEnabled + spacing: Style.marginL + Layout.fillWidth: true + + NComboBox { + + label: I18n.tr("panels.wallpaper.automation-change-mode-label") + description: I18n.tr("panels.wallpaper.automation-change-mode-description") + Layout.fillWidth: true + model: [ + { + "key": "random", + "name": I18n.tr("common.random") + }, + { + "key": "alphabetical", + "name": I18n.tr("panels.wallpaper.automation-change-mode-alphabetical") + } + ] + currentKey: Settings.data.wallpaper.wallpaperChangeMode || "random" + onSelected: key => Settings.data.wallpaper.wallpaperChangeMode = key + defaultValue: Settings.getDefaultValue("wallpaper.wallpaperChangeMode") + } + + NSpinBox { + label: I18n.tr("panels.wallpaper.automation-interval-label") + description: I18n.tr("panels.wallpaper.automation-interval-description") + Layout.fillWidth: true + from: 1 + to: 1440 + stepSize: 1 + suffix: "m" + value: Math.round(Settings.data.wallpaper.randomIntervalSec / 60) + defaultValue: Settings.getDefaultValue("wallpaper.randomIntervalSec") !== undefined ? Math.round(Settings.getDefaultValue("wallpaper.randomIntervalSec") / 60) : undefined + onValueChanged: { + let newSec = value * 60; + if (newSec !== Settings.data.wallpaper.randomIntervalSec) { + Settings.data.wallpaper.randomIntervalSec = newSec; + WallpaperService.restartRandomWallpaperTimer(); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Wallpaper/GeneralSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Wallpaper/GeneralSubTab.qml new file mode 100644 index 0000000..712d71c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Wallpaper/GeneralSubTab.qml @@ -0,0 +1,261 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Compositor +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + + property var screen + + signal openMainFolderPicker + signal openMonitorFolderPicker(string monitorName) + + NToggle { + label: I18n.tr("panels.wallpaper.settings-enable-management-label") + description: I18n.tr("panels.wallpaper.settings-enable-management-description") + checked: Settings.data.wallpaper.enabled + onToggled: checked => Settings.data.wallpaper.enabled = checked + defaultValue: Settings.getDefaultValue("wallpaper.enabled") + } + + ColumnLayout { + enabled: Settings.data.wallpaper.enabled + spacing: Style.marginL + Layout.fillWidth: true + + RowLayout { + + NLabel { + label: I18n.tr("tooltips.wallpaper-selector") + description: I18n.tr("panels.wallpaper.settings-selector-description") + Layout.alignment: Qt.AlignTop + } + + NIconButton { + icon: "wallpaper-selector" + tooltipText: I18n.tr("tooltips.wallpaper-selector") + onClicked: PanelService.getPanel("wallpaperPanel", root.screen)?.toggle() + } + } + + NComboBox { + label: I18n.tr("common.position") + description: I18n.tr("panels.wallpaper.settings-selector-position-description") + Layout.fillWidth: true + model: [ + { + "key": "follow_bar", + "name": I18n.tr("positions.follow-bar") + }, + { + "key": "center", + "name": I18n.tr("positions.center") + }, + { + "key": "top_center", + "name": I18n.tr("positions.top-center") + }, + { + "key": "top_left", + "name": I18n.tr("positions.top-left") + }, + { + "key": "top_right", + "name": I18n.tr("positions.top-right") + }, + { + "key": "bottom_left", + "name": I18n.tr("positions.bottom-left") + }, + { + "key": "bottom_right", + "name": I18n.tr("positions.bottom-right") + }, + { + "key": "bottom_center", + "name": I18n.tr("positions.bottom-center") + } + ] + currentKey: Settings.data.wallpaper.panelPosition + onSelected: key => Settings.data.wallpaper.panelPosition = key + defaultValue: Settings.getDefaultValue("wallpaper.panelPosition") + } + + NComboBox { + label: I18n.tr("panels.wallpaper.settings-view-mode-label") + description: I18n.tr("panels.wallpaper.settings-view-mode-description") + Layout.fillWidth: true + model: [ + { + "key": "single", + "name": I18n.tr("panels.wallpaper.view-mode-single") + }, + { + "key": "recursive", + "name": I18n.tr("panels.wallpaper.view-mode-recursive") + }, + { + "key": "browse", + "name": I18n.tr("panels.wallpaper.view-mode-browse") + } + ] + currentKey: Settings.data.wallpaper.viewMode + onSelected: key => Settings.data.wallpaper.viewMode = key + defaultValue: Settings.getDefaultValue("wallpaper.viewMode") + } + + NTextInputButton { + id: wallpaperPathInput + label: I18n.tr("panels.wallpaper.settings-folder-label") + description: I18n.tr("panels.wallpaper.settings-folder-description") + text: Settings.data.wallpaper.directory + buttonIcon: "folder-open" + buttonTooltip: I18n.tr("panels.wallpaper.settings-folder-label") + Layout.fillWidth: true + onInputTextChanged: text => Settings.data.wallpaper.directory = text + onButtonClicked: root.openMainFolderPicker() + } + + NToggle { + label: I18n.tr("panels.wallpaper.settings-monitor-specific-label") + description: I18n.tr("panels.wallpaper.settings-monitor-specific-description") + checked: Settings.data.wallpaper.enableMultiMonitorDirectories + onToggled: checked => Settings.data.wallpaper.enableMultiMonitorDirectories = checked + defaultValue: Settings.getDefaultValue("wallpaper.enableMultiMonitorDirectories") + } + + NBox { + visible: Settings.data.wallpaper.enableMultiMonitorDirectories + Layout.fillWidth: true + radius: Style.radiusM + color: Color.mSurface + border.color: Color.mOutline + border.width: Style.borderS + implicitHeight: contentCol.implicitHeight + Style.margin2L + clip: true + + ColumnLayout { + id: contentCol + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + Repeater { + model: Quickshell.screens || [] + delegate: ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NText { + text: (modelData.name || "Unknown") + color: Color.mPrimary + font.weight: Style.fontWeightBold + pointSize: Style.fontSizeM + } + + NTextInputButton { + id: monitorDirInput + text: WallpaperService.getMonitorDirectory(modelData.name) + buttonIcon: "folder-open" + buttonTooltip: I18n.tr("panels.wallpaper.settings-monitor-specific-tooltip") + Layout.fillWidth: true + onInputEditingFinished: WallpaperService.setMonitorDirectory(modelData.name, monitorDirInput.text) + onButtonClicked: root.openMonitorFolderPicker(modelData.name) + } + } + } + } + } + } + + NDivider { + Layout.fillWidth: true + } + + NToggle { + label: I18n.tr("panels.wallpaper.settings-use-original-images-label") + description: I18n.tr("panels.wallpaper.settings-use-original-images-description") + checked: Settings.data.wallpaper.useOriginalImages + enabled: Settings.data.wallpaper.enabled + onToggled: checked => Settings.data.wallpaper.useOriginalImages = checked + defaultValue: Settings.getDefaultValue("wallpaper.useOriginalImages") + } + + RowLayout { + spacing: Style.marginM + Layout.fillWidth: true + enabled: Settings.data.wallpaper.enabled + + NLabel { + label: I18n.tr("panels.wallpaper.settings-clear-cache-label") + description: I18n.tr("panels.wallpaper.settings-clear-cache-description") + Layout.fillWidth: true + } + + NButton { + icon: "trash" + text: I18n.tr("panels.wallpaper.settings-clear-cache-button") + outlined: true + onClicked: { + ImageCacheService.clearLarge(); + ToastService.showNotice(I18n.tr("panels.wallpaper.settings-clear-cache-toast")); + } + } + } + + NDivider { + Layout.fillWidth: true + visible: CompositorService.isNiri + } + + ColumnLayout { + visible: CompositorService.isNiri + enabled: Settings.data.wallpaper.enabled + spacing: Style.marginL + Layout.fillWidth: true + + NToggle { + label: I18n.tr("panels.wallpaper.settings-enable-overview-label") + description: I18n.tr("panels.wallpaper.settings-enable-overview-description") + checked: Settings.data.wallpaper.enabled && Settings.data.wallpaper.overviewEnabled + onToggled: checked => Settings.data.wallpaper.overviewEnabled = checked + defaultValue: Settings.getDefaultValue("wallpaper.overviewEnabled") + } + + NValueSlider { + Layout.fillWidth: true + visible: Settings.data.wallpaper.overviewEnabled + label: I18n.tr("panels.wallpaper.settings-overview-blur-strength-label") + description: I18n.tr("panels.wallpaper.settings-overview-blur-strength-description") + from: 0.0 + to: 1.0 + stepSize: 0.01 + showReset: true + value: Settings.data.wallpaper.overviewBlur + onMoved: value => Settings.data.wallpaper.overviewBlur = value + text: ((Settings.data.wallpaper.overviewBlur) * 100).toFixed(0) + "%" + defaultValue: Settings.getDefaultValue("wallpaper.overviewBlur") + } + + NValueSlider { + Layout.fillWidth: true + visible: Settings.data.wallpaper.overviewEnabled + label: I18n.tr("panels.wallpaper.settings-overview-tint-label") + description: I18n.tr("panels.wallpaper.settings-overview-tint-description") + from: 0.0 + to: 1.0 + stepSize: 0.01 + showReset: true + value: Settings.data.wallpaper.overviewTint + onMoved: value => Settings.data.wallpaper.overviewTint = value + text: ((Settings.data.wallpaper.overviewTint) * 100).toFixed(0) + "%" + defaultValue: Settings.getDefaultValue("wallpaper.overviewTint") + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Wallpaper/LookAndFeelSubTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Wallpaper/LookAndFeelSubTab.qml new file mode 100644 index 0000000..2adb36c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Wallpaper/LookAndFeelSubTab.qml @@ -0,0 +1,114 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginL + Layout.fillWidth: true + enabled: Settings.data.wallpaper.enabled + + property var screen + + NComboBox { + label: I18n.tr("panels.wallpaper.look-feel-fill-mode-label") + description: I18n.tr("panels.wallpaper.look-feel-fill-mode-description") + model: WallpaperService.fillModeModel + currentKey: Settings.data.wallpaper.fillMode + onSelected: key => Settings.data.wallpaper.fillMode = key + defaultValue: Settings.getDefaultValue("wallpaper.fillMode") + } + + RowLayout { + NLabel { + label: I18n.tr("bar.audio-visualizer.color-name-label") + description: I18n.tr("panels.wallpaper.look-feel-fill-color-description") + Layout.alignment: Qt.AlignTop + } + + NColorPicker { + screen: root.screen + selectedColor: Settings.data.wallpaper.fillColor + onColorSelected: color => Settings.data.wallpaper.fillColor = color + } + } + + NDivider { + Layout.fillWidth: true + } + + ColumnLayout { + spacing: Style.marginS + Layout.fillWidth: true + + NLabel { + label: I18n.tr("panels.wallpaper.look-feel-transition-type-label") + description: I18n.tr("panels.wallpaper.look-feel-transition-type-description") + } + + Repeater { + model: WallpaperService.allTransitions + + NCheckbox { + required property string modelData + label: { + var key = "wallpaper.transitions." + modelData; + return I18n.tr(key); + } + labelSize: Style.fontSizeM + checked: Settings.data.wallpaper.transitionType.includes(modelData) + onToggled: checked => { + var arr = Array.from(Settings.data.wallpaper.transitionType); + if (checked) { + if (!arr.includes(modelData)) + arr.push(modelData); + } else { + arr = arr.filter(k => k !== modelData); + } + Settings.data.wallpaper.transitionType = arr; + } + } + } + } + + NDivider { + Layout.fillWidth: true + } + + NToggle { + label: I18n.tr("panels.wallpaper.look-feel-skip-startup-transition-label") + description: I18n.tr("panels.wallpaper.look-feel-skip-startup-transition-description") + checked: Settings.data.wallpaper.skipStartupTransition + onToggled: Settings.data.wallpaper.skipStartupTransition = checked + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.wallpaper.look-feel-transition-duration-label") + description: I18n.tr("panels.wallpaper.look-feel-transition-duration-description") + from: 500 + to: 10000 + stepSize: 100 + showReset: true + value: Settings.data.wallpaper.transitionDuration + onMoved: value => Settings.data.wallpaper.transitionDuration = value + text: (Settings.data.wallpaper.transitionDuration / 1000).toFixed(1) + "s" + defaultValue: Settings.getDefaultValue("wallpaper.transitionDuration") + } + + NValueSlider { + Layout.fillWidth: true + label: I18n.tr("panels.wallpaper.look-feel-edge-smoothness-label") + description: I18n.tr("panels.wallpaper.look-feel-edge-smoothness-description") + from: 0.0 + to: 1.0 + showReset: true + value: Settings.data.wallpaper.transitionEdgeSmoothness + onMoved: value => Settings.data.wallpaper.transitionEdgeSmoothness = value + text: Math.round(Settings.data.wallpaper.transitionEdgeSmoothness * 100) + "%" + defaultValue: Settings.getDefaultValue("wallpaper.transitionEdgeSmoothness") + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Wallpaper/WallpaperTab.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Wallpaper/WallpaperTab.qml new file mode 100644 index 0000000..64d1a31 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Settings/Tabs/Wallpaper/WallpaperTab.qml @@ -0,0 +1,93 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + spacing: 0 + + property var screen + + function openMainFolderPicker() { + mainFolderPicker.open(); + } + + function openMonitorFolderPicker(monitorName) { + specificFolderMonitorName = monitorName; + monitorFolderPicker.open(); + } + + property string specificFolderMonitorName: "" + + NTabBar { + id: subTabBar + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + distributeEvenly: true + currentIndex: tabView.currentIndex + + NTabButton { + text: I18n.tr("common.general") + tabIndex: 0 + checked: subTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("common.look") + tabIndex: 1 + checked: subTabBar.currentIndex === 1 + } + NTabButton { + text: I18n.tr("common.automation") + tabIndex: 2 + checked: subTabBar.currentIndex === 2 + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + + NTabView { + id: tabView + currentIndex: subTabBar.currentIndex + + GeneralSubTab { + screen: root.screen + onOpenMainFolderPicker: root.openMainFolderPicker() + onOpenMonitorFolderPicker: monitorName => root.openMonitorFolderPicker(monitorName) + } + LookAndFeelSubTab { + screen: root.screen + } + AutomationSubTab {} + } + + NFilePicker { + id: mainFolderPicker + selectionMode: "folders" + title: I18n.tr("setup.wallpaper.dir-select-title") + initialPath: Settings.data.wallpaper.directory || Quickshell.env("HOME") + "/Pictures" + onAccepted: paths => { + if (paths.length > 0) { + Settings.data.wallpaper.directory = paths[0]; + } + } + } + + NFilePicker { + id: monitorFolderPicker + selectionMode: "folders" + title: I18n.tr("panels.wallpaper.settings-select-monitor-folder") + initialPath: WallpaperService.getMonitorDirectory(specificFolderMonitorName) || Quickshell.env("HOME") + "/Pictures" + onAccepted: paths => { + if (paths.length > 0) { + WallpaperService.setMonitorDirectory(specificFolderMonitorName, paths[0]); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SetupWizard/SetupAppearanceStep.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SetupWizard/SetupAppearanceStep.qml new file mode 100644 index 0000000..34a6430 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SetupWizard/SetupAppearanceStep.qml @@ -0,0 +1,395 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Io +import qs.Commons +import qs.Services.System +import qs.Services.Theming +import qs.Widgets + +ColumnLayout { + id: root + spacing: Style.marginM + + function extractSchemeName(path) { + var basename = path.split('/').pop(); + return basename.replace('.json', ''); + } + + // Cache for scheme colors (mirrors ColorSchemeTab approach) + property var schemeColorsCache: ({}) + property int cacheVersion: 0 + + function getSchemeColor(schemeName, key) { + try { + var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"; + var data = schemeColorsCache[schemeName]; + if (data && data[mode] && data[mode][key]) + return data[mode][key]; + } catch (e) {} + return Color.mSurfaceVariant; + } + + // Match ColorSchemeTab helpers + function schemeLoaded(schemeName, jsonData) { + var value = jsonData || {}; + schemeColorsCache[schemeName] = value; + cacheVersion++; + Logger.i("SetupAppearanceStep", `Loaded scheme ${schemeName}`); + } + + Connections { + target: ColorSchemeService + function onSchemesChanged() { + Logger.i("SetupAppearanceStep", `Color schemes changed: ${ColorSchemeService.schemes.length}`); + schemeColorsCache = {}; + cacheVersion++; + } + } + + // Beautiful header with icon + RowLayout { + Layout.fillWidth: true + Layout.bottomMargin: Style.marginL + spacing: Style.marginM + + Rectangle { + width: 40 + height: 40 + radius: Style.radiusL + color: Color.mSurfaceVariant + opacity: 0.6 + + NIcon { + icon: "palette" + pointSize: Style.fontSizeL + color: Color.mPrimary + anchors.centerIn: parent + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NText { + text: I18n.tr("common.appearance") + pointSize: Style.fontSizeXL + font.weight: Style.fontWeightBold + color: Color.mPrimary + } + + NText { + text: I18n.tr("setup.appearance.subheader") + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + } + } + + NScrollView { + id: appearanceScrollView + Layout.fillWidth: true + Layout.fillHeight: true + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + + ColumnLayout { + width: appearanceScrollView.availableWidth + spacing: Style.marginM + + // Dark Mode Toggle + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + Rectangle { + width: 28 + height: 28 + radius: Style.radiusM + color: Color.mSurface + + NIcon { + icon: "moon" + pointSize: Style.fontSizeL + color: Color.mPrimary + anchors.centerIn: parent + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + NText { + text: I18n.tr("tooltips.switch-to-dark-mode") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: I18n.tr("panels.color-scheme.dark-mode-switch-description") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + NToggle { + checked: Settings.data.colorSchemes.darkMode + onToggled: checked => Settings.data.colorSchemes.darkMode = checked + } + } + + // Divider + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + opacity: 0.2 + Layout.topMargin: Style.marginS + Layout.bottomMargin: Style.marginS + } + + // Wallpaper Colors Toggle + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + Rectangle { + width: 28 + height: 28 + radius: Style.radiusM + color: Color.mSurface + + NIcon { + icon: "color-picker" + pointSize: Style.fontSizeL + color: Color.mPrimary + anchors.centerIn: parent + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + NText { + text: I18n.tr("panels.color-scheme.color-source-use-wallpaper-colors-label") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: I18n.tr("panels.color-scheme.color-source-use-wallpaper-colors-description") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + NToggle { + checked: Settings.data.colorSchemes.useWallpaperColors + onToggled: checked => { + if (checked) { + Settings.data.colorSchemes.useWallpaperColors = true; + AppThemeService.generate(); + } else { + Settings.data.colorSchemes.useWallpaperColors = false; + if (Settings.data.colorSchemes.predefinedScheme) { + ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme); + } + } + } + } + } + + // Divider + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + opacity: 0.2 + Layout.topMargin: Style.marginS + Layout.bottomMargin: Style.marginS + visible: !Settings.data.colorSchemes.useWallpaperColors + } + + // Predefined schemes section (visible when wallpaper colors disabled) + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginM + visible: !Settings.data.colorSchemes.useWallpaperColors + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + Rectangle { + width: 28 + height: 28 + radius: Style.radiusM + color: Color.mSurface + + NIcon { + icon: "palette" + pointSize: Style.fontSizeL + color: Color.mPrimary + anchors.centerIn: parent + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + NText { + text: I18n.tr("panels.color-scheme.predefined-title") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: I18n.tr("panels.color-scheme.predefined-desc") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + } + + // Predefined schemes Grid (matches ColorSchemeTab) + GridLayout { + id: schemesGrid + columns: Math.max(2, Math.floor((parent.width - Style.margin2M) / 180)) + rowSpacing: Style.marginM + columnSpacing: Style.marginM + Layout.fillWidth: true + + Repeater { + model: ColorSchemeService.schemes + + delegate: Rectangle { + id: schemeItem + + property string schemePath: modelData + property string schemeName: root.extractSchemeName(modelData) + + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + height: 50 + radius: Style.radiusS + color: root.cacheVersion >= 0 ? root.getSchemeColor(schemeName, "mSurface") : root.getSchemeColor(schemeName, "mSurface") + border.width: Style.borderL + border.color: itemMouseArea.containsMouse ? Color.mHover : (Settings.data.colorSchemes.predefinedScheme === schemeName ? Color.mSecondary : Color.mOutline) + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginXS + + NText { + text: schemeItem.schemeName + pointSize: Style.fontSizeS + font.weight: Style.fontWeightMedium + color: Color.mOnSurface + Layout.fillWidth: true + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + wrapMode: Text.WordWrap + maximumLineCount: 1 + } + + Rectangle { + width: 14 + height: 14 + radius: width * 0.5 + color: root.cacheVersion >= 0 ? (function () { + var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"; + var cached = root.schemeColorsCache[schemeItem.schemeName]; + return (cached && cached[mode] && cached[mode].mPrimary) || root.getSchemeColor(schemeItem.schemeName, "mPrimary"); + })() : Color.mPrimary + } + Rectangle { + width: 14 + height: 14 + radius: width * 0.5 + color: root.cacheVersion >= 0 ? (function () { + var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"; + var cached = root.schemeColorsCache[schemeItem.schemeName]; + return (cached && cached[mode] && cached[mode].mSecondary) || root.getSchemeColor(schemeItem.schemeName, "mSecondary"); + })() : Color.mSecondary + } + Rectangle { + width: 14 + height: 14 + radius: width * 0.5 + color: root.cacheVersion >= 0 ? (function () { + var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"; + var cached = root.schemeColorsCache[schemeItem.schemeName]; + return (cached && cached[mode] && cached[mode].mTertiary) || root.getSchemeColor(schemeItem.schemeName, "mTertiary"); + })() : Color.mTertiary + } + Rectangle { + width: 14 + height: 14 + radius: width * 0.5 + color: root.cacheVersion >= 0 ? (function () { + var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"; + var cached = root.schemeColorsCache[schemeItem.schemeName]; + return (cached && cached[mode] && cached[mode].mError) || root.getSchemeColor(schemeItem.schemeName, "mError"); + })() : Color.mError + } + } + + MouseArea { + id: itemMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.data.colorSchemes.useWallpaperColors = false; + Settings.data.colorSchemes.predefinedScheme = schemeItem.schemeName; + ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme); + } + } + } + } + } + } + + // Bottom spacer + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + } + } + + // Hidden loader to populate schemeColorsCache from files + Item { + visible: false + Repeater { + model: ColorSchemeService.schemes + delegate: Item { + FileView { + path: modelData + blockLoading: false + onLoaded: { + var schemeName = root.extractSchemeName(path); + try { + var jsonData = JSON.parse(text()); + root.schemeLoaded(schemeName, jsonData); + } catch (e) { + root.schemeLoaded(schemeName, null); + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SetupWizard/SetupCustomizeStep.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SetupWizard/SetupCustomizeStep.qml new file mode 100644 index 0000000..955f29c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SetupWizard/SetupCustomizeStep.qml @@ -0,0 +1,617 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + + property real selectedScaleRatio: 1.0 + property string selectedBarPosition: "top" + + signal scaleRatioChanged(real ratio) + signal barPositionChanged(string position) + + spacing: Style.marginM + + // Beautiful header with icon + RowLayout { + Layout.fillWidth: true + Layout.bottomMargin: Style.marginL + spacing: Style.marginM + + Rectangle { + width: 40 + height: 40 + radius: Style.radiusL + color: Color.mSurfaceVariant + opacity: 0.6 + + NIcon { + icon: "palette" + pointSize: Style.fontSizeL + color: Color.mPrimary + anchors.centerIn: parent + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NText { + text: I18n.tr("setup.customize.header") + pointSize: Style.fontSizeXL + font.weight: Style.fontWeightBold + color: Color.mPrimary + } + + NText { + text: I18n.tr("setup.customize.subheader") + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + } + } + + NScrollView { + id: customizeScrollView + Layout.fillWidth: true + Layout.fillHeight: true + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + + ColumnLayout { + width: customizeScrollView.availableWidth + spacing: Style.marginM + + // Bar Position section + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginM + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + Rectangle { + width: 28 + height: 28 + radius: Style.radiusM + color: Color.mSurface + + NIcon { + icon: "layout-2" + pointSize: Style.fontSizeL + color: Color.mPrimary + anchors.centerIn: parent + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + NText { + text: I18n.tr("panels.bar.appearance-position-label") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: I18n.tr("panels.bar.appearance-position-description") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + Repeater { + model: [ + { + "key": "top", + "name": I18n.tr("positions.top"), + "icon": "arrow-up" + }, + { + "key": "bottom", + "name": I18n.tr("positions.bottom"), + "icon": "arrow-down" + }, + { + "key": "left", + "name": I18n.tr("positions.left"), + "icon": "arrow-left" + }, + { + "key": "right", + "name": I18n.tr("positions.right"), + "icon": "arrow-right" + } + ] + delegate: Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 40 + radius: Style.radiusM + border.width: Style.borderS + + property bool isActive: selectedBarPosition === modelData.key + + color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mSurfaceVariant + border.color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mOutline + opacity: (hoverHandler.hovered || isActive) ? 1.0 : 0.8 + + NText { + text: modelData.name + pointSize: Style.fontSizeM + font.weight: (hoverHandler.hovered || parent.isActive) ? Style.fontWeightBold : Style.fontWeightMedium + color: (hoverHandler.hovered || parent.isActive) ? Color.mOnPrimary : Color.mOnSurface + anchors.centerIn: parent + } + + HoverHandler { + id: hoverHandler + } + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + selectedBarPosition = modelData.key; + barPositionChanged(modelData.key); + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + } + } + } + + // Divider + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + opacity: 0.2 + Layout.topMargin: Style.marginS + Layout.bottomMargin: Style.marginS + } + + // Bar Density section + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + Rectangle { + width: 32 + height: 32 + radius: Style.radiusM + color: Color.mSurface + NIcon { + icon: "minimize" + pointSize: Style.fontSizeL + color: Color.mPrimary + anchors.centerIn: parent + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + NText { + text: I18n.tr("panels.bar.appearance-density-label") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + NText { + text: I18n.tr("panels.bar.appearance-density-description") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + RowLayout { + spacing: Style.marginS + Repeater { + model: [ + { + "key": "mini", + "name": I18n.tr("options.bar.density-mini") + }, + { + "key": "compact", + "name": I18n.tr("options.bar.density-compact") + }, + { + "key": "default", + "name": I18n.tr("options.bar.density-default") + }, + { + "key": "comfortable", + "name": I18n.tr("options.bar.density-comfortable") + }, + { + "key": "spacious", + "name": I18n.tr("options.bar.density-spacious") + } + ] + delegate: Rectangle { + radius: Style.radiusM + border.width: Style.borderS + Layout.preferredHeight: 32 + Layout.preferredWidth: Math.max(90, densityText.implicitWidth + Style.margin2XL) + + property bool isActive: Settings.data.bar.density === modelData.key + + color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mSurfaceVariant + border.color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mOutline + opacity: (hoverHandler.hovered || isActive) ? 1.0 : 0.8 + + NText { + id: densityText + text: modelData.name + pointSize: Style.fontSizeS + font.weight: (hoverHandler.hovered || parent.isActive) ? Style.fontWeightBold : Style.fontWeightMedium + color: (hoverHandler.hovered || parent.isActive) ? Color.mOnPrimary : Color.mOnSurface + anchors.centerIn: parent + } + + HoverHandler { + id: hoverHandler + } + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.data.bar.density = modelData.key; + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + } + } + } + + // Divider + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + opacity: 0.2 + Layout.topMargin: Style.marginS + Layout.bottomMargin: Style.marginS + } + + // UI Scale section + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginM + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + Rectangle { + width: 32 + height: 32 + radius: Style.radiusM + color: Color.mSurface + NIcon { + icon: "maximize" + pointSize: Style.fontSizeL + color: Color.mPrimary + anchors.centerIn: parent + } + } + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + NText { + text: I18n.tr("panels.user-interface.scaling-label") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + NText { + text: I18n.tr("panels.user-interface.scaling-description") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + } + + NValueSlider { + Layout.fillWidth: true + from: 0.8 + to: 1.2 + stepSize: 0.05 + value: selectedScaleRatio + onMoved: function (value) { + selectedScaleRatio = value; + scaleRatioChanged(value); + } + text: Math.floor(selectedScaleRatio * 100) + "%" + } + } + + // Divider + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + opacity: 0.2 + Layout.topMargin: Style.marginS + Layout.bottomMargin: Style.marginS + } + + // Bar Type section + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginM + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + Rectangle { + width: 28 + height: 28 + radius: Style.radiusM + color: Color.mSurface + + NIcon { + icon: "layout-2" + pointSize: Style.fontSizeL + color: Color.mPrimary + anchors.centerIn: parent + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + NText { + text: I18n.tr("panels.bar.appearance-type-label") ?? "Bar Type" + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + + NText { + text: I18n.tr("panels.bar.appearance-type-description") ?? "Choose the style of the bar: Simple, Floating or Framed" + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + Repeater { + model: [ + { + "key": "simple", + "name": I18n.tr("options.bar.type-simple") ?? "Simple" + }, + { + "key": "floating", + "name": I18n.tr("options.bar.type-floating") ?? "Floating" + }, + { + "key": "framed", + "name": I18n.tr("options.bar.type-framed") ?? "Framed" + } + ] + delegate: Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 40 + radius: Style.radiusM + border.width: Style.borderS + + property bool isActive: Settings.data.bar.barType === modelData.key + + color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mSurfaceVariant + border.color: (hoverHandler.hovered || isActive) ? Color.mPrimary : Color.mOutline + opacity: (hoverHandler.hovered || isActive) ? 1.0 : 0.8 + + NText { + text: modelData.name + pointSize: Style.fontSizeM + font.weight: (hoverHandler.hovered || parent.isActive) ? Style.fontWeightBold : Style.fontWeightMedium + color: (hoverHandler.hovered || parent.isActive) ? Color.mOnPrimary : Color.mOnSurface + anchors.centerIn: parent + } + + HoverHandler { + id: hoverHandler + } + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Settings.data.bar.barType = modelData.key; + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + } + } + } + + // Divider + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + opacity: 0.2 + Layout.topMargin: Style.marginS + Layout.bottomMargin: Style.marginS + } + + // Divider + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + opacity: 0.2 + Layout.topMargin: Style.marginS + Layout.bottomMargin: Style.marginS + } + + // Dim Desktop opacity + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + Rectangle { + width: 32 + height: 32 + radius: Style.radiusM + color: Color.mSurface + NIcon { + icon: "screen-share" + pointSize: Style.fontSizeL + color: Color.mPrimary + anchors.centerIn: parent + } + } + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + NText { + text: I18n.tr("panels.user-interface.dimmer-opacity-label") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + NText { + text: I18n.tr("panels.user-interface.dimmer-opacity-description") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + NValueSlider { + Layout.fillWidth: true + Layout.topMargin: Style.marginXS + from: 0 + to: 1 + stepSize: 0.01 + value: Settings.data.general.dimmerOpacity + onMoved: value => Settings.data.general.dimmerOpacity = value + text: Math.floor(Settings.data.general.dimmerOpacity * 100) + "%" + } + } + } + + // Divider + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + opacity: 0.2 + Layout.topMargin: Style.marginS + Layout.bottomMargin: Style.marginS + } + + // Drop Shadows toggle + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + Rectangle { + width: 32 + height: 32 + radius: Style.radiusM + color: Color.mSurface + NIcon { + icon: "shadow" + pointSize: Style.fontSizeL + color: Color.mPrimary + anchors.centerIn: parent + } + } + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + NText { + text: I18n.tr("panels.user-interface.shadows-label") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + } + NText { + text: I18n.tr("panels.user-interface.shadows-description") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + NToggle { + checked: Settings.data.general.enableShadows + onToggled: checked => Settings.data.general.enableShadows = checked + } + } + + Item { + Layout.fillWidth: true + Layout.preferredHeight: Style.marginL + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SetupWizard/SetupDockStep.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SetupWizard/SetupDockStep.qml new file mode 100644 index 0000000..103e47f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SetupWizard/SetupDockStep.qml @@ -0,0 +1,210 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.Compositor +import qs.Widgets + +ColumnLayout { + id: root + + spacing: Style.marginM + + // Header + RowLayout { + Layout.fillWidth: true + Layout.bottomMargin: Style.marginL + spacing: Style.marginM + + Rectangle { + width: 40 + height: 40 + radius: Style.radiusL + color: Color.mSurfaceVariant + opacity: 0.6 + + NIcon { + icon: "device-desktop" + pointSize: Style.fontSizeL + color: Color.mPrimary + anchors.centerIn: parent + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NText { + text: I18n.tr("panels.dock.title") || "Dock" + pointSize: Style.fontSizeXL + font.weight: Style.fontWeightBold + color: Color.mPrimary + } + + NText { + text: I18n.tr("panels.dock.monitors-desc") + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + } + } + + // Options + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginL + + NToggle { + Layout.fillWidth: true + label: I18n.tr("panels.dock.enabled-label") + description: I18n.tr("panels.dock.enabled-description") + checked: Settings.data.dock.enabled + onToggled: checked => Settings.data.dock.enabled = checked + } + + // Display behavior + NComboBox { + visible: Settings.data.dock.enabled + Layout.fillWidth: true + label: I18n.tr("panels.display.title") + description: I18n.tr("panels.dock.appearance-display-description") + model: [ + { + "key": "always_visible", + "name": I18n.tr("hide-modes.visible") + }, + { + "key": "auto_hide", + "name": I18n.tr("panels.dock.appearance-display-auto-hide") + }, + { + "key": "exclusive", + "name": I18n.tr("panels.dock.appearance-display-exclusive") + } + ] + currentKey: Settings.data.dock.displayMode + onSelected: key => Settings.data.dock.displayMode = key + } + + // Background opacity + ColumnLayout { + visible: Settings.data.dock.enabled + spacing: Style.marginXXS + Layout.fillWidth: true + NLabel { + label: I18n.tr("panels.osd.background-opacity-label") + description: I18n.tr("panels.dock.appearance-background-opacity-description") + } + NValueSlider { + Layout.fillWidth: true + from: 0 + to: 1 + stepSize: 0.01 + value: Settings.data.dock.backgroundOpacity + onMoved: value => Settings.data.dock.backgroundOpacity = value + text: Math.floor(Settings.data.dock.backgroundOpacity * 100) + "%" + } + } + + // Floating distance + ColumnLayout { + visible: Settings.data.dock.enabled + spacing: Style.marginXXS + Layout.fillWidth: true + NLabel { + label: I18n.tr("panels.dock.appearance-floating-distance-label") + description: I18n.tr("panels.dock.appearance-floating-distance-description") + } + NValueSlider { + Layout.fillWidth: true + from: 0 + to: 4 + stepSize: 0.01 + value: Settings.data.dock.floatingRatio + onMoved: value => Settings.data.dock.floatingRatio = value + text: Math.floor(Settings.data.dock.floatingRatio * 100) + "%" + } + } + + // Icon size + ColumnLayout { + visible: Settings.data.dock.enabled + spacing: Style.marginXXS + Layout.fillWidth: true + NLabel { + label: I18n.tr("panels.dock.appearance-icon-size-label") + description: I18n.tr("panels.dock.appearance-icon-size-description") + } + NValueSlider { + Layout.fillWidth: true + from: 0 + to: 2 + stepSize: 0.01 + value: Settings.data.dock.size + onMoved: value => Settings.data.dock.size = value + text: Math.floor(Settings.data.dock.size * 100) + "%" + } + } + + NToggle { + visible: Settings.data.dock.enabled + Layout.fillWidth: true + label: I18n.tr("panels.dock.monitors-only-same-monitor-label") + description: I18n.tr("panels.dock.monitors-only-same-monitor-description") + checked: Settings.data.dock.onlySameOutput + onToggled: checked => Settings.data.dock.onlySameOutput = checked + } + + NToggle { + visible: Settings.data.dock.enabled + Layout.fillWidth: true + label: I18n.tr("panels.dock.appearance-colorize-icons-label") + description: I18n.tr("panels.dock.appearance-colorize-icons-description") + checked: Settings.data.dock.colorizeIcons + onToggled: checked => Settings.data.dock.colorizeIcons = checked + } + + NHeader { + visible: Settings.data.dock.enabled + label: I18n.tr("panels.dock.monitors-title") + description: I18n.tr("panels.dock.monitors-desc") + } + + Repeater { + visible: Settings.data.dock.enabled + model: Quickshell.screens || [] + delegate: NCheckbox { + Layout.fillWidth: true + readonly property real compositorScale: { + const info = CompositorService.displayScales[modelData.name]; + return (info && info.scale) ? info.scale : 1.0; + } + label: modelData.name || "Unknown" + visible: Settings.data.dock.enabled + description: { + I18n.tr("system.monitor-description", { + "model": modelData.model, + "width": modelData.width * compositorScale, + "height": modelData.height * compositorScale, + "scale": compositorScale + }); + } + checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1 + onToggled: checked => { + if (checked) { + const arr = (Settings.data.dock.monitors || []).slice(); + if (arr.indexOf(modelData.name) === -1) + arr.push(modelData.name); + Settings.data.dock.monitors = arr; + } else { + Settings.data.dock.monitors = (Settings.data.dock.monitors || []).filter(function (n) { + return n !== modelData.name; + }); + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SetupWizard/SetupWallpaperStep.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SetupWizard/SetupWallpaperStep.qml new file mode 100644 index 0000000..fa8c410 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SetupWizard/SetupWallpaperStep.qml @@ -0,0 +1,463 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.UI +import qs.Widgets + +ColumnLayout { + id: root + + property string selectedDirectory: "" + property string selectedWallpaper: "" + property string previewCachedPath: "" + + signal directoryChanged(string directory) + signal wallpaperChanged(string wallpaper) + + onSelectedWallpaperChanged: { + if (!selectedWallpaper) { + previewCachedPath = ""; + return; + } + if (ImageCacheService.initialized) { + ImageCacheService.getLarge(selectedWallpaper, Screen.width, Screen.height, function (path, success) { + if (root) + root.previewCachedPath = success ? path : selectedWallpaper; + }); + } else { + previewCachedPath = selectedWallpaper; + } + } + + spacing: Style.marginL + + // Beautiful header with icon + ColumnLayout { + Layout.fillWidth: true + Layout.bottomMargin: Style.marginL + spacing: Style.marginM + + RowLayout { + spacing: Style.marginM + + Rectangle { + width: 40 + height: 40 + radius: Style.radiusL + color: Color.mSurfaceVariant + opacity: 0.6 + + NIcon { + icon: "image" + pointSize: Style.fontSizeL + color: Color.mPrimary + anchors.centerIn: parent + } + } + + ColumnLayout { + spacing: Style.marginXS + + NText { + text: I18n.tr("setup.wallpaper.header") + pointSize: Style.fontSizeXL + font.weight: Style.fontWeightBold + color: Color.mPrimary + } + + NText { + text: I18n.tr("setup.wallpaper.subheader") + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + } + } + } + } + + // Large preview area + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 180 + color: Color.mSurfaceVariant + radius: Style.radiusL + + // Image with rounded corners + NImageRounded { + anchors.fill: parent + visible: previewCachedPath !== "" + imagePath: previewCachedPath + radius: Style.radiusL + borderColor: previewCachedPath !== "" ? Color.mPrimary : Color.mOutline + borderWidth: previewCachedPath !== "" ? 2 : 1 + imageFillMode: Image.PreserveAspectCrop + } + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginL + visible: selectedWallpaper === "" + + Rectangle { + Layout.alignment: Qt.AlignHCenter + width: 64 + height: 64 + radius: width / 2 + color: Color.mPrimary + + NIcon { + icon: "sparkles" + pointSize: Style.fontSizeXXL + color: Color.mOnPrimary + anchors.centerIn: parent + } + } + + NText { + text: I18n.tr("setup.wallpaper.select-prompt") + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + font.weight: Style.fontWeightMedium + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + // Wallpaper gallery strip + Item { + Layout.fillWidth: true + Layout.preferredHeight: 92 + visible: filteredWallpapers.length > 0 + + NScrollView { + id: galleryScroll + anchors.fill: parent + horizontalPolicy: ScrollBar.AsNeeded + verticalPolicy: ScrollBar.AlwaysOff + showGradientMasks: false + reserveScrollbarSpace: false + + // Enable vertical mouse wheel to scroll the horizontal strip by moving contentX + WheelHandler { + target: galleryScroll.contentItem + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onWheel: event => { + const flick = galleryScroll.contentItem; + if (!flick) + return; + const delta = event.pixelDelta.x !== 0 || event.pixelDelta.y !== 0 ? (event.pixelDelta.y !== 0 ? event.pixelDelta.y : event.pixelDelta.x) : (event.angleDelta.y !== 0 ? event.angleDelta.y : event.angleDelta.x); + // Move opposite of wheel to scroll content to the right for wheel down + const step = -delta; + const maxX = Math.max(0, flick.contentWidth - flick.width); + let newX = flick.contentX + step; + if (newX < 0) + newX = 0; + if (newX > maxX) + newX = maxX; + flick.contentX = newX; + event.accepted = true; + } + } + + RowLayout { + spacing: Style.marginM + height: parent.height + + Repeater { + model: filteredWallpapers + delegate: Item { + id: thumbDelegate + readonly property int baseWidth: 120 + readonly property int baseHeight: 80 + readonly property int thumbRadius: Style.radiusM + property string cachedPath: "" + + Layout.preferredWidth: baseWidth + Layout.preferredHeight: baseHeight + + Component.onCompleted: { + if (ImageCacheService.initialized) { + ImageCacheService.getThumbnail(modelData, function (path, success) { + if (thumbDelegate) + thumbDelegate.cachedPath = success ? path : modelData; + }); + } else { + cachedPath = modelData; + } + } + + // Rounded thumbnail using NImageRounded with caching + NImageRounded { + id: thumbImage + anchors.fill: parent + imagePath: thumbDelegate.cachedPath + radius: thumbDelegate.thumbRadius + borderColor: selectedWallpaper === modelData ? Color.mPrimary : Color.mOutline + borderWidth: selectedWallpaper === modelData ? 2 : 1 + imageFillMode: Image.PreserveAspectCrop + } + + // Loading/error state background + Rectangle { + anchors.fill: parent + color: Color.mSurfaceVariant + radius: thumbDelegate.thumbRadius + visible: thumbImage.status === Image.Loading || thumbImage.status === Image.Error || thumbDelegate.cachedPath === "" + + NIcon { + icon: "image" + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + anchors.centerIn: parent + } + } + + NBusyIndicator { + anchors.centerIn: parent + visible: thumbImage.status === Image.Loading || thumbDelegate.cachedPath === "" + running: visible + size: 18 + } + + // Hover overlay + Rectangle { + anchors.fill: parent + radius: thumbDelegate.thumbRadius + color: Color.mPrimary + opacity: hoverHandler.hovered ? 0.1 : 0 + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + + // Selection badge + Rectangle { + visible: selectedWallpaper === modelData + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 6 + width: 24 + height: 24 + radius: width / 2 + color: Color.mPrimary + + NIcon { + icon: "check" + pointSize: Style.fontSizeS + color: Color.mOnPrimary + anchors.centerIn: parent + } + } + + HoverHandler { + id: hoverHandler + } + + TapHandler { + onTapped: { + selectedWallpaper = modelData; + wallpaperChanged(modelData); + } + } + } + } + } + } + } + + // Helpful info card + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 80 + color: Color.mSurfaceVariant + radius: Style.radiusM + opacity: 0.4 + visible: filteredWallpapers.length === 0 + + RowLayout { + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + NIcon { + icon: "folder-open" + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginXS + NText { + text: filteredWallpapers.length === 0 && selectedDirectory !== "" ? I18n.tr("setup.wallpaper.none-in-dir") : I18n.tr("setup.wallpaper.no-dir") + pointSize: Style.fontSizeM + font.weight: Style.fontWeightBold + color: Color.mOnSurfaceVariant + } + NText { + text: selectedDirectory !== "" ? I18n.tr("setup.wallpaper.no-valid", { + "dir": selectedDirectory + }) : I18n.tr("setup.wallpaper.choose-dir") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + opacity: 0.8 + } + } + } + } + + // Directory selection + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NTextInputButton { + id: wallpaperPathInput + label: I18n.tr("setup.wallpaper.dir-label") + description: I18n.tr("setup.wallpaper.dir-description") + text: selectedDirectory + buttonIcon: "folder-open" + buttonTooltip: I18n.tr("setup.wallpaper.dir-browse") + Layout.fillWidth: true + onInputTextChanged: text => { + selectedDirectory = text; + directoryChanged(text); + } + onButtonClicked: directoryPicker.open() + } + } + + // Internal properties and functions + property list wallpapersList: [] + property list filteredWallpapers: [] + + function updateFilteredWallpapers() { + filteredWallpapers = wallpapersList; + } + + function refreshWallpapers() { + if (!selectedDirectory || selectedDirectory === "") { + wallpapersList = []; + filteredWallpapers = []; + return; + } + if (typeof WallpaperService !== "undefined" && WallpaperService.getWallpapersList) { + var wallpapers = WallpaperService.getWallpapersList(Screen.name); + wallpapersList = wallpapers; + updateFilteredWallpapers(); + if (wallpapersList.length > 0 && selectedWallpaper === "") { + selectedWallpaper = wallpapersList[0]; + } + } else { + readDirectoryImages(selectedDirectory); + } + } + + function readDirectoryImages(directoryPath) { + // Build find command args dynamically from ImageCacheService filters + var filters = ImageCacheService.imageFilters; + var findArgs = ["find", directoryPath, "-type", "f", "("]; + for (var i = 0; i < filters.length; i++) { + if (i > 0) { + findArgs.push("-o"); + } + findArgs.push("-iname"); + findArgs.push(filters[i]); + } + findArgs.push(")"); + directoryScanner.command = findArgs; + directoryScanner.running = true; + return []; + } + + onSelectedDirectoryChanged: { + if (typeof Settings !== "undefined" && Settings.data && Settings.data.wallpaper) { + Settings.data.wallpaper.directory = selectedDirectory; + } + if (typeof WallpaperService !== "undefined" && WallpaperService.refreshWallpapersList) { + WallpaperService.refreshWallpapersList(); + } + Qt.callLater(refreshWallpapers); + } + + Connections { + target: WallpaperService + enabled: typeof WallpaperService !== "undefined" + function onWallpaperListChanged(screenName, count) { + if (screenName === Screen.name) { + Qt.callLater(refreshWallpapers); + } + } + } + + Timer { + id: initialRefreshTimer + interval: 1000 + running: false + repeat: false + onTriggered: refreshWallpapers() + } + + Component.onCompleted: { + if (typeof Settings !== "undefined" && Settings.data && Settings.data.wallpaper && Settings.data.wallpaper.directory) { + selectedDirectory = Settings.data.wallpaper.directory; + } else { + selectedDirectory = Quickshell.env("HOME") + "/Pictures/Wallpapers"; + } + if (typeof WallpaperService !== "undefined" && WallpaperService.currentWallpaper) { + selectedWallpaper = WallpaperService.currentWallpaper; + } + initialRefreshTimer.start(); + } + + NFilePicker { + id: directoryPicker + selectionMode: "folders" + title: I18n.tr("setup.wallpaper.dir-select-title") + initialPath: selectedDirectory || Quickshell.env("HOME") + "/Pictures" + onAccepted: paths => { + if (paths.length > 0) { + selectedDirectory = paths[0]; + directoryChanged(paths[0]); + } + } + } + + Process { + id: directoryScanner + command: [] + running: false + stdout: StdioCollector {} + stderr: StdioCollector {} + onExited: function (exitCode) { + if (exitCode === 0) { + var lines = stdout.text.split('\n'); + var images = []; + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (line !== '') { + images.push(line); + } + } + wallpapersList = images; + updateFilteredWallpapers(); + if (wallpapersList.length > 0 && selectedWallpaper === "") { + selectedWallpaper = wallpapersList[0]; + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SetupWizard/SetupWizard.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SetupWizard/SetupWizard.qml new file mode 100644 index 0000000..849e4dd --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SetupWizard/SetupWizard.qml @@ -0,0 +1,477 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Modules.MainScreen +import qs.Services.Noctalia +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +SmartPanel { + id: root + + // When true, only shows step 0 with modified text for returning users (telemetry notification) + property bool telemetryOnlyMode: false + + signal telemetryWizardCompleted + + preferredWidth: Math.round(preferredWidthRatio * 2560 * Style.uiScaleRatio) + preferredHeight: Math.round(preferredHeightRatio * 1440 * Style.uiScaleRatio) + preferredWidthRatio: 0.4 + preferredHeightRatio: root.telemetryOnlyMode ? 0.45 : 0.6 + + panelAnchorHorizontalCenter: true + panelAnchorVerticalCenter: true + + closeWithEscape: false + + panelContent: Item { + id: panelContent + + // Wizard state (lazy-loaded with panelContent) + property int currentStep: 0 + readonly property int totalSteps: root.telemetryOnlyMode ? 1 : 5 + property bool isCompleting: false + + // Setup wizard data + property string selectedWallpaperDirectory: Settings.defaultWallpapersDirectory + property string selectedWallpaper: "" + property real selectedScaleRatio: 1.0 + property string selectedBarPosition: "top" + + Component.onCompleted: { + selectedScaleRatio = Settings.data.general.scaleRatio; + selectedBarPosition = Settings.data.bar.position; + selectedWallpaperDirectory = Settings.data.wallpaper.directory || Settings.defaultWallpapersDirectory; + } + + Connections { + target: Settings + function onSettingsSaved() { + if (panelContent.isCompleting) { + Logger.i("SetupWizard", "Settings saved, closing panel"); + panelContent.isCompleting = false; + root.close(); + } + } + } + + Timer { + id: closeTimer + interval: 2000 + onTriggered: { + if (panelContent.isCompleting) { + Logger.w("SetupWizard", "Settings save timeout, closing panel anyway"); + panelContent.isCompleting = false; + root.close(); + } + } + } + + function completeSetup() { + if (isCompleting) { + Logger.w("SetupWizard", "completeSetup() called while already completing, ignoring"); + return; + } + + try { + Logger.i("SetupWizard", root.telemetryOnlyMode ? "Completing telemetry wizard" : "Completing setup with selected options"); + isCompleting = true; + + // In telemetry-only mode, we only need to save the telemetry setting + if (!root.telemetryOnlyMode) { + if (typeof WallpaperService !== "undefined" && WallpaperService.refreshWallpapersList) { + if (selectedWallpaperDirectory !== Settings.data.wallpaper.directory) { + Settings.data.wallpaper.directory = selectedWallpaperDirectory; + WallpaperService.refreshWallpapersList(); + } + + if (selectedWallpaper !== "") { + WallpaperService.changeWallpaper(selectedWallpaper, undefined); + } + } + + Settings.data.general.scaleRatio = selectedScaleRatio; + Settings.data.bar.position = selectedBarPosition; + } + + // Mark the current version as seen to prevent telemetry wizard on next startup + // (only for full setup wizard - telemetry wizard lets changelog mark it seen) + if (!root.telemetryOnlyMode) { + UpdateService.markChangelogSeen(UpdateService.currentVersion); + } + + // Initialize telemetry now that user has made their choice + TelemetryService.init(); + + // Save settings immediately and wait for settingsSaved signal before closing + Settings.saveImmediate(); + Logger.i("SetupWizard", "Setup completed successfully, waiting for settings save confirmation"); + + // Emit signal for telemetry wizard completion (shell.qml will show changelog) + if (root.telemetryOnlyMode) { + root.telemetryWizardCompleted(); + } + + // Fallback: if settingsSaved signal doesn't fire within 2 seconds, close anyway + closeTimer.start(); + } catch (error) { + Logger.e("SetupWizard", "Error completing setup:", error); + isCompleting = false; + } + } + + function applyWallpaperSettings() { + if (typeof WallpaperService !== "undefined" && WallpaperService.refreshWallpapersList) { + if (selectedWallpaperDirectory !== Settings.data.wallpaper.directory) { + Settings.data.wallpaper.directory = selectedWallpaperDirectory; + WallpaperService.refreshWallpapersList(); + } + + if (selectedWallpaper !== "") { + WallpaperService.changeWallpaper(selectedWallpaper, undefined); + } + } + } + + function applyUISettings() { + Settings.data.general.scaleRatio = selectedScaleRatio; + Settings.data.bar.position = selectedBarPosition; + } + + ColumnLayout { + id: wizardContent + anchors.fill: parent + anchors.margins: Style.marginXL + spacing: Style.marginL + + // Step content - takes most of the space + Item { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: Math.round(300 * Style.uiScaleRatio) + + StackLayout { + id: stepStack + anchors.fill: parent + currentIndex: currentStep + + // Step 0: Welcome - Beautiful centered design + Item { + ColumnLayout { + anchors.centerIn: parent + width: Math.round(Math.max(parent.width * 0.5, 420)) + spacing: Style.marginXL + + // Logo with subtle glow effect + Item { + Layout.fillWidth: true + Layout.preferredHeight: 120 + Layout.alignment: Qt.AlignHCenter + + Rectangle { + anchors.centerIn: parent + width: 120 + height: 120 + radius: width / 2 + color: Color.mPrimary + opacity: 0.08 + scale: 1.3 + } + + Image { + anchors.centerIn: parent + width: 110 + height: 110 + source: Qt.resolvedUrl(Quickshell.shellDir + "/Assets/noctalia.svg") + fillMode: Image.PreserveAspectFit + smooth: true + + Rectangle { + anchors.fill: parent + color: Color.mSurfaceVariant + radius: width / 2 + border.color: Color.mOutline + border.width: Style.borderM + visible: parent.status === Image.Error + + NIcon { + icon: "sparkles" + pointSize: Style.fontSizeXXL * 1.5 + color: Color.mPrimary + anchors.centerIn: parent + } + } + + // Subtle pulse animation + SequentialAnimation on scale { + running: true + loops: Animation.Infinite + NumberAnimation { + from: 1.0 + to: 1.05 + duration: 2000 + easing.type: Easing.InOutQuad + } + NumberAnimation { + from: 1.05 + to: 1.0 + duration: 2000 + easing.type: Easing.InOutQuad + } + } + } + } + + // Welcome text with gradient feel + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + spacing: Style.marginM + + NText { + text: root.telemetryOnlyMode ? I18n.tr("setup.telemetry-wizard-title") : I18n.tr("setup.welcome-title") + pointSize: Style.fontSizeXXL * 1.4 + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + } + + NText { + text: root.telemetryOnlyMode ? I18n.tr("setup.telemetry-wizard-subtitle") : I18n.tr("setup.welcome-subtitle") + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + + // Friendly subtext + Rectangle { + Layout.fillWidth: true + Layout.topMargin: Style.marginL + Layout.preferredHeight: childrenRect.height + Style.margin2M + color: Color.mSurfaceVariant + radius: Style.radiusL + + NText { + anchors.centerIn: parent + width: parent.width - Style.margin2L + text: root.telemetryOnlyMode ? I18n.tr("setup.telemetry-wizard-note") : I18n.tr("setup.welcome-note") + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } + } + + // Telemetry toggle + NToggle { + Layout.fillWidth: true + Layout.topMargin: Style.marginM + label: I18n.tr("panels.about.telemetry-enabled") + description: I18n.tr("panels.about.telemetry-desc") + checked: Settings.data.general.telemetryEnabled + onToggled: checked => Settings.data.general.telemetryEnabled = checked + } + } + } + } + + // Step 1: Wallpaper Setup + SetupWallpaperStep { + id: step1 + selectedDirectory: panelContent.selectedWallpaperDirectory + selectedWallpaper: panelContent.selectedWallpaper + onDirectoryChanged: function (directory) { + panelContent.selectedWallpaperDirectory = directory; + panelContent.applyWallpaperSettings(); + } + onWallpaperChanged: function (wallpaper) { + panelContent.selectedWallpaper = wallpaper; + panelContent.applyWallpaperSettings(); + } + } + + // Step 2: Appearance - Dark mode and color source + SetupAppearanceStep { + id: step3 + } + + // Step 3: UI Configuration + SetupCustomizeStep { + id: step2 + selectedScaleRatio: panelContent.selectedScaleRatio + selectedBarPosition: panelContent.selectedBarPosition + onScaleRatioChanged: function (ratio) { + panelContent.selectedScaleRatio = ratio; + panelContent.applyUISettings(); + } + onBarPositionChanged: function (position) { + panelContent.selectedBarPosition = position; + panelContent.applyUISettings(); + } + } + + // Step 4: Dock Setup + SetupDockStep { + id: stepDock + } + } + } + + // Elegant divider + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + opacity: 0.2 + visible: !root.telemetryOnlyMode + } + + // Modern progress indicator with labels + Item { + Layout.fillWidth: true + Layout.preferredHeight: 32 + visible: !root.telemetryOnlyMode + + RowLayout { + anchors.centerIn: parent + spacing: Style.marginM + + Repeater { + model: [ + { + "icon": "sparkles", + "label": I18n.tr("setup.welcome") + }, + { + "icon": "image", + "label": I18n.tr("common.wallpaper") + }, + { + "icon": "palette", + "label": I18n.tr("common.appearance") + }, + { + "icon": "settings", + "label": I18n.tr("common.customize") + }, + { + "icon": "device-desktop", + "label": I18n.tr("panels.dock.title") + } + ] + delegate: RowLayout { + spacing: Style.marginS + + Rectangle { + width: 24 + height: 24 + radius: width / 2 + color: index <= currentStep ? Color.mPrimary : Color.mSurfaceVariant + border.color: index === currentStep ? Color.mPrimary : "transparent" + border.width: index === currentStep ? 2 : 0 + + NIcon { + icon: modelData.icon + pointSize: Style.fontSizeS + color: index <= currentStep ? Color.mOnPrimary : Color.mOnSurfaceVariant + anchors.centerIn: parent + } + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + } + } + } + + NText { + text: modelData.label + pointSize: Style.fontSizeS + color: index <= currentStep ? Color.mPrimary : Color.mOnSurfaceVariant + font.weight: index === currentStep ? Style.fontWeightBold : Style.fontWeightRegular + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + } + } + } + + // Connector line + Rectangle { + width: 40 + height: 2 + radius: 1 + color: index < currentStep ? Color.mPrimary : Color.mSurfaceVariant + visible: index < totalSteps - 1 + + Behavior on color { + ColorAnimation { + duration: Style.animationNormal + } + } + } + } + } + } + } + + // Smooth navigation buttons + Item { + Layout.fillWidth: true + Layout.preferredHeight: 44 + Layout.topMargin: Style.marginS + + RowLayout { + anchors.fill: parent + spacing: Style.marginM + + NButton { + text: I18n.tr("setup.skip-setup") + outlined: true + visible: !root.telemetryOnlyMode + Layout.preferredHeight: 44 + onClicked: { + panelContent.completeSetup(); + } + } + + Item { + Layout.fillWidth: true + } + + NButton { + text: "← " + I18n.tr("common.back") + outlined: true + visible: currentStep > 0 && !root.telemetryOnlyMode + Layout.preferredHeight: 44 + onClicked: { + if (currentStep > 0) { + currentStep--; + } + } + } + + NButton { + text: root.telemetryOnlyMode ? I18n.tr("setup.telemetry-wizard-done") : (currentStep === totalSteps - 1 ? I18n.tr("setup.all-done") : I18n.tr("common.continue") + " →") + Layout.preferredHeight: 44 + onClicked: { + if (currentStep < totalSteps - 1) { + currentStep++; + } else { + panelContent.completeSetup(); + } + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SystemStats/SystemStatsPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SystemStats/SystemStatsPanel.qml new file mode 100644 index 0000000..2cc85c1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/SystemStats/SystemStatsPanel.qml @@ -0,0 +1,403 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.MainScreen +import qs.Services.System +import qs.Services.UI +import qs.Widgets + +SmartPanel { + id: root + + Component.onCompleted: SystemStatService.registerComponent("panel-systemstats") + Component.onDestruction: SystemStatService.unregisterComponent("panel-systemstats") + + preferredWidth: Math.round(440 * Style.uiScaleRatio) + + panelContent: Item { + id: panelContent + property real contentPreferredHeight: mainColumn.implicitHeight + Style.margin2L + readonly property real cardHeight: 90 * Style.uiScaleRatio + + // Get diskPath from bar's SystemMonitor widget if available, otherwise use "/" + readonly property string diskPath: { + const sysMonWidget = BarService.lookupWidget("SystemMonitor"); + if (sysMonWidget && sysMonWidget.diskPath) { + return sysMonWidget.diskPath; + } + return "/"; + } + + ColumnLayout { + id: mainColumn + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + // HEADER + NBox { + Layout.fillWidth: true + implicitHeight: headerRow.implicitHeight + Style.margin2M + + RowLayout { + id: headerRow + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + NIcon { + icon: "device-analytics" + pointSize: Style.fontSizeXXL + color: Color.mPrimary + } + + NText { + text: I18n.tr("system-monitor.title") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + root.close(); + } + } + } + } + + // CPU Card (dual-line: usage % + temperature °C) + NBox { + Layout.fillWidth: true + Layout.preferredHeight: panelContent.cardHeight + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginS + anchors.bottomMargin: Style.radiusM * 0.5 + spacing: Style.marginXS + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NIcon { + icon: "cpu-usage" + pointSize: Style.fontSizeXS + color: Color.mPrimary + } + + NText { + text: `${Math.round(SystemStatService.cpuUsage)}% (${SystemStatService.cpuFreq.replace(/[^0-9.]/g, "")} GHz)` + pointSize: Style.fontSizeXS + color: Color.mPrimary + font.family: Settings.data.ui.fontFixed + } + + NIcon { + icon: "cpu-temperature" + pointSize: Style.fontSizeXS + color: Color.mSecondary + } + + NText { + text: `${Math.round(SystemStatService.cpuTemp)}°C` + pointSize: Style.fontSizeXS + color: Color.mSecondary + font.family: Settings.data.ui.fontFixed + Layout.rightMargin: Style.marginS + } + + Item { + Layout.fillWidth: true + } + + NText { + text: I18n.tr("system-monitor.cpu-usage") + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + } + + NGraph { + Layout.fillWidth: true + Layout.fillHeight: true + values: SystemStatService.cpuHistory + values2: SystemStatService.cpuTempHistory + minValue: 0 + maxValue: 100 + minValue2: Math.max(SystemStatService.cpuTempHistoryMin - 5, 0) + maxValue2: Math.max(SystemStatService.cpuTempHistoryMax + 5, 1) + color: Color.mPrimary + color2: Color.mSecondary + strokeWidth: Math.max(1, Style.uiScaleRatio) + fill: true + fillOpacity: 0.15 + updateInterval: SystemStatService.cpuUsageIntervalMs + } + } + } + + // Memory Card (single-line + optional swap indicator) + NBox { + Layout.fillWidth: true + Layout.preferredHeight: panelContent.cardHeight + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginS + anchors.bottomMargin: Style.radiusM * 0.5 + spacing: Style.marginXS + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NIcon { + icon: "memory" + pointSize: Style.fontSizeXS + color: Color.mPrimary + } + + NText { + text: `${Math.round(SystemStatService.memPercent)}% (${(SystemStatService.memGb).toFixed(1)} GiB)` + pointSize: Style.fontSizeXS + color: Color.mPrimary + font.family: Settings.data.ui.fontFixed + } + + Item { + Layout.fillWidth: true + } + + NText { + text: I18n.tr("common.memory") + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + } + + NGraph { + Layout.fillWidth: true + Layout.fillHeight: true + values: SystemStatService.memHistory + minValue: 0 + maxValue: 100 + color: Color.mPrimary + strokeWidth: Math.max(1, Style.uiScaleRatio) + fill: true + fillOpacity: 0.15 + updateInterval: SystemStatService.memIntervalMs + } + } + } + + // Network Card (dual-line: RX + TX speeds) + NBox { + Layout.fillWidth: true + Layout.preferredHeight: panelContent.cardHeight + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginS + anchors.bottomMargin: Style.radiusM * 0.5 + spacing: Style.marginXS + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginXS + + NIcon { + icon: "download-speed" + pointSize: Style.fontSizeXS + color: Color.mPrimary + } + + NText { + text: SystemStatService.formatSpeed(SystemStatService.rxSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2") + "/s" + pointSize: Style.fontSizeXS + color: Color.mPrimary + font.family: Settings.data.ui.fontFixed + Layout.rightMargin: Style.marginS + } + + NIcon { + icon: "upload-speed" + pointSize: Style.fontSizeXS + color: Color.mSecondary + } + + NText { + text: SystemStatService.formatSpeed(SystemStatService.txSpeed).replace(/([0-9.]+)([A-Za-z]+)/, "$1 $2") + "/s" + pointSize: Style.fontSizeXS + color: Color.mSecondary + font.family: Settings.data.ui.fontFixed + } + + Item { + Layout.fillWidth: true + } + + NText { + text: I18n.tr("common.network") + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + } + + NGraph { + Layout.fillWidth: true + Layout.fillHeight: true + values: SystemStatService.rxSpeedHistory + values2: SystemStatService.txSpeedHistory + minValue: 0 + maxValue: SystemStatService.rxMaxSpeed + minValue2: 0 + maxValue2: SystemStatService.txMaxSpeed + color: Color.mPrimary + color2: Color.mSecondary + strokeWidth: Math.max(1, Style.uiScaleRatio) + fill: true + fillOpacity: 0.15 + updateInterval: SystemStatService.networkIntervalMs + animateScale: true + } + } + } + + // Detailed Stats section + NBox { + Layout.fillWidth: true + implicitHeight: detailsColumn.implicitHeight + Style.margin2M + + ColumnLayout { + id: detailsColumn + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Style.marginM + spacing: Style.marginXS + + // Load Average + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + visible: SystemStatService.nproc > 0 + + NIcon { + icon: "cpu-usage" + pointSize: Style.fontSizeM + color: Color.mPrimary + } + + NText { + text: I18n.tr("system-monitor.load-average") + ":" + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + NText { + text: `${SystemStatService.loadAvg1.toFixed(2)} • ${SystemStatService.loadAvg5.toFixed(2)} • ${SystemStatService.loadAvg15.toFixed(2)}` + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + } + } + + // GPU Temperature (only if available) + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + visible: SystemStatService.gpuAvailable + + NIcon { + icon: "gpu-temperature" + pointSize: Style.fontSizeM + color: Color.mPrimary + } + + NText { + text: I18n.tr("system-monitor.gpu-temp") + ":" + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + NText { + text: `${Math.round(SystemStatService.gpuTemp)}°C` + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + } + } + + // Disk usage + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NIcon { + icon: "storage" + pointSize: Style.fontSizeM + color: Color.mPrimary + } + + NText { + text: I18n.tr("system-monitor.disk") + ":" + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + NText { + text: { + const usedGb = SystemStatService.diskUsedGb[panelContent.diskPath] || 0; + const sizeGb = SystemStatService.diskSizeGb[panelContent.diskPath] || 0; + const percent = SystemStatService.diskPercents[panelContent.diskPath] || 0; + return `${percent}% (${usedGb.toFixed(1)} / ${sizeGb.toFixed(1)} GB)`; + } + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + elide: Text.ElideMiddle + } + } + + // Swap details (only visible if swap is enabled) + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + visible: SystemStatService.swapTotalGb > 0 + + NIcon { + icon: "exchange" + pointSize: Style.fontSizeM + color: Color.mPrimary + } + + NText { + text: I18n.tr("bar.system-monitor.swap-usage-label") + ":" + pointSize: Style.fontSizeXS + color: Color.mOnSurfaceVariant + } + + NText { + text: `${(SystemStatService.swapGb).toFixed(1)} / ${(SystemStatService.swapTotalGb).toFixed(1)} GiB` + pointSize: Style.fontSizeXS + color: Color.mOnSurface + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Tray/TrayDrawerPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Tray/TrayDrawerPanel.qml new file mode 100644 index 0000000..e490587 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Tray/TrayDrawerPanel.qml @@ -0,0 +1,289 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.SystemTray +import Quickshell.Widgets +import qs.Commons +import qs.Modules.MainScreen +import qs.Services.UI +import qs.Widgets + +// A compact grid panel listing all tray items, opened from the Tray widget +SmartPanel { + id: root + + // Do not give exclusive focus to the TrayDrawer or it will prevent the dropdown menu to request it. + exclusiveKeyboard: false + + // Widget info for menu functionality (set by Tray widget when opening) + property string widgetSection: "" + property int widgetIndex: -1 + + // Sizing properties must stay at root for preferredWidth/Height + readonly property int maxColumns: 8 + readonly property real cellSize: Math.round(Style.capsuleHeight * 0.65) + readonly property real outerPadding: Style.marginM + readonly property real innerSpacing: Style.marginM + + // All tray items from SystemTray + readonly property var trayValuesAll: (SystemTray.items && SystemTray.items.values) ? SystemTray.items.values : [] + + // Filtered items - computed in panelContent where isPinned is available + property var trayValues: [] + + readonly property int itemCount: trayValues.length + readonly property int columns: Math.max(1, Math.min(maxColumns, itemCount)) + readonly property int rows: Math.max(1, Math.ceil(itemCount / Math.max(1, columns))) + + preferredWidth: (columns * cellSize) + ((columns - 1) * innerSpacing) + (2 * outerPadding) + preferredHeight: (rows * cellSize) + ((rows - 1) * innerSpacing) + (2 * outerPadding) + + // Auto-close drawer when all items are pinned (drawer becomes empty) + onTrayValuesChanged: { + if (visible && trayValues.length === 0) { + close(); + } + } + + // Force refresh panelContent settings when drawer opens + onOpened: { + if (panelContent && panelContent.settingsVersion !== undefined) { + panelContent.settingsVersion++; + } + } + + panelContent: Item { + id: panelContent + + // Settings state (lazy-loaded with panelContent) + property int settingsVersion: 0 + + readonly property var widgetSettings: { + // Reference settingsVersion to force recalculation when it changes + void (settingsVersion); + if (root.widgetSection === "" || root.widgetIndex < 0) + return {}; + var widgets = Settings.getBarWidgetsForScreen(root.screen?.name)[root.widgetSection]; + if (!widgets || root.widgetIndex >= widgets.length) + return {}; + var settings = widgets[root.widgetIndex]; + if (!settings || settings.id !== "Tray") + return {}; + return settings; + } + + readonly property var pinnedList: widgetSettings.pinned || [] + readonly property bool hidePassive: widgetSettings.hidePassive !== undefined ? widgetSettings.hidePassive : true + + // Filter tray items - this runs in panelContent context where isPinned is available + function updateFilteredItems() { + var filtered = []; + for (var i = 0; i < root.trayValuesAll.length; i++) { + var item = root.trayValuesAll[i]; + if (!item) + continue; + + // Filter out passive items if hidePassive is enabled + if (hidePassive && item.status !== undefined && (item.status === SystemTray.Passive || item.status === 0)) { + continue; + } + + // Filter out pinned items + if (isPinned(item)) { + continue; + } + + filtered.push(item); + } + root.trayValues = filtered; + } + + // Update filtered items when dependencies change + Component.onCompleted: updateFilteredItems() + onPinnedListChanged: updateFilteredItems() + onHidePassiveChanged: updateFilteredItems() + + Connections { + target: root + function onTrayValuesAllChanged() { + panelContent.updateFilteredItems(); + } + } + + // Helper functions (lazy-loaded with panelContent) + function wildCardMatch(str, rule) { + if (!str || !rule) + return false; + const placeholder = '\uE000'; + let processedRule = rule.replace(/\*/g, placeholder); + let escaped = processedRule.replace(/[.+?^${}()|[\]\\]/g, '\\$&'); + let pattern = '^' + escaped.replace(new RegExp(placeholder, 'g'), '.*') + '$'; + try { + return new RegExp(pattern, 'i').test(str); + } catch (e) { + return false; + } + } + + function isPinned(item) { + if (!pinnedList || pinnedList.length === 0) + return false; + const title = item?.tooltipTitle || item?.name || item?.id || ""; + for (var i = 0; i < pinnedList.length; i++) { + if (wildCardMatch(title, pinnedList[i])) + return true; + } + return false; + } + + // Popup menu state (lazy-loaded with panelContent) + property int popupMenuUpdateTrigger: 0 + + readonly property var popupMenuWindow: { + void (popupMenuUpdateTrigger); + return PanelService.getPopupMenuWindow(screen); + } + + readonly property var trayMenu: popupMenuWindow ? popupMenuWindow.trayMenuLoader : null + + // Dynamic content sizing + property real contentPreferredWidth: (root.columns * root.cellSize) + ((root.columns - 1) * root.innerSpacing) + (2 * root.outerPadding) + property real contentPreferredHeight: (root.rows * root.cellSize) + ((root.rows - 1) * root.innerSpacing) + (2 * root.outerPadding) + + // Connections (lazy-loaded with panelContent) + Connections { + target: Settings + function onSettingsSaved() { + panelContent.settingsVersion++; + } + } + + Connections { + target: PanelService + function onPopupMenuWindowRegistered(registeredScreen) { + if (registeredScreen === screen) { + panelContent.popupMenuUpdateTrigger++; + } + } + } + + Grid { + id: grid + anchors.fill: parent + anchors.margins: root.outerPadding + spacing: root.innerSpacing + columns: root.columns + rowSpacing: root.innerSpacing + columnSpacing: root.innerSpacing + + Repeater { + id: repeater + model: root.trayValues + + delegate: Item { + width: root.cellSize + height: root.cellSize + + IconImage { + id: trayIcon + anchors.fill: parent + asynchronous: true + backer.fillMode: Image.PreserveAspectFit + source: { + let icon = modelData?.icon || ""; + if (!icon) + return ""; + if (icon.includes("?path=")) { + const chunks = icon.split("?path="); + const name = chunks[0]; + const path = chunks[1]; + const fileName = name.substring(name.lastIndexOf("/") + 1); + return `file://${path}/${fileName}`; + } + return icon; + } + + layer.enabled: panelContent.widgetSettings.colorizeIcons !== false + layer.effect: ShaderEffect { + property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant + property real colorizeMode: 1.0 + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb") + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + + onClicked: mouse => { + if (!modelData) + return; + if (mouse.button === Qt.LeftButton) { + if (!modelData.onlyMenu) { + modelData.activate(); + } + if ((PanelService.openedPanel !== null) && !PanelService.openedPanel.isClosing) { + PanelService.openedPanel.close(); + } + } else if (mouse.button === Qt.MiddleButton) { + modelData.secondaryActivate && modelData.secondaryActivate(); + if ((PanelService.openedPanel !== null) && !PanelService.openedPanel.isClosing) { + PanelService.openedPanel.close(); + } + } else if (mouse.button === Qt.RightButton) { + TooltipService.hideImmediately(); + + if (panelContent.popupMenuWindow && panelContent.popupMenuWindow.visible) { + panelContent.popupMenuWindow.close(); + return; + } + + if (modelData.hasMenu && modelData.menu && panelContent.trayMenu && panelContent.trayMenu.item) { + const barPosition = Settings.getBarPositionForScreen(root.screen?.name); + // Increased spacing for better alignment with other context menus + let menuX, menuY; + + if (barPosition === "left") { + menuX = trayIcon.width + Style.marginL; + menuY = 0; + } else if (barPosition === "right") { + menuX = -panelContent.trayMenu.item.width - Style.marginL; + menuY = 0; + } else if (barPosition === "bottom") { + // For bottom bar: let TrayMenu handle positioning by passing anchorY >= 0 + // TrayMenu will position above the anchor item + menuX = (trayIcon.width / 2) - (panelContent.trayMenu.item.width / 2); + menuY = trayIcon.height + Style.marginL; + } else { + // For top bar: position menu below the icon with more spacing + menuX = (trayIcon.width / 2) - (panelContent.trayMenu.item.width / 2); + menuY = trayIcon.height + Style.marginL; + } + + PanelService.showTrayMenu(root.screen, modelData, panelContent.trayMenu.item, trayIcon, menuX, menuY, root.widgetSection, root.widgetIndex); + } + } + } + + onWheel: wheel => { + if (wheel.angleDelta.y > 0) + modelData?.scrollUp(); + else if (wheel.angleDelta.y < 0) + modelData?.scrollDown(); + } + + onEntered: { + if (panelContent.popupMenuWindow) { + panelContent.popupMenuWindow.close(); + } + TooltipService.show(trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection(root.screen?.name)); + } + onExited: TooltipService.hide() + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Wallpaper/WallhavenSettingsPopup.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Wallpaper/WallhavenSettingsPopup.qml new file mode 100644 index 0000000..f19858a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Wallpaper/WallhavenSettingsPopup.qml @@ -0,0 +1,993 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Services.UI +import qs.Widgets + +Popup { + id: root + + property ShellScreen screen + + // Measure the ENV placeholder text at current font settings + TextMetrics { + id: envPlaceholderMetrics + text: I18n.tr("wallpaper.panel.apikey-managed-by-env") + font.pointSize: Style.fontSizeM + } + + // Dynamic width: use measured ENV placeholder width + input padding, or fallback to 440 + width: Math.max(440, Math.round(envPlaceholderMetrics.width + (Style.marginL * 4)), Math.round(contentColumn.implicitWidth + Style.margin2L)) + height: Math.round(contentColumn.implicitHeight + Style.margin2L) + padding: Style.marginL + modal: true + dim: false + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + function showAt(item) { + open(); + } + + onOpened: { + // Center on screen after popup is opened and parent position is known + if (screen && parent) { + var parentPos = parent.mapToItem(null, 0, 0); + x = Math.round((screen.width - width) / 2 - parentPos.x); + y = Math.round((screen.height - height) / 2 - parentPos.y); + } + Qt.callLater(() => { + if (resolutionWidthInput.inputItem) { + resolutionWidthInput.inputItem.forceActiveFocus(); + } + }); + } + + function hide() { + close(); + } + + function updateResolution(triggerSearch) { + if (typeof WallhavenService === "undefined") { + return; + } + + var width = Settings.data.wallpaper.wallhavenResolutionWidth || ""; + var height = Settings.data.wallpaper.wallhavenResolutionHeight || ""; + var mode = Settings.data.wallpaper.wallhavenResolutionMode || "atleast"; + + if (width && height) { + var resolution = width + "x" + height; + if (mode === "atleast") { + WallhavenService.minResolution = resolution; + WallhavenService.resolutions = ""; + } else { + WallhavenService.minResolution = ""; + WallhavenService.resolutions = resolution; + } + } else { + WallhavenService.minResolution = ""; + WallhavenService.resolutions = ""; + } + + // Trigger new search with updated resolution only if requested + if (triggerSearch && Settings.data.wallpaper.useWallhaven) { + WallhavenService.search(Settings.data.wallpaper.wallhavenQuery || "", 1); + } + } + + background: Rectangle { + id: backgroundRect + color: Color.mSurface + radius: Style.radiusL + border.color: Color.mOutline + border.width: Style.borderM + + NDropShadow { + source: backgroundRect + } + } + + contentItem: ColumnLayout { + id: contentColumn + spacing: Style.marginM + + // Header + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NIcon { + icon: "settings" + pointSize: Style.fontSizeL + color: Color.mPrimary + } + + NText { + text: I18n.tr("wallpaper.panel.wallhaven-settings-title") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: root.hide() + } + } + + NDivider { + Layout.fillWidth: true + } + + // API Key + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NText { + text: I18n.tr("wallpaper.panel.apikey-label") + color: Color.mOnSurface + pointSize: Style.fontSizeM + } + + NTextInput { + id: apiKeyInput + Layout.fillWidth: true + enabled: !WallhavenService.apiKeyManagedByEnv + placeholderText: WallhavenService.apiKeyManagedByEnv ? I18n.tr("wallpaper.panel.apikey-managed-by-env") : I18n.tr("wallpaper.panel.apikey-placeholder") + text: WallhavenService.apiKeyManagedByEnv ? "" : (Settings.data.wallpaper.wallhavenApiKey || "") + + // Fix for password echo mode + Component.onCompleted: { + if (apiKeyInput.inputItem) { + apiKeyInput.inputItem.echoMode = TextInput.Password; + } + } + + onEditingFinished: { + if (!WallhavenService.apiKeyManagedByEnv) { + Settings.data.wallpaper.wallhavenApiKey = text; + } + } + } + + NText { + text: I18n.tr("wallpaper.panel.apikey-help") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + } + + NDivider { + Layout.fillWidth: true + } + + // Sorting + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NText { + text: I18n.tr("wallpaper.panel.sorting-label") + color: Color.mOnSurface + pointSize: Style.fontSizeM + Layout.preferredWidth: implicitWidth + } + + NComboBox { + id: sortingComboBox + Layout.fillWidth: true + Layout.minimumWidth: 200 + model: [ + { + "key": "date_added", + "name": I18n.tr("wallpaper.panel.sorting-date-added") + }, + { + "key": "relevance", + "name": I18n.tr("wallpaper.panel.sorting-relevance") + }, + { + "key": "random", + "name": I18n.tr("common.random") + }, + { + "key": "views", + "name": I18n.tr("wallpaper.panel.sorting-views") + }, + { + "key": "favorites", + "name": I18n.tr("wallpaper.panel.sorting-favorites") + }, + { + "key": "toplist", + "name": I18n.tr("wallpaper.panel.sorting-toplist") + } + ] + currentKey: Settings.data.wallpaper.wallhavenSorting || "relevance" + onSelected: key => { + Settings.data.wallpaper.wallhavenSorting = key; + if (typeof WallhavenService !== "undefined") { + WallhavenService.sorting = key; + WallhavenService.search(Settings.data.wallpaper.wallhavenQuery || "", 1); + } + } + } + } + + // Order + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + visible: sortingComboBox.currentKey !== "random" + + NText { + text: I18n.tr("wallpaper.panel.order-label") + color: Color.mOnSurface + pointSize: Style.fontSizeM + Layout.preferredWidth: implicitWidth + } + + NComboBox { + id: orderComboBox + Layout.fillWidth: true + Layout.minimumWidth: 200 + model: [ + { + "key": "desc", + "name": I18n.tr("wallpaper.panel.order-desc") + }, + { + "key": "asc", + "name": I18n.tr("wallpaper.panel.order-asc") + } + ] + currentKey: Settings.data.wallpaper.wallhavenOrder || "desc" + onSelected: key => { + Settings.data.wallpaper.wallhavenOrder = key; + if (typeof WallhavenService !== "undefined") { + WallhavenService.order = key; + WallhavenService.search(Settings.data.wallpaper.wallhavenQuery || "", 1); + } + } + } + } + + // Purity selector + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NText { + text: I18n.tr("wallpaper.panel.purity-label") + color: Color.mOnSurface + pointSize: Style.fontSizeM + Layout.preferredWidth: implicitWidth + } + + Item { + Layout.fillWidth: true + } + + RowLayout { + id: purityRow + spacing: Style.marginL + + function getPurityValue(index) { + var purity = Settings.data.wallpaper.wallhavenPurity || "100"; + return purity.length > index && purity.charAt(index) === "1"; + } + + function updatePurity(sfw, sketchy, nsfw) { + var purity = (sfw ? "1" : "0") + (sketchy ? "1" : "0") + (nsfw ? "1" : "0"); + Settings.data.wallpaper.wallhavenPurity = purity; + // Update checkboxes immediately + sfwToggle.checked = sfw; + sketchyToggle.checked = sketchy; + nsfwToggle.checked = nsfw; + if (typeof WallhavenService !== "undefined") { + WallhavenService.purity = purity; + WallhavenService.search(Settings.data.wallpaper.wallhavenQuery || "", 1); + } + } + + Connections { + target: Settings.data.wallpaper + function onWallhavenPurityChanged() { + sfwToggle.checked = purityRow.getPurityValue(0); + sketchyToggle.checked = purityRow.getPurityValue(1); + nsfwToggle.checked = purityRow.getPurityValue(2); + } + function onWallhavenApiKeyChanged() { + // If API key is removed (and no ENV key), disable NSFW + if (!WallhavenService.apiKey && nsfwToggle.checked) { + nsfwToggle.toggled(false); + } + } + } + + Component.onCompleted: { + sfwToggle.checked = purityRow.getPurityValue(0); + sketchyToggle.checked = purityRow.getPurityValue(1); + nsfwToggle.checked = purityRow.getPurityValue(2); + } + + // SFW checkbox + Item { + Layout.preferredWidth: sfwCheckboxRow.implicitWidth + Layout.preferredHeight: sfwCheckboxRow.implicitHeight + + RowLayout { + id: sfwCheckboxRow + anchors.fill: parent + spacing: Style.marginS + + NText { + text: I18n.tr("wallpaper.panel.purity-sfw") + color: Color.mOnSurface + pointSize: Style.fontSizeM + } + + Rectangle { + id: sfwBox + implicitWidth: Math.round(Style.baseWidgetSize * 0.7) + implicitHeight: Math.round(Style.baseWidgetSize * 0.7) + radius: Style.radiusXS + color: sfwToggle.checked ? Color.mPrimary : Color.mSurface + border.color: Color.mOutline + border.width: Style.borderS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NIcon { + visible: sfwToggle.checked + anchors.centerIn: parent + anchors.horizontalCenterOffset: -1 + icon: "check" + color: Color.mOnPrimary + pointSize: Math.max(Style.fontSizeXS, sfwBox.width * 0.5) + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: sfwToggle.toggled(!sfwToggle.checked) + } + } + } + } + + // Sketchy checkbox + Item { + Layout.preferredWidth: sketchyCheckboxRow.implicitWidth + Layout.preferredHeight: sketchyCheckboxRow.implicitHeight + + RowLayout { + id: sketchyCheckboxRow + anchors.fill: parent + spacing: Style.marginS + + NText { + text: I18n.tr("wallpaper.panel.purity-sketchy") + color: Color.mOnSurface + pointSize: Style.fontSizeM + } + + Rectangle { + id: sketchyBox + implicitWidth: Math.round(Style.baseWidgetSize * 0.7) + implicitHeight: Math.round(Style.baseWidgetSize * 0.7) + radius: Style.radiusXS + color: sketchyToggle.checked ? Color.mPrimary : Color.mSurface + border.color: Color.mOutline + border.width: Style.borderS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NIcon { + visible: sketchyToggle.checked + anchors.centerIn: parent + anchors.horizontalCenterOffset: -1 + icon: "check" + color: Color.mOnPrimary + pointSize: Math.max(Style.fontSizeXS, sketchyBox.width * 0.5) + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: sketchyToggle.toggled(!sketchyToggle.checked) + } + } + } + } + + // NSFW checkbox + Item { + Layout.preferredWidth: nsfwCheckboxRow.implicitWidth + Layout.preferredHeight: nsfwCheckboxRow.implicitHeight + visible: WallhavenService.apiKey !== "" + + RowLayout { + id: nsfwCheckboxRow + anchors.fill: parent + spacing: Style.marginS + + NText { + text: I18n.tr("wallpaper.panel.purity-nsfw") + color: Color.mOnSurface + pointSize: Style.fontSizeM + } + + Rectangle { + id: nsfwBox + implicitWidth: Math.round(Style.baseWidgetSize * 0.7) + implicitHeight: Math.round(Style.baseWidgetSize * 0.7) + radius: Style.radiusXS + color: nsfwToggle.checked ? Color.mPrimary : Color.mSurface + border.color: Color.mOutline + border.width: Style.borderS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NIcon { + visible: nsfwToggle.checked + anchors.centerIn: parent + anchors.horizontalCenterOffset: -1 + icon: "check" + color: Color.mOnPrimary + pointSize: Math.max(Style.fontSizeXS, nsfwBox.width * 0.5) + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: nsfwToggle.toggled(!nsfwToggle.checked) + } + } + } + } + + // Invisible objects for logic + QtObject { + id: sfwToggle + property bool checked: false + signal toggled(bool checked) + onToggled: checked => { + purityRow.updatePurity(checked, purityRow.getPurityValue(1), purityRow.getPurityValue(2)); + } + } + + QtObject { + id: sketchyToggle + property bool checked: false + signal toggled(bool checked) + onToggled: checked => { + purityRow.updatePurity(purityRow.getPurityValue(0), checked, purityRow.getPurityValue(2)); + } + } + + QtObject { + id: nsfwToggle + property bool checked: false + signal toggled(bool checked) + onToggled: checked => { + purityRow.updatePurity(purityRow.getPurityValue(0), purityRow.getPurityValue(1), checked); + } + } + } + } + + // Aspect Ratio + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NText { + text: I18n.tr("wallpaper.panel.ratios-label") + color: Color.mOnSurface + pointSize: Style.fontSizeM + Layout.preferredWidth: implicitWidth + } + + NComboBox { + id: ratioComboBox + Layout.fillWidth: true + Layout.minimumWidth: 200 + model: [ + { + "key": "", + "name": I18n.tr("wallpaper.panel.ratios-any") + }, + { + "key": "landscape", + "name": I18n.tr("wallpaper.panel.ratios-all-wide") + }, + { + "key": "portrait", + "name": I18n.tr("wallpaper.panel.ratios-all-portrait") + }, + { + "key": "16x9", + "name": "16x9" + }, + { + "key": "16x10", + "name": "16x10" + }, + { + "key": "21x9", + "name": "21x9" + }, + { + "key": "32x9", + "name": "32x9" + }, + { + "key": "48x9", + "name": "48x9" + }, + { + "key": "9x16", + "name": "9x16" + }, + { + "key": "10x16", + "name": "10x16" + }, + { + "key": "9x18", + "name": "9x18" + }, + { + "key": "1x1", + "name": "1x1" + }, + { + "key": "3x2", + "name": "3x2" + }, + { + "key": "4x3", + "name": "4x3" + }, + { + "key": "5x4", + "name": "5x4" + } + ] + currentKey: Settings.data.wallpaper.wallhavenRatios || "" + onSelected: key => { + Settings.data.wallpaper.wallhavenRatios = key; + if (typeof WallhavenService !== "undefined") { + WallhavenService.ratios = key; + WallhavenService.search(Settings.data.wallpaper.wallhavenQuery || "", 1); + } + } + } + } + + // Categories + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NText { + text: I18n.tr("wallpaper.panel.categories-label") + color: Color.mOnSurface + pointSize: Style.fontSizeM + Layout.preferredWidth: implicitWidth + } + + Item { + Layout.fillWidth: true + } + + RowLayout { + id: categoriesRow + spacing: Style.marginL + + function getCategoryValue(index) { + var cats = Settings.data.wallpaper.wallhavenCategories || "111"; + return cats.length > index && cats.charAt(index) === "1"; + } + + function updateCategories(general, anime, people) { + var categories = (general ? "1" : "0") + (anime ? "1" : "0") + (people ? "1" : "0"); + Settings.data.wallpaper.wallhavenCategories = categories; + // Update checkboxes immediately + generalToggle.checked = general; + animeToggle.checked = anime; + peopleToggle.checked = people; + if (typeof WallhavenService !== "undefined") { + WallhavenService.categories = categories; + WallhavenService.search(Settings.data.wallpaper.wallhavenQuery, 1); + } + } + + Connections { + target: Settings.data.wallpaper + function onWallhavenCategoriesChanged() { + generalToggle.checked = categoriesRow.getCategoryValue(0); + animeToggle.checked = categoriesRow.getCategoryValue(1); + peopleToggle.checked = categoriesRow.getCategoryValue(2); + } + } + + Component.onCompleted: { + generalToggle.checked = categoriesRow.getCategoryValue(0); + animeToggle.checked = categoriesRow.getCategoryValue(1); + peopleToggle.checked = categoriesRow.getCategoryValue(2); + } + + // General checkbox + Item { + Layout.preferredWidth: generalCheckboxRow.implicitWidth + Layout.preferredHeight: generalCheckboxRow.implicitHeight + + RowLayout { + id: generalCheckboxRow + anchors.fill: parent + spacing: Style.marginS + + NText { + text: I18n.tr("common.general") + color: Color.mOnSurface + pointSize: Style.fontSizeM + } + + Rectangle { + id: generalBox + implicitWidth: Math.round(Style.baseWidgetSize * 0.7) + implicitHeight: Math.round(Style.baseWidgetSize * 0.7) + radius: Style.radiusXS + color: generalToggle.checked ? Color.mPrimary : Color.mSurface + border.color: Color.mOutline + border.width: Style.borderS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NIcon { + visible: generalToggle.checked + anchors.centerIn: parent + anchors.horizontalCenterOffset: -1 + icon: "check" + color: Color.mOnPrimary + pointSize: Math.max(Style.fontSizeXS, generalBox.width * 0.5) + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: generalToggle.toggled(!generalToggle.checked) + } + } + } + } + + // Anime checkbox + Item { + Layout.preferredWidth: animeCheckboxRow.implicitWidth + Layout.preferredHeight: animeCheckboxRow.implicitHeight + + RowLayout { + id: animeCheckboxRow + anchors.fill: parent + spacing: Style.marginS + + NText { + text: I18n.tr("wallpaper.panel.categories-anime") + color: Color.mOnSurface + pointSize: Style.fontSizeM + } + + Rectangle { + id: animeBox + implicitWidth: Math.round(Style.baseWidgetSize * 0.7) + implicitHeight: Math.round(Style.baseWidgetSize * 0.7) + radius: Style.radiusXS + color: animeToggle.checked ? Color.mPrimary : Color.mSurface + border.color: Color.mOutline + border.width: Style.borderS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NIcon { + visible: animeToggle.checked + anchors.centerIn: parent + anchors.horizontalCenterOffset: -1 + icon: "check" + color: Color.mOnPrimary + pointSize: Math.max(Style.fontSizeXS, animeBox.width * 0.5) + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: animeToggle.toggled(!animeToggle.checked) + } + } + } + } + + // People checkbox + Item { + Layout.preferredWidth: peopleCheckboxRow.implicitWidth + Layout.preferredHeight: peopleCheckboxRow.implicitHeight + + RowLayout { + id: peopleCheckboxRow + anchors.fill: parent + spacing: Style.marginS + + NText { + text: I18n.tr("wallpaper.panel.categories-people") + color: Color.mOnSurface + pointSize: Style.fontSizeM + } + + Rectangle { + id: peopleBox + implicitWidth: Math.round(Style.baseWidgetSize * 0.7) + implicitHeight: Math.round(Style.baseWidgetSize * 0.7) + radius: Style.radiusXS + color: peopleToggle.checked ? Color.mPrimary : Color.mSurface + border.color: Color.mOutline + border.width: Style.borderS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NIcon { + visible: peopleToggle.checked + anchors.centerIn: parent + anchors.horizontalCenterOffset: -1 + icon: "check" + color: Color.mOnPrimary + pointSize: Math.max(Style.fontSizeXS, peopleBox.width * 0.5) + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: peopleToggle.toggled(!peopleToggle.checked) + } + } + } + } + + // Invisible checkboxes to maintain the signal handlers + QtObject { + id: generalToggle + property bool checked: false + signal toggled(bool checked) + onToggled: checked => { + categoriesRow.updateCategories(checked, categoriesRow.getCategoryValue(1), categoriesRow.getCategoryValue(2)); + } + } + + QtObject { + id: animeToggle + property bool checked: false + signal toggled(bool checked) + onToggled: checked => { + categoriesRow.updateCategories(categoriesRow.getCategoryValue(0), checked, categoriesRow.getCategoryValue(2)); + } + } + + QtObject { + id: peopleToggle + property bool checked: false + signal toggled(bool checked) + onToggled: checked => { + categoriesRow.updateCategories(categoriesRow.getCategoryValue(0), categoriesRow.getCategoryValue(1), checked); + } + } + } + } + + // Resolution filter + ColumnLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NText { + text: I18n.tr("wallpaper.panel.resolution-label") + color: Color.mOnSurface + pointSize: Style.fontSizeM + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NText { + text: I18n.tr("wallpaper.panel.resolution-mode-label") + color: Color.mOnSurface + pointSize: Style.fontSizeM + Layout.preferredWidth: implicitWidth + } + + NComboBox { + id: resolutionModeComboBox + Layout.fillWidth: true + Layout.minimumWidth: 200 + model: [ + { + "key": "atleast", + "name": I18n.tr("wallpaper.panel.resolution-atleast") + }, + { + "key": "exact", + "name": I18n.tr("wallpaper.panel.resolution-exact") + } + ] + currentKey: Settings.data.wallpaper.wallhavenResolutionMode || "atleast" + + Connections { + target: Settings.data.wallpaper + function onWallhavenResolutionModeChanged() { + if (resolutionModeComboBox.currentKey !== Settings.data.wallpaper.wallhavenResolutionMode) { + resolutionModeComboBox.currentKey = Settings.data.wallpaper.wallhavenResolutionMode || "atleast"; + } + } + } + + onSelected: key => { + Settings.data.wallpaper.wallhavenResolutionMode = key; + updateResolution(false); + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + NTextInput { + id: resolutionWidthInput + Layout.preferredWidth: 80 + placeholderText: I18n.tr("common.width") + inputMethodHints: Qt.ImhDigitsOnly + text: Settings.data.wallpaper.wallhavenResolutionWidth || "" + + Component.onCompleted: { + if (resolutionWidthInput.inputItem) { + resolutionWidthInput.inputItem.focusPolicy = Qt.StrongFocus; + // Ensure the TextField can receive keyboard input + resolutionWidthInput.inputItem.activeFocusOnPress = true; + } + } + + // Ensure focus when clicked + onActiveFocusChanged: { + if (activeFocus && resolutionWidthInput.inputItem) { + resolutionWidthInput.inputItem.forceActiveFocus(); + } + } + + Connections { + target: Settings.data.wallpaper + function onWallhavenResolutionWidthChanged() { + if (resolutionWidthInput.text !== Settings.data.wallpaper.wallhavenResolutionWidth) { + resolutionWidthInput.text = Settings.data.wallpaper.wallhavenResolutionWidth || ""; + } + } + } + + onEditingFinished: { + Settings.data.wallpaper.wallhavenResolutionWidth = text; + updateResolution(false); + } + } + + NText { + text: "×" + color: Color.mOnSurface + pointSize: Style.fontSizeM + Layout.preferredWidth: implicitWidth + } + + NTextInput { + id: resolutionHeightInput + Layout.preferredWidth: 80 + placeholderText: I18n.tr("common.height") + inputMethodHints: Qt.ImhDigitsOnly + text: Settings.data.wallpaper.wallhavenResolutionHeight || "" + + Component.onCompleted: { + if (resolutionHeightInput.inputItem) { + resolutionHeightInput.inputItem.focusPolicy = Qt.StrongFocus; + resolutionHeightInput.inputItem.activeFocusOnPress = true; + } + } + + // Ensure focus when clicked + onActiveFocusChanged: { + if (activeFocus && resolutionHeightInput.inputItem) { + resolutionHeightInput.inputItem.forceActiveFocus(); + } + } + + Connections { + target: Settings.data.wallpaper + function onWallhavenResolutionHeightChanged() { + if (resolutionHeightInput.text !== Settings.data.wallpaper.wallhavenResolutionHeight) { + resolutionHeightInput.text = Settings.data.wallpaper.wallhavenResolutionHeight || ""; + } + } + } + + onEditingFinished: { + Settings.data.wallpaper.wallhavenResolutionHeight = text; + updateResolution(false); + } + } + } + } + + // Apply button + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginM + } + + NButton { + Layout.fillWidth: true + text: I18n.tr("common.apply") + onClicked: { + // Ensure all settings are synced to the service + if (typeof WallhavenService !== "undefined" && Settings.data.wallpaper.useWallhaven) { + // Sync all settings to the service + WallhavenService.categories = Settings.data.wallpaper.wallhavenCategories; + WallhavenService.purity = Settings.data.wallpaper.wallhavenPurity; + WallhavenService.sorting = Settings.data.wallpaper.wallhavenSorting; + WallhavenService.order = Settings.data.wallpaper.wallhavenOrder; + WallhavenService.ratios = Settings.data.wallpaper.wallhavenRatios; + WallhavenService.apiKey = Settings.data.wallpaper.wallhavenApiKey; + + // Update resolution settings (without triggering search) + updateResolution(false); + + // Refresh the wallpaper search with current settings + WallhavenService.search(Settings.data.wallpaper.wallhavenQuery || "", 1); + + // Close the popup after applying (delay to prevent click propagation) + Qt.callLater(() => { + root.hide(); + }); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Wallpaper/WallpaperPanel.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Wallpaper/WallpaperPanel.qml new file mode 100644 index 0000000..a183fdc --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Panels/Wallpaper/WallpaperPanel.qml @@ -0,0 +1,1937 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Modules.MainScreen +import qs.Modules.Panels.Settings +import qs.Services.Theming +import qs.Services.UI +import qs.Widgets + +SmartPanel { + id: root + + preferredWidth: 800 * Style.uiScaleRatio + preferredHeight: 650 * Style.uiScaleRatio + preferredWidthRatio: 0.5 + preferredHeightRatio: 0.5 + + // Positioning + readonly property string screenBarPosition: Settings.getBarPositionForScreen(screen?.name) + readonly property string panelPosition: { + if (Settings.data.wallpaper.panelPosition === "follow_bar") { + if (screenBarPosition === "left" || screenBarPosition === "right") { + return `center_${screenBarPosition}`; + } else { + return `${screenBarPosition}_center`; + } + } else { + return Settings.data.wallpaper.panelPosition; + } + } + panelAnchorHorizontalCenter: panelPosition === "center" || panelPosition.endsWith("_center") + panelAnchorVerticalCenter: panelPosition === "center" + panelAnchorLeft: panelPosition !== "center" && panelPosition.endsWith("_left") + panelAnchorRight: panelPosition !== "center" && panelPosition.endsWith("_right") + panelAnchorBottom: panelPosition.startsWith("bottom_") + panelAnchorTop: panelPosition.startsWith("top_") + + // Store direct reference to content for instant access + property var contentItem: null + + // Override keyboard handlers to enable grid navigation + function onDownPressed() { + if (!contentItem) + return; + let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex); + if (view?.gridView) { + if (!view.gridView.hasActiveFocus) { + view.gridView.forceActiveFocus(); + if (view.gridView.currentIndex < 0 && view.gridView.model.length > 0) { + view.gridView.currentIndex = 0; + } + } else { + if (view.gridView.currentIndex < 0 && view.gridView.model.length > 0) { + view.gridView.currentIndex = 0; + } else { + view.gridView.moveCurrentIndexDown(); + } + } + } + } + + function onUpPressed() { + if (!contentItem) + return; + let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex); + if (view?.gridView?.hasActiveFocus) { + if (view.gridView.currentIndex < 0 && view.gridView.model.length > 0) { + view.gridView.currentIndex = 0; + } else { + view.gridView.moveCurrentIndexUp(); + } + } + } + + function onLeftPressed() { + if (!contentItem) + return; + let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex); + if (view?.gridView?.hasActiveFocus) { + if (view.gridView.currentIndex < 0 && view.gridView.model.length > 0) { + view.gridView.currentIndex = 0; + } else { + view.gridView.moveCurrentIndexLeft(); + } + } + } + + function onRightPressed() { + if (!contentItem) + return; + let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex); + if (view?.gridView?.hasActiveFocus) { + if (view.gridView.currentIndex < 0 && view.gridView.model.length > 0) { + view.gridView.currentIndex = 0; + } else { + view.gridView.moveCurrentIndexRight(); + } + } + } + + function onReturnPressed() { + if (!contentItem) + return; + + // Check if Wallhaven page input has focus + if (contentItem.wallhavenView && contentItem.wallhavenView.visible && contentItem.wallhavenView.pageInput && contentItem.wallhavenView.pageInput.inputItem.activeFocus) { + contentItem.wallhavenView.pageInput.submitPage(); + return; + } + + let view = contentItem.screenRepeater.itemAt(contentItem.currentScreenIndex); + if (view?.gridView?.hasActiveFocus) { + let gridView = view.gridView; + if (gridView.currentIndex >= 0 && gridView.currentIndex < gridView.model.count) { + var item = gridView.model.get(gridView.currentIndex); + view.selectItem(item.path, item.isDirectory); + } + } + } + + function onEnterPressed() { + onReturnPressed(); + } + + panelContent: Rectangle { + id: panelContent + + property alias wallhavenView: wallhavenView + property int currentScreenIndex: { + if (screen !== null) { + for (var i = 0; i < Quickshell.screens.length; i++) { + if (Quickshell.screens[i].name == screen.name) { + return i; + } + } + } + return 0; + } + property var currentScreen: Quickshell.screens[currentScreenIndex] + property string filterText: "" + property int appearanceTabIndex: 0 + readonly property bool headerScreensStripAvailable: !Settings.data.wallpaper.setWallpaperOnAllMonitors || Settings.data.wallpaper.enableMultiMonitorDirectories + readonly property bool headerDevicesButtonVisible: Quickshell.screens.length > 1 || Settings.data.wallpaper.enableMultiMonitorDirectories + property alias screenRepeater: screenRepeater + + Component.onCompleted: { + root.contentItem = panelContent; + } + + // Function to update Wallhaven resolution filter + function updateWallhavenResolution() { + if (typeof WallhavenService === "undefined") { + return; + } + + var width = Settings.data.wallpaper.wallhavenResolutionWidth || ""; + var height = Settings.data.wallpaper.wallhavenResolutionHeight || ""; + var mode = Settings.data.wallpaper.wallhavenResolutionMode || "atleast"; + + if (width && height) { + var resolution = width + "x" + height; + if (mode === "atleast") { + WallhavenService.minResolution = resolution; + WallhavenService.resolutions = ""; + } else { + WallhavenService.minResolution = ""; + WallhavenService.resolutions = resolution; + } + } else { + WallhavenService.minResolution = ""; + WallhavenService.resolutions = ""; + } + + // Trigger new search with updated resolution + if (Settings.data.wallpaper.useWallhaven) { + if (wallhavenView) { + wallhavenView.loading = true; + } + WallhavenService.search(Settings.data.wallpaper.wallhavenQuery || "", 1); + } + } + + color: "transparent" + + // Wallhaven settings popup + Loader { + id: wallhavenSettingsPopup + source: "WallhavenSettingsPopup.qml" + onLoaded: { + if (item) { + item.screen = screen; + } + } + } + + // Solid color picker dialog + NColorPickerDialog { + id: solidColorPicker + screen: root.screen + selectedColor: Settings.data.wallpaper.solidColor + onColorSelected: color => WallpaperService.setSolidColor(color.toString()) + } + + // Focus management + Connections { + target: root + function onOpened() { + // Ensure contentItem is set + if (!root.contentItem) { + root.contentItem = panelContent; + } + // Reset grid view selections + for (var i = 0; i < screenRepeater.count; i++) { + let item = screenRepeater.itemAt(i); + if (item && item.gridView) { + item.gridView.currentIndex = -1; + } + } + if (wallhavenView && wallhavenView.gridView) { + wallhavenView.gridView.currentIndex = -1; + } + panelContent.appearanceTabIndex = Settings.data.colorSchemes.darkMode ? 1 : 0; + WallpaperService.wallpaperSelectionAppearance = panelContent.appearanceTabIndex === 1 ? "dark" : "light"; + // Give initial focus to search input + Qt.callLater(() => { + if (searchInput.inputItem) { + searchInput.inputItem.forceActiveFocus(); + } + }); + } + } + + // Debounce timer for search + Timer { + id: searchDebounceTimer + interval: 150 + onTriggered: { + panelContent.filterText = searchInput.text; + // Trigger update on all screen views + for (var i = 0; i < screenRepeater.count; i++) { + let item = screenRepeater.itemAt(i); + if (item && item.updateFiltered) { + item.updateFiltered(); + } + } + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + // Debounce timer for Wallhaven search + Timer { + id: wallhavenSearchDebounceTimer + interval: 500 + onTriggered: { + Settings.data.wallpaper.wallhavenQuery = searchInput.text; + if (typeof WallhavenService !== "undefined") { + wallhavenView.loading = true; + WallhavenService.search(searchInput.text, 1); + } + } + } + + // Header + NBox { + Layout.fillWidth: true + Layout.preferredHeight: headerColumn.implicitHeight + Style.margin2L + color: Color.mSurfaceVariant + + ColumnLayout { + id: headerColumn + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NIcon { + icon: "settings-wallpaper-selector" + pointSize: Style.fontSizeXXL + color: Color.mPrimary + } + + NText { + text: I18n.tr("wallpaper.panel.title") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NIconButton { + visible: Settings.data.wallpaper.enabled + icon: "sun" + tooltipText: Settings.data.wallpaper.linkLightAndDarkWallpapers ? I18n.tr("wallpaper.panel.header-sun-linked-tooltip") : I18n.tr("wallpaper.panel.header-sun-separate-tooltip") + baseSize: Style.baseWidgetSize * 0.8 + colorBg: !Settings.data.wallpaper.linkLightAndDarkWallpapers ? Color.mPrimary : Color.smartAlpha(Color.mSurfaceVariant) + colorFg: !Settings.data.wallpaper.linkLightAndDarkWallpapers ? Color.mOnPrimary : Color.mPrimary + onClicked: Settings.data.wallpaper.linkLightAndDarkWallpapers = !Settings.data.wallpaper.linkLightAndDarkWallpapers + } + + NIconButton { + visible: Settings.data.wallpaper.enabled && panelContent.headerDevicesButtonVisible + icon: "devices" + tooltipText: Settings.data.wallpaper.setWallpaperOnAllMonitors ? I18n.tr("wallpaper.panel.header-devices-apply-all-tooltip") : I18n.tr("wallpaper.panel.header-devices-per-monitor-tooltip") + baseSize: Style.baseWidgetSize * 0.8 + colorBg: !Settings.data.wallpaper.setWallpaperOnAllMonitors ? Color.mPrimary : Color.smartAlpha(Color.mSurfaceVariant) + colorFg: !Settings.data.wallpaper.setWallpaperOnAllMonitors ? Color.mOnPrimary : Color.mPrimary + onClicked: Settings.data.wallpaper.setWallpaperOnAllMonitors = !Settings.data.wallpaper.setWallpaperOnAllMonitors + } + + NIconButton { + icon: "palette" + tooltipText: I18n.tr("wallpaper.panel.solid-color-tooltip") + baseSize: Style.baseWidgetSize * 0.8 + colorBg: Settings.data.wallpaper.useSolidColor ? Color.mPrimary : Color.mSurfaceVariant + colorFg: Settings.data.wallpaper.useSolidColor ? Color.mOnPrimary : Color.mPrimary + onClicked: solidColorPicker.open() + } + + NIconButton { + icon: "settings" + tooltipText: I18n.tr("panels.wallpaper.settings-title") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + var settingsPanel = PanelService.getPanel("settingsPanel", screen); + settingsPanel.requestedTab = SettingsPanel.Tab.Wallpaper; + settingsPanel.open(); + } + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: root.close() + } + } + + NDivider { + Layout.fillWidth: true + } + + NTabBar { + id: appearanceTabBar + visible: Settings.data.wallpaper.enabled && !Settings.data.wallpaper.linkLightAndDarkWallpapers + Layout.fillWidth: true + currentIndex: panelContent.appearanceTabIndex + spacing: Style.marginM + distributeEvenly: true + + onCurrentIndexChanged: { + if (currentIndex < 0) { + return; + } + panelContent.appearanceTabIndex = currentIndex; + WallpaperService.wallpaperSelectionAppearance = currentIndex === 1 ? "dark" : "light"; + Settings.data.colorSchemes.darkMode = currentIndex === 1; + } + + NTabButton { + text: I18n.tr("wallpaper.panel.appearance-light-tab") + tabIndex: 0 + checked: appearanceTabBar.currentIndex === 0 + } + NTabButton { + text: I18n.tr("wallpaper.panel.appearance-dark-tab") + tabIndex: 1 + checked: appearanceTabBar.currentIndex === 1 + } + } + + NTabBar { + id: screenTabBar + visible: panelContent.headerScreensStripAvailable + Layout.fillWidth: true + currentIndex: currentScreenIndex + onCurrentIndexChanged: currentScreenIndex = currentIndex + spacing: Style.marginM + distributeEvenly: true + + Repeater { + model: Quickshell.screens + NTabButton { + required property var modelData + required property int index + text: modelData.name || `Screen ${index + 1}` + tabIndex: index + checked: { + screenTabBar.currentIndex === index; + } + } + } + } + + // Unified search input and source + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NTextInput { + id: searchInput + placeholderText: Settings.data.wallpaper.useWallhaven ? I18n.tr("placeholders.search-wallhaven") : I18n.tr("placeholders.search-wallpapers") + fontSize: Style.fontSizeM + Layout.fillWidth: true + + property bool initializing: true + Component.onCompleted: { + // Initialize text based on current mode + if (Settings.data.wallpaper.useWallhaven) { + searchInput.text = Settings.data.wallpaper.wallhavenQuery || ""; + } else { + searchInput.text = panelContent.filterText || ""; + } + // Give focus to search input + if (searchInput.inputItem && searchInput.inputItem.visible) { + searchInput.inputItem.forceActiveFocus(); + } + // Mark initialization as complete after a short delay + Qt.callLater(function () { + searchInput.initializing = false; + }); + } + + Connections { + target: Settings.data.wallpaper + function onUseWallhavenChanged() { + // Update text when mode changes + if (Settings.data.wallpaper.useWallhaven) { + searchInput.text = Settings.data.wallpaper.wallhavenQuery || ""; + } else { + searchInput.text = panelContent.filterText || ""; + } + } + } + + onTextChanged: { + // Don't trigger search during initialization - Component.onCompleted will handle initial search + if (initializing) { + return; + } + if (Settings.data.wallpaper.useWallhaven) { + wallhavenSearchDebounceTimer.restart(); + } else { + searchDebounceTimer.restart(); + } + } + + onEditingFinished: { + if (Settings.data.wallpaper.useWallhaven) { + wallhavenSearchDebounceTimer.stop(); + // Only search if the query actually changed + if (typeof WallhavenService !== "undefined" && text !== WallhavenService.currentQuery) { + Settings.data.wallpaper.wallhavenQuery = text; + wallhavenView.loading = true; + WallhavenService.search(text, 1); + } + } + } + + Keys.onPressed: event => { + if (Keybinds.checkKey(event, 'down', Settings)) { + if (Settings.data.wallpaper.useWallhaven) { + if (wallhavenView && wallhavenView.gridView) { + wallhavenView.gridView.forceActiveFocus(); + } + } else { + let currentView = screenRepeater.itemAt(currentScreenIndex); + if (currentView && currentView.gridView) { + currentView.gridView.forceActiveFocus(); + } + } + event.accepted = true; + } + } + } + + NIconButton { + icon: "color-swatch" + tooltipText: Settings.data.colorSchemes.useWallpaperColors ? I18n.tr("wallpaper.panel.color-extraction-enabled") : I18n.tr("wallpaper.panel.color-extraction-disabled") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + Settings.data.colorSchemes.useWallpaperColors = !Settings.data.colorSchemes.useWallpaperColors; + if (Settings.data.colorSchemes.useWallpaperColors) { + AppThemeService.generate(); + } else { + ColorSchemeService.setPredefinedScheme(Settings.data.colorSchemes.predefinedScheme); + } + } + } + + NComboBox { + id: colorSchemeComboBox + Layout.fillWidth: false + Layout.minimumWidth: 200 + minimumWidth: 200 + + property bool _initialized: false + property bool _userChanging: false + Component.onCompleted: Qt.callLater(() => { + _initialized = true; + }) + + model: Settings.data.colorSchemes.useWallpaperColors ? TemplateProcessor.schemeTypes : ColorSchemeService.schemes.map(s => ({ + "key": ColorSchemeService.getBasename(s), + "name": ColorSchemeService.getBasename(s) + })) + currentKey: Settings.data.colorSchemes.useWallpaperColors ? Settings.data.colorSchemes.generationMethod : Settings.data.colorSchemes.predefinedScheme + onCurrentKeyChanged: { + if (!_initialized) + return; + if (_userChanging) { + _userChanging = false; + return; + } + schemeGlowAnimation.restart(); + } + onSelected: key => { + _userChanging = true; + if (Settings.data.colorSchemes.useWallpaperColors) { + Settings.data.colorSchemes.generationMethod = key; + AppThemeService.generate(); + } else { + ColorSchemeService.setPredefinedScheme(key); + } + Qt.callLater(() => { + _userChanging = false; + }); + } + + SequentialAnimation { + id: schemeGlowAnimation + NumberAnimation { + target: colorSchemeComboBox + property: "opacity" + to: 0.3 + duration: Style.animationSlow + easing.type: Easing.OutCubic + } + NumberAnimation { + target: colorSchemeComboBox + property: "opacity" + to: 1.0 + duration: Style.animationSlow + easing.type: Easing.InCubic + } + } + } + + NComboBox { + id: sourceComboBox + Layout.fillWidth: false + + model: [ + { + "key": "local", + "name": I18n.tr("common.local") + }, + { + "key": "wallhaven", + "name": I18n.tr("wallpaper.panel.source-wallhaven") + } + ] + currentKey: Settings.data.wallpaper.useWallhaven ? "wallhaven" : "local" + property bool skipNextSelected: false + Component.onCompleted: { + // Skip the first onSelected if it fires during initialization + skipNextSelected = true; + Qt.callLater(function () { + skipNextSelected = false; + }); + } + onSelected: key => { + if (skipNextSelected) { + return; + } + var useWallhaven = (key === "wallhaven"); + Settings.data.wallpaper.useWallhaven = useWallhaven; + // Update search input text based on mode + if (useWallhaven) { + searchInput.text = Settings.data.wallpaper.wallhavenQuery || ""; + } else { + searchInput.text = panelContent.filterText || ""; + } + if (useWallhaven && typeof WallhavenService !== "undefined") { + // Update service properties when switching to Wallhaven + // Don't search here - Component.onCompleted will handle it when the component is created + // This prevents duplicate searches + WallhavenService.categories = Settings.data.wallpaper.wallhavenCategories; + WallhavenService.purity = Settings.data.wallpaper.wallhavenPurity; + WallhavenService.sorting = Settings.data.wallpaper.wallhavenSorting; + WallhavenService.order = Settings.data.wallpaper.wallhavenOrder; + + // Update resolution settings + panelContent.updateWallhavenResolution(); + + // If the view is already initialized, trigger a new search when switching to it + // Preserve current page when switching back to Wallhaven source + if (wallhavenView && wallhavenView.initialized && !WallhavenService.fetching) { + wallhavenView.loading = true; + WallhavenService.search(Settings.data.wallpaper.wallhavenQuery || "", WallhavenService.currentPage); + } + } + } + } + + // Settings button (only visible for Wallhaven) + NIconButton { + id: wallhavenSettingsButton + icon: "settings" + tooltipText: I18n.tr("wallpaper.panel.wallhaven-settings-title") + baseSize: Style.baseWidgetSize * 0.8 + visible: Settings.data.wallpaper.useWallhaven + onClicked: { + if (searchInput.inputItem) { + searchInput.inputItem.focus = false; + } + if (wallhavenSettingsPopup.item) { + wallhavenSettingsPopup.item.showAt(wallhavenSettingsButton); + } + } + } + } + } + } + + // Content stack: Wallhaven or Local + NBox { + Layout.fillWidth: true + Layout.fillHeight: true + color: Color.mSurfaceVariant + + StackLayout { + id: contentStack + anchors.fill: parent + anchors.margins: Style.marginL + + currentIndex: Settings.data.wallpaper.useWallhaven ? 1 : 0 + + // Local wallpapers + StackLayout { + id: screenStack + currentIndex: currentScreenIndex + + Repeater { + id: screenRepeater + model: Quickshell.screens + delegate: WallpaperScreenView { + targetScreen: modelData + } + } + } + + // Wallhaven wallpapers + WallhavenView { + id: wallhavenView + } + } + } + } + } + + // Component for each screen's wallpaper view + component WallpaperScreenView: Item { + id: wallpaperScreenView + property var targetScreen + property alias gridView: wallpaperGridView + + // Local reactive state for this screen + property list wallpapersList: [] + property string currentWallpaper: "" + property var filteredItems: [] // Combined list of { path, name, isDirectory } + property var wallpapersWithNames: [] // Cached basenames for files + property var directoriesList: [] // List of directories in browse mode + + // ListModel for the grid — enables animated reordering via move() + ListModel { + id: wallpaperModel + } + + // Browse mode properties + property string currentBrowsePath: WallpaperService.getCurrentBrowsePath(targetScreen?.name ?? "") + property bool isBrowseMode: Settings.data.wallpaper.viewMode === "browse" + property int _browseScanGeneration: 0 + + // Favorited paths (any light/dark) first, then the rest + function sortFavoritesToTop(items) { + var favs = []; + var rest = []; + for (var i = 0; i < items.length; i++) { + var it = items[i]; + if (!it.isDirectory && WallpaperService.isFavorite(it.path)) { + favs.push(it); + } else { + rest.push(it); + } + } + return favs.concat(rest); + } + + // Rebuild filteredItems and sync to wallpaperModel (full replacement, no animation). + // When skipSync is true the caller will animate the model itself. + function updateFiltered(skipSync) { + var combinedItems = []; + + // In browse mode, add directories first + if (isBrowseMode) { + for (var i = 0; i < directoriesList.length; i++) { + var dirPath = directoriesList[i]; + combinedItems.push({ + "path": dirPath, + "name": dirPath.split('/').pop(), + "isDirectory": true + }); + } + } + + // Add files + for (var i = 0; i < wallpapersList.length; i++) { + combinedItems.push({ + "path": wallpapersList[i], + "name": wallpapersList[i].split('/').pop(), + "isDirectory": false + }); + } + + combinedItems = sortFavoritesToTop(combinedItems); + + // Apply filter if text is present + if (!panelContent.filterText || panelContent.filterText.trim().length === 0) { + filteredItems = combinedItems; + if (!skipSync) + syncModel(); + return; + } + + const results = FuzzySort.go(panelContent.filterText.trim(), combinedItems, { + "key": 'name', + "limit": 200 + }); + var filtered = results.map(function (r) { + return r.obj; + }); + filteredItems = sortFavoritesToTop(filtered); + if (!skipSync) + syncModel(); + } + + // Copy filteredItems into the ListModel (full rebuild, no animation). + function syncModel() { + wallpaperModel.clear(); + for (var i = 0; i < filteredItems.length; i++) { + wallpaperModel.append(filteredItems[i]); + } + wallpaperGridView.currentIndex = -1; + wallpaperGridView.positionViewAtBeginning(); + } + + // Animate a single item moving to its new position after a favorite toggle. + function handleFavoriteMove(path) { + // Find where the item currently sits in the model + var fromIndex = -1; + for (var i = 0; i < wallpaperModel.count; i++) { + if (wallpaperModel.get(i).path === path) { + fromIndex = i; + break; + } + } + if (fromIndex === -1) + return; + + // Find where it should be in the freshly-computed filteredItems + var toIndex = -1; + for (var j = 0; j < filteredItems.length; j++) { + if (filteredItems[j].path === path) { + toIndex = j; + break; + } + } + if (toIndex === -1 || fromIndex === toIndex) + return; + + wallpaperGridView.animateMovement = true; + wallpaperModel.move(fromIndex, toIndex, 1); + animateMovementResetTimer.restart(); + } + + // Turn off move animations shortly after the move completes so that + // non-favorite model rebuilds (sort, filter, navigation) don't animate. + Timer { + id: animateMovementResetTimer + readonly property int settleDelay: 50 + interval: Style.animationNormal + settleDelay + onTriggered: { + wallpaperGridView.animateMovement = false; + reconcileModel(); + } + } + + // Reorder wallpaperModel to match filteredItems using in-place moves. + // Avoids clear+rebuild which would destroy delegates and flash thumbnails. + function reconcileModel() { + for (var i = 0; i < filteredItems.length; i++) { + var currentPos = -1; + for (var j = i; j < wallpaperModel.count; j++) { + if (wallpaperModel.get(j).path === filteredItems[i].path) { + currentPos = j; + break; + } + } + if (currentPos !== -1 && currentPos !== i) { + wallpaperModel.move(currentPos, i, 1); + } + } + } + + Component.onCompleted: { + refreshWallpaperScreenData(); + } + + Connections { + target: WallpaperService + function onWallpaperChanged(screenName, path) { + if (targetScreen !== null && screenName === targetScreen.name) { + currentWallpaper = WallpaperService.getWallpaperPathForSlot(targetScreen.name, WallpaperService.wallpaperSelectionAppearance); + } + } + function onWallpaperSelectionAppearanceChanged() { + if (targetScreen !== null) { + currentWallpaper = WallpaperService.getWallpaperPathForSlot(targetScreen.name, WallpaperService.wallpaperSelectionAppearance); + updateFiltered(false); + } + } + function onWallpaperDirectoryChanged(screenName, directory) { + if (targetScreen !== null && screenName === targetScreen.name) { + // Reset browse path when root directory changes + if (isBrowseMode) { + WallpaperService.navigateToRoot(targetScreen.name); + } + refreshWallpaperScreenData(); + } + } + function onWallpaperListChanged(screenName, count) { + if (targetScreen !== null && screenName === targetScreen.name) { + refreshWallpaperScreenData(); + } + } + function onBrowsePathChanged(screenName, path) { + if (targetScreen !== null && screenName === targetScreen.name) { + currentBrowsePath = path; + refreshWallpaperScreenData(); + } + } + function onFavoritesChanged(path) { + updateFiltered(true); // recompute filteredItems but skip full model rebuild + handleFavoriteMove(path); // animate the item to its new position + } + } + + function refreshWallpaperScreenData() { + if (targetScreen === null) { + return; + } + + currentWallpaper = WallpaperService.getWallpaperPathForSlot(targetScreen.name, WallpaperService.wallpaperSelectionAppearance); + + if (isBrowseMode) { + // In browse mode, scan current directory for both files and directories + var browsePath = WallpaperService.getCurrentBrowsePath(targetScreen.name); + currentBrowsePath = browsePath; + + // Bump generation so stale scan callbacks from rapid navigation are ignored + var gen = ++_browseScanGeneration; + WallpaperService.scanDirectoryWithDirs(targetScreen.name, browsePath, function (result) { + if (gen !== _browseScanGeneration) + return; // Stale callback from a superseded navigation + wallpapersList = result.files; + directoriesList = result.directories; + Logger.d("WallpaperPanel", "Browse mode: Got", wallpapersList.length, "files and", directoriesList.length, "directories for screen", targetScreen.name); + updateFiltered(); + }); + } else { + // Normal mode: just use the wallpaper list from service + wallpapersList = WallpaperService.getWallpapersList(targetScreen.name); + directoriesList = []; + Logger.d("WallpaperPanel", "Got", wallpapersList.length, "wallpapers for screen", targetScreen.name); + updateFiltered(); + } + } + + function selectItem(path, isDirectory) { + if (isDirectory) { + WallpaperService.setBrowsePath(targetScreen.name, path); + } else { + var screen = Settings.data.wallpaper.setWallpaperOnAllMonitors ? undefined : targetScreen.name; + WallpaperService.changeWallpaper(path, screen, WallpaperService.wallpaperSelectionAppearance); + WallpaperService.applyFavoriteTheme(path, screen, WallpaperService.wallpaperSelectionAppearance); + } + } + + // Helper function to cycle view modes + function cycleViewMode() { + var mode = Settings.data.wallpaper.viewMode; + if (mode === "single") { + Settings.data.wallpaper.viewMode = "recursive"; + } else if (mode === "recursive") { + Settings.data.wallpaper.viewMode = "browse"; + } else { + Settings.data.wallpaper.viewMode = "single"; + } + } + + // Helper function to get icon for current view mode + function getViewModeIcon() { + var mode = Settings.data.wallpaper.viewMode; + if (mode === "single") + return "folder"; + if (mode === "recursive") + return "folders"; + return "folder-open"; + } + + // Helper function to get tooltip for current view mode + function getViewModeTooltip() { + var mode = Settings.data.wallpaper.viewMode; + var modeName; + if (mode === "single") + modeName = I18n.tr("panels.wallpaper.view-mode-single"); + else if (mode === "recursive") + modeName = I18n.tr("panels.wallpaper.view-mode-recursive"); + else + modeName = I18n.tr("panels.wallpaper.view-mode-browse"); + return I18n.tr("panels.wallpaper.view-mode-cycle-tooltip").replace("{mode}", modeName); + } + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginM + + // Combined toolbar: navigation (left) + actions (right) + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + + // Left side: navigation (back, home, path) + NIconButton { + icon: "arrow-left" + tooltipText: I18n.tr("wallpaper.browse.go-up") + enabled: isBrowseMode && currentBrowsePath !== WallpaperService.getMonitorDirectory(targetScreen?.name ?? "") + onClicked: WallpaperService.navigateUp(targetScreen?.name ?? "") + baseSize: Style.baseWidgetSize * 0.8 + } + + NIconButton { + icon: "home" + tooltipText: I18n.tr("wallpaper.browse.go-root") + enabled: isBrowseMode && currentBrowsePath !== WallpaperService.getMonitorDirectory(targetScreen?.name ?? "") + onClicked: WallpaperService.navigateToRoot(targetScreen?.name ?? "") + baseSize: Style.baseWidgetSize * 0.8 + } + + NScrollText { + text: isBrowseMode ? currentBrowsePath : WallpaperService.getMonitorDirectory(targetScreen?.name ?? "") + Layout.fillWidth: true + scrollMode: NScrollText.ScrollMode.Hover + fadeCornerRadius: Style.radiusM + + NText { + text: isBrowseMode ? currentBrowsePath : WallpaperService.getMonitorDirectory(targetScreen?.name ?? "") + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + } + } + + // Right side: actions (view mode, hide filenames, refresh) + NIconButton { + property string sortOrder: Settings.data.wallpaper.sortOrder || "name" + icon: { + if (sortOrder === "date_desc") + return "clock"; + if (sortOrder === "date_asc") + return "history"; + if (sortOrder === "name_desc") + return "sort-descending"; + if (sortOrder === "random") + return "arrows-shuffle"; + return "sort-ascending"; + } + tooltipText: { + if (sortOrder === "date_desc") + return I18n.tr("wallpaper.panel.sort-date-desc"); + if (sortOrder === "date_asc") + return I18n.tr("wallpaper.panel.sort-date-asc"); + if (sortOrder === "name_desc") + return I18n.tr("wallpaper.panel.sort-name-desc"); + if (sortOrder === "random") + return I18n.tr("wallpaper.panel.sort-random"); + return I18n.tr("wallpaper.panel.sort-name-asc"); + } + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + var next = "name"; + if (sortOrder === "name") + next = "date_desc"; + else if (sortOrder === "date_desc") + next = "name"; // Toggle simpler: Name -> Newest -> Name + // Expanded cycle: Name -> Newest -> Oldest -> Z-A -> Random -> Name + // User just asked for "newest first", so let's make it easy to reach. + // Let's do: Name (A-Z) -> Newest -> Oldest -> Name (Z-A) -> ... + + if (sortOrder === "name") + next = "date_desc"; + else if (sortOrder === "date_desc") + next = "date_asc"; + else if (sortOrder === "date_asc") + next = "name_desc"; + else if (sortOrder === "name_desc") + next = "random"; + else + next = "name"; + + Settings.data.wallpaper.sortOrder = next; + } + } + + NIconButton { + icon: getViewModeIcon() + tooltipText: getViewModeTooltip() + baseSize: Style.baseWidgetSize * 0.8 + onClicked: cycleViewMode() + } + + NIconButton { + icon: Settings.data.wallpaper.hideWallpaperFilenames ? "id-off" : "id" + tooltipText: Settings.data.wallpaper.hideWallpaperFilenames ? I18n.tr("panels.wallpaper.settings-hide-wallpaper-filenames-tooltip-show") : I18n.tr("panels.wallpaper.settings-hide-wallpaper-filenames-tooltip-hide") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: Settings.data.wallpaper.hideWallpaperFilenames = !Settings.data.wallpaper.hideWallpaperFilenames + } + + NIconButton { + icon: Settings.data.wallpaper.showHiddenFiles ? "eye" : "eye-closed" + tooltipText: Settings.data.wallpaper.showHiddenFiles ? I18n.tr("panels.wallpaper.settings-show-hidden-files-tooltip-hide") : I18n.tr("panels.wallpaper.settings-show-hidden-files-tooltip-show") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: Settings.data.wallpaper.showHiddenFiles = !Settings.data.wallpaper.showHiddenFiles + } + + NIconButton { + icon: "refresh" + tooltipText: I18n.tr("tooltips.refresh-wallpaper-list") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + if (isBrowseMode) { + refreshWallpaperScreenData(); + } else { + WallpaperService.refreshWallpapersList(); + } + } + } + } + + NGridView { + id: wallpaperGridView + + Layout.fillWidth: true + Layout.fillHeight: true + + visible: !WallpaperService.scanning + interactive: true + keyNavigationEnabled: true + keyNavigationWraps: false + highlightFollowsCurrentItem: false + currentIndex: -1 + + model: wallpaperModel + + Component.onCompleted: { + positionViewAtBeginning(); + } + + property int columns: (screen.width > 1920) ? 5 : 4 + property int itemSize: cellWidth + + cellWidth: Math.floor((availableWidth - leftMargin - rightMargin) / columns) + cellHeight: Math.floor(itemSize * 0.7) + Style.marginXS + Style.fontSizeXS + Style.marginM + + leftMargin: Style.marginS + rightMargin: Style.marginS + topMargin: Style.marginS + bottomMargin: Style.marginS + + onCurrentIndexChanged: { + if (currentIndex >= 0) { + positionViewAtIndex(currentIndex, GridView.Contain); + } + } + + onKeyPressed: event => { + if (Keybinds.checkKey(event, 'enter', Settings)) { + if (currentIndex >= 0 && currentIndex < filteredItems.length) { + selectItem(filteredItems[currentIndex]); + } + event.accepted = true; + } + } + + delegate: Item { + id: wallpaperItemWrapper + width: wallpaperGridView.cellWidth + height: wallpaperGridView.cellHeight + + ColumnLayout { + id: wallpaperItem + anchors.fill: parent + anchors.margins: Style.marginXS + + property string wallpaperPath: model.path ?? "" + property bool isDirectory: model.isDirectory ?? false + property bool isSelected: !isDirectory && (wallpaperPath === currentWallpaper) + property bool isFavorited: { + WallpaperService.favoritesRevision; + return !isDirectory && WallpaperService.isFavorite(wallpaperPath); + } + property string filename: model.name ?? wallpaperPath.split('/').pop() + property string cachedPath: "" + + spacing: Style.marginXS + + Component.onCompleted: { + if (!isDirectory && ImageCacheService.initialized) { + ImageCacheService.getThumbnail(wallpaperPath, function (path, success) { + if (wallpaperItem) + wallpaperItem.cachedPath = success ? path : wallpaperPath; + }); + } else if (!isDirectory) { + cachedPath = wallpaperPath; + } + } + + Item { + id: imageContainer + Layout.fillWidth: true + Layout.fillHeight: true + + property real imageHeight: Math.round(wallpaperGridView.itemSize * 0.67) + + // Directory display + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: imageContainer.imageHeight + color: Color.mSurfaceVariant + radius: Style.radiusM + visible: wallpaperItem.isDirectory + border.color: wallpaperGridView.currentIndex === index ? Color.mHover : Color.mSurface + border.width: Math.max(1, Style.borderL * 1.5) + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginS + + NIcon { + icon: "folder" + pointSize: Style.fontSizeXXXL + color: Color.mPrimary + Layout.alignment: Qt.AlignHCenter + } + } + } + + // Image display (for non-directories) + NImageRounded { + id: img + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: imageContainer.imageHeight + visible: !wallpaperItem.isDirectory + imagePath: wallpaperItem.cachedPath + radius: Style.radiusM + borderColor: { + if (wallpaperItem.isSelected) { + return Color.mSecondary; + } + if (wallpaperGridView.currentIndex === index) { + return Color.mHover; + } + return Color.mSurface; + } + borderWidth: Math.max(1, Style.borderL * 1.5) + imageFillMode: Image.PreserveAspectCrop + } + + // Loading/error state background (for non-directories) + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: imageContainer.imageHeight + color: Color.mSurfaceVariant + radius: Style.radiusM + visible: !wallpaperItem.isDirectory && (img.status === Image.Loading || img.status === Image.Error || wallpaperItem.cachedPath === "") + + NIcon { + icon: "image" + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + anchors.centerIn: parent + } + } + + NBusyIndicator { + anchors.horizontalCenter: parent.horizontalCenter + y: (imageContainer.imageHeight - height) / 2 + visible: !wallpaperItem.isDirectory && (img.status === Image.Loading || wallpaperItem.cachedPath === "") + running: visible + size: 18 + } + + Rectangle { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginS + width: 28 + height: 28 + radius: width / 2 + z: 6 + color: Color.mSecondary + border.color: Color.mOutline + border.width: Style.borderS + visible: wallpaperItem.isSelected + + NIcon { + icon: "check" + pointSize: Style.fontSizeM + color: Color.mOnSecondary + anchors.centerIn: parent + } + + MouseArea { + anchors.fill: parent + onClicked: {} + } + } + + // Favorite star button (top-left) + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + anchors.margins: Style.marginS + width: 28 + height: 28 + radius: width / 2 + visible: !wallpaperItem.isDirectory && (wallpaperItem.isFavorited || hoverHandler.hovered || wallpaperGridView.currentIndex === index) + color: { + if (wallpaperItem.isFavorited) + return starHoverHandler.hovered ? Color.mHover : Color.mPrimary; + return starHoverHandler.hovered ? Color.mSurfaceVariant : Color.mSurface; + } + opacity: wallpaperItem.isFavorited || starHoverHandler.hovered ? 1.0 : 0.7 + z: 11 + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + + NIcon { + icon: wallpaperItem.isFavorited ? "star-filled" : "star" + pointSize: Style.fontSizeM + color: { + if (wallpaperItem.isFavorited) + return starHoverHandler.hovered ? Color.mOnHover : Color.mOnPrimary; + return starHoverHandler.hovered ? Color.mOnSurface : Color.mOnSurfaceVariant; + } + anchors.centerIn: parent + } + + HoverHandler { + id: starHoverHandler + } + + TapHandler { + onTapped: { + var mon = Settings.data.wallpaper.setWallpaperOnAllMonitors ? undefined : (wallpaperScreenView.targetScreen ? wallpaperScreenView.targetScreen.name : undefined); + WallpaperService.toggleFavorite(wallpaperItem.wallpaperPath, WallpaperService.wallpaperSelectionAppearance, mon); + } + } + } + + // Palette color dots (bottom-center, favorites only) — taps must not fall through to selectItem + Item { + id: paletteRow + anchors.bottom: img.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: Style.marginS + z: 10 + implicitWidth: paletteRowRow.implicitWidth + implicitHeight: paletteRowRow.implicitHeight + width: implicitWidth + height: implicitHeight + visible: wallpaperItem.isFavorited && paletteRow.colors.length > 0 + + property int diameter: 25 * Style.uiScaleRatio + property int _favRevision: 0 + property var favData: { + _favRevision; + WallpaperService.favoritesRevision; + Settings.data.wallpaper.linkLightAndDarkWallpapers; + return WallpaperService.getFavoriteForDisplay(wallpaperItem.wallpaperPath); + } + property var colors: favData && favData.paletteColors ? favData.paletteColors : [] + property bool isDark: { + if (!favData) { + return false; + } + if (favData.appearance === "dark") { + return true; + } + if (favData.appearance === "light") { + return false; + } + return favData.darkMode === true; + } + + Connections { + target: WallpaperService + function onFavoriteDataUpdated(path) { + if (path === wallpaperItem.wallpaperPath) + paletteRow._favRevision++; + } + } + + Row { + id: paletteRowRow + spacing: Style.marginXS + + // Sun/moon only when light/dark share one wallpaper (which appearance the favorite targets) + Rectangle { + width: paletteRow.diameter + height: paletteRow.diameter + radius: width * 0.5 + visible: Settings.data.wallpaper.linkLightAndDarkWallpapers + color: Color.mSurface + border.color: Color.mShadow + border.width: Style.borderS + + NIcon { + icon: paletteRow.isDark ? "moon" : "sun" + pointSize: parent.width * 0.45 + color: Color.mOnSurface + anchors.centerIn: parent + } + } + + Repeater { + model: paletteRow.colors + + Rectangle { + width: paletteRow.diameter + height: paletteRow.diameter + radius: width * 0.5 + color: modelData + border.color: Color.mShadow + border.width: Style.borderS + } + } + } + + TapHandler { + onTapped: {} + } + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: imageContainer.imageHeight + color: Color.mSurface + radius: Style.radiusM + opacity: (hoverHandler.hovered || wallpaperItem.isSelected || wallpaperGridView.currentIndex === index) ? 0 : 0.3 + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + + HoverHandler { + id: hoverHandler + } + + TapHandler { + onTapped: { + wallpaperGridView.forceActiveFocus(); + wallpaperGridView.currentIndex = index; + selectItem(wallpaperItem.wallpaperPath, wallpaperItem.isDirectory); + } + } + } + + NText { + text: wallpaperItem.filename + visible: !Settings.data.wallpaper.hideWallpaperFilenames + color: (hoverHandler.hovered || wallpaperItem.isSelected || wallpaperGridView.currentIndex === index) ? Color.mOnSurface : Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + Layout.fillWidth: true + Layout.leftMargin: Style.marginS + Layout.rightMargin: Style.marginS + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + } + } + } + + // Empty / scanning state + Rectangle { + color: Color.mSurface + radius: Style.radiusM + border.color: Color.mOutline + border.width: Style.borderS + visible: (wallpaperModel.count === 0 && !WallpaperService.scanning) || WallpaperService.scanning + Layout.fillWidth: true + Layout.preferredHeight: 130 + + ColumnLayout { + anchors.fill: parent + visible: WallpaperService.scanning + NBusyIndicator { + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + } + } + + ColumnLayout { + anchors.fill: parent + visible: wallpaperModel.count === 0 && !WallpaperService.scanning + Item { + Layout.fillHeight: true + } + NIcon { + icon: "folder-open" + pointSize: Style.fontSizeXXL + color: Color.mOnSurface + Layout.alignment: Qt.AlignHCenter + } + NText { + text: (panelContent.filterText && panelContent.filterText.length > 0) ? I18n.tr("wallpaper.no-match") : (isBrowseMode ? I18n.tr("wallpaper.browse.empty-directory") : I18n.tr("wallpaper.no-wallpaper")) + color: Color.mOnSurface + font.weight: Style.fontWeightBold + Layout.alignment: Qt.AlignHCenter + } + NText { + text: (panelContent.filterText && panelContent.filterText.length > 0) ? I18n.tr("wallpaper.try-different-search") : (isBrowseMode ? I18n.tr("wallpaper.browse.go-up-hint") : I18n.tr("wallpaper.configure-directory")) + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter + } + Item { + Layout.fillHeight: true + } + } + } + } + } + + // Component for Wallhaven wallpapers view + component WallhavenView: Item { + id: wallhavenViewRoot + property alias gridView: wallhavenGridView + property alias pageInput: pageInput + + property var wallpapers: [] + property bool loading: false + property string errorMessage: "" + property bool initialized: false + property bool searchScheduled: false + + Connections { + target: typeof WallhavenService !== "undefined" ? WallhavenService : null + function onSearchCompleted(results, meta) { + wallhavenViewRoot.wallpapers = results || []; + wallhavenViewRoot.loading = false; + wallhavenViewRoot.errorMessage = ""; + wallhavenViewRoot.searchScheduled = false; + } + function onSearchFailed(error) { + wallhavenViewRoot.loading = false; + wallhavenViewRoot.errorMessage = error || ""; + wallhavenViewRoot.searchScheduled = false; + } + } + + Component.onCompleted: { + // Initialize service properties and perform initial search if Wallhaven is active + if (typeof WallhavenService !== "undefined" && Settings.data.wallpaper.useWallhaven && !initialized) { + // Set flags immediately to prevent race conditions + if (WallhavenService.initialSearchScheduled) { + // Another instance already scheduled the search, just initialize properties + initialized = true; + return; + } + + // We're the first one - claim the search + initialized = true; + WallhavenService.initialSearchScheduled = true; + WallhavenService.categories = Settings.data.wallpaper.wallhavenCategories; + WallhavenService.purity = Settings.data.wallpaper.wallhavenPurity; + WallhavenService.sorting = Settings.data.wallpaper.wallhavenSorting; + WallhavenService.order = Settings.data.wallpaper.wallhavenOrder; + + // Initialize resolution settings + var width = Settings.data.wallpaper.wallhavenResolutionWidth || ""; + var height = Settings.data.wallpaper.wallhavenResolutionHeight || ""; + var mode = Settings.data.wallpaper.wallhavenResolutionMode || "atleast"; + if (width && height) { + var resolution = width + "x" + height; + if (mode === "atleast") { + WallhavenService.minResolution = resolution; + WallhavenService.resolutions = ""; + } else { + WallhavenService.minResolution = ""; + WallhavenService.resolutions = resolution; + } + } else { + WallhavenService.minResolution = ""; + WallhavenService.resolutions = ""; + } + + // Now check if we can actually search (fetching check is in WallhavenService.search) + // Use persisted currentPage to maintain state across window reopening + loading = true; + WallhavenService.search(Settings.data.wallpaper.wallhavenQuery || "", WallhavenService.currentPage); + } + } + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginM + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + NGridView { + id: wallhavenGridView + + anchors.fill: parent + + visible: !loading && errorMessage === "" && (wallpapers && wallpapers.length > 0) + interactive: true + keyNavigationEnabled: true + keyNavigationWraps: false + highlightFollowsCurrentItem: false + currentIndex: -1 + + model: wallpapers || [] + + onModelChanged: { + // Reset selection and scroll position when model changes + currentIndex = -1; + positionViewAtBeginning(); + } + + Component.onCompleted: { + positionViewAtBeginning(); + } + + property int columns: (screen.width > 1920) ? 5 : 4 + property int itemSize: cellWidth + + cellWidth: Math.floor((availableWidth - leftMargin - rightMargin) / columns) + cellHeight: Math.floor(itemSize * 0.7) + Style.marginXS + Style.fontSizeXS + Style.marginM + + leftMargin: Style.marginS + rightMargin: Style.marginS + topMargin: Style.marginS + bottomMargin: Style.marginS + + onCurrentIndexChanged: { + if (currentIndex >= 0) { + positionViewAtIndex(currentIndex, GridView.Contain); + } + } + + onKeyPressed: event => { + if (Keybinds.checkKey(event, 'enter', Settings)) { + if (currentIndex >= 0 && currentIndex < wallpapers.length) { + let wallpaper = wallpapers[currentIndex]; + wallhavenDownloadAndApply(wallpaper); + } + event.accepted = true; + } + } + + delegate: Item { + id: wallhavenItemWrapper + width: wallhavenGridView.cellWidth + height: wallhavenGridView.cellHeight + + ColumnLayout { + id: wallhavenItem + anchors.fill: parent + anchors.margins: Style.marginXS + + property string thumbnailUrl: (modelData && typeof WallhavenService !== "undefined") ? WallhavenService.getThumbnailUrl(modelData, "large") : "" + property string wallpaperId: (modelData && modelData.id) ? modelData.id : "" + + spacing: Style.marginXS + + Item { + id: imageContainer + Layout.fillWidth: true + Layout.fillHeight: true + + property real imageHeight: Math.round(wallhavenGridView.itemSize * 0.67) + + NImageRounded { + id: img + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: imageContainer.imageHeight + imagePath: wallhavenItem.thumbnailUrl + radius: Style.radiusM + borderColor: { + if (wallhavenGridView.currentIndex === index) { + return Color.mHover; + } + return Color.mSurface; + } + borderWidth: Math.max(1, Style.borderL * 1.5) + imageFillMode: Image.PreserveAspectCrop + } + + // Loading/error state background + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: imageContainer.imageHeight + color: Color.mSurfaceVariant + radius: Style.radiusM + visible: img.status === Image.Loading || img.status === Image.Error || wallhavenItem.thumbnailUrl === "" + + NIcon { + icon: "image" + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + anchors.centerIn: parent + } + } + + NBusyIndicator { + anchors.horizontalCenter: parent.horizontalCenter + y: (imageContainer.imageHeight - height) / 2 + visible: img.status === Image.Loading + running: visible + size: 18 + } + + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + height: imageContainer.imageHeight + color: Color.mSurface + radius: Style.radiusM + opacity: (hoverHandler.hovered || wallhavenGridView.currentIndex === index) ? 0 : 0.3 + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + + HoverHandler { + id: hoverHandler + } + + TapHandler { + onTapped: { + wallhavenGridView.forceActiveFocus(); + wallhavenGridView.currentIndex = index; + wallhavenDownloadAndApply(modelData); + } + } + } + + NText { + text: wallhavenItem.wallpaperId || I18n.tr("common.unknown") + visible: !Settings.data.wallpaper.hideWallpaperFilenames + color: (hoverHandler.hovered || wallhavenGridView.currentIndex === index) ? Color.mOnSurface : Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + Layout.fillWidth: true + Layout.leftMargin: Style.marginS + Layout.rightMargin: Style.marginS + Layout.alignment: Qt.AlignHCenter + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + } + } + } + + // Loading overlay - fills same space as GridView to prevent jumping + Rectangle { + anchors.fill: parent + color: Color.mSurface + radius: Style.radiusM + border.color: Color.mOutline + border.width: Style.borderS + visible: loading || (typeof WallhavenService !== "undefined" && WallhavenService.fetching) + z: 10 + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + Item { + Layout.fillHeight: true + } + + NBusyIndicator { + size: Style.baseWidgetSize * 1.5 + color: Color.mPrimary + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: I18n.tr("wallpaper.wallhaven.loading") + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeM + Layout.alignment: Qt.AlignHCenter + } + + Item { + Layout.fillHeight: true + } + } + } + + // Error overlay + Rectangle { + anchors.fill: parent + color: Color.mSurface + radius: Style.radiusM + border.color: Color.mOutline + border.width: Style.borderS + visible: errorMessage !== "" && !loading + z: 10 + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + Item { + Layout.fillHeight: true + } + + NIcon { + icon: "alert-circle" + pointSize: Style.fontSizeXXL + color: Color.mError + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: errorMessage + color: Color.mOnSurface + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + } + + Item { + Layout.fillHeight: true + } + } + } + + // Empty state overlay + Rectangle { + anchors.fill: parent + color: Color.mSurface + radius: Style.radiusM + border.color: Color.mOutline + border.width: Style.borderS + visible: (!wallpapers || wallpapers.length === 0) && !loading && errorMessage === "" + z: 10 + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + Item { + Layout.fillHeight: true + } + + NIcon { + icon: "image" + pointSize: Style.fontSizeXXL + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: I18n.tr("wallpaper.wallhaven.no-results") + color: Color.mOnSurface + wrapMode: Text.WordWrap + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + } + + Item { + Layout.fillHeight: true + } + } + } + } + + // Pagination + RowLayout { + Layout.fillWidth: true + visible: errorMessage === "" && typeof WallhavenService !== "undefined" + spacing: Style.marginS + + Item { + Layout.fillWidth: true + } + + NIconButton { + icon: "chevron-left" + enabled: !loading && WallhavenService.currentPage > 1 && !WallhavenService.fetching + onClicked: WallhavenService.previousPage() + } + + RowLayout { + spacing: Style.marginXS + + NText { + text: I18n.tr("wallpaper.wallhaven.page-prefix") + color: Color.mOnSurface + } + + NTextInput { + id: pageInput + text: "" + WallhavenService.currentPage + Layout.preferredWidth: 50 * Style.uiScaleRatio + Layout.maximumWidth: 50 * Style.uiScaleRatio + Layout.fillWidth: false + minimumInputWidth: 50 * Style.uiScaleRatio + horizontalAlignment: Text.AlignHCenter + inputMethodHints: Qt.ImhDigitsOnly + enabled: !loading && !WallhavenService.fetching + showClearButton: false + + Connections { + target: WallhavenService + function onCurrentPageChanged() { + pageInput.text = "" + WallhavenService.currentPage; + } + } + + function submitPage() { + var page = parseInt(text); + if (!isNaN(page) && page >= 1 && page <= WallhavenService.lastPage) { + if (page !== WallhavenService.currentPage) { + WallhavenService.search(Settings.data.wallpaper.wallhavenQuery || "", page); + } + } else { + // Reset to current page if invalid + text = "" + WallhavenService.currentPage; + } + // Force focus loss to ensure UI updates cleanly + pageInput.inputItem.focus = false; + } + + onEditingFinished: submitPage() + } + + NText { + text: I18n.tr("wallpaper.wallhaven.page-suffix").replace("{total}", WallhavenService.lastPage) + color: Color.mOnSurface + } + } + + NIconButton { + icon: "chevron-right" + enabled: WallhavenService.currentPage < WallhavenService.lastPage && !WallhavenService.fetching + onClicked: WallhavenService.nextPage() + } + + Item { + Layout.fillWidth: true + } + } + } + + // ------------------------------- + function wallhavenDownloadAndApply(wallpaper, targetScreen) { + if (typeof WallhavenService !== "undefined") { + WallhavenService.downloadWallpaper(wallpaper, function (success, localPath) { + if (success) { + var whScreen = Settings.data.wallpaper.setWallpaperOnAllMonitors ? undefined : Quickshell.screens[currentScreenIndex].name; + if (!Settings.data.wallpaper.setWallpaperOnAllMonitors && currentScreenIndex < Quickshell.screens.length) { + WallpaperService.changeWallpaper(localPath, Quickshell.screens[currentScreenIndex].name, WallpaperService.wallpaperSelectionAppearance); + } else { + WallpaperService.changeWallpaper(localPath, undefined, WallpaperService.wallpaperSelectionAppearance); + } + WallpaperService.applyFavoriteTheme(localPath, whScreen, WallpaperService.wallpaperSelectionAppearance); + } + }); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Toast/Toast.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Toast/Toast.qml new file mode 100644 index 0000000..a3cf7df --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Toast/Toast.qml @@ -0,0 +1,415 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.System +import qs.Widgets + +Item { + id: root + + property string title: "" + property string description: "" + property string icon: "" + property string type: "notice" + property int duration: 3000 + property string actionLabel: "" + property var actionCallback: null + readonly property real initialScale: 0.7 + + signal hidden + + readonly property bool isCompact: Settings.data.notifications?.density === "compact" + readonly property int notificationWidth: Math.round((isCompact ? 320 : 440) * Style.uiScaleRatio) + readonly property int shadowPadding: Style.shadowBlurMax + Style.marginL + + width: notificationWidth + shadowPadding * 2 + height: Math.round(contentLayout.implicitHeight + Style.margin2M * 2 + shadowPadding * 2) + visible: true + opacity: 0 + scale: initialScale + + property real progress: 1.0 + property bool isHovered: false + property real swipeOffset: 0 + property real swipeOffsetY: 0 + property real pressGlobalX: 0 + property real pressGlobalY: 0 + property bool isSwiping: false + readonly property string location: Settings.data.notifications?.location || "top_right" + readonly property bool isLeft: location.endsWith("_left") + readonly property bool isRight: location.endsWith("_right") + readonly property bool useVerticalSwipe: location === "bottom" || location === "top" + readonly property real swipeStartThreshold: Math.round(18 * Style.uiScaleRatio) + readonly property real swipeDismissThreshold: Math.max(110, background.width * 0.32) + readonly property real verticalSwipeDismissThreshold: Math.max(70, background.height * 0.35) + + transform: Translate { + x: root.swipeOffset + y: root.swipeOffsetY + } + + function clampSwipeDelta(deltaX) { + if (isRight) + return Math.max(0, deltaX); + if (isLeft) + return Math.min(0, deltaX); + return deltaX; + } + + function clampVerticalSwipeDelta(deltaY) { + if (location === "bottom") + return Math.max(0, deltaY); + if (location === "top") + return Math.min(0, deltaY); + return deltaY; + } + + HoverHandler { + onHoveredChanged: { + isHovered = hovered; + if (isHovered) { + resumeTimer.stop(); + if (progressAnimation.running && !progressAnimation.paused) { + progressAnimation.pause(); + } + } else { + resumeTimer.start(); + } + } + } + + Timer { + id: resumeTimer + interval: 50 + repeat: false + onTriggered: { + if (!isHovered && progressAnimation.paused) { + progressAnimation.resume(); + } + } + } + + // Background rectangle (apply shadows here) + Rectangle { + id: background + anchors.fill: parent + anchors.margins: shadowPadding + radius: Style.radiusL + color: Qt.alpha(Color.mSurface, Color.adaptiveOpacity(Settings.data.notifications.backgroundOpacity) || 1.0) + + // Colored border based on type + border.width: Style.borderS + border.color: { + var baseColor; + switch (root.type) { + case "error": + baseColor = Color.mError; + break; + default: + baseColor = Color.mOutline; + break; + } + return Qt.alpha(baseColor, Color.adaptiveOpacity(Settings.data.notifications.backgroundOpacity) || 1.0); + } + + // Progress bar + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + height: 2 + color: "transparent" + + Rectangle { + id: progressBar + readonly property real progressWidth: background.width - (2 * background.radius) + height: parent.height + // Mirrored logic: centers the bar as it shrinks + x: background.radius + (progressWidth * (1 - root.progress)) / 2 + width: progressWidth * root.progress + + color: { + var baseColor; + switch (root.type) { + case "warning": + baseColor = Color.mPrimary; + break; + case "error": + baseColor = Color.mError; + break; + default: + baseColor = Color.mPrimary; // Match standard notification color + break; + } + return Qt.alpha(baseColor, Color.adaptiveOpacity(Settings.data.notifications.backgroundOpacity) || 1.0); + } + } + } + } + + NDropShadow { + anchors.fill: background + source: background + autoPaddingEnabled: true + } + + NumberAnimation { + id: progressAnimation + target: root + property: "progress" + from: 1.0 + to: 0.0 + duration: root.duration + easing.type: Easing.Linear + onFinished: { + if (root.progress === 0.0 && root.visible) { + root.hide(); + } + } + } + + // Timer: hideTimer removed, using progressAnimation + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + Behavior on scale { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + Behavior on swipeOffset { + enabled: !root.isSwiping + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + Behavior on swipeOffsetY { + enabled: !root.isSwiping + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + Timer { + id: hideAnimation + interval: Style.animationFast + onTriggered: { + root.visible = false; + root.hidden(); + } + } + + // Cleanup on destruction + Component.onDestruction: { + progressAnimation.stop(); + hideAnimation.stop(); + } + + // Click anywhere dismiss the toast (must be before content so action link can override) + MouseArea { + id: toastDragArea + anchors.fill: background + acceptedButtons: Qt.LeftButton + hoverEnabled: true + onPressed: mouse => { + const globalPoint = toastDragArea.mapToGlobal(mouse.x, mouse.y); + root.pressGlobalX = globalPoint.x; + root.pressGlobalY = globalPoint.y; + root.isSwiping = false; + } + onPositionChanged: mouse => { + if (!(mouse.buttons & Qt.LeftButton)) + return; + const globalPoint = toastDragArea.mapToGlobal(mouse.x, mouse.y); + const rawDeltaX = globalPoint.x - root.pressGlobalX; + const rawDeltaY = globalPoint.y - root.pressGlobalY; + const deltaX = root.clampSwipeDelta(rawDeltaX); + const deltaY = root.clampVerticalSwipeDelta(rawDeltaY); + if (!root.isSwiping) { + if (root.useVerticalSwipe) { + if (Math.abs(deltaY) < root.swipeStartThreshold) + return; + root.isSwiping = true; + } else { + if (Math.abs(deltaX) < root.swipeStartThreshold) + return; + root.isSwiping = true; + } + } + if (root.useVerticalSwipe) { + root.swipeOffset = 0; + root.swipeOffsetY = deltaY; + } else { + root.swipeOffset = deltaX; + root.swipeOffsetY = 0; + } + } + onReleased: mouse => { + if (mouse.button !== Qt.LeftButton) + return; + if (root.isSwiping) { + root.isSwiping = false; + const dismissDistance = root.useVerticalSwipe ? Math.abs(root.swipeOffsetY) : Math.abs(root.swipeOffset); + const threshold = root.useVerticalSwipe ? root.verticalSwipeDismissThreshold : root.swipeDismissThreshold; + if (dismissDistance >= threshold) { + root.hide(); + } else { + root.swipeOffset = 0; + root.swipeOffsetY = 0; + } + return; + } + root.hide(); + } + onCanceled: { + root.isSwiping = false; + root.swipeOffset = 0; + root.swipeOffsetY = 0; + } + cursorShape: Qt.PointingHandCursor + } + + RowLayout { + id: contentLayout + anchors.fill: background + anchors.topMargin: isCompact ? Style.marginS : Style.marginM + anchors.bottomMargin: isCompact ? Style.marginS : Style.marginM + anchors.leftMargin: isCompact ? Style.marginM : Style.margin2M + anchors.rightMargin: isCompact ? Style.marginM : Style.margin2M + spacing: isCompact ? Style.marginM : Style.marginL + + // Icon + NIcon { + icon: if (root.icon !== "") { + return root.icon; + } else if (type === "warning") { + return "toast-warning"; + } else if (type === "error") { + return "toast-error"; + } else { + return "toast-notice"; + } + color: { + switch (type) { + case "warning": + return Color.mPrimary; + case "error": + return Color.mError; + default: + return Color.mOnSurface; + } + } + pointSize: isCompact ? Style.fontSizeXL : Style.fontSizeXXL * 1.5 + Layout.alignment: Qt.AlignVCenter + } + + // Label and description + ColumnLayout { + spacing: Style.marginXXS + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + + NText { + Layout.fillWidth: true + text: root.title + color: Color.mOnSurface + pointSize: isCompact ? Style.fontSizeM : Style.fontSizeL + font.weight: Style.fontWeightBold + wrapMode: Text.WordWrap + visible: text.length > 0 + } + + NText { + Layout.fillWidth: true + text: root.description + color: Color.mOnSurface + pointSize: isCompact ? Style.fontSizeS : Style.fontSizeM + wrapMode: Text.WordWrap + maximumLineCount: isCompact ? 2 : 20 + elide: isCompact ? Text.ElideRight : Text.ElideNone + visible: text.length > 0 + } + + // Action button + NButton { + text: root.actionLabel + visible: root.actionLabel.length > 0 && root.actionCallback !== null + Layout.topMargin: Style.marginXS + fontSize: Style.fontSizeS + backgroundColor: Color.mPrimary + textColor: hovered ? Color.mOnHover : Color.mOnPrimary + hoverColor: Color.mHover + outlined: false + implicitHeight: 24 + + onClicked: { + if (root.actionCallback) { + root.actionCallback(); + root.hide(); + } + } + } + } + } + + function show(msgTitle, msgDescription, msgIcon, msgType, msgDuration, msgActionLabel, msgActionCallback) { + // Stop all timers first + progressAnimation.stop(); + hideAnimation.stop(); + + title = msgTitle; + description = msgDescription || ""; + icon = msgIcon || ""; + type = msgType || "notice"; + duration = msgDuration || 3000; + actionLabel = msgActionLabel || ""; + actionCallback = msgActionCallback || null; + + visible = true; + opacity = 1.0; + scale = 1.0; + progress = 1.0; + isHovered = false; + isSwiping = false; + swipeOffset = 0; + swipeOffsetY = 0; + + // Configure and start animation + progressAnimation.duration = duration; + progressAnimation.from = 1.0; + progressAnimation.to = 0.0; + progressAnimation.restart(); + } + + function hide() { + progressAnimation.stop(); + isSwiping = false; + swipeOffset = 0; + swipeOffsetY = 0; + opacity = 0; + scale = initialScale; + hideAnimation.restart(); + } + + function hideImmediately() { + hideAnimation.stop(); + progressAnimation.stop(); + isSwiping = false; + swipeOffset = 0; + swipeOffsetY = 0; + opacity = 0; + scale = initialScale; + root.hidden(); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Toast/ToastOverlay.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Toast/ToastOverlay.qml new file mode 100644 index 0000000..d2ae91b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Toast/ToastOverlay.qml @@ -0,0 +1,22 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Widgets + +Variants { + model: { + const screens = Quickshell.screens.filter(screen => Settings.data.notifications.monitors.includes(screen.name)); + // Empty list can mean two things : + // - No (visible) notification display activated in settings + // - One or more (not visible) displays are activated but unplugged + // In both cases we fallback to show notification on all screens + return screens.length === 0 ? Quickshell.screens : screens; + } + + delegate: ToastScreen { + required property ShellScreen modelData + screen: modelData + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Toast/ToastScreen.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Toast/ToastScreen.qml new file mode 100644 index 0000000..22f4253 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Toast/ToastScreen.qml @@ -0,0 +1,246 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services.UI + +Item { + id: root + + required property ShellScreen screen + + // Local queue for this screen only (bounded to prevent memory issues) + property var messageQueue: [] + property int maxQueueSize: 10 + property bool isShowingToast: false + + // If true, immediately show new toasts + property bool replaceOnNew: true + + Connections { + target: ToastService + + function onNotify(title, description, icon, type, duration, actionLabel, actionCallback) { + root.enqueueToast({ + "title": title, + "description": description, + "icon": icon, + "type": type, + "duration": duration, + "actionLabel": actionLabel || "", + "actionCallback": actionCallback || null, + "timestamp": Date.now() + }); + } + + function onDismiss() { + root.dismissToast(); + } + } + + // Clear queue on component destruction to prevent orphaned toasts + Component.onDestruction: { + messageQueue = []; + isShowingToast = false; + hideTimer.stop(); + quickSwitchTimer.stop(); + } + + function dismissToast() { + if (windowLoader.item) { + hideTimer.stop(); + windowLoader.item.hideToast(); + } + } + + function enqueueToast(toastData) { + // Safe logging - fix the substring bug + var descPreview = (toastData.description || "").substring(0, 100).replace(/\n/g, " "); + Logger.d("ToastScreen", "Queuing", toastData.type, ":", toastData.title, descPreview); + + // Bounded queue to prevent unbounded memory growth + if (messageQueue.length >= maxQueueSize) { + Logger.d("ToastScreen", "Queue full, dropping oldest toast"); + messageQueue.shift(); + } + + if (replaceOnNew && isShowingToast) { + // Cancel current toast and clear queue for latest toast + messageQueue = []; // Clear existing queue + messageQueue.push(toastData); + + // Hide current toast immediately + root.dismissToast(); + + // Process new toast after a brief delay + isShowingToast = false; + quickSwitchTimer.restart(); + } else { + // Queue the toast + messageQueue.push(toastData); + processQueue(); + } + } + + Timer { + id: quickSwitchTimer + interval: 50 // Brief delay for smooth transition + onTriggered: root.processQueue() + } + + function processQueue() { + if (messageQueue.length === 0 || isShowingToast) { + return; + } + + var data = messageQueue.shift(); + isShowingToast = true; + + // Store the toast data for when loader is ready + windowLoader.pendingToast = data; + + // Activate the loader - onStatusChanged will handle showing the toast + windowLoader.active = true; + } + + function onToastHidden() { + isShowingToast = false; + + // Deactivate the loader to completely remove the window and free memory + windowLoader.active = false; + + // Small delay before processing next toast + hideTimer.restart(); + } + + Timer { + id: hideTimer + interval: 200 + onTriggered: root.processQueue() + } + + // The loader that creates/destroys the PanelWindow as needed + // This is good for RAM efficiency when toasts are infrequent + Loader { + id: windowLoader + active: false // Only active when showing a toast + + // Store pending toast data + property var pendingToast: null + + onStatusChanged: { + // When loader becomes ready, show the pending toast + if (status === Loader.Ready && pendingToast !== null) { + item.showToast(pendingToast.title, pendingToast.description, pendingToast.icon, pendingToast.type, pendingToast.duration, pendingToast.actionLabel, pendingToast.actionCallback); + pendingToast = null; + } + } + + sourceComponent: PanelWindow { + id: panel + + property alias toastItem: toastItem + + screen: root.screen + + // Parse location setting + readonly property string location: Settings.data.notifications?.location || "top_right" + readonly property bool isTop: location.startsWith("top") + readonly property bool isBottom: location.startsWith("bottom") + readonly property bool isLeft: location.endsWith("_left") + readonly property bool isRight: location.endsWith("_right") + readonly property bool isCentered: location === "top" || location === "bottom" + + readonly property bool isFramed: Settings.data.bar.barType === "framed" + readonly property real frameThickness: Settings.data.bar.frameThickness ?? 8 + + readonly property string barPos: Settings.getBarPositionForScreen(panel.screen?.name) + readonly property bool isFloating: Settings.data.bar.barType === "floating" + readonly property real barHeight: Style.getBarHeightForScreen(panel.screen?.name) + + // Calculate bar and frame offsets for each edge separately + readonly property int barOffsetTop: { + if (barPos !== "top") + return isFramed ? frameThickness : 0; + const floatMarginV = isFloating ? Math.ceil(Settings.data.bar.marginVertical) : 0; + return barHeight + floatMarginV; + } + + readonly property int barOffsetBottom: { + if (barPos !== "bottom") + return isFramed ? frameThickness : 0; + const floatMarginV = isFloating ? Math.ceil(Settings.data.bar.marginVertical) : 0; + return barHeight + floatMarginV; + } + + readonly property int barOffsetLeft: { + if (barPos !== "left") + return isFramed ? frameThickness : 0; + const floatMarginH = isFloating ? Math.ceil(Settings.data.bar.marginHorizontal) : 0; + return barHeight + floatMarginH; + } + + readonly property int barOffsetRight: { + if (barPos !== "right") + return isFramed ? frameThickness : 0; + const floatMarginH = isFloating ? Math.ceil(Settings.data.bar.marginHorizontal) : 0; + return barHeight + floatMarginH; + } + + readonly property int shadowPadding: Style.shadowBlurMax + Style.marginL + + // Anchoring + anchors.top: isTop + anchors.bottom: isBottom + anchors.left: isLeft + anchors.right: isRight + + // Margins for PanelWindow - only apply bar offset for the specific edge where the bar is + margins.top: isTop ? barOffsetTop - shadowPadding + Style.marginM : 0 + margins.bottom: isBottom ? barOffsetBottom - shadowPadding + Style.marginM : 0 + margins.left: isLeft ? barOffsetLeft - shadowPadding + Style.marginM : 0 + margins.right: isRight ? barOffsetRight - shadowPadding + Style.marginM : 0 + + implicitWidth: Math.round(toastItem.width) + implicitHeight: Math.round(toastItem.height) + + color: "transparent" + + WlrLayershell.layer: (Settings.data.notifications && Settings.data.notifications.overlayLayer) ? WlrLayer.Overlay : WlrLayer.Top + WlrLayershell.namespace: "noctalia-toast-" + (screen?.name || "unknown") + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + WlrLayershell.exclusionMode: ExclusionMode.Ignore + + // Make shadow area click-through, only toast content is clickable + mask: Region { + x: 0 + y: 0 + width: panel.width + height: panel.height + intersection: Intersection.Xor + + Region { + // The clickable content area is inset by shadowPadding from all edges + x: panel.shadowPadding + y: panel.shadowPadding + width: Math.max(0, panel.width - panel.shadowPadding * 2) + height: Math.max(0, panel.height - panel.shadowPadding * 2) + intersection: Intersection.Subtract + } + } + + function showToast(title, description, icon, type, duration, actionLabel, actionCallback) { + toastItem.show(title, description, icon, type, duration, actionLabel, actionCallback); + } + + function hideToast() { + toastItem.hideImmediately(); + } + + Toast { + id: toastItem + onHidden: root.onToastHidden() + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Modules/Tooltip/Tooltip.qml b/fedora/.config/quickshell/noctalia-shell/Modules/Tooltip/Tooltip.qml new file mode 100644 index 0000000..d3deb3d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Modules/Tooltip/Tooltip.qml @@ -0,0 +1,701 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Commons +import qs.Widgets + +PopupWindow { + id: root + + property string text: "" + property var rows: null // Array of arrays for grid mode: [["col1", "col2"], ["row2col1", "row2col2"]] + property bool isGridMode: rows !== null && Array.isArray(rows) && rows.length > 0 + property int columnCount: isGridMode && rows.length > 0 ? rows[0].length : 0 + property string direction: "auto" // "auto", "left", "right", "top", "bottom" + property int margin: Style.marginXS // distance from target + property int padding: Style.marginM + property int gridPaddingVertical: Style.marginXS // extra vertical padding for grid mode + property int delay: 0 + property int hideDelay: 0 + property int maxWidth: 340 + + property int animationDuration: Style.animationFast + property real animationScale: 0.85 + + // Font scale calculation matching NText behavior + readonly property real gridFontScale: { + const baseScale = (tooltipText.family === Settings.data.ui.fontDefault ? Settings.data.ui.fontDefaultScale : Settings.data.ui.fontFixedScale); + return baseScale * Style.uiScaleRatio; + } + + // For measuring grid cell widths - must match actual NText font + TextMetrics { + id: cellMetrics + font.family: tooltipText.family + font.pointSize: Math.max(1, Style.fontSizeS * gridFontScale) + } + + // For measuring grid row height - hidden NText matches actual grid items exactly + NText { + id: rowHeightMeasure + visible: false + text: "Ag" // Characters with ascenders and descenders + pointSize: Style.fontSizeS + family: tooltipText.family + } + + // Internal properties + property var targetItem: null + property real anchorX: 0 + property real anchorY: 0 + property bool isPositioned: false + property bool animatingOut: true + property int screenWidth: 1920 + property int screenHeight: 1080 + property int screenX: 0 + property int screenY: 0 + + visible: false + color: "transparent" + + anchor.item: targetItem + anchor.rect.x: anchorX + anchor.rect.y: anchorY + + // Timer for showing tooltip after delay + Timer { + id: showTimer + interval: root.delay + repeat: false + onTriggered: { + root.positionAndShow(); + } + } + + // Timer for hiding tooltip after delay + Timer { + id: hideTimer + interval: root.hideDelay + repeat: false + onTriggered: { + root.startHideAnimation(); + } + } + + // Show animation + ParallelAnimation { + id: showAnimation + + PropertyAnimation { + target: tooltipContainer + property: "opacity" + from: 0.0 + to: 1.0 + duration: root.animationDuration + easing.type: Easing.OutCubic + } + + PropertyAnimation { + target: tooltipContainer + property: "scale" + from: root.animationScale + to: 1.0 + duration: root.animationDuration + easing.type: Easing.OutBack + easing.overshoot: 1.2 + } + } + + // Hide animation + ParallelAnimation { + id: hideAnimation + + PropertyAnimation { + target: tooltipContainer + property: "opacity" + from: 1.0 + to: 0.0 + duration: root.animationDuration * 0.75 // Slightly faster hide + easing.type: Easing.InCubic + } + + PropertyAnimation { + target: tooltipContainer + property: "scale" + from: 1.0 + to: root.animationScale + duration: root.animationDuration * 0.75 + easing.type: Easing.InCubic + } + + onFinished: { + root.completeHide(); + } + } + + // Function to show tooltip (content can be string or array of arrays for grid mode) + function show(target, content, customDirection, showDelay, fontFamily) { + if (!target || !content || content === "" || (Array.isArray(content) && content.length === 0)) + return; + + root.delay = showDelay; + + // Stop any running timers and animations + hideTimer.stop(); + showTimer.stop(); + hideAnimation.stop(); + animatingOut = false; + + // If we're already showing for a different target, hide immediately + if (visible && targetItem !== target) { + hideImmediately(); + } + + // Set content based on type + if (Array.isArray(content)) { + rows = content; + text = ""; + } else { + // Convert \n to
for RichText format + const processedText = content.replace(/\n/g, '
'); + text = processedText; + rows = null; + } + + targetItem = target; + + // Find the correct screen dimensions based on target's global position + // Respect all screens positionning + const targetGlobal = target.mapToGlobal(target.width / 2, target.height / 2); + let foundScreen = false; + for (let i = 0; i < Quickshell.screens.length; i++) { + const s = Quickshell.screens[i]; + if (targetGlobal.x >= s.x && targetGlobal.x < s.x + s.width && targetGlobal.y >= s.y && targetGlobal.y < s.y + s.height) { + screenWidth = s.width; + screenHeight = s.height; + screenX = s.x; + screenY = s.y; + foundScreen = true; + break; + } + } + if (!foundScreen) { + Logger.w("Tooltip", "No screen found for target position!"); + } + + // Initialize animation state (hidden) + tooltipContainer.opacity = 0.0; + tooltipContainer.scale = root.animationScale; + + // Start show timer (will position and then make visible) + showTimer.start(); + + if (customDirection !== undefined) { + direction = customDirection; + } else { + direction = "auto"; + } + + tooltipText.family = fontFamily ? fontFamily : Settings.data.ui.fontDefault; + } + + // Calculate grid dimensions using measurements that match actual NText rendering + function calculateGridSize() { + if (!rows || rows.length === 0) + return { + width: 0, + height: 0 + }; + + const numCols = rows[0].length; + const numRows = rows.length; + let columnWidths = []; + + // Find max width for each column using TextMetrics + for (let col = 0; col < numCols; col++) { + let maxWidth = 0; + for (let row = 0; row < numRows; row++) { + cellMetrics.text = rows[row][col] || ""; + if (cellMetrics.width > maxWidth) { + maxWidth = cellMetrics.width; + } + } + columnWidths.push(maxWidth); + } + + // Calculate total width: sum of columns + spacing between columns + let totalWidth = 0; + for (let i = 0; i < columnWidths.length; i++) { + totalWidth += columnWidths[i]; + } + totalWidth += (numCols - 1) * Style.marginM; // columnSpacing + + // Calculate total height using hidden NText for accurate row height + const rowHeight = rowHeightMeasure.implicitHeight; + const totalHeight = numRows * rowHeight; + + return { + width: totalWidth, + height: totalHeight + }; + } + + // Function to position and display the tooltip + function positionAndShow() { + if (!targetItem || !targetItem.parent) { + return; + } + + // Calculate tooltip dimensions based on content mode + let contentWidth, contentHeight; + if (isGridMode) { + const gridSize = calculateGridSize(); + contentWidth = gridSize.width; + contentHeight = gridSize.height; + } else { + contentWidth = tooltipText.implicitWidth; + contentHeight = tooltipText.implicitHeight; + } + + const extraPad = isGridMode ? gridPaddingVertical : 0; + const tipWidth = Math.ceil(Math.min(contentWidth + ((padding + extraPad) * 2), maxWidth)); + root.implicitWidth = tipWidth; + + const tipHeight = Math.ceil(contentHeight + ((padding + extraPad) * 2)); + root.implicitHeight = tipHeight; + + // Get target's global position and convert to screen-relative + var targetGlobalAbs = targetItem.mapToGlobal(0, 0); + var targetGlobal = { + "x": targetGlobalAbs.x - screenX, + "y": targetGlobalAbs.y - screenY + }; + const targetWidth = targetItem.width; + const targetHeight = targetItem.height; + + var newAnchorX = 0; + var newAnchorY = 0; + + if (direction === "auto") { + // Calculate available space in each direction (screen-relative) + const spaceLeft = targetGlobal.x; + const spaceRight = screenWidth - (targetGlobal.x + targetWidth); + const spaceTop = targetGlobal.y; + const spaceBottom = screenHeight - (targetGlobal.y + targetHeight); + + // Try positions in order of available space + const positions = [ + { + "dir": "bottom", + "space": spaceBottom, + "x": (targetWidth - tipWidth) / 2, + "y": targetHeight + margin, + "fits": spaceBottom >= tipHeight + margin + }, + { + "dir": "top", + "space": spaceTop, + "x": (targetWidth - tipWidth) / 2, + "y": -tipHeight - margin, + "fits": spaceTop >= tipHeight + margin + }, + { + "dir": "right", + "space": spaceRight, + "x": targetWidth + margin, + "y": (targetHeight - tipHeight) / 2, + "fits": spaceRight >= tipWidth + margin + }, + { + "dir": "left", + "space": spaceLeft, + "x": -tipWidth - margin, + "y": (targetHeight - tipHeight) / 2, + "fits": spaceLeft >= tipWidth + margin + } + ]; + + // Find first position that fits + var selectedPosition = null; + for (var i = 0; i < positions.length; i++) { + if (positions[i].fits) { + selectedPosition = positions[i]; + break; + } + } + + // If none fit perfectly + if (!selectedPosition) { + // Sort by available space and use position with most space + positions.sort(function (a, b) { + return b.space - a.space; + }); + selectedPosition = positions[0]; + } + + newAnchorX = selectedPosition.x; + newAnchorY = selectedPosition.y; + } else { + // Manual direction positioning + switch (direction) { + case "left": + newAnchorX = -tipWidth - margin; + newAnchorY = (targetHeight - tipHeight) / 2; + break; + case "right": + newAnchorX = targetWidth + margin; + newAnchorY = (targetHeight - tipHeight) / 2; + break; + case "top": + newAnchorX = (targetWidth - tipWidth) / 2; + newAnchorY = -tipHeight - margin; + break; + case "bottom": + newAnchorX = (targetWidth - tipWidth) / 2; + newAnchorY = targetHeight + margin; + break; + } + } + + // Adjust horizontal position to keep tooltip on screen. + // For top/bottom tooltips, always adjust horizontally (they don't overlap horizontally). + // For left/right tooltips, check for overlap before adjusting to avoid landing on the target. + const globalX = targetGlobal.x + newAnchorX; + const effectiveDirection = direction === "auto" ? selectedPosition.dir : direction; + const isHorizontalTooltip = (effectiveDirection === "top" || effectiveDirection === "bottom"); + + if (globalX < 0) { + const adjustedX = -targetGlobal.x + margin; + if (isHorizontalTooltip) { + newAnchorX = adjustedX; + } else { + const wouldOverlap = adjustedX < targetWidth && adjustedX + tipWidth > 0; + if (!wouldOverlap) + newAnchorX = adjustedX; + } + } else if (globalX + tipWidth > screenWidth) { + const adjustedX = screenWidth - targetGlobal.x - tipWidth - margin; + if (isHorizontalTooltip) { + newAnchorX = adjustedX; + } else { + const wouldOverlap = adjustedX < targetWidth && adjustedX + tipWidth > 0; + if (!wouldOverlap) + newAnchorX = adjustedX; + } + } + + // Adjust vertical position to keep tooltip on screen. + // For left/right tooltips, always adjust vertically (they don't overlap vertically). + // For top/bottom tooltips, check for overlap before adjusting to avoid landing on the target. + const globalY = targetGlobal.y + newAnchorY; + const isVerticalTooltip = (effectiveDirection === "left" || effectiveDirection === "right"); + + if (globalY < 0) { + const adjustedY = -targetGlobal.y + margin; + if (isVerticalTooltip) { + newAnchorY = adjustedY; + } else { + const wouldOverlap = adjustedY < targetHeight && adjustedY + tipHeight > 0; + if (!wouldOverlap) + newAnchorY = adjustedY; + } + } else if (globalY + tipHeight > screenHeight) { + const adjustedY = screenHeight - targetGlobal.y - tipHeight - margin; + if (isVerticalTooltip) { + newAnchorY = adjustedY; + } else { + const wouldOverlap = adjustedY < targetHeight && adjustedY + tipHeight > 0; + if (!wouldOverlap) + newAnchorY = adjustedY; + } + } + + // Apply position first (before making visible) + // Use floor for negative values to push tooltip away from target + anchorX = newAnchorX < 0 ? Math.floor(newAnchorX) : Math.round(newAnchorX); + anchorY = newAnchorY < 0 ? Math.floor(newAnchorY) : Math.round(newAnchorY); + isPositioned = true; + + // Now make visible and start animation + root.visible = true; + showAnimation.start(); + } + + // Function to hide tooltip + function hide() { + // Stop show timer if it's running + showTimer.stop(); + + // Stop hide timer if it's running + hideTimer.stop(); + + if (hideDelay > 0 && visible && !animatingOut) { + hideTimer.start(); + } else { + startHideAnimation(); + } + } + + function startHideAnimation() { + if (!visible || animatingOut) + return; + animatingOut = true; + showAnimation.stop(); // Stop show animation if running + hideAnimation.start(); + } + + function completeHide() { + visible = false; + animatingOut = false; + text = ""; + rows = null; + isPositioned = false; + tooltipContainer.opacity = 1.0; + tooltipContainer.scale = 1.0; + } + + // Quick hide without delay or animation + function hideImmediately() { + showTimer.stop(); + hideTimer.stop(); + showAnimation.stop(); + hideAnimation.stop(); + animatingOut = false; + completeHide(); + } + + // Update content function (supports both text and rows) + function updateContent(newContent) { + if (visible && targetItem) { + if (Array.isArray(newContent)) { + rows = newContent; + text = ""; + } else { + // Convert \n to
for RichText format + const processedText = newContent.replace(/\n/g, '
'); + text = processedText; + rows = null; + } + + // Defer dimension recalculation to allow GridLayout to update + Qt.callLater(updateContentDeferred); + } + } + + function updateContentDeferred() { + if (!visible || !targetItem) { + return; + } + + // Recalculate dimensions based on content mode + // Use calculateGridSize() for consistency with initial show + let contentWidth, contentHeight; + if (isGridMode) { + const gridSize = calculateGridSize(); + contentWidth = gridSize.width; + contentHeight = gridSize.height; + } else { + contentWidth = tooltipText.implicitWidth; + contentHeight = tooltipText.implicitHeight; + } + + const extraPad = isGridMode ? gridPaddingVertical : 0; + const tipWidth = Math.ceil(Math.min(contentWidth + ((padding + extraPad) * 2), maxWidth)); + root.implicitWidth = tipWidth; + + const tipHeight = Math.ceil(contentHeight + ((padding + extraPad) * 2)); + root.implicitHeight = tipHeight; + + // Reposition based on current direction (screen-relative) + // Round all values to avoid sub-pixel positioning issues with fractional scaling + var targetGlobalAbs = targetItem.mapToGlobal(0, 0); + var targetGlobal = { + "x": targetGlobalAbs.x - screenX, + "y": targetGlobalAbs.y - screenY + }; + const targetWidth = targetItem.width; + const targetHeight = targetItem.height; + + // Recalculate base anchor position (center on target for top/bottom, etc.) + var newAnchorX = anchorX; + var newAnchorY = anchorY; + + // Determine which direction the tooltip is currently positioned + // and recalculate the centering for that direction + var isHorizontalTooltip = false; + var isVerticalTooltip = false; + if (anchorY > targetHeight / 2) { + // Tooltip is below target + newAnchorX = (targetWidth - tipWidth) / 2; + isHorizontalTooltip = true; + } else if (anchorY < -tipHeight / 2) { + // Tooltip is above target + newAnchorX = (targetWidth - tipWidth) / 2; + isHorizontalTooltip = true; + } else if (anchorX > targetWidth / 2) { + // Tooltip is to the right + newAnchorY = (targetHeight - tipHeight) / 2; + isVerticalTooltip = true; + } else if (anchorX < -tipWidth / 2) { + // Tooltip is to the left + newAnchorY = (targetHeight - tipHeight) / 2; + isVerticalTooltip = true; + } + + // Adjust horizontal position to keep tooltip on screen if needed. + const globalX = targetGlobal.x + newAnchorX; + if (globalX < 0) { + const adjustedX = -targetGlobal.x + margin; + if (isHorizontalTooltip) { + newAnchorX = adjustedX; + } else { + const wouldOverlap = adjustedX < targetWidth && adjustedX + tipWidth > 0; + if (!wouldOverlap) + newAnchorX = adjustedX; + } + } else if (globalX + tipWidth > screenWidth) { + const adjustedX = screenWidth - targetGlobal.x - tipWidth - margin; + if (isHorizontalTooltip) { + newAnchorX = adjustedX; + } else { + const wouldOverlap = adjustedX < targetWidth && adjustedX + tipWidth > 0; + if (!wouldOverlap) + newAnchorX = adjustedX; + } + } + + // Adjust vertical position to keep tooltip on screen if needed. + const globalY = targetGlobal.y + newAnchorY; + if (globalY < 0) { + const adjustedY = -targetGlobal.y + margin; + if (isVerticalTooltip) { + newAnchorY = adjustedY; + } else { + const wouldOverlap = adjustedY < targetHeight && adjustedY + tipHeight > 0; + if (!wouldOverlap) + newAnchorY = adjustedY; + } + } else if (globalY + tipHeight > screenHeight) { + const adjustedY = screenHeight - targetGlobal.y - tipHeight - margin; + if (isVerticalTooltip) { + newAnchorY = adjustedY; + } else { + const wouldOverlap = adjustedY < targetHeight && adjustedY + tipHeight > 0; + if (!wouldOverlap) + newAnchorY = adjustedY; + } + } + + // Apply the new anchor positions + // Use floor for negative values to push tooltip away from target + anchorX = newAnchorX < 0 ? Math.floor(newAnchorX) : Math.round(newAnchorX); + anchorY = newAnchorY < 0 ? Math.floor(newAnchorY) : Math.round(newAnchorY); + + // Force anchor update + Qt.callLater(() => { + if (root.anchor && root.visible) { + root.anchor.updateAnchor(); + } + }); + } + + // Backward compatibility alias + function updateText(newText) { + updateContent(newText); + } + + // Reset function to clean up state + function reset() { + // Stop all timers and animations + showTimer.stop(); + hideTimer.stop(); + showAnimation.stop(); + hideAnimation.stop(); + + // Clear all state + visible = false; + animatingOut = false; + text = ""; + rows = null; + isPositioned = false; + + // Reset to defaults + direction = "auto"; + delay = 0; + hideDelay = 0; + + // Reset container state + tooltipContainer.opacity = 1.0; + tooltipContainer.scale = 1.0; + } + + // Tooltip content container for animations + Item { + id: tooltipContainer + anchors.fill: parent + + // Animation properties + opacity: 1.0 + scale: 1.0 + transformOrigin: Item.Center + + Rectangle { + anchors.fill: parent + anchors.margins: border.width + color: Color.mSurface + border.color: Color.mOutline + border.width: Style.borderS + radius: Math.min(Style.radiusS, Math.min(width, height) / 3) + + // Only show content when we have content + visible: root.text !== "" || root.isGridMode + + // Text content (default mode) + NText { + id: tooltipText + visible: !root.isGridMode + anchors.centerIn: parent + anchors.margins: root.padding + text: root.text + pointSize: Style.fontSizeS + family: Settings.data.ui.fontFixed + color: Color.mOnSurfaceVariant + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + wrapMode: Text.WordWrap + width: Math.min(implicitWidth, root.maxWidth - (root.padding * 2)) + richTextEnabled: true + } + + // Grid content (grid mode) + GridLayout { + id: gridContent + visible: root.isGridMode + anchors.centerIn: parent + columns: root.columnCount + rowSpacing: 0 + columnSpacing: Style.marginM + + Repeater { + model: root.isGridMode ? [].concat.apply([], root.rows) : [] + + NText { + text: modelData + pointSize: Style.fontSizeS + family: tooltipText.family + color: Color.mOnSurfaceVariant + Layout.preferredHeight: rowHeightMeasure.implicitHeight + } + } + } + } + } + + Component.onCompleted: { + reset(); + } + + Component.onDestruction: { + reset(); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/README.md b/fedora/.config/quickshell/noctalia-shell/README.md new file mode 100644 index 0000000..b5f6b90 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/README.md @@ -0,0 +1,189 @@ +# Noctalia Shell + +**_quiet by design_** + +

+ Noctalia Logo +

+ +

+ + Install Noctalia + +

+ +


+ +

+ + Last commit + + + GitHub stars + + + Documentation + + + Discord + +

+ +--- + +## What is Noctalia? + +A beautiful, minimal desktop shell for Wayland that actually gets out of your way. Built on [Quickshell](https://quickshell.outfoxxed.me/) (Qt/QML) with a warm lavender aesthetic that you can easily customize to match your vibe. + +**✨ Key Features:** +- 🪟 Native support for Niri, Hyprland, Sway, Scroll, Labwc and MangoWC +- 🎨 Extensive theming with predefined color schemes and automatic color generation from your wallpaper +- 🖼️ Wallpaper management with Wallhaven integration +- 🔔 Notification system with history and Do Not Disturb +- 🖥️ Multi-monitor support +- 🔒 Lock screen +- 🧩 Desktop widgets (clock, media player and more) +- 💡 OSD for volume and brightness +- 🔌 Nearly 100 plugins available ([explore plugins](https://noctalia.dev/plugins/)) +- 🪄 Setup wizard for first-time users +- ⚡ Built on Quickshell for performance + +--- + +## Preview + +https://github.com/user-attachments/assets/bf46f233-8d66-439a-a1ae-ab0446270f2d + +
+Screenshots + +![Dark 1](/Assets/Screenshots/noctalia-dark-1.png) +![Dark 2](/Assets/Screenshots/noctalia-dark-2.png) +![Dark 3](/Assets/Screenshots/noctalia-dark-3.png) + +![Light 1](/Assets/Screenshots/noctalia-light-1.png) +![Light 2](/Assets/Screenshots/noctalia-light-2.png) +![Light 3](/Assets/Screenshots/noctalia-light-3.png) + +
+ +--- + +## 📋 Requirements + +- Wayland compositor (see supported compositors below) +- Quickshell: [noctalia-qs](https://github.com/noctalia-dev/noctalia-qs) +- Additional dependencies are listed in our [documentation](https://docs.noctalia.dev) + +--- + +## 🚀 Getting Started + +**New to Noctalia?** +Check out our comprehensive documentation and installation guide to get up and running! + +

+ + Installation Guide + + + FAQ + + + Discord + +

+ +--- + +## 🖥️ Wayland Compositors + +Noctalia provides native support for **Niri**, **Hyprland**, **Sway**, **Scroll**, **Labwc** and **MangoWC**. Other Wayland compositors may work but could require additional configuration for compositor-specific features like workspaces and window management. + +--- + +## Scope + +Noctalia is a **desktop shell**, not a full desktop environment. It provides the visual layer that sits on top of your Wayland compositor (bars, panels, notifications, a dock, and widgets) but it intentionally stays within that boundary. Understanding this helps set the right expectations for feature requests. + +### What Noctalia does + +Noctalia focuses on the things a shell is responsible for: status bar, panels, application launcher, notifications, lock screen, idle management, OSD, theming, wallpapers, desktop widgets, dock, and multi-monitor support. + +### What belongs in a plugin + +If a feature is useful to some users but not essential to the core shell experience, it's a great candidate for a [plugin](https://noctalia.dev/plugins/). The plugin system is designed to make this easy: plugins can add bar widgets, panels, launcher providers, desktop widgets, and more. + +Some examples of features that are better suited as plugins: +- Compositor-specific extras (e.g., Steam overlay for Hyprland) +- Hardware-specific controls (e.g., laptop fan profiles, battery thresholds) +- Third-party service integrations (e.g., smart home controls, Tailscale) +- Niche productivity tools (e.g., Pomodoro timer, RSS reader, Docker manager) +- Alternative visualizations or widgets + +If you have an idea that fits this category, consider [building a plugin](https://docs.noctalia.dev/development/guideline) for it! + +### What falls outside our scope + +Some features go beyond what a desktop shell can or should do. These are typically responsibilities of the compositor, a dedicated application, or the system itself: + +- **File management**: use a file manager application +- **Display/login greeter**: this runs before the shell and is managed separately +- **Window management and overview**: workspace switching and window tiling are compositor responsibilities +- **Removable drive mounting**: handled by system services like udisks and desktop applications +- **Screen mirroring/casting**: managed by the compositor or dedicated tools + +We appreciate feature suggestions, but if a request falls into this category, it's likely outside what Noctalia can provide. When in doubt, feel free to ask in our [Discord](https://discord.noctalia.dev). + +--- + +## 🤝 Contributing + +We welcome contributions of any size - bug fixes, new features, documentation improvements, or custom themes and configs. + +**Get involved:** +- **Found a bug?** [Open an issue](https://github.com/noctalia-dev/noctalia-shell/issues/new) +- **Want to code?** Check out our [development guidelines](https://docs.noctalia.dev/development/guideline) +- **Need help?** Join our [Discord](https://discord.noctalia.dev) + +--- + +## 💜 Credits + +A heartfelt thank you to our incredible community of [**contributors**](https://github.com/noctalia-dev/noctalia-shell/graphs/contributors). We are immensely grateful for your dedicated participation and the constructive feedback you've provided, which continue to shape and improve our project for everyone. + +--- + +## ☕ Donations + +While all donations are greatly appreciated, they are completely voluntary. +Thank you to everyone who supports the project! 💜 + +

+ + Buy Me a Coffee + + + Ko-fi + +

+ +--- + +## 📄 License + +MIT License - see [LICENSE](./LICENSE) for details. + +--- + +## ⭐ Star History + +

+ + Star History + +

diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/bash/template-apply.sh b/fedora/.config/quickshell/noctalia-shell/Scripts/bash/template-apply.sh new file mode 100755 index 0000000..0e9d62b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/bash/template-apply.sh @@ -0,0 +1,540 @@ +#!/usr/bin/env -S bash + +# Ensure at least one argument is provided. +if [ "$#" -lt 1 ]; then + # Print usage information to standard error. + echo "Error: No application specified." >&2 + echo "Usage: $0 {kitty|ghostty|foot|alacritty|wezterm|fuzzel|walker|pywalfox|cava|yazi|labwc|niri|hyprland|sway|scroll|mango|btop|zathura} [dark|light]" >&2 + exit 1 +fi + +APP_NAME="$1" +MODE="${2:-}" # Optional second argument for dark/light mode + +# --- Apply theme based on the application name --- +case "$APP_NAME" in +kitty) + KITTY_CONF="$HOME/.config/kitty/kitty.conf" + if [ -w "$KITTY_CONF" ]; then + kitty +kitten themes --reload-in=all noctalia + else + kitty +runpy "from kitty.utils import *; reload_conf_in_all_kitties()" + fi + ;; + +ghostty) + # Check both potential config files + CONFIG_FILES=("$HOME/.config/ghostty/config" "$HOME/.config/ghostty/config.ghostty") + FOUND_CONFIG=false + + for CONFIG_FILE in "${CONFIG_FILES[@]}"; do + if [ -f "$CONFIG_FILE" ]; then + FOUND_CONFIG=true + # Check if theme is already set to noctalia (flexible spacing) + if grep -qE "^theme\s*=\s*noctalia$" "$CONFIG_FILE"; then + : # Already correct + elif grep -qE "^theme\s*=" "$CONFIG_FILE"; then + # Replace existing theme line in-place + sed -i -E 's/^theme\s*=.*/theme = noctalia/' "$CONFIG_FILE" + else + # Add the new theme line to the end of the file + echo "theme = noctalia" >>"$CONFIG_FILE" + fi + fi + done + + if [ "$FOUND_CONFIG" = true ]; then + # Only signal if ghostty is running + pgrep -f ghostty >/dev/null && pkill -SIGUSR2 ghostty || true + else + echo "Error: No ghostty config file found at ${CONFIG_FILES[*]}" >&2 + exit 1 + fi + ;; + +foot) + CONFIG_FILE="$HOME/.config/foot/foot.ini" + + # Check if the config file exists, create it if it doesn't. + if [ ! -f "$CONFIG_FILE" ]; then + # Create the config directory if it doesn't exist + mkdir -p "$(dirname "$CONFIG_FILE")" + # Create the config file with the noctalia theme + cat >"$CONFIG_FILE" <<'EOF' +[main] +include=~/.config/foot/themes/noctalia +EOF + else + # Check if theme is already set to noctalia + if ! grep -q "include.*noctalia" "$CONFIG_FILE"; then + # Remove any existing theme include line to prevent duplicates. + sed -i '/include=.*themes/d' "$CONFIG_FILE" + if grep -q '^\[main\]' "$CONFIG_FILE"; then + # Insert the include line after the existing [main] section header + sed -i '/^\[main\]/a include=~/.config/foot/themes/noctalia' "$CONFIG_FILE" + else + # If [main] doesn't exist, create it at the beginning with the include + sed -i '1i [main]\ninclude=~/.config/foot/themes/noctalia\n' "$CONFIG_FILE" + fi + fi + fi + ;; + +alacritty) + CONFIG_FILE="$HOME/.config/alacritty/alacritty.toml" + NEW_THEME_PATH='~/.config/alacritty/themes/noctalia.toml' + + # Check if the config file exists, create it if it doesn't. + if [ ! -f "$CONFIG_FILE" ]; then + # Create the config directory if it doesn't exist + mkdir -p "$(dirname "$CONFIG_FILE")" + # Create the config file with the noctalia theme import + cat >"$CONFIG_FILE" <<'EOF' +[general] +import = [ + "~/.config/alacritty/themes/noctalia.toml" +] +EOF + else + # Check if noctalia theme is already imported (any path variant) + if grep -q 'noctalia\.toml' "$CONFIG_FILE"; then + # Update old relative path to new absolute path if needed + if grep -q '"themes/noctalia.toml"' "$CONFIG_FILE"; then + sed -i 's|"themes/noctalia.toml"|"'"$NEW_THEME_PATH"'"|g' "$CONFIG_FILE" + fi + # Already has noctalia import with correct path, nothing to do + else + # No noctalia import found, add it + if grep -q '^\[general\]' "$CONFIG_FILE"; then + # Check if import line already exists under [general] + if grep -q '^import\s*=' "$CONFIG_FILE"; then + # Append to existing import array (before the closing bracket) + sed -i '/^import\s*=\s*\[/,/\]/{/\]/s|]| "'"$NEW_THEME_PATH"'",\n]|}' "$CONFIG_FILE" + else + # Add import line after [general] section header + sed -i '/^\[general\]/a import = ["'"$NEW_THEME_PATH"'"]' "$CONFIG_FILE" + fi + else + # Create [general] section with import at the beginning of the file + sed -i '1i [general]\nimport = ["'"$NEW_THEME_PATH"'"]\n' "$CONFIG_FILE" + fi + fi + fi + ;; + +wezterm) + CONFIG_FILE="$HOME/.config/wezterm/wezterm.lua" + WEZTERM_SCHEME_LINE='config.color_scheme = "Noctalia"' + + # Check if the config file exists. + if [ -f "$CONFIG_FILE" ]; then + + # Check if theme is already set to Noctalia (matches 'Noctalia' or "Noctalia") + if ! grep -q "^\s*config\.color_scheme\s*=\s*['\"]Noctalia['\"]\s*" "$CONFIG_FILE"; then + # Not set to Noctalia. Check if *any* color_scheme line exists. + if grep -q '^\s*config\.color_scheme\s*=' "$CONFIG_FILE"; then + # It exists, so we replace it with our desired line. + sed -i "s|^\(\s*config\.color_scheme\s*=\s*\).*$|\1\"Noctalia\"|" "$CONFIG_FILE" + else + # It doesn't exist, so we add it before the 'return config' line. + if grep -q '^\s*return\s*config' "$CONFIG_FILE"; then + # 'return config' exists. Insert the line before it. + sed -i '/^\s*return\s*config/i\'"$WEZTERM_SCHEME_LINE" "$CONFIG_FILE" + else + # This is a problem. We can't find the insertion point. + echo "Warning: 'config.color_scheme' not set and 'return config' line not found." >&2 + echo " Make sure $CONFIG_FILE is correct: https://wezterm.org/config/files.html" >&2 + fi + fi + fi + # touching the config file fools wezterm into reloading it + touch "$CONFIG_FILE" + else + echo "Error: wezterm.lua not found at $CONFIG_FILE" >&2 + echo "Instructions to create it: https://wezterm.org/config/files.html" >&2 + exit 1 + fi + ;; + +fuzzel) + CONFIG_FILE="$HOME/.config/fuzzel/fuzzel.ini" + + # Check if the config file exists, create it if it doesn't. + if [ ! -f "$CONFIG_FILE" ]; then + # Create the config directory if it doesn't exist + mkdir -p "$(dirname "$CONFIG_FILE")" + # Create the config file with the noctalia theme + cat >"$CONFIG_FILE" <<'EOF' +include=~/.config/fuzzel/themes/noctalia +EOF + else + # Check if theme is already set to noctalia + if grep -q "^include=~/.config/fuzzel/themes/noctalia$" "$CONFIG_FILE"; then + : # Already correct + elif grep -q "^include=.*themes" "$CONFIG_FILE"; then + # Replace existing theme include line in-place + sed -i 's|^include=.*themes.*|include=~/.config/fuzzel/themes/noctalia|' "$CONFIG_FILE" + else + # Add the new theme include line + echo "include=~/.config/fuzzel/themes/noctalia" >>"$CONFIG_FILE" + fi + fi + ;; + +walker) + CONFIG_FILE="$HOME/.config/walker/config.toml" + + # Check if the config file exists. + if [ -f "$CONFIG_FILE" ]; then + # Check if theme is already set to noctalia (flexible spacing) + if grep -qE '^theme\s*=\s*"noctalia"' "$CONFIG_FILE"; then + : # Already correct + elif grep -qE '^theme\s*=' "$CONFIG_FILE"; then + # Replace existing theme line in-place + sed -i -E 's/^theme\s*=.*/theme = "noctalia"/' "$CONFIG_FILE" + else + echo 'theme = "noctalia"' >>"$CONFIG_FILE" + fi + else + echo "Error: walker config file not found at $CONFIG_FILE" >&2 + exit 1 + fi + ;; + +vicinae) + # Apply the theme + vicinae theme set noctalia + ;; + +pywalfox) + # Set dark/light mode first if MODE is specified + if [ -n "$MODE" ]; then + if [ "$MODE" = "dark" ] || [ "$MODE" = "light" ]; then + pywalfox "$MODE" + else + echo "Warning: Invalid mode '$MODE'. Expected 'dark' or 'light'. Skipping mode switch." >&2 + fi + fi + # Update the theme + pywalfox update + ;; + +cava) + CONFIG_FILE="$HOME/.config/cava/config" + THEME_MODIFIED=false + + # Check if the config file exists. + if [ -f "$CONFIG_FILE" ]; then + # Check if [color] section exists + if grep -q '^\[color\]' "$CONFIG_FILE"; then + # Check if theme is already set to noctalia under [color] (flexible spacing) + if sed -n '/^\[color\]/,/^\[/p' "$CONFIG_FILE" | grep -qE '^theme\s*=\s*"noctalia"'; then + : # Already correct + elif sed -n '/^\[color\]/,/^\[/p' "$CONFIG_FILE" | grep -qE '^theme\s*='; then + # Replace existing theme line under [color] + sed -i -E '/^\[color\]/,/^\[/{s/^theme\s*=.*/theme = "noctalia"/}' "$CONFIG_FILE" + THEME_MODIFIED=true + else + # Add theme line after [color] + sed -i '/^\[color\]/a theme = "noctalia"' "$CONFIG_FILE" + THEME_MODIFIED=true + fi + else + # Add [color] section with theme at the end of file + echo "" >>"$CONFIG_FILE" + echo "[color]" >>"$CONFIG_FILE" + echo 'theme = "noctalia"' >>"$CONFIG_FILE" + THEME_MODIFIED=true + fi + + # Reload cava if it's running, but only if it's not using stdin config + if pgrep -f cava >/dev/null; then + # Check if Cava is running with -p /dev/stdin (standalone cava) + if ! pgrep -af cava | grep -q -- "-p.*stdin"; then + pkill -USR1 cava + fi + fi + else + echo "Error: cava config file not found at $CONFIG_FILE" >&2 + exit 1 + fi + ;; + +yazi) + CONFIG_FILE="$HOME/.config/yazi/theme.toml" + + # Create config directory if it doesn't exist + mkdir -p "$(dirname "$CONFIG_FILE")" + + if [ ! -f "$CONFIG_FILE" ]; then + cat >"$CONFIG_FILE" <<'EOF' +[flavor] +dark = "noctalia" +light = "noctalia" +EOF + else + # Check if [flavor] section exists + if grep -q '^\[flavor\]' "$CONFIG_FILE"; then + # Update or add dark/light lines under [flavor] + if sed -n '/^\[flavor\]/,/^\[/p' "$CONFIG_FILE" | grep -q '^dark\s*='; then + sed -i '/^\[flavor\]/,/^\[/{s/^dark\s*=.*/dark = "noctalia"/}' "$CONFIG_FILE" + else + sed -i '/^\[flavor\]/a dark = "noctalia"' "$CONFIG_FILE" + fi + if sed -n '/^\[flavor\]/,/^\[/p' "$CONFIG_FILE" | grep -q '^light\s*='; then + sed -i '/^\[flavor\]/,/^\[/{s/^light\s*=.*/light = "noctalia"/}' "$CONFIG_FILE" + else + sed -i '/^\[flavor\]/,/^dark/a light = "noctalia"' "$CONFIG_FILE" + fi + else + # Add [flavor] section at the end + echo "" >>"$CONFIG_FILE" + echo "[flavor]" >>"$CONFIG_FILE" + echo 'dark = "noctalia"' >>"$CONFIG_FILE" + echo 'light = "noctalia"' >>"$CONFIG_FILE" + fi + fi + ;; + +labwc) + # Update the theme + labwc -r + ;; + +niri) + CONFIG_FILE="$HOME/.config/niri/config.kdl" + INCLUDE_LINE='include "./noctalia.kdl"' + + # Check if the config file exists. + if [ ! -f "$CONFIG_FILE" ]; then + mkdir -p "$(dirname "$CONFIG_FILE")" + echo -e "\n$INCLUDE_LINE\n" >"$CONFIG_FILE" + else + # Check if noctalia include already exists (flexible: quotes, ./ prefix) + if grep -qE 'include\s+["'"'"'](\./)?noctalia\.kdl["'"'"']' "$CONFIG_FILE"; then + : # Already included + else + # Add the include line to the end of the file + echo -e "\n$INCLUDE_LINE\n" >>"$CONFIG_FILE" + fi + fi + ;; + +hyprland) + echo "🎨 Applying 'noctalia' theme to Hyprland..." + CONFIG_DIR="$HOME/.config/hypr" + CONFIG_FILE="$CONFIG_DIR/hyprland.conf" + THEME_FILE="$CONFIG_DIR/noctalia/noctalia-colors.conf" + + INCLUDE_LINE="source = $THEME_FILE" + + # Check if the config file exists. + if [ ! -f "$CONFIG_FILE" ]; then + echo "Config file not found, creating $CONFIG_FILE..." + mkdir -p "$(dirname "$CONFIG_FILE")" + echo -e "\n$INCLUDE_LINE\n" >"$CONFIG_FILE" + echo "Created new config file with noctalia theme." + else + # Check if noctalia theme source already exists (flexible matching) + if grep -qE 'source\s*=\s*.*noctalia.*\.conf' "$CONFIG_FILE"; then + echo "Theme already included, skipping modification." + else + # Only convert symlink when we actually need to write (NixOS read-only symlinks) + if [ -L "$CONFIG_FILE" ] && [ ! -w "$CONFIG_FILE" ]; then + echo "Detected read-only symlink, converting to local file..." + cp --remove-destination "$(readlink -f "$CONFIG_FILE")" "$CONFIG_FILE" + chmod +w "$CONFIG_FILE" + fi + # Add the include line to the end of the file + echo -e "\n$INCLUDE_LINE\n" >>"$CONFIG_FILE" + echo "✅ Added noctalia theme include to config." + fi + fi + + # Reload hyprland + hyprctl reload + ;; + +sway) + echo "🎨 Applying 'noctalia' theme to Sway..." + CONFIG_DIR="$HOME/.config/sway" + CONFIG_FILE="$CONFIG_DIR/config" + INCLUDE_LINE='include ~/.config/sway/noctalia' + + # Check if the config file exists. + if [ ! -f "$CONFIG_FILE" ]; then + echo "Config file not found, creating $CONFIG_FILE..." + mkdir -p "$(dirname "$CONFIG_FILE")" + echo -e "\n$INCLUDE_LINE\n" >"$CONFIG_FILE" + echo "Created new config file with noctalia theme." + else + # Check if noctalia include already exists (flexible matching) + if grep -qE 'include\s+.*noctalia' "$CONFIG_FILE"; then + echo "Theme already included, skipping modification." + else + # Only convert symlink when we actually need to write (NixOS read-only symlinks) + if [ -L "$CONFIG_FILE" ] && [ ! -w "$CONFIG_FILE" ]; then + echo "Detected read-only symlink, converting to local file..." + cp --remove-destination "$(readlink -f "$CONFIG_FILE")" "$CONFIG_FILE" + chmod +w "$CONFIG_FILE" + fi + # Add the include line to the end of the file + echo -e "\n$INCLUDE_LINE\n" >>"$CONFIG_FILE" + echo "✅ Added noctalia theme include to config." + fi + fi + + # Reload sway + swaymsg reload + ;; + +scroll) + echo "Applying 'noctalia' theme to Scroll..." + CONFIG_DIR="$HOME/.config/scroll" + CONFIG_FILE="$CONFIG_DIR/config" + INCLUDE_LINE='include ~/.config/scroll/noctalia' + + # Check if the config file exists. + if [ ! -f "$CONFIG_FILE" ]; then + echo "Config file not found, creating $CONFIG_FILE..." + mkdir -p "$(dirname "$CONFIG_FILE")" + echo -e "\n$INCLUDE_LINE\n" >"$CONFIG_FILE" + echo "Created new config file with noctalia theme." + else + # Check if noctalia include already exists (flexible matching) + if grep -qE 'include\s+.*noctalia' "$CONFIG_FILE"; then + echo "Theme already included, skipping modification." + else + # Only convert symlink when we actually need to write + if [ -L "$CONFIG_FILE" ] && [ ! -w "$CONFIG_FILE" ]; then + echo "Detected read-only symlink, converting to local file..." + cp --remove-destination "$(readlink -f "$CONFIG_FILE")" "$CONFIG_FILE" + chmod +w "$CONFIG_FILE" + fi + # Add the include line to the end of the file + echo -e "\n$INCLUDE_LINE\n" >>"$CONFIG_FILE" + echo "Added noctalia theme include to config." + fi + fi + + # Reload scroll + scrollmsg reload + ;; + +mango) + CONFIG_DIR="$HOME/.config/mango" + MAIN_CONFIG="$CONFIG_DIR/config.conf" + THEME_FILE="$CONFIG_DIR/noctalia.conf" + BACKUP_FILE="$CONFIG_DIR/theme.conf.bak" + # This sources the noctalia theme file + SOURCE_LINE="source = $THEME_FILE" + + # Color variables that should be moved to theme file + COLOR_VARS="shadowscolor|rootcolor|bordercolor|focuscolor|maximizescreencolor|urgentcolor|scratchpadcolor|globalcolor|overlaycolor" + + # Create config directory if it doesn't exist + mkdir -p "$CONFIG_DIR" + + # Check if theme is already sourced in main config + if [ -f "$MAIN_CONFIG" ] && grep -qF "$SOURCE_LINE" "$MAIN_CONFIG"; then + : # Theme already set + else + # First-time setup: backup and remove legacy color definitions + + # Scan all .conf files in config directory for legacy color variables + for conf_file in "$CONFIG_DIR"/*.conf; do + # Skip if no .conf files exist or if it's the theme file itself + [ -e "$conf_file" ] || continue + [ "$conf_file" = "$THEME_FILE" ] && continue + + # Check if this file contains any color variable definitions + if grep -qE "^($COLOR_VARS)\s*=" "$conf_file"; then + # Extract and append color definitions to backup file + grep -E "^($COLOR_VARS)\s*=" "$conf_file" >>"$BACKUP_FILE" + + # Remove color definitions from original file + if [ -L "$conf_file" ] && [ ! -w "$conf_file" ]; then + # Read-only symlink (e.g. NixOS): convert to local file + cp --remove-destination "$(readlink -f "$conf_file")" "$conf_file" + chmod +w "$conf_file" + sed -i -E "/^($COLOR_VARS)\s*=/d" "$conf_file" + else + # Edit the real file, preserving any writable symlink + sed -i -E "/^($COLOR_VARS)\s*=/d" "$(readlink -f "$conf_file")" + fi + fi + done + + # Only convert symlink when we actually need to write + if [ -L "$MAIN_CONFIG" ] && [ ! -w "$MAIN_CONFIG" ]; then + echo "Detected read-only symlink, converting to local file..." + cp --remove-destination "$(readlink -f "$MAIN_CONFIG")" "$MAIN_CONFIG" + chmod +w "$MAIN_CONFIG" + fi + + # Add source line to main config + if [ -f "$MAIN_CONFIG" ]; then + echo "" >>"$MAIN_CONFIG" + echo "# This sources the noctalia theme" >>"$MAIN_CONFIG" + echo -e "\n$SOURCE_LINE\n" >>"$MAIN_CONFIG" + else + echo "# This sources the noctalia theme" >"$MAIN_CONFIG" + echo -e "\n$SOURCE_LINE\n" >>"$MAIN_CONFIG" + fi + fi + + # Trigger live reload + if command -v mmsg >/dev/null 2>&1; then + mmsg -s -d reload_config + else + echo "Warning: mmsg command not found, manual restart may be needed." >&2 + fi + ;; + +btop) + CONFIG_FILE="$HOME/.config/btop/btop.conf" + + if [ -f "$CONFIG_FILE" ]; then + # Check if theme is already set to noctalia (flexible spacing) + if grep -qE '^color_theme\s*=\s*"noctalia"' "$CONFIG_FILE"; then + : # Already correct + elif grep -qE '^color_theme\s*=' "$CONFIG_FILE"; then + # Replace existing color_theme line in-place + sed -i -E 's/^color_theme\s*=.*/color_theme = "noctalia"/' "$CONFIG_FILE" + else + echo 'color_theme = "noctalia"' >>"$CONFIG_FILE" + fi + + if pgrep -x btop >/dev/null; then + pkill -SIGUSR2 -x btop + fi + else + echo "Warning: btop config file not found at $CONFIG_FILE" >&2 + fi + ;; + +zathura) + ZATHURA_INSTANCES=$(dbus-send --session \ + --dest=org.freedesktop.DBus \ + --type=method_call \ + --print-reply \ + /org/freedesktop/DBus \ + org.freedesktop.DBus.ListNames | + grep -o 'org.pwmt.zathura.PID-[0-9]*') + + for id in $ZATHURA_INSTANCES; do + dbus-send --session \ + --dest="$id" \ + --type=method_call \ + /org/pwmt/zathura \ + org.pwmt.zathura.ExecuteCommand \ + string:"source" + done + ;; + +*) + # Handle unknown application names. + echo "Error: Unknown application '$APP_NAME'." >&2 + exit 1 + ;; +esac diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/dev/build-settings-search-index.py b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/build-settings-search-index.py new file mode 100755 index 0000000..aa5e760 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/build-settings-search-index.py @@ -0,0 +1,436 @@ +#!/usr/bin/env python3 +""" +Build settings search index from QML source files. + +Parses settings tab QML files to extract searchable metadata +(i18n keys, widget types, tab/sub-tab locations, visibility conditions). + +Output: Assets/settings-search-index.json + +Usage: + python Scripts/dev/build-settings-search-index.py +""" + +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent.parent +SETTINGS_DIR = ROOT / "Modules" / "Panels" / "Settings" +TABS_DIR = SETTINGS_DIR / "Tabs" +OUTPUT = ROOT / "Assets" / "settings-search-index.json" + +# Widget types that have searchable label/description +WIDGET_TYPES = ( + "NToggle", + "NComboBox", + "NValueSlider", + "NSpinBox", + "NSearchableComboBox", + "NTextInputButton", + "NTextInput", + "NCheckbox", + "NLabel", + "NColorChoice", + "HookRow", +) + +# Regex patterns +RE_WIDGET_OPEN = re.compile( + r"^\s*(" + "|".join(WIDGET_TYPES) + r")\s*\{", re.MULTILINE +) +RE_LABEL = re.compile(r'label:\s*I18n\.tr\("([^"]+)"') +RE_DESCRIPTION = re.compile(r'description:\s*I18n\.tr\("([^"]+)"') +RE_VISIBLE = re.compile(r'^\s*visible:\s*(.+?)(?:\s*;)?\s*$') + +# Prefixes that indicate externally-resolvable conditions (singleton services or globals). +# Conditions referencing local variables (root., parent., model, index, etc.) are skipped. +RESOLVABLE_PREFIXES = ( + "CompositorService.", + "Settings.data.", + "Quickshell.", + "IdleService.", + "SystemStatService.", + "SoundService.", + "BluetoothService.", + "LocationService.", + "false", +) + + +def parse_component_declarations(content: str) -> dict[str, str]: + """ + Parse Component declarations from SettingsContent.qml. + + Returns: component_id -> QML type name (e.g. "generalTab" -> "GeneralTab") + """ + components = {} + # Match patterns like: + # Component { + # id: generalTab + # GeneralTab {} + # } + pattern = re.compile( + r"Component\s*\{\s*\n\s*id:\s*(\w+)\s*\n\s*(\w+)\s*\{", + re.MULTILINE, + ) + for m in pattern.finditer(content): + comp_id = m.group(1) + type_name = m.group(2) + components[comp_id] = type_name + + return components + + +def parse_tabs_model_order(content: str) -> list[tuple[str, str]]: + """ + Parse updateTabsModel() to get the ordered list of (source_id, label_key) pairs. + + Returns: list of (component_id, i18n_label_key) in display order. + """ + # Find the updateTabsModel function body + match = re.search(r"function updateTabsModel\(\)\s*\{", content) + if not match: + return [] + + func_body = content[match.end():] + + # Extract tab entries: each has "label" and "source" fields + entries = [] + for m in re.finditer( + r'"label":\s*"([^"]+)"[^}]*?"source":\s*(\w+)', + func_body, + re.DOTALL, + ): + entries.append((m.group(2), m.group(1))) + + return entries + + +def build_tab_mappings(content: str) -> tuple[dict[str, int], dict[str, str]]: + """ + Build mappings from QML type name to tabsModel index and label key. + + Parses Component declarations and updateTabsModel() order. + Returns: (type_to_index, type_to_label) + - type_to_index: e.g. {"GeneralTab": 0, ...} + - type_to_label: e.g. {"GeneralTab": "common.general", ...} + """ + components = parse_component_declarations(content) + entries = parse_tabs_model_order(content) + + type_to_index = {} + type_to_label = {} + for idx, (source_id, label_key) in enumerate(entries): + type_name = components.get(source_id) + if type_name: + type_to_index[type_name] = idx + type_to_label[type_name] = label_key + + return type_to_index, type_to_label + + +def get_subtab_info(parent_tab_file: Path) -> tuple[list[str], list[str | None]]: + """ + Parse a parent tab file to get subtab order and labels. + + Returns: (subtab_type_names, subtab_label_keys) + - subtab_type_names: list of component names like ["VolumesSubTab", ...] + - subtab_label_keys: list of i18n keys like ["common.volumes", ...] (same order) + """ + content = parent_tab_file.read_text() + + # Extract NTabButton labels in order from NTabBar + labels = [] + in_tabbar = False + tabbar_depth = 0 + + for line in content.splitlines(): + stripped = line.strip() + + if not in_tabbar: + if re.match(r"NTabBar\s*\{", stripped): + in_tabbar = True + tabbar_depth = 1 + continue + continue + + tabbar_depth += stripped.count("{") - stripped.count("}") + + if tabbar_depth <= 0: + break + + # Match text: I18n.tr("...") inside NTabButton + m = re.search(r'text:\s*I18n\.tr\("([^"]+)"', stripped) + if m: + labels.append(m.group(1)) + + # Extract subtab component names from NTabView + subtabs = [] + in_tabview = False + tabview_depth = 0 + + for line in content.splitlines(): + stripped = line.strip() + + if not in_tabview: + if re.match(r"NTabView\s*\{", stripped): + in_tabview = True + tabview_depth = 1 + continue + continue + + tabview_depth += stripped.count("{") - stripped.count("}") + + if tabview_depth <= 0: + break + + # Match component instantiations like "VolumesSubTab {}" or "VolumesSubTab {" + m = re.match(r"(\w+SubTab)\s*\{", stripped) + if m: + subtabs.append(m.group(1)) + + # Pad labels list if shorter than subtabs (shouldn't happen, but safety) + while len(labels) < len(subtabs): + labels.append(None) + + return subtabs, labels[:len(subtabs)] + + +def resolve_tab_info( + qml_file: Path, + type_to_index: dict[str, int], + type_to_label: dict[str, str], +) -> tuple[int | None, str | None, int | None, str | None]: + """ + Determine the tab index, tab label, sub-tab index, and sub-tab label for a QML file. + + Returns (tab_index, tab_label_key, sub_tab_index, sub_tab_label_key) + """ + parent = qml_file.parent + stem = qml_file.stem + + # Top-level tab files (directly in Tabs/) + if parent == TABS_DIR: + tab_index = type_to_index.get(stem) + tab_label = type_to_label.get(stem) + return tab_index, tab_label, None, None + + # Sub-directory files + dir_name = parent.name # e.g. "Audio", "Bar" + parent_type = f"{dir_name}Tab" # e.g. "AudioTab" + tab_index = type_to_index.get(parent_type) + tab_label = type_to_label.get(parent_type) + + if tab_index is None: + return None, None, None, None + + # Skip the parent tab file itself (e.g. AudioTab.qml) — still scan for widgets + if stem.endswith("Tab") and not stem.endswith("SubTab"): + return tab_index, tab_label, None, None + + # Determine sub-tab index and label from parent tab's NTabBar/NTabView + parent_tab_file = parent / f"{dir_name}Tab.qml" + if not parent_tab_file.exists(): + return tab_index, tab_label, None, None + + subtab_names, subtab_labels = get_subtab_info(parent_tab_file) + try: + idx = subtab_names.index(stem) + sub_label = subtab_labels[idx] if idx < len(subtab_labels) else None + return tab_index, tab_label, idx, sub_label + except ValueError: + # File doesn't map to any subtab (e.g. a dialog). If the parent tab + # has subtabs, the focus ring can't reach widgets inside dialogs, so + # exclude them from the index. + if subtab_names: + return None, None, None, None + return tab_index, tab_label, None, None + + +def is_resolvable_condition(cond: str) -> bool: + """Check if a visibility condition can be resolved at runtime by the shell.""" + # Strip negation for prefix checking + check = cond.lstrip("!").lstrip(" ").lstrip("(").lstrip(" ") + return any(check.startswith(p) for p in RESOLVABLE_PREFIXES) + + +def build_scope_visibility(content: str) -> dict[int, list[str]]: + """ + For each line number, compute the list of inherited visibility conditions + from all enclosing QML scopes. + + Tracks brace-depth to maintain a scope stack. When a ``visible:`` property + is found, it is associated with the innermost scope. The conditions are + inherited by all lines within that scope. + + Returns: line_number -> list of condition strings + """ + lines = content.splitlines() + # Each entry: visibility condition string or None + scope_stack: list[str | None] = [] + result: dict[int, list[str]] = {} + + for line_num, raw_line in enumerate(lines): + stripped = raw_line.strip() + + # Record inherited conditions BEFORE processing this line's braces. + # This means a widget opening on this line inherits from parent scopes, + # not from its own scope (which hasn't been populated yet). + result[line_num] = [c for c in scope_stack if c is not None] + + # Process opening braces — push new scopes + n_opens = stripped.count("{") + for _ in range(n_opens): + scope_stack.append(None) + + # Check for visible: property — assign to the innermost scope + vis_match = RE_VISIBLE.match(stripped) + if vis_match and scope_stack: + cond = vis_match.group(1).strip() + if cond != "true": + scope_stack[-1] = cond + + # Process closing braces — pop scopes + n_closes = stripped.count("}") + for _ in range(n_closes): + if scope_stack: + scope_stack.pop() + + return result + + +def extract_widget_blocks(content: str) -> list[tuple[str, str, int]]: + """ + Extract (widget_type, block_text, start_line) tuples from QML content. + + Uses brace-depth tracking to capture the full widget block. + """ + results = [] + lines = content.splitlines() + i = 0 + + while i < len(lines): + m = RE_WIDGET_OPEN.match(lines[i]) + if m: + widget_type = m.group(1) + depth = 0 + block_lines = [] + start_line = i + j = i + + while j < len(lines): + line = lines[j] + block_lines.append(line) + depth += line.count("{") - line.count("}") + if depth <= 0: + break + j += 1 + + block_text = "\n".join(block_lines) + results.append((widget_type, block_text, start_line)) + i = j + 1 + else: + i += 1 + + return results + + +def extract_entries( + qml_file: Path, + type_to_index: dict[str, int], + type_to_label: dict[str, str], +) -> list[dict]: + """Extract all searchable settings entries from a QML file.""" + tab_index, tab_label, sub_tab, sub_tab_label = resolve_tab_info( + qml_file, type_to_index, type_to_label + ) + if tab_index is None: + return [] + + content = qml_file.read_text() + scope_vis = build_scope_visibility(content) + entries = [] + + for widget_type, block, start_line in extract_widget_blocks(content): + label_match = RE_LABEL.search(block) + if not label_match: + continue + + label_key = label_match.group(1) + desc_match = RE_DESCRIPTION.search(block) + desc_key = desc_match.group(1) if desc_match else None + + # Collect visibility conditions: ancestor scopes + widget's own visible: + conditions = list(scope_vis.get(start_line, [])) + widget_vis = re.search( + r'^\s*visible:\s*(.+?)(?:\s*;)?\s*$', block, re.MULTILINE + ) + if widget_vis: + cond = widget_vis.group(1).strip() + if cond != "true" and cond not in conditions: + conditions.append(cond) + + # Keep only externally-resolvable conditions + conditions = [c for c in conditions if is_resolvable_condition(c)] + + entry = { + "labelKey": label_key, + "descriptionKey": desc_key, + "widget": widget_type, + "tab": tab_index, + "tabLabel": tab_label, + "subTab": sub_tab, + } + if sub_tab_label is not None: + entry["subTabLabel"] = sub_tab_label + if conditions: + entry["visibleWhen"] = conditions + + entries.append(entry) + + return entries + + +def main(): + if not TABS_DIR.exists(): + print(f"Error: Tabs directory not found: {TABS_DIR}", file=sys.stderr) + sys.exit(1) + + settings_content = SETTINGS_DIR / "SettingsContent.qml" + if not settings_content.exists(): + print(f"Error: SettingsContent.qml not found: {settings_content}", file=sys.stderr) + sys.exit(1) + + # Build type -> tabsModel index/label mappings from SettingsContent.qml + content = settings_content.read_text() + type_to_index, type_to_label = build_tab_mappings(content) + + if not type_to_index: + print("Error: Could not parse tab model from SettingsContent.qml", file=sys.stderr) + sys.exit(1) + + print(f"Parsed {len(type_to_index)} tab types from SettingsContent.qml") + + all_entries = [] + seen_labels = set() + + # Scan all QML files in Tabs/ (recursive) + for qml_file in sorted(TABS_DIR.rglob("*.qml")): + entries = extract_entries(qml_file, type_to_index, type_to_label) + for entry in entries: + if entry["labelKey"] not in seen_labels: + seen_labels.add(entry["labelKey"]) + all_entries.append(entry) + + # Write output + OUTPUT.parent.mkdir(parents=True, exist_ok=True) + with open(OUTPUT, "w") as f: + json.dump(all_entries, f, indent=2) + + print(f"Generated {len(all_entries)} entries -> {OUTPUT.relative_to(ROOT)}") + + +if __name__ == "__main__": + main() diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/dev/colorscheme-registry.sh b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/colorscheme-registry.sh new file mode 100755 index 0000000..e67f6cd --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/colorscheme-registry.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Generate a registry.json from all color schemes in Assets/ColorScheme +# Output format matches ~/Development/misc/noctalia/noctalia-colorschemes/registry.json + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +COLORSCHEME_DIR="$PROJECT_ROOT/Assets/ColorScheme" + +# Start JSON output +echo '{' +echo ' "version": 1,' +echo ' "themes": [' + +first=true +for dir in "$COLORSCHEME_DIR"/*/; do + [ -d "$dir" ] || continue + + name=$(basename "$dir") + json_file="$dir/$name.json" + + [ -f "$json_file" ] || continue + + # Read the JSON file content + content=$(cat "$json_file") + + # Extract dark and light objects using jq + dark=$(echo "$content" | jq -c '.dark') + light=$(echo "$content" | jq -c '.light') + + # Skip if missing dark or light + [ "$dark" = "null" ] || [ "$light" = "null" ] && continue + + # Add comma before all but first entry + if [ "$first" = true ]; then + first=false + else + echo ',' + fi + + # Output theme entry + printf ' {\n' + printf ' "name": "%s",\n' "$name" + printf ' "path": "%s",\n' "$name" + printf ' "dark": %s,\n' "$dark" + printf ' "light": %s\n' "$light" + printf ' }' +done + +echo '' +echo ' ]' +echo '}' diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/dev/i18n-pull.sh b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/i18n-pull.sh new file mode 100755 index 0000000..3585824 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/i18n-pull.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +# Pull translations from Noctalia Translate API +# Usage: ./i18n-pull.sh +# Example: ./i18n-pull.sh noctalia-shell ./Assets/Translations + +set -e + +# Configuration +API_BASE="${I18N_API_BASE:-https://i18n.noctalia.dev}" +PROJECT_SLUG="${1:-noctalia-shell}" +OUTPUT_DIR="${2:-./Assets/Translations}" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${YELLOW}Pulling translations for project: ${PROJECT_SLUG}${NC}" +echo "API: ${API_BASE}" +echo "Output: ${OUTPUT_DIR}" +echo "" + +# Confirmation +read -p "Pull translations and overwrite local files? [y/N] " -n 1 -r +echo "" +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 0 +fi +echo "" + +# Create output directory if it doesn't exist +mkdir -p "$OUTPUT_DIR" + +# Pull all translations +RESPONSE=$(curl -s -w "\n%{http_code}" "${API_BASE}/api/projects/${PROJECT_SLUG}/pull") +HTTP_CODE=$(echo "$RESPONSE" | tail -n1) +BODY=$(echo "$RESPONSE" | sed '$d') + +if [ "$HTTP_CODE" != "200" ]; then + echo -e "${RED}Error: HTTP ${HTTP_CODE}${NC}" + echo "$BODY" + exit 1 +fi + +# Check if jq is available +if ! command -v jq &> /dev/null; then + echo -e "${RED}Error: jq is required but not installed${NC}" + echo "Install with: apt install jq (Linux) or brew install jq (macOS)" + exit 1 +fi + +# Get list of locales +LOCALES=$(echo "$BODY" | jq -r 'keys[]') + +if [ -z "$LOCALES" ]; then + echo -e "${RED}Error: No translations found${NC}" + exit 1 +fi + +# Save each locale to a separate file +COUNT=0 +for LOCALE in $LOCALES; do + OUTPUT_FILE="${OUTPUT_DIR}/${LOCALE}.json" + echo "$BODY" | jq ".[\"${LOCALE}\"]" > "$OUTPUT_FILE" + echo -e "${GREEN}Saved: ${OUTPUT_FILE}${NC}" + COUNT=$((COUNT + 1)) +done + +echo "" +echo -e "${GREEN}Successfully pulled ${COUNT} language(s)${NC}" \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/dev/i18n-push.sh b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/i18n-push.sh new file mode 100755 index 0000000..433727a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/i18n-push.sh @@ -0,0 +1,143 @@ +#!/bin/bash + +# Push translations to Noctalia Translate API +# Usage: TRANSLATION_PUSH_SECRET=your_secret ./push-translations.sh [--overwrite] [--lang ] [/path/to/Assets/Translations] +# Or set the secret in environment and pass the path as argument +# +# Options: +# --overwrite Overwrite existing translations +# --lang Upload only a single language (e.g., --lang fr) + +set -e + +# Parse arguments +OVERWRITE=false +TRANSLATIONS_DIR="Assets/Translations" +SINGLE_LANG="" + +while [[ $# -gt 0 ]]; do + case $1 in + --overwrite) + OVERWRITE=true + shift + ;; + --lang) + SINGLE_LANG="$2" + shift 2 + ;; + *) + TRANSLATIONS_DIR="$1" + shift + ;; + esac +done + +# Configuration +API_URL="${TRANSLATION_API_URL:-https://i18n.noctalia.dev}" +PROJECT_SLUG="${TRANSLATION_PROJECT:-noctalia-shell}" + +# Check for secret +if [ -z "$NOCTALIA_SHELL_TRANSLATION_PUSH_SECRET" ]; then + echo "Error: NOCTALIA_SHELL_TRANSLATION_PUSH_SECRET environment variable is required" + exit 1 +fi + +# Check if directory exists +if [ ! -d "$TRANSLATIONS_DIR" ]; then + echo "Error: Directory not found: $TRANSLATIONS_DIR" + exit 1 +fi + +# Check for jq +if ! command -v jq &> /dev/null; then + echo "Error: jq is required but not installed" + echo "Install with: apt install jq" + exit 1 +fi + +echo "Pushing translations from: $TRANSLATIONS_DIR" +echo "Target: $API_URL/api/projects/$PROJECT_SLUG/push" + +# Build combined JSON object +COMBINED_JSON="{}" + +if [ -n "$SINGLE_LANG" ]; then + # Single language mode + file="$TRANSLATIONS_DIR/$SINGLE_LANG.json" + if [ ! -f "$file" ]; then + echo "Error: Language file not found: $file" + exit 1 + fi + echo " Loading: $SINGLE_LANG ($SINGLE_LANG.json)" + COMBINED_JSON=$(echo "$COMBINED_JSON" | jq --arg locale "$SINGLE_LANG" --slurpfile content "$file" '. + {($locale): $content[0]}') +else + # All languages mode + for file in "$TRANSLATIONS_DIR"/*.json; do + if [ -f "$file" ]; then + # Extract locale from filename (e.g., en.json -> en) + filename=$(basename "$file") + locale="${filename%.json}" + + echo " Loading: $locale ($filename)" + + # Add this locale's translations to the combined object + COMBINED_JSON=$(echo "$COMBINED_JSON" | jq --arg locale "$locale" --slurpfile content "$file" '. + {($locale): $content[0]}') + fi + done +fi + +# Count locales +LOCALE_COUNT=$(echo "$COMBINED_JSON" | jq 'keys | length') +echo "Found $LOCALE_COUNT locale(s)" + +if [ "$LOCALE_COUNT" -eq 0 ]; then + echo "Error: No JSON files found in $TRANSLATIONS_DIR" + exit 1 +fi + +# Check if English exists (only required when pushing all languages) +if [ -z "$SINGLE_LANG" ] && ! echo "$COMBINED_JSON" | jq -e '.en' > /dev/null 2>&1; then + echo "Error: English (en.json) is required" + exit 1 +fi + +# Confirmation +echo "" +if [ -n "$SINGLE_LANG" ]; then + read -p "Push '$SINGLE_LANG' to $API_URL? [y/N] " -n 1 -r +else + read -p "Push $LOCALE_COUNT locale(s) to $API_URL? [y/N] " -n 1 -r +fi +echo "" +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Aborted." + exit 0 +fi + +# Build URL with optional overwrite parameter +PUSH_URL="$API_URL/api/projects/$PROJECT_SLUG/push" +if [ "$OVERWRITE" = true ]; then + PUSH_URL="$PUSH_URL?overwrite=true" + echo "Overwrite mode enabled" +fi + +# Push to API +echo "Pushing to API..." +RESPONSE=$(echo "$COMBINED_JSON" | curl -s -w "\n%{http_code}" -X POST \ + "$PUSH_URL" \ + -H "Authorization: Bearer $NOCTALIA_SHELL_TRANSLATION_PUSH_SECRET" \ + -H "Content-Type: application/json" \ + -d @-) + +# Extract HTTP status code (last line) and body (everything else) +HTTP_CODE=$(echo "$RESPONSE" | tail -n1) +BODY=$(echo "$RESPONSE" | sed '$d') + +if [ "$HTTP_CODE" -eq 200 ]; then + echo "Success!" + echo "$BODY" | jq . +else + echo "Error: HTTP $HTTP_CODE" + echo "$BODY" | jq . 2>/dev/null || echo "$BODY" + exit 1 +fi diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/dev/notifications-test-replace.sh b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/notifications-test-replace.sh new file mode 100755 index 0000000..50fcf1a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/notifications-test-replace.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Test script for notification replacement functionality + +echo "Testing notification replacement..." +echo "" + +# Send initial notification and capture the ID +echo "Step 1: Sending initial notification 'asdf'..." +NOTIF_ID=$(notify-send -p "asdf") +echo "Notification ID: $NOTIF_ID" +echo "" + +# Wait a moment for the notification to appear +sleep 1 + +# Replace the notification +echo "Step 2: Replacing notification $NOTIF_ID with 'test'..." +notify-send -r "$NOTIF_ID" -p "test" +echo "" + +echo "The notification should now show 'test' instead of 'asdf'." +echo "If it still shows 'asdf', the replacement is not working." + diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/dev/notifications-test.sh b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/notifications-test.sh new file mode 100755 index 0000000..db985c8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/notifications-test.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env -S bash + +echo "Sending test notifications..." + +# Send a bunch of notifications with numbers +for i in {1..4}; do + notify-send "Notification $i" "This is test notification number $i with a very long text that will probably break the layout or maybe not? Who knows? Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + sleep 1 +done + +echo "All notifications sent!" + +# Additional tests for icon/image handling +if command -v notify-send >/dev/null 2>&1; then + echo "Sending icon/image tests..." + + # 1) Themed icon name + notify-send -i dialog-information "Icon name test" "Should resolve from theme (dialog-information)" + + sleep 1 + + # 2) Absolute path if a sample image exists + SAMPLE_IMG="/usr/share/pixmaps/steam.png" + if [ -f "$SAMPLE_IMG" ]; then + notify-send -i "$SAMPLE_IMG" "Absolute path test" "Should show the provided image path" + fi + + sleep 1 + + # 3) file:// URL form + if [ -f "$SAMPLE_IMG" ]; then + notify-send -i "file://$SAMPLE_IMG" "file:// URL test" "Should display after stripping scheme" + fi + + sleep 1 + + echo "Icon/image tests sent!" +fi + +# A test notification with actions +gdbus call --session \ + --dest org.freedesktop.Notifications \ + --object-path /org/freedesktop/Notifications \ + --method org.freedesktop.Notifications.Notify \ + "my-app" \ + 0 \ + "dialog-question" \ + "Confirmation Required" \ + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Do you want to proceed with the action? " \ + "['default', 'OK', 'cancel', 'Cancel', 'maybe', 'Maybe', 'undecided', 'Undecided']" \ + "{}" \ + 5000 diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/dev/qmlfmt.sh b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/qmlfmt.sh new file mode 100755 index 0000000..049070c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/qmlfmt.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env -S bash +set -euo pipefail + +# QML Formatter Script + +# Suppress Qt debug logging from qmlformat +export QT_LOGGING_RULES="qt.qmldom.*=false" + +# Find qmlformat binary +QMLFORMAT="" +for path in "/usr/lib64/qt6/bin/qmlformat" "/usr/lib/qt6/bin/qmlformat"; do + if [ -x "$path" ]; then + QMLFORMAT="$path" + break + fi +done + +# Fallback to PATH +if [ -z "$QMLFORMAT" ] && command -v qmlformat &>/dev/null; then + QMLFORMAT="qmlformat" +fi + +if [ -z "$QMLFORMAT" ]; then + echo "No 'qmlformat' found in standard locations or PATH." >&2 + echo "To proceed, install it via 'qt6-tools', 'qt6-declarative-tools' or 'qt6-qtdeclarative-devel'" >&2 + exit 1 +fi + +# Detect qmlformat version for flag compatibility +EXTRA_FLAGS="" +if version=$("$QMLFORMAT" --version 2>&1) && [[ "$version" =~ ([0-9]+\.[0-9]+) ]]; then + if [[ "$(printf '%s\n6.10\n' "${BASH_REMATCH[1]}" | sort -V | head -1)" == "6.10" ]]; then + EXTRA_FLAGS="-S --semicolon-rule always" + fi +fi + +format_file() { + ${QMLFORMAT} -w 2 -W 360 ${EXTRA_FLAGS} -i "$1" || { echo "Failed: $1" >&2; return 1; } +} + +export -f format_file +export QMLFORMAT EXTRA_FLAGS + +# Find all .qml files +mapfile -t all_files < <(find "${1:-.}" -name "*.qml" -type f) +[ ${#all_files[@]} -eq 0 ] && { echo "No QML files found"; exit 0; } + +echo "Formatting ${#all_files[@]} files..." +printf '%s\0' "${all_files[@]}" | \ + xargs -0 -P "${QMLFMT_JOBS:-$(nproc)}" -I {} bash -c 'format_file "$@"' _ {} \ + && echo "Done" || { echo "Errors occurred" >&2; exit 1; } diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/dev/shaders-compile.sh b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/shaders-compile.sh new file mode 100755 index 0000000..415c60d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/shaders-compile.sh @@ -0,0 +1,92 @@ +#!/bin/bash + +# Find qsb binary in common locations. +QSB_PATHS=( + "/usr/lib/qt6/bin/qsb" + "/usr/lib64/qt6/bin/qsb" +) + +QSB_BIN="" +for path in "${QSB_PATHS[@]}"; do + if [ -x "$path" ]; then + QSB_BIN="$path" + break + fi +done + +if [ -z "$QSB_BIN" ]; then + echo "Error: qsb binary not found in any of: ${QSB_PATHS[*]}" + exit 1 +fi + +# Directory containing the source shaders. +SOURCE_DIR="Shaders/frag/" + +# Directory where the compiled shaders will be saved. +DEST_DIR="Shaders/qsb/" + +# Check if the source directory exists. +if [ ! -d "$SOURCE_DIR" ]; then + echo "Source directory $SOURCE_DIR not found!" + exit 1 +fi + +# Create the destination directory if it doesn't exist. +mkdir -p "$DEST_DIR" + +# Array to hold the list of full paths to the shaders. +SHADERS_TO_COMPILE=() + +# Specific files mode. +if [ "$#" -gt 0 ]; then + + # Loop through all command-line arguments ($@ holds all arguments). + for SINGLE_FILE in "$@"; do + + # Construct the full path to the source file. + FULL_PATH="$SOURCE_DIR$SINGLE_FILE" + + # Check if the specified file exists in the SOURCE_DIR. + if [ ! -f "$FULL_PATH" ]; then + echo "Error: Specified file '$SINGLE_FILE' not found in $SOURCE_DIR! Skipping." + continue + fi + + # Add the valid file to the compilation list. + SHADERS_TO_COMPILE+=("$FULL_PATH") + done + + # Check if any valid files were found to compile. + if [ ${#SHADERS_TO_COMPILE[@]} -eq 0 ]; then + echo "No valid shaders found to compile." + exit 1 + fi + +# Whole directory mode (no argument provided). +else + # Use find to generate the list of files and assign it to the array. + while IFS= read -r shader_path; do + if [ -n "$shader_path" ]; then + SHADERS_TO_COMPILE+=("$shader_path") + fi + done < <(find "$SOURCE_DIR" -maxdepth 1 -name "*.frag") + +fi + +# Loop through the list of shaders to compile. +for shader in "${SHADERS_TO_COMPILE[@]}"; do + + # Get the base name of the file (e.g., wp_fade). + shader_name=$(basename "$shader" .frag) + + # Construct the output path for the compiled shader. + output_path="$DEST_DIR$shader_name.frag.qsb" + + # Construct and run the qsb command. + "$QSB_BIN" --qt6 -o "$output_path" "$shader" + + # Print a message to confirm compilation. + echo "Compiled $(basename "$shader") to $output_path" +done + +echo "Shader compilation complete." diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/dev/template-processor-analysis.py b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/template-processor-analysis.py new file mode 100755 index 0000000..72d0588 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/dev/template-processor-analysis.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python3 +""" +Analyze Noctalia's template-processor color extraction. + +Usage: + ./template-processor-analysis.py + ./template-processor-analysis.py ~/Pictures/Wallpapers/example.png + +Shows extracted colors for all scheme types and compares M3 schemes with matugen. + +Scheme types: +- M3 schemes (tonal-spot, fruit-salad, rainbow, content): Compared with matugen +- vibrant: Prioritizes the most saturated colors regardless of area +- faithful: Prioritizes dominant colors by area coverage +- dysfunctional: Like faithful but picks the 2nd most dominant color family +- muted: Preserves hue but caps saturation low (for monochrome wallpapers) +""" + +import argparse +import json +import subprocess +import sys +from pathlib import Path + +# Add the theming lib to path +SCRIPT_DIR = Path(__file__).parent.resolve() +THEMING_DIR = SCRIPT_DIR.parent / "python" / "src" / "theming" +sys.path.insert(0, str(THEMING_DIR)) + +from lib.color import Color +from lib.hct import Hct + + +def hue_diff(h1: float, h2: float) -> float: + """Calculate circular hue difference.""" + diff = abs(h1 - h2) + return min(diff, 360.0 - diff) + + +def hex_to_rgb(hex_color: str) -> tuple[int, int, int]: + """Convert hex to RGB tuple.""" + h = hex_color.lstrip('#') + return tuple(int(h[i:i+2], 16) for i in (0, 2, 4)) + + +def rgb_distance(hex1: str, hex2: str) -> float: + """Calculate Euclidean RGB distance (0-441 range).""" + r1, g1, b1 = hex_to_rgb(hex1) + r2, g2, b2 = hex_to_rgb(hex2) + return ((r1-r2)**2 + (g1-g2)**2 + (b1-b2)**2) ** 0.5 + + +def get_hct(hex_color: str) -> Hct: + """Convert hex color to HCT.""" + return Color.from_hex(hex_color).to_hct() + + +def hue_to_name(hue: float) -> str: + """Convert hue to color name.""" + if hue < 30 or hue >= 330: + return "RED" + elif hue < 60: + return "ORANGE" + elif hue < 90: + return "YELLOW" + elif hue < 150: + return "GREEN" + elif hue < 210: + return "CYAN" + elif hue < 270: + return "BLUE" + elif hue < 330: + return "PURPLE" + return "RED" + + +def run_our_processor(image_path: Path, scheme: str) -> dict | None: + """Run our template-processor and return colors.""" + cmd = [ + sys.executable, + str(THEMING_DIR / "template-processor.py"), + str(image_path), + "--scheme-type", scheme, + "--dark" + ] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + data = json.loads(result.stdout) + return data.get("dark", {}) + except (subprocess.CalledProcessError, json.JSONDecodeError) as e: + print(f"Error running our processor: {e}", file=sys.stderr) + return None + + +def run_matugen(image_path: Path, scheme: str) -> dict | None: + """Run matugen and return colors.""" + matugen_scheme = f"scheme-{scheme}" + cmd = [ + "matugen", "image", str(image_path), + "--json", "hex", + "--dry-run", + "-t", matugen_scheme + ] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + data = json.loads(result.stdout) + colors = data.get("colors", {}) + # Extract dark mode values + return {k: v.get("dark", v) for k, v in colors.items() if isinstance(v, dict)} + except subprocess.CalledProcessError as e: + print(f"Error running matugen: {e}", file=sys.stderr) + return None + except json.JSONDecodeError as e: + print(f"Error parsing matugen output: {e}", file=sys.stderr) + return None + + +def analyze_vibrant_faithful_muted(image_path: Path) -> None: + """Analyze vibrant, faithful, dysfunctional, and muted mode outputs.""" + print("\n" + "=" * 78) + print("VIBRANT vs FAITHFUL vs DYSFUNCTIONAL vs MUTED COMPARISON") + print("=" * 78) + print() + print("Vibrant: Prioritizes the most saturated colors regardless of area") + print("Faithful: Prioritizes dominant colors by area coverage") + print("Dysfunctional: Like faithful but picks 2nd most dominant color family") + print("Muted: Preserves hue but caps saturation low (monochrome wallpapers)") + print() + print("-" * 78) + print(f"{'Mode':<14} {'Color':<12} {'Hex':<10} {'Hue':>8} {'Chroma':>8} {'Name':<10}") + print("-" * 78) + + for scheme in ["vibrant", "faithful", "dysfunctional", "muted"]: + colors = run_our_processor(image_path, scheme) + if not colors: + print(f"{scheme}: Failed to get colors") + continue + + for key in ["primary", "secondary", "tertiary"]: + hex_color = colors.get(key, "") + if not hex_color: + continue + + try: + hct = get_hct(hex_color) + name = hue_to_name(hct.hue) + print(f"{scheme:<14} {key:<12} {hex_color:<10} {hct.hue:>7.1f}° {hct.chroma:>7.1f} {name:<10}") + except Exception as e: + print(f"{scheme:<14} {key:<12} Error: {e}") + + print("-" * 78) + + # Summary comparison + vibrant = run_our_processor(image_path, "vibrant") + faithful = run_our_processor(image_path, "faithful") + dysfunctional = run_our_processor(image_path, "dysfunctional") + muted = run_our_processor(image_path, "muted") + + if vibrant and faithful and dysfunctional and muted: + print() + print("Summary:") + v_hct = get_hct(vibrant.get("primary", "#000000")) + f_hct = get_hct(faithful.get("primary", "#000000")) + d_hct = get_hct(dysfunctional.get("primary", "#000000")) + m_hct = get_hct(muted.get("primary", "#000000")) + + v_name = hue_to_name(v_hct.hue) + f_name = hue_to_name(f_hct.hue) + d_name = hue_to_name(d_hct.hue) + m_name = hue_to_name(m_hct.hue) + + vf_diff = hue_diff(v_hct.hue, f_hct.hue) + fd_diff = hue_diff(f_hct.hue, d_hct.hue) + + print(f" Vibrant primary: {vibrant.get('primary')} ({v_name}, hue {v_hct.hue:.0f}°, chroma {v_hct.chroma:.1f})") + print(f" Faithful primary: {faithful.get('primary')} ({f_name}, hue {f_hct.hue:.0f}°, chroma {f_hct.chroma:.1f})") + print(f" Dysfunctional primary:{dysfunctional.get('primary')} ({d_name}, hue {d_hct.hue:.0f}°, chroma {d_hct.chroma:.1f})") + print(f" Muted primary: {muted.get('primary')} ({m_name}, hue {m_hct.hue:.0f}°, chroma {m_hct.chroma:.1f})") + print(f" V-F hue diff: {vf_diff:.1f}°") + print(f" F-D hue diff: {fd_diff:.1f}°") + + if vf_diff > 60: + print(f" → Vibrant/Faithful picked DIFFERENT color families ({v_name} vs {f_name})") + else: + print(f" → Vibrant/Faithful picked SIMILAR colors") + + if fd_diff > 30: + print(f" → Faithful/Dysfunctional picked DIFFERENT color families ({f_name} vs {d_name})") + else: + print(f" → Faithful/Dysfunctional picked SIMILAR colors (may only have 1 dominant family)") + + # Note the muted chroma reduction + if m_hct.chroma < 20: + print(f" → Muted successfully reduced chroma to {m_hct.chroma:.1f}") + else: + print(f" → Muted chroma still moderately high ({m_hct.chroma:.1f})") + + +def compare_m3_schemes(image_path: Path, has_matugen: bool) -> None: + """Compare all M3 schemes between our processor and matugen.""" + schemes = ["tonal-spot", "fruit-salad", "rainbow", "content", "monochrome"] + color_keys = ["primary", "secondary", "tertiary", "surface", "on_surface"] + + print("\n" + "=" * 78) + print("M3 SCHEMES" + (" (compared with matugen)" if has_matugen else "")) + print("=" * 78) + + if has_matugen: + # Header for comparison mode + print(f"{'Scheme':<12} {'Color':<14} {'Ours':<10} {'Matugen':<10} {'Diff':>10} {'Match':<10}") + print("-" * 78) + + for scheme in schemes: + ours = run_our_processor(image_path, scheme) + matugen = run_matugen(image_path, scheme) + + if not ours or not matugen: + print(f"{scheme}: Failed to get colors") + continue + + for key in color_keys: + our_hex = ours.get(key, "") + mat_hex = matugen.get(key, "") + + if not our_hex or not mat_hex: + continue + + try: + our_hct = get_hct(our_hex) + mat_hct = get_hct(mat_hex) + avg_chroma = (our_hct.chroma + mat_hct.chroma) / 2 + + # For low-chroma colors, use RGB distance instead of hue + if avg_chroma < 15: + rgb_dist = rgb_distance(our_hex, mat_hex) + if rgb_dist < 10: + match = "excellent" + elif rgb_dist < 25: + match = "good" + elif rgb_dist < 50: + match = "fair" + else: + match = "poor" + diff_str = f"{rgb_dist:>5.1f} rgb" + else: + diff = hue_diff(our_hct.hue, mat_hct.hue) + if diff < 5: + match = "excellent" + elif diff < 15: + match = "good" + elif diff < 30: + match = "fair" + else: + match = "poor" + diff_str = f"{diff:>5.1f} hue" + + print(f"{scheme:<12} {key:<14} {our_hex:<10} {mat_hex:<10} {diff_str:>10} {match:<10}") + except Exception as e: + print(f"{scheme:<12} {key:<14} Error: {e}") + + print("-" * 78) + else: + # Header for standalone mode + print(f"{'Scheme':<12} {'Color':<14} {'Hex':<10} {'Hue':>8} {'Chroma':>8} {'Name':<10}") + print("-" * 78) + + for scheme in schemes: + ours = run_our_processor(image_path, scheme) + + if not ours: + print(f"{scheme}: Failed to get colors") + continue + + for key in ["primary", "secondary", "tertiary"]: + our_hex = ours.get(key, "") + if not our_hex: + continue + + try: + hct = get_hct(our_hex) + name = hue_to_name(hct.hue) + print(f"{scheme:<12} {key:<14} {our_hex:<10} {hct.hue:>7.1f}° {hct.chroma:>7.1f} {name:<10}") + except Exception as e: + print(f"{scheme:<12} {key:<14} Error: {e}") + + print("-" * 78) + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Analyze Noctalia template-processor color extraction" + ) + parser.add_argument( + "wallpaper", + type=Path, + help="Path to wallpaper image" + ) + parser.add_argument( + "--no-matugen", + action="store_true", + help="Skip matugen comparison" + ) + + args = parser.parse_args() + + if not args.wallpaper.exists(): + print(f"Error: File not found: {args.wallpaper}", file=sys.stderr) + return 1 + + print(f"\nAnalyzing: {args.wallpaper.name}") + + # Check if matugen is available + has_matugen = False + if not args.no_matugen: + try: + subprocess.run(["matugen", "--version"], capture_output=True, check=True) + has_matugen = True + except (subprocess.CalledProcessError, FileNotFoundError): + print("Note: matugen not found, skipping M3 comparison") + + # Always show vibrant vs faithful vs muted first (most useful) + analyze_vibrant_faithful_muted(args.wallpaper) + + # Then show M3 schemes + compare_m3_schemes(args.wallpaper, has_matugen) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/calendar/calendar-events.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/calendar/calendar-events.py new file mode 100755 index 0000000..e080eb8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/calendar/calendar-events.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +import gi +gi.require_version('EDataServer', '1.2') +gi.require_version('ECal', '2.0') +gi.require_version('ICalGLib', "3.0") + +import json, sys +from datetime import datetime, timedelta, timezone +from zoneinfo import ZoneInfo +from gi.repository import ECal, EDataServer, ICalGLib + +start_time = int(sys.argv[1]) +end_time = int(sys.argv[2]) + +print(f"Starting with time range: {start_time} to {end_time}", file=sys.stderr) + +all_events = [] + +def safe_get_time(ical_time): + if not ical_time: + return None, False + try: + year, month, day = ical_time.get_year(), ical_time.get_month(), ical_time.get_day() + is_all_day = hasattr(ical_time, "is_date") and ical_time.is_date() + if is_all_day: + # All-day events (birthdays, holidays) should not need + # to be timezone converted + return int(datetime(year, month, day).timestamp()), True + + hour, minute, second = ical_time.get_hour(), ical_time.get_minute(), ical_time.get_second() + + # Determine timezone for proper conversion + tz_obj = ical_time.get_timezone() if hasattr(ical_time, 'get_timezone') else None + tzid = tz_obj.get_tzid() if tz_obj else None + tz = None + if ical_time.is_utc() if hasattr(ical_time, 'is_utc') else False: + tz = timezone.utc # Explicit UTC time + elif tzid: + # Evolution uses non-standard format: /freeassociation.sourceforge.net/America/Los_Angeles + # Strip prefix to get IANA name: America/Los_Angeles + iana = tzid.replace('/freeassociation.sourceforge.net/', '') if tzid.startswith('/') else tzid + try: tz = ZoneInfo(iana) + except: pass + + # Create timezone-aware datetime + dt = datetime(year, month, day, hour, minute, second, tzinfo=tz) + return int(dt.timestamp()), False + except: + return None, False + +def add_event(summary, calendar_name, start_ts, end_ts, location="", description="", all_day=False, calendar_uid="", uid=""): + all_events.append({ + 'calendar': calendar_name, + 'summary': summary, + 'start': start_ts, + 'end': end_ts, + 'location': location, + 'description': description, + 'calendar_uid': calendar_uid, + 'uid': uid + }) + +registry = EDataServer.SourceRegistry.new_sync(None) +sources = registry.list_sources(EDataServer.SOURCE_EXTENSION_CALENDAR) + +for source in sources: + if not source.get_enabled(): + continue + + calendar_name = source.get_display_name() + print(f"\nProcessing calendar: {calendar_name}", file=sys.stderr) + + try: + client = ECal.Client.connect_sync(source, ECal.ClientSourceType.EVENTS, 5, None) + + start_dt = datetime.fromtimestamp(start_time) + end_dt = datetime.fromtimestamp(end_time) + start_str = start_dt.strftime("%Y%m%dT%H%M%S") + end_str = end_dt.strftime("%Y%m%dT%H%M%S") + + query = f'(occur-in-time-range? (make-time "{start_str}") (make-time "{end_str}"))' + success, raw_events = client.get_object_list_sync(query, None) + + if not success or not raw_events: + continue + + for raw_obj in raw_events: + obj = raw_obj[1] if isinstance(raw_obj, tuple) else raw_obj + comp = None + + if isinstance(obj, ICalGLib.Component): + comp = obj + elif isinstance(obj, ECal.Component): + try: + ical_str = obj.to_string() + temp_comp = ICalGLib.Component.new_from_string(ical_str) + if temp_comp.getName() == "VEVENT": + comp = temp_comp + except Exception: + comp = None + + if not comp: + summary = getattr(obj, "get_summary", lambda: "(No title)")() + dtstart = getattr(obj, "get_dtstart", lambda: None)() + dtend = getattr(obj, "get_dtend", lambda: None)() + location = getattr(obj, "get_location", lambda: "")() or "" + description = getattr(obj, "get_description", lambda: "")() or "" + start_ts, all_day = safe_get_time(dtstart) + end_ts, _ = safe_get_time(dtend) + if start_ts: + if end_ts is None: + end_ts = start_ts + 3600 + event_uid = getattr(obj, "get_uid", lambda: "")() or "" + add_event(summary, calendar_name, start_ts, end_ts, location, description, + calendar_uid=source.get_uid(), uid=event_uid) + continue + + summary = getattr(comp, "get_summary", lambda: "(No title)")() + location = getattr(comp, "get_location", lambda: "")() or "" + description = getattr(comp, "get_description", lambda: "")() or "" + dtstart = getattr(comp, "get_dtstart", lambda: None)() + dtend = getattr(comp, "get_dtend", lambda: None)() + start_ts, all_day = safe_get_time(dtstart) + end_ts, _ = safe_get_time(dtend) + if end_ts is None and start_ts is not None: + end_ts = start_ts + 3600 + + rrule_getter = getattr(comp, "get_first_property", None) + if rrule_getter: + rrule_prop = comp.get_first_property(73) # ICAL_RRULE_PROPERTY + if rrule_prop: + rrule_value = rrule_prop.get_value() # ICalGLib.Value + + try: + recurrence = rrule_value.get_recur() # -> ICalGLib.Recurrence + + except AttributeError: + rrule_str = str(rrule_value) + recurrence = ICalGLib.Recurrence.new_from_string(rrule_str) + + if recurrence: + freq = recurrence.get_freq() + + rdates = getattr(comp, "get_rdate_list", lambda: [])() + exdates = getattr(comp, "get_exdate_list", lambda: [])() + + # --- normal event --- + if not rrule_prop and not rdates: + add_event(summary, calendar_name, start_ts, end_ts, location, description, + calendar_uid=source.get_uid(), uid=comp.get_uid() or "") + continue + + # --- recurrent events --- + if freq: + summary = comp.get_summary() or "(No title)" + dtstart = comp.get_dtstart() + dtend = comp.get_dtend() + start_ts, all_day = safe_get_time(dtstart) + end_ts, _ = safe_get_time(dtend) + if end_ts is None and start_ts is not None: + end_ts = start_ts + 3600 # 1h default + + interval = recurrence.get_interval() or 1 + count = recurrence.get_count() + until_dt = recurrence.get_until() + until_ts, _ = safe_get_time(until_dt) if until_dt else (None, False) + if until_ts is None: + until_ts = end_time + + occurrences = [] + current_ts = start_ts + added = 0 + + match freq: + case 0: #SECONDLY + delta = timedelta(seconds=interval) + while (current_ts <= until_ts) and (not count or added < count): + occurrences.append((current_ts, current_ts + (end_ts - start_ts))) + current_ts += int(delta.total_seconds()) + added += 1 + + case 1: #MINUTELY + delta = timedelta(minutes=interval) + while (current_ts <= until_ts) and (not count or added < count): + occurrences.append((current_ts, current_ts + (end_ts - start_ts))) + current_ts += int(delta.total_seconds()) + added += 1 + + case 2: #HOURLY + delta = timedelta(hours=interval) + while (current_ts <= until_ts) and (not count or added < count): + occurrences.append((current_ts, current_ts + (end_ts - start_ts))) + current_ts += int(delta.total_seconds()) + added += 1 + + case 3: # DAILY + delta = timedelta(days=interval) + while (current_ts <= until_ts) and (not count or added < count): + occurrences.append((current_ts, current_ts + (end_ts - start_ts))) + current_ts += int(delta.total_seconds()) + added += 1 + + case 4: # WEEKLY + delta = timedelta(weeks=interval) + while (current_ts <= until_ts) and (not count or added < count): + occurrences.append((current_ts, current_ts + (end_ts - start_ts))) + current_ts += int(delta.total_seconds()) + added += 1 + + case 5: # MONTHLY + from dateutil.relativedelta import relativedelta + dt = datetime.fromtimestamp(current_ts) + while (current_ts <= until_ts) and (not count or added < count): + occurrences.append((current_ts, current_ts + (end_ts - start_ts))) + dt += relativedelta(months=interval) + current_ts = int(dt.timestamp()) + added += 1 + + case 6: # YEARLY + from dateutil.relativedelta import relativedelta + dt = datetime.fromtimestamp(current_ts) + while (current_ts <= until_ts) and (not count or added < count): + occurrences.append((current_ts, current_ts + (end_ts - start_ts))) + dt += relativedelta(years=interval) + current_ts = int(dt.timestamp()) + added += 1 + + case _: # NONE + occurrences.append((start_ts, end_ts)) + + # --- add occurences to all_events --- + for occ_start, occ_end in occurrences: + add_event(summary, calendar_name, occ_start, occ_end, location, description, + calendar_uid=source.get_uid(), uid=comp.get_uid() or "") + + + except Exception as e: + print(f" Error for {calendar_name}: {e}", file=sys.stderr) + +all_events.sort(key=lambda x: x['start']) +print(json.dumps(all_events, indent=4)) + diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/calendar/check-calendar.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/calendar/check-calendar.py new file mode 100755 index 0000000..463d2e4 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/calendar/check-calendar.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +import gi + +gi.require_version('EDataServer', '1.2') +gi.require_version('ECal', '2.0') + +try: + from gi.repository import ECal, EDataServer + print("available") +except ImportError as e: + print(f"unavailable: {e}") diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/calendar/khal-events.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/calendar/khal-events.py new file mode 100755 index 0000000..6b54542 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/calendar/khal-events.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +import json +import os +import re +import subprocess +import sys +from datetime import datetime +from pathlib import Path + +def get_khal_date_format(): + """Read the khal config and extract the longdatetimeformat.""" + xdg_config = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) + config_path = Path(xdg_config) / 'khal' / 'config' + + if not config_path.exists(): + return '%c' + + with open(config_path, 'r') as f: + for line in f: + if m := re.match(r'^longdatetimeformat\s?=\s?(.+?)\s*$', line): + date_format = m.group(1).strip() + return date_format + + return '%c' + + +def to_khal(date_str, khal_format): + dt = datetime.strptime(date_str, "%Y-%m-%d") + return dt.strftime(khal_format) + +def from_khal(date_str, khal_format): + if not date_str: + return '' + dt = datetime.strptime(date_str, khal_format) + return dt.isoformat() + +def convert_event(event, khal_format): + event['start-long-full'] = from_khal(event.get('start-long-full', ''), khal_format) + event['end-long-full'] = from_khal(event.get('end-long-full', ''), khal_format) + return event + +def main(): + start_date = sys.argv[1] + duration = sys.argv[2] + + khal_format = get_khal_date_format() + khal_start = to_khal(start_date, khal_format) + + cmd = [ + 'khal', 'list', + '--json', 'uid', + '--json', 'title', + '--json', 'start-long-full', + '--json', 'end-long-full', + '--json', 'calendar', + '--json', 'description', + '--json', 'location', + '--json', 'repeat-pattern', + khal_start, + duration + ] + + result = subprocess.run(cmd, capture_output=True, text=True) + output = result.stdout.strip() + + for line in output.split('\n'): + day_events = json.loads(line) + print(json.dumps([convert_event(e, khal_format) for e in day_events])) + +if __name__ == '__main__': + main() diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/calendar/list-calendars.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/calendar/list-calendars.py new file mode 100755 index 0000000..12c67bd --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/calendar/list-calendars.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +import gi + +gi.require_version('EDataServer', '1.2') +import json + +from gi.repository import EDataServer + +registry = EDataServer.SourceRegistry.new_sync(None) +sources = registry.list_sources(EDataServer.SOURCE_EXTENSION_CALENDAR) + +calendars = [] +for source in sources: + if source.get_enabled(): + calendars.append({ + 'uid': source.get_uid(), + 'name': source.get_display_name(), + 'enabled': True + }) + +print(json.dumps(calendars)) diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/network/bluetooth-pair.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/network/bluetooth-pair.py new file mode 100644 index 0000000..6d8eddc --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/network/bluetooth-pair.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +import errno +import os +import pty +import select +import subprocess +import sys +import time +# flake8: noqa: E501 # Line too long +version = "0.0.2-1" + + +def log(msg) -> None: + sys.stdout.write(f"[pair] {msg}\n") + sys.stdout.flush() # Flush to ensure the message is passed + + +def pair_fast(): + if len(sys.argv) < 5: + log("Usage: bluetooth-pair.py ") + sys.exit(2) + + addr = sys.argv[1] + # We won't use pair_wait_seconds in the same way, but we'll respect the timeout logic. + pair_wait_seconds = float(sys.argv[2]) + if pair_wait_seconds < 30: + log(f"Warning: pairWaitSeconds ({pair_wait_seconds}s) is too short. Enforcing 45s minimum.") + pair_wait_seconds = 45.0 + + attempts = int(sys.argv[3]) + interval_sec = float(sys.argv[4]) + + if not addr or len(addr) < 17: + # Basic MAC address length check + log(f"Invalid Bluetooth address: '{addr}'") + sys.exit(2) + + # m/s PTY for interactive control + mfd, sfd = pty.openpty() + + # Start bluetoothctl + subprocess.Popen(['bluetoothctl'], stdin=sfd, stdout=sfd, stderr=sfd, close_fds=True, text=True) + + os.close(sfd) + + def send_command(cmd): + log(f"Sending cmd: {cmd}") + os.write(mfd, (cmd + "\n").encode('utf-8')) + + def read_output(timeout=1.0): + # Reads available output from mfd + output = b"" + end_time = time.time() + timeout + while time.time() < end_time: + r, _, _ = select.select([mfd], [], [], 0.1) + if mfd in r: + try: + data = os.read(mfd, 1024) + if not data: + break + output += data + except OSError as e: + if e.errno == errno.EIO: + break + raise + else: + pass + return output.decode('utf-8', errors='replace') + + log("Initializing bluetoothctl...") + time.sleep(1) # Wait for startup + # initial_out = read_output(timeout=1) + # print(initial_out) # Debug + + send_command("agent on") + send_command("default-agent") + # send_command("power on") # If we are pairing bluetooth is already powered on + time.sleep(0.5) + + # Pair directly since the device is already discovered in the UI/Panel (Removed previous scan/wait part) + log(f"Attempting to pair with {addr}...") + send_command(f"pair {addr}") + + # Loop to watch for confirmation or success + start_time = time.time() + paired = False + + log("Waiting for pairing sequence start...") + while time.time() - start_time < pair_wait_seconds: + out = read_output(timeout=0.5) + if out: + print(out, end='') + # Device not found yet + device_not_discovered: list[str] = [f"Device {addr} not available"] + if any(e in out for e in device_not_discovered): + log(f"Device {addr} is discovered yet...") + pair_wait_seconds += 30 # Add additional time for device discovery + + # Confirm Passkey + # Numberic Comparison (NC) 1 of 4 - Tested pairing with my iPhone. + expected_confirmation: list[str] = ["Confirm passkey", "yes/no", "Request confirmation"] + if any(e in out for e in expected_confirmation): + log("Detected passkey prompt. Sending 'yes'.") + send_command("yes") + + # Authorization Request + expected_auth: list[str] = ["Authorize service", "Request authorization"] + if any(e in out for e in expected_auth): + log("Detected authorization request. Sending 'yes'.") + send_command("yes") + + # Interactive PIN/Passkey Entry (Device displays code, User must enter on PC) + expected_pin: list[str] = ["Enter passkey", "Enter PIN code", "Passkey: "] + if any(e in out for e in expected_pin): + log("Device requested PIN/Passkey. Waiting for user input...") + log("PIN_REQUIRED") # Signal to service, to prompt user. + + try: + # Read PIN from stdin (blocking) + user_pin = sys.stdin.readline().strip() + if user_pin: + log(f"Received PIN: {user_pin}, relaying to bluetoothctl...") + send_command(user_pin) + except Exception as e: + log(f"Error reading stdin: {e}") + break + + # Just Works (JW) is implicit (no prompt) + expected_success: list[str] = ["Pairing successful", "Paired: yes", "Bonded: yes"] + if any(e in out for e in expected_success): + paired = True + log("Pairing successful detected in stream.") + break + + if "Failed to pair" in out: + log("Pairing failed explicitly.") + break + + expected_already_paired: list[str] = ["Already joined", "Already exists"] + if any(e in out for e in expected_already_paired): + paired = True + log("Device already paired.") + break + + # Double check pairing status via info command if not sure + if not paired: + send_command(f"info {addr}") + time.sleep(1) + out = read_output(timeout=1) + if "Paired: yes" in out: + paired = True + + if paired: + log("Device is paired. Trusting...") + send_command(f"trust {addr}") + time.sleep(1) + + log("Connecting...") + connected = False + for i in range(attempts): + send_command(f"connect {addr}") + # Wait a bit for connection + time.sleep(interval_sec) + + # Check status + send_command(f"info {addr}") + time.sleep(1) + out = read_output(timeout=1) + if "Connected: yes" in out: + log("Connected successfully, we are done here.") + connected = True + break + else: + log(f"Connection attempt {i + 1}/{attempts} failed. Retrying...") + + if connected: + send_command("quit") + sys.exit(0) + else: + log("Failed to connect after all attempts.") + send_command("quit") + sys.exit(1) + + else: + log("Failed to pair within timeout.") + send_command("quit") + sys.exit(1) + + +if __name__ == "__main__": + pair_fast() diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/gtk-refresh.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/gtk-refresh.py new file mode 100644 index 0000000..be21bbc --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/gtk-refresh.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 + +import asyncio +import os +import sys +import shutil +from pathlib import Path + + +async def run_command(*args): + process = await asyncio.create_subprocess_exec( + *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await process.communicate() + if process.returncode != 0: + print(f"Error running {' '.join(args)}: {stderr.decode().strip()}", file=sys.stderr) + return stdout.decode().strip() + + +def theme_exists(theme_name: str) -> bool: + """Check if a GTK theme exists in common locations.""" + search_paths = [ + Path.home() / ".themes", + Path.home() / ".local/share/themes", + Path("/usr/share/themes"), + Path("/usr/local/share/themes"), + ] + + # Add paths from XDG_DATA_DIRS + xdg_data_dirs = os.environ.get("XDG_DATA_DIRS", "") + if xdg_data_dirs: + for path in xdg_data_dirs.split(":"): + if path: + search_paths.append(Path(path) / "themes") + + for base_path in search_paths: + if (base_path / theme_name).is_dir(): + return True + + return False + + +GTK_IMPORT = '@import url("noctalia.css");' + + +def ensure_gtk_css_import(gtk_css: Path, colors_file: Path, label: str) -> bool: + """ + Append the noctalia.css import to gtk.css if not already present. + If gtk.css doesn't exist, create it with the import. + Does not overwrite user modifications (similar to niri template). + """ + if not colors_file.exists(): + print(f"Error: {label} noctalia.css not found at {colors_file}", file=sys.stderr) + return False + + if gtk_css.exists() or gtk_css.is_symlink(): + content = gtk_css.read_text() + # Already has the import (flexible: allow optional whitespace / different quoting) + if "noctalia.css" in content and "@import" in content: + return True + # Need to modify — handle symlinks carefully + target = gtk_css + if gtk_css.is_symlink(): + resolved = gtk_css.resolve() + if os.access(resolved, os.W_OK): + # Writable symlink (e.g. dotfiles): edit the target directly + target = resolved + else: + # Read-only symlink (e.g. NixOS): convert to local file + gtk_css.unlink() + gtk_css.write_text(resolved.read_text()) + # Append import to the end + new_content = content.rstrip() + if new_content and not new_content.endswith("\n"): + new_content += "\n" + new_content += "\n" + GTK_IMPORT + "\n" + target.write_text(new_content) + print(f"Appended {label} noctalia.css import to gtk.css") + else: + gtk_css.write_text(GTK_IMPORT + "\n") + print(f"Created {label} gtk.css with noctalia.css import") + return True + + +async def apply_gtk3_colors(config_dir: Path): + gtk3_dir = config_dir / "gtk-3.0" + colors_file = gtk3_dir / "noctalia.css" + gtk_css = gtk3_dir / "gtk.css" + return ensure_gtk_css_import(gtk_css, colors_file, "GTK3") + + +async def apply_gtk4_colors(config_dir: Path): + gtk4_dir = config_dir / "gtk-4.0" + colors_file = gtk4_dir / "noctalia.css" + gtk_css = gtk4_dir / "gtk.css" + return ensure_gtk_css_import(gtk_css, colors_file, "GTK4") + + +async def sync_system_appearance(mode: str, *, update_gtk_theme: bool = True) -> None: + """ + Push light/dark to org.gnome.desktop.interface (gsettings or dconf fallback). + Used by the GTK template post-hook and ColorSchemeService when "Sync system theme" + is on (both set color-scheme and gtk-theme when themes exist). --appearance-only + skips CSS and only updates color-scheme for narrow tooling use. + """ + has_gsettings = shutil.which("gsettings") + has_dconf = shutil.which("dconf") + + if not has_gsettings and not has_dconf: + print("No gsettings or dconf found, skip system appearance sync") + return + + target_theme = "adw-gtk3" if mode == "light" else "adw-gtk3-dark" + theme_available = update_gtk_theme and theme_exists(target_theme) + if update_gtk_theme and not theme_available: + print(f"Theme '{target_theme}' not found, skipping GTK theme set") + + if has_gsettings: + schemas = await run_command("gsettings", "list-schemas") + if schemas and "org.gnome.desktop.interface" in schemas: + await run_command("gsettings", "set", "org.gnome.desktop.interface", "color-scheme", f"prefer-{mode}") + if theme_available: + await run_command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", f"{target_theme}") + return + + if has_dconf: + await run_command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", f"'prefer-{mode}'") + if theme_available: + await run_command("dconf", "write", "/org/gnome/desktop/interface/gtk-theme", f"'{target_theme}'") + + +async def get_config_dir() -> Path: + # Returns the XDG config home (e.g. ~/.config) + # GTK config lives at ~/.config/gtk-3.0/ and ~/.config/gtk-4.0/. + + # 1. XDG standard + if value := os.environ.get("XDG_CONFIG_HOME"): + return Path(value).expanduser() + + # 2. fallback + return Path.home() / ".config" + + +def parse_args(): + argv = sys.argv[1:] + appearance_only = False + if argv and argv[0] == "--appearance-only": + appearance_only = True + argv = argv[1:] + if len(argv) != 1 or argv[0] not in ("dark", "light"): + print( + "Usage: gtk-refresh.py [--appearance-only] (dark|light)", + file=sys.stderr, + ) + sys.exit(1) + return appearance_only, argv[0] + + +async def main(): + appearance_only, mode = parse_args() + + if appearance_only: + await sync_system_appearance(mode, update_gtk_theme=False) + return + + config_dir = await get_config_dir() + + if not config_dir.is_dir(): + print(f"Error: Config directory not found: {config_dir}", file=sys.stderr) + sys.exit(1) + + (config_dir / "gtk-3.0").mkdir(parents=True, exist_ok=True) + (config_dir / "gtk-4.0").mkdir(parents=True, exist_ok=True) + + results = await asyncio.gather(apply_gtk3_colors(config_dir), apply_gtk4_colors(config_dir)) + + if all(results): + await sync_system_appearance(mode, update_gtk_theme=True) + print("GTK colors applied successfully") + else: + # Still push light/dark preference so portal/GTK apps follow the shell even when + # gtk.css / noctalia.css setup failed. + await sync_system_appearance(mode, update_gtk_theme=False) + sys.exit(1) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/__init__.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/__init__.py new file mode 100644 index 0000000..644a55a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/__init__.py @@ -0,0 +1,58 @@ +""" +Theming library - Color extraction and theme generation. + +This package provides: +- HCT color space implementation (CAM16, Hct, TonalPalette) +- Material Design 3 scheme generation +- Color utilities (RGB, HSL conversions) +- Image reading and palette extraction +- Template rendering (Matugen compatible) +""" + +from .color import Color, rgb_to_hsl, hsl_to_rgb, adjust_surface +from .hct import Hct, Cam16, TonalPalette, TemperatureCache, fix_if_disliked +from .material import MaterialScheme, SchemeContent, harmonize_color +from .contrast import ensure_contrast, contrast_ratio, is_dark +from .image import read_image, ImageReadError +from .palette import extract_palette +from .quantizer import extract_source_color, source_color_to_rgb +from .theme import generate_theme +from .renderer import TemplateRenderer +from .scheme import expand_predefined_scheme, inject_terminal_colors + +__all__ = [ + # Color + "Color", + "rgb_to_hsl", + "hsl_to_rgb", + "adjust_surface", + # HCT + "Hct", + "Cam16", + "TonalPalette", + "TemperatureCache", + "fix_if_disliked", + # Material + "MaterialScheme", + "SchemeContent", + "harmonize_color", + # Contrast + "ensure_contrast", + "contrast_ratio", + "is_dark", + # Image + "read_image", + "ImageReadError", + # Palette + "extract_palette", + # Quantizer (Wu + Score algorithm matching matugen) + "extract_source_color", + "source_color_to_rgb", + # Theme + "generate_theme", + # Renderer + "TemplateRenderer", + # Scheme + "expand_predefined_scheme", + "inject_terminal_colors", +] diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/color.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/color.py new file mode 100644 index 0000000..5881cad --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/color.py @@ -0,0 +1,353 @@ +""" +Color representation and conversion utilities. + +This module provides the Color class and functions for converting between +RGB, HSL, and Lab color spaces. +""" + +import math +from dataclasses import dataclass +from typing import TYPE_CHECKING + +# Type aliases +RGB = tuple[int, int, int] +HSL = tuple[float, float, float] +LAB = tuple[float, float, float] + +if TYPE_CHECKING: + from .hct import Hct + + +@dataclass +class Color: + """Represents a color with RGB values (0-255).""" + r: int + g: int + b: int + + @classmethod + def from_rgb(cls, rgb: RGB) -> 'Color': + return cls(rgb[0], rgb[1], rgb[2]) + + @classmethod + def from_hex(cls, hex_str: str) -> 'Color': + """Parse hex color string (#RRGGBB or RRGGBB).""" + hex_str = hex_str.lstrip('#') + return cls( + int(hex_str[0:2], 16), + int(hex_str[2:4], 16), + int(hex_str[4:6], 16) + ) + + def to_rgb(self) -> RGB: + return (self.r, self.g, self.b) + + def to_hex(self) -> str: + """Convert to hex string (#RRGGBB).""" + return f"#{self.r:02x}{self.g:02x}{self.b:02x}" + + def to_hsl(self) -> HSL: + """Convert RGB to HSL.""" + return rgb_to_hsl(self.r, self.g, self.b) + + def to_hct(self) -> 'Hct': + """Convert to HCT color space.""" + from .hct import Hct + return Hct.from_rgb(self.r, self.g, self.b) + + @classmethod + def from_hsl(cls, h: float, s: float, l: float) -> 'Color': + """Create Color from HSL values.""" + r, g, b = hsl_to_rgb(h, s, l) + return cls(r, g, b) + + @classmethod + def from_hct(cls, hct: 'Hct') -> 'Color': + """Create Color from HCT.""" + r, g, b = hct.to_rgb() + return cls(r, g, b) + + +def rgb_to_hsl(r: int, g: int, b: int) -> HSL: + """ + Convert RGB (0-255) to HSL (0-360, 0-1, 0-1). + + Args: + r: Red component (0-255) + g: Green component (0-255) + b: Blue component (0-255) + + Returns: + Tuple of (hue, saturation, lightness) + """ + r_norm = r / 255.0 + g_norm = g / 255.0 + b_norm = b / 255.0 + + max_c = max(r_norm, g_norm, b_norm) + min_c = min(r_norm, g_norm, b_norm) + delta = max_c - min_c + + # Lightness + l = (max_c + min_c) / 2.0 + + if delta == 0: + h = 0.0 + s = 0.0 + else: + # Saturation + s = delta / (1 - abs(2 * l - 1)) if l != 0 and l != 1 else 0 + + # Hue + if max_c == r_norm: + h = 60.0 * (((g_norm - b_norm) / delta) % 6) + elif max_c == g_norm: + h = 60.0 * (((b_norm - r_norm) / delta) + 2) + else: + h = 60.0 * (((r_norm - g_norm) / delta) + 4) + + return (h, s, l) + + +def hsl_to_rgb(h: float, s: float, l: float) -> RGB: + """ + Convert HSL (0-360, 0-1, 0-1) to RGB (0-255). + + Args: + h: Hue (0-360) + s: Saturation (0-1) + l: Lightness (0-1) + + Returns: + Tuple of (r, g, b) + """ + if s == 0: + # Achromatic (gray) + v = int(round(l * 255)) + return (v, v, v) + + def hue_to_rgb(p: float, q: float, t: float) -> float: + if t < 0: + t += 1 + if t > 1: + t -= 1 + if t < 1/6: + return p + (q - p) * 6 * t + if t < 1/2: + return q + if t < 2/3: + return p + (q - p) * (2/3 - t) * 6 + return p + + q = l * (1 + s) if l < 0.5 else l + s - l * s + p = 2 * l - q + h_norm = h / 360.0 + + r = hue_to_rgb(p, q, h_norm + 1/3) + g = hue_to_rgb(p, q, h_norm) + b = hue_to_rgb(p, q, h_norm - 1/3) + + return ( + int(round(r * 255)), + int(round(g * 255)), + int(round(b * 255)) + ) + + +def adjust_lightness(color: Color, target_l: float) -> Color: + """Adjust a color's lightness to a target value (0-1).""" + h, s, _ = color.to_hsl() + return Color.from_hsl(h, s, target_l) + + +def shift_hue(color: Color, degrees: float) -> Color: + """Shift a color's hue by specified degrees.""" + h, s, l = color.to_hsl() + new_h = (h + degrees) % 360 + return Color.from_hsl(new_h, s, l) + + +def hue_distance(h1: float, h2: float) -> float: + """Calculate minimum angular distance between two hues (0-180).""" + diff = abs(h1 - h2) + return min(diff, 360 - diff) + + +def adjust_surface(color: Color, s_max: float, l_target: float) -> Color: + """Derive a surface color from a base color with saturation limit and target lightness.""" + h, s, _ = color.to_hsl() + return Color.from_hsl(h, min(s, s_max), l_target) + + +def saturate(color: Color, amount: float) -> Color: + """Adjust saturation by amount (-1 to 1).""" + h, s, l = color.to_hsl() + new_s = max(0.0, min(1.0, s + amount)) + return Color.from_hsl(h, new_s, l) + + +# ============================================================================= +# Lab Color Space (CIE L*a*b*) +# ============================================================================= + +# D65 white point +_WHITE_X = 95.047 +_WHITE_Y = 100.0 +_WHITE_Z = 108.883 + + +def _linearize(channel: int) -> float: + """Convert sRGB channel (0-255) to linear RGB (0-1).""" + normalized = channel / 255.0 + if normalized <= 0.04045: + return normalized / 12.92 + return math.pow((normalized + 0.055) / 1.055, 2.4) + + +def _delinearize(linear: float) -> int: + """Convert linear RGB (0-1) to sRGB channel (0-255).""" + if linear <= 0.0031308: + normalized = linear * 12.92 + else: + normalized = 1.055 * math.pow(linear, 1.0 / 2.4) - 0.055 + return max(0, min(255, round(normalized * 255))) + + +def _lab_f(t: float) -> float: + """Lab forward transform function.""" + if t > 0.008856: + return math.pow(t, 1.0 / 3.0) + return (903.3 * t + 16.0) / 116.0 + + +def _lab_f_inv(t: float) -> float: + """Lab inverse transform function.""" + if t > 0.206893: + return t * t * t + return (116.0 * t - 16.0) / 903.3 + + +def rgb_to_lab(r: int, g: int, b: int) -> LAB: + """ + Convert sRGB (0-255) to CIE L*a*b*. + + Returns: + Tuple of (L*, a*, b*) where L* is 0-100 + """ + # sRGB to linear RGB + linear_r = _linearize(r) + linear_g = _linearize(g) + linear_b = _linearize(b) + + # Linear RGB to XYZ (D65) + x = 0.4124564 * linear_r + 0.3575761 * linear_g + 0.1804375 * linear_b + y = 0.2126729 * linear_r + 0.7151522 * linear_g + 0.0721750 * linear_b + z = 0.0193339 * linear_r + 0.1191920 * linear_g + 0.9503041 * linear_b + + # Scale to 0-100 range + x *= 100.0 + y *= 100.0 + z *= 100.0 + + # XYZ to Lab + fx = _lab_f(x / _WHITE_X) + fy = _lab_f(y / _WHITE_Y) + fz = _lab_f(z / _WHITE_Z) + + L = 116.0 * fy - 16.0 + a = 500.0 * (fx - fy) + b = 200.0 * (fy - fz) + + return (L, a, b) + + +def lab_to_rgb(L: float, a: float, b: float) -> RGB: + """ + Convert CIE L*a*b* to sRGB (0-255). + + Args: + L: Lightness (0-100) + a: Green-red component + b: Blue-yellow component + + Returns: + Tuple of (r, g, b) + """ + # Lab to XYZ + fy = (L + 16.0) / 116.0 + fx = a / 500.0 + fy + fz = fy - b / 200.0 + + x = _WHITE_X * _lab_f_inv(fx) + y = _WHITE_Y * _lab_f_inv(fy) + z = _WHITE_Z * _lab_f_inv(fz) + + # Scale back to 0-1 range + x /= 100.0 + y /= 100.0 + z /= 100.0 + + # XYZ to linear RGB + linear_r = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z + linear_g = -0.9692660 * x + 1.8760108 * y + 0.0415560 * z + linear_b = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z + + # Clamp and delinearize + return ( + _delinearize(max(0.0, min(1.0, linear_r))), + _delinearize(max(0.0, min(1.0, linear_g))), + _delinearize(max(0.0, min(1.0, linear_b))) + ) + + +def lab_distance(lab1: LAB, lab2: LAB) -> float: + """ + Calculate Euclidean distance between two Lab colors. + + This is a simple perceptual distance metric. + """ + dL = lab1[0] - lab2[0] + da = lab1[1] - lab2[1] + db = lab1[2] - lab2[2] + return math.sqrt(dL * dL + da * da + db * db) + + +def find_closest_color( + compare_to: str, + colors: list[dict[str, str]] +) -> str: + """ + Find the closest named color from a list (matugen-compatible). + + Uses Lab color space Euclidean distance for perceptual color matching. + + Args: + compare_to: Hex color to compare (e.g., "#ff5500") + colors: List of {"name": "...", "color": "#..."} dicts + + Returns: + Name of the closest color, or empty string if no colors provided + """ + if not colors: + return "" + + # Parse target color + target = Color.from_hex(compare_to) + target_lab = rgb_to_lab(target.r, target.g, target.b) + + closest_name = "" + closest_dist = float('inf') + + for entry in colors: + try: + entry_color = Color.from_hex(entry["color"]) + entry_lab = rgb_to_lab(entry_color.r, entry_color.g, entry_color.b) + dist = lab_distance(target_lab, entry_lab) + if dist < closest_dist: + closest_dist = dist + closest_name = entry["name"] + except (KeyError, ValueError): + # Skip invalid entries + continue + + return closest_name diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/contrast.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/contrast.py new file mode 100644 index 0000000..2f67157 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/contrast.py @@ -0,0 +1,123 @@ +""" +Contrast calculation utilities (WCAG luminance and contrast). + +This module provides functions for calculating relative luminance, +contrast ratios, and ensuring accessible color combinations. +""" + +from .color import Color + + +def relative_luminance(r: int, g: int, b: int) -> float: + """ + Calculate relative luminance per WCAG 2.1. + + The formula converts sRGB to linear RGB, then applies the luminance formula: + L = 0.2126 * R + 0.7152 * G + 0.0722 * B + + Args: + r, g, b: RGB components (0-255) + + Returns: + Relative luminance (0-1) + """ + def linearize(c: int) -> float: + c_norm = c / 255.0 + if c_norm <= 0.03928: + return c_norm / 12.92 + return ((c_norm + 0.055) / 1.055) ** 2.4 + + r_lin = linearize(r) + g_lin = linearize(g) + b_lin = linearize(b) + + return 0.2126 * r_lin + 0.7152 * g_lin + 0.0722 * b_lin + + +def contrast_ratio(color1: Color, color2: Color) -> float: + """ + Calculate WCAG contrast ratio between two colors. + + Returns a value between 1:1 (identical) and 21:1 (black/white). + """ + l1 = relative_luminance(color1.r, color1.g, color1.b) + l2 = relative_luminance(color2.r, color2.g, color2.b) + + lighter = max(l1, l2) + darker = min(l1, l2) + + return (lighter + 0.05) / (darker + 0.05) + + +def is_dark(color: Color) -> bool: + """Determine if a color is perceptually dark.""" + return relative_luminance(color.r, color.g, color.b) < 0.179 + + +def ensure_contrast( + foreground: Color, + background: Color, + min_ratio: float = 4.5, + prefer_light: bool | None = None +) -> Color: + """ + Adjust foreground color to meet minimum contrast ratio against background. + + Args: + foreground: The color to adjust + background: The background color (not modified) + min_ratio: Minimum contrast ratio (default 4.5 for WCAG AA) + prefer_light: If True, prefer lightening; if False, prefer darkening; + if None, auto-detect based on background + + Returns: + Adjusted foreground color meeting contrast requirements + """ + current_ratio = contrast_ratio(foreground, background) + if current_ratio >= min_ratio: + return foreground + + h, s, l = foreground.to_hsl() + bg_dark = is_dark(background) + + # Determine direction to adjust + if prefer_light is None: + prefer_light = bg_dark + + # Binary search for the right lightness + if prefer_light: + low, high = l, 1.0 + else: + low, high = 0.0, l + + best_color = foreground + for _ in range(20): # Max iterations + mid = (low + high) / 2 + test_color = Color.from_hsl(h, s, mid) + ratio = contrast_ratio(test_color, background) + + if ratio >= min_ratio: + best_color = test_color + if prefer_light: + high = mid + else: + low = mid + else: + if prefer_light: + low = mid + else: + high = mid + + return best_color + + +def get_contrasting_color(background: Color, min_ratio: float = 4.5) -> Color: + """Get a contrasting foreground color (black or white variant).""" + if is_dark(background): + # Light foreground for dark background + fg = Color(243, 237, 247) # Off-white + else: + # Dark foreground for light background + fg = Color(14, 14, 67) # Dark blue-black + + return ensure_contrast(fg, background, min_ratio) diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/hct.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/hct.py new file mode 100644 index 0000000..2500e36 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/hct.py @@ -0,0 +1,1071 @@ +""" +HCT (Hue, Chroma, Tone) Color Space Implementation. + +Based on Material Color Utilities (Google). +HCT combines CAM16 hue and chroma with CIELAB lightness (L*) for +Material Design 3's perceptual color space. +""" + +from __future__ import annotations +import math + +# ============================================================================= +# Type Definitions +# ============================================================================= + +RGB = tuple[int, int, int] + +# ============================================================================= +# CAM16 / HCT Color Space Implementation +# ============================================================================= + +# sRGB to XYZ matrix (D65 illuminant) +SRGB_TO_XYZ = [ + [0.41233895, 0.35762064, 0.18051042], + [0.2126, 0.7152, 0.0722], + [0.01932141, 0.11916382, 0.95034478], +] + +# XYZ to sRGB matrix +XYZ_TO_SRGB = [ + [3.2413774792388685, -1.5376652402851851, -0.49885366846268053], + [-0.9691452513005321, 1.8758853451067872, 0.04156585616912061], + [0.05562093689691305, -0.20395524564742123, 1.0571799111220335], +] + + +class ViewingConditions: + """CAM16 viewing conditions for sRGB display.""" + # White point (D65) + WHITE_POINT_D65 = [95.047, 100.0, 108.883] + + # Precomputed values for standard conditions + n = 0.18418651851244416 + aw = 29.980997194447333 + nbb = 1.0169191804458755 + ncb = 1.0169191804458755 + c = 0.69 + nc = 1.0 + fl = 0.3884814537800353 + fl_root = 0.7894826179304937 + z = 1.909169568483652 + + # RGB to CAM16 adaptation matrix + RGB_D = [1.0211931250282205, 0.9862992588498498, 0.9338046048498166] + + +def _linearize(channel: int) -> float: + """Convert sRGB channel (0-255) to linear RGB (0-1).""" + normalized = channel / 255.0 + if normalized <= 0.040449936: + return normalized / 12.92 + return math.pow((normalized + 0.055) / 1.055, 2.4) + + +def _delinearize(linear: float) -> int: + """Convert linear RGB (0-1) to sRGB channel (0-255).""" + if linear <= 0.0031308: + normalized = linear * 12.92 + else: + normalized = 1.055 * math.pow(linear, 1.0 / 2.4) - 0.055 + return max(0, min(255, round(normalized * 255))) + + +def _matrix_multiply(matrix: list[list[float]], vector: list[float]) -> list[float]: + """Multiply 3x3 matrix by 3-element vector.""" + return [ + matrix[0][0] * vector[0] + matrix[0][1] * vector[1] + matrix[0][2] * vector[2], + matrix[1][0] * vector[0] + matrix[1][1] * vector[1] + matrix[1][2] * vector[2], + matrix[2][0] * vector[0] + matrix[2][1] * vector[1] + matrix[2][2] * vector[2], + ] + + +def _signum(x: float) -> float: + """Return sign of x: -1, 0, or 1.""" + if x < 0: + return -1.0 + elif x > 0: + return 1.0 + return 0.0 + + +def _lerp(a: float, b: float, t: float) -> float: + """Linear interpolation between a and b.""" + return a + (b - a) * t + + +def _sanitize_degrees(degrees: float) -> float: + """Ensure degrees is in [0, 360) range.""" + degrees = degrees % 360.0 + if degrees < 0: + degrees += 360.0 + return degrees + + +# ============================================================================= +# HCT Solver - Ported from Material Color Utilities +# ============================================================================= + +# Matrices for chromatic adaptation +_SCALED_DISCOUNT_FROM_LINRGB = [ + [0.001200833568784504, 0.002389694492170889, 0.0002795742885861124], + [0.0005891086651375999, 0.0029785502573438758, 0.0003270666104008398], + [0.00010146692491640572, 0.0005364214359186694, 0.0032979401770712076], +] + +_LINRGB_FROM_SCALED_DISCOUNT = [ + [1373.2198709594231, -1100.4251190754821, -7.278681089101213], + [-271.815969077903, 559.6580465940733, -32.46047482791194], + [1.9622899599665666, -57.173814538844006, 308.7233197812385], +] + +_Y_FROM_LINRGB = [0.2126, 0.7152, 0.0722] + +# Critical planes for bisection (precomputed delinearized values 0-254) +_CRITICAL_PLANES = [ + 0.015176349177441876, 0.045529047532325624, 0.07588174588720938, + 0.10623444424209313, 0.13658714259697685, 0.16693984095186062, + 0.19729253930674434, 0.2276452376616281, 0.2579979360165119, + 0.28835063437139563, 0.3188300904430532, 0.350925934958123, + 0.3848314933096426, 0.42057480301049466, 0.458183274052838, + 0.4976837250274023, 0.5391024159806381, 0.5824650784040898, + 0.6277969426914107, 0.6751227633498623, 0.7244668422128921, + 0.775853049866786, 0.829304845476233, 0.8848452951698498, + 0.942497089126609, 1.0022825574869039, 1.0642236851973577, + 1.1283421258858297, 1.1946592148522128, 1.2631959812511864, + 1.3339731595349034, 1.407011200216447, 1.4823302800086415, + 1.5599503113873272, 1.6398909516233677, 1.7221716113234105, + 1.8068114625156377, 1.8938294463134073, 1.9832442801866852, + 2.075074464868551, 2.1693382909216234, 2.2660538449872063, + 2.36523901573795, 2.4669114995532007, 2.5710888059345764, + 2.6777882626779785, 2.7870270208169257, 2.898822059350997, + 3.0131901897720907, 3.1301480604002863, 3.2497121605402226, + 3.3718988244681087, 3.4967242352587946, 3.624204428461639, + 3.754355295633311, 3.887192587735158, 4.022731918402185, + 4.160988767090289, 4.301978482107941, 4.445716283538092, + 4.592217266055746, 4.741496401646282, 4.893568542229298, + 5.048448422192488, 5.20615066083972, 5.3666897647573375, + 5.5300801301023865, 5.696336044816294, 5.865471690767354, + 6.037501145825082, 6.212438385869475, 6.390297286737924, + 6.571091626112461, 6.7548350853498045, 6.941541251256611, + 7.131223617812143, 7.323895587840543, 7.5195704746346665, + 7.7182615035334345, 7.919981813454504, 8.124744458384042, + 8.332562408825165, 8.543448553206703, 8.757415699253682, + 8.974476575321063, 9.194643831691977, 9.417930041841839, + 9.644347703669503, 9.873909240696694, 10.106627003236781, + 10.342513269534024, 10.58158024687427, 10.8238400726681, + 11.069304815507364, 11.317986476196008, 11.569896988756009, + 11.825048221409341, 12.083451977536606, 12.345119996613247, + 12.610063955123938, 12.878295467455942, 13.149826086772048, + 13.42466730586372, 13.702830557985108, 13.984327217668513, + 14.269168601521828, 14.55736596900856, 14.848930523210871, + 15.143873411576273, 15.44220572664832, 15.743938506781891, + 16.04908273684337, 16.35764934889634, 16.66964922287304, + 16.985093187232053, 17.30399201960269, 17.62635644741625, + 17.95219714852476, 18.281524751807332, 18.614349837764564, + 18.95068293910138, 19.290534541298456, 19.633915083172692, + 19.98083495742689, 20.331304511189067, 20.685334046541502, + 21.042933821039977, 21.404114048223256, 21.76888489811322, + 22.137256497705877, 22.50923893145328, 22.884842241736916, + 23.264076429332462, 23.6469514538663, 24.033477234264016, + 24.42366364919083, 24.817520537484558, 25.21505769858089, + 25.61628489293138, 26.021211842414342, 26.429848230738664, + 26.842203703840827, 27.258287870275353, 27.678110301598522, + 28.10168053274597, 28.529008062403893, 28.96010235337422, + 29.39497283293396, 29.83362889318845, 30.276079891419332, + 30.722335150426627, 31.172403958865512, 31.62629557157785, + 32.08401920991837, 32.54558406207592, 33.010999283389665, + 33.4802739966603, 33.953417292456834, 34.430438229418264, + 34.911345834551085, 35.39614910352207, 35.88485700094671, + 36.37747846067349, 36.87402238606382, 37.37449765026789, + 37.87891309649659, 38.38727753828926, 38.89959975977785, + 39.41588851594697, 39.93615253289054, 40.460400508064545, + 40.98864111053629, 41.520882981230194, 42.05713473317016, + 42.597404951718396, 43.141702194811224, 43.6900349931913, + 44.24241185063697, 44.798841244188324, 45.35933162437017, + 45.92389141541209, 46.49252901546552, 47.065252796817916, + 47.64207110610409, 48.22299226451468, 48.808024568002054, + 49.3971762874833, 49.9904556690408, 50.587870934119984, + 51.189430279724725, 51.79514187861014, 52.40501387947288, + 53.0190544071392, 53.637271562750364, 54.259673423945976, + 54.88626804504493, 55.517063457223934, 56.15206766869424, + 56.79128866487574, 57.43473440856916, 58.08241284012621, + 58.734331877617365, 59.39049941699807, 60.05092333227251, + 60.715611475655585, 61.38457167773311, 62.057811747619894, + 62.7353394731159, 63.417162620860914, 64.10328893648692, + 64.79372614476921, 65.48848194977529, 66.18756403501224, + 66.89098006357258, 67.59873767827808, 68.31084450182222, + 69.02730813691093, 69.74813616640164, 70.47333615344107, + 71.20291564160104, 71.93688215501312, 72.67524319850172, + 73.41800625771542, 74.16517879925733, 74.9167682708136, + 75.67278210128072, 76.43322770089146, 77.1981124613393, + 77.96744375590167, 78.74122893956174, 79.51947534912904, + 80.30219030335869, 81.08938110306934, 81.88105503125999, + 82.67721935322541, 83.4778813166706, 84.28304815182372, + 85.09272707154808, 85.90692527145302, 86.72564993000343, + 87.54890820862819, 88.3767072518277, 89.2090541872801, + 90.04595612594655, 90.88742016217518, 91.73345337380438, + 92.58406282226491, 93.43925555268066, 94.29903859396902, + 95.16341895893969, 96.03240364439274, 96.9059996312159, + 97.78421388448044, 98.6670533535366, 99.55452497210776, +] + + +class HctSolver: + """ + Solves HCT to RGB conversion with proper gamut mapping. + + Ported from Material Color Utilities (Rust/TypeScript). + When the requested chroma is out of gamut, this solver finds + the maximum achievable chroma while preserving the exact hue. + """ + + @staticmethod + def _sanitize_radians(angle: float) -> float: + """Ensure angle is in [0, 2π) range.""" + return (angle + math.pi * 8) % (math.pi * 2) + + @staticmethod + def _true_delinearized(rgb_component: float) -> float: + """Delinearize RGB component (0-100) to (0-255).""" + normalized = rgb_component / 100.0 + if normalized <= 0.0031308: + delinearized = normalized * 12.92 + else: + delinearized = 1.055 * (normalized ** (1.0 / 2.4)) - 0.055 + return delinearized * 255.0 + + @staticmethod + def _chromatic_adaptation(component: float) -> float: + """Apply chromatic adaptation.""" + af = abs(component) ** 0.42 + return _signum(component) * 400.0 * af / (af + 27.13) + + @staticmethod + def _hue_of(linrgb: list[float]) -> float: + """Calculate hue of linear RGB color in radians.""" + scaled_discount = _matrix_multiply(_SCALED_DISCOUNT_FROM_LINRGB, linrgb) + + r_a = HctSolver._chromatic_adaptation(scaled_discount[0]) + g_a = HctSolver._chromatic_adaptation(scaled_discount[1]) + b_a = HctSolver._chromatic_adaptation(scaled_discount[2]) + + # redness-greenness + a = (11.0 * r_a - 12.0 * g_a + b_a) / 11.0 + # yellowness-blueness + b = (r_a + g_a - 2.0 * b_a) / 9.0 + + return math.atan2(b, a) + + @staticmethod + def _are_in_cyclic_order(a: float, b: float, c: float) -> bool: + """Check if a, b, c are in cyclic order.""" + delta_ab = HctSolver._sanitize_radians(b - a) + delta_ac = HctSolver._sanitize_radians(c - a) + return delta_ab < delta_ac + + @staticmethod + def _intercept(source: float, mid: float, target: float) -> float: + """Solve lerp equation: find t such that lerp(source, target, t) = mid.""" + return (mid - source) / (target - source) + + @staticmethod + def _lerp_point(source: list[float], t: float, target: list[float]) -> list[float]: + """Linear interpolation between two 3D points.""" + return [ + source[0] + (target[0] - source[0]) * t, + source[1] + (target[1] - source[1]) * t, + source[2] + (target[2] - source[2]) * t, + ] + + @staticmethod + def _set_coordinate(source: list[float], coordinate: float, + target: list[float], axis: int) -> list[float]: + """Find point on segment where axis equals coordinate.""" + t = HctSolver._intercept(source[axis], coordinate, target[axis]) + return HctSolver._lerp_point(source, t, target) + + @staticmethod + def _is_bounded(x: float) -> bool: + """Check if x is in [0, 100].""" + return 0.0 <= x <= 100.0 + + @staticmethod + def _nth_vertex(y: float, n: int) -> list[float]: + """ + Get nth vertex of RGB cube intersection with Y plane. + + Returns [-1, -1, -1] if vertex is outside cube. + """ + k_r, k_g, k_b = _Y_FROM_LINRGB + + coord_a = 0.0 if n % 4 <= 1 else 100.0 + coord_b = 0.0 if n % 2 == 0 else 100.0 + + if n < 4: + g = coord_a + b = coord_b + r = (y - k_g * g - k_b * b) / k_r + if HctSolver._is_bounded(r): + return [r, g, b] + return [-1.0, -1.0, -1.0] + elif n < 8: + b = coord_a + r = coord_b + g = (y - k_r * r - k_b * b) / k_g + if HctSolver._is_bounded(g): + return [r, g, b] + return [-1.0, -1.0, -1.0] + else: + r = coord_a + g = coord_b + b = (y - k_r * r - k_g * g) / k_b + if HctSolver._is_bounded(b): + return [r, g, b] + return [-1.0, -1.0, -1.0] + + @staticmethod + def _bisect_to_segment(y: float, target_hue: float) -> list[list[float]]: + """Find segment on RGB cube containing target hue.""" + left = [-1.0, -1.0, -1.0] + right = [-1.0, -1.0, -1.0] + left_hue = 0.0 + right_hue = 0.0 + initialized = False + uncut = True + + for n in range(12): + mid = HctSolver._nth_vertex(y, n) + + if mid[0] < 0: + continue + + mid_hue = HctSolver._hue_of(mid) + + if not initialized: + left = mid + right = mid + left_hue = mid_hue + right_hue = mid_hue + initialized = True + continue + + if uncut or HctSolver._are_in_cyclic_order(left_hue, mid_hue, right_hue): + uncut = False + + if HctSolver._are_in_cyclic_order(left_hue, target_hue, mid_hue): + right = mid + right_hue = mid_hue + else: + left = mid + left_hue = mid_hue + + return [left, right] + + @staticmethod + def _mid_point(a: list[float], b: list[float]) -> list[float]: + """Calculate midpoint of two 3D points.""" + return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2] + + @staticmethod + def _critical_plane_below(x: float) -> int: + """Get critical plane index below x.""" + return int(math.floor(x - 0.5)) + + @staticmethod + def _critical_plane_above(x: float) -> int: + """Get critical plane index above x.""" + return int(math.ceil(x - 0.5)) + + @staticmethod + def _bisect_to_limit(y: float, target_hue: float) -> list[float]: + """ + Find color on RGB cube boundary with exact target hue. + + This is the key function for hue-preserving gamut mapping. + """ + segment = HctSolver._bisect_to_segment(y, target_hue) + left = segment[0] + left_hue = HctSolver._hue_of(left) + right = segment[1] + + for axis in range(3): + if abs(left[axis] - right[axis]) > 1e-10: + if left[axis] < right[axis]: + l_plane = HctSolver._critical_plane_below( + HctSolver._true_delinearized(left[axis])) + r_plane = HctSolver._critical_plane_above( + HctSolver._true_delinearized(right[axis])) + else: + l_plane = HctSolver._critical_plane_above( + HctSolver._true_delinearized(left[axis])) + r_plane = HctSolver._critical_plane_below( + HctSolver._true_delinearized(right[axis])) + + for _ in range(8): + if abs(r_plane - l_plane) <= 1: + break + + m_plane = int((l_plane + r_plane) / 2) + # Clamp to valid index range + m_plane = max(0, min(len(_CRITICAL_PLANES) - 1, m_plane)) + mid_plane_coordinate = _CRITICAL_PLANES[m_plane] + mid = HctSolver._set_coordinate(left, mid_plane_coordinate, right, axis) + mid_hue = HctSolver._hue_of(mid) + + if HctSolver._are_in_cyclic_order(left_hue, target_hue, mid_hue): + right = mid + r_plane = m_plane + else: + left = mid + left_hue = mid_hue + l_plane = m_plane + + return HctSolver._mid_point(left, right) + + @staticmethod + def _inverse_chromatic_adaptation(adapted: float) -> float: + """Inverse of chromatic adaptation.""" + adapted_abs = abs(adapted) + base = max(0.0, 27.13 * adapted_abs / (400.0 - adapted_abs)) + return _signum(adapted) * (base ** (1.0 / 0.42)) + + @staticmethod + def _find_result_by_j(hue_radians: float, chroma: float, y: float) -> tuple[int, int, int] | None: + """ + Try to find exact color with given hue, chroma, and Y. + + Returns None if out of gamut. + """ + j = math.sqrt(y) * 11.0 + + t_inner_coeff = 1.0 / ((1.64 - (0.29 ** ViewingConditions.n)) ** 0.73) + e_hue = 0.25 * (math.cos(hue_radians + 2.0) + 3.8) + p1 = e_hue * (50000.0 / 13.0) * ViewingConditions.nc * ViewingConditions.ncb + h_sin = math.sin(hue_radians) + h_cos = math.cos(hue_radians) + + for iteration in range(5): + j_normalized = j / 100.0 + if chroma == 0 or j == 0: + alpha = 0.0 + else: + alpha = chroma / math.sqrt(j_normalized) + + t = (alpha * t_inner_coeff) ** (1.0 / 0.9) + ac = ViewingConditions.aw * (j_normalized ** (1.0 / ViewingConditions.c / ViewingConditions.z)) + p2 = ac / ViewingConditions.nbb + gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11.0 * t * h_cos + 108.0 * t * h_sin) + a = gamma * h_cos + b = gamma * h_sin + + r_a = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0 + g_a = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0 + b_a = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0 + + r_cscaled = HctSolver._inverse_chromatic_adaptation(r_a) + g_cscaled = HctSolver._inverse_chromatic_adaptation(g_a) + b_cscaled = HctSolver._inverse_chromatic_adaptation(b_a) + + linrgb = _matrix_multiply(_LINRGB_FROM_SCALED_DISCOUNT, + [r_cscaled, g_cscaled, b_cscaled]) + + # Check if in gamut + if linrgb[0] < 0 or linrgb[1] < 0 or linrgb[2] < 0: + return None + + k_r, k_g, k_b = _Y_FROM_LINRGB + fnj = k_r * linrgb[0] + k_g * linrgb[1] + k_b * linrgb[2] + + if fnj <= 0: + return None + + if iteration == 4 or abs(fnj - y) < 0.002: + if linrgb[0] > 100.01 or linrgb[1] > 100.01 or linrgb[2] > 100.01: + return None + + # Convert linear RGB to sRGB + return ( + _delinearize(linrgb[0] / 100.0), + _delinearize(linrgb[1] / 100.0), + _delinearize(linrgb[2] / 100.0), + ) + + # Newton iteration + j = j - (fnj - y) * j / (2.0 * fnj) + + return None + + @staticmethod + def solve_to_rgb(hue_degrees: float, chroma: float, tone: float) -> tuple[int, int, int]: + """ + Solve HCT to RGB with proper gamut mapping. + + If the exact color is out of gamut, finds the maximum achievable + chroma while preserving the exact hue. + """ + if chroma < 0.0001 or tone < 0.0001 or tone > 99.9999: + # Achromatic - just convert tone to gray + y = lstar_to_y(tone) + gray = _delinearize(y / 100.0) + return (gray, gray, gray) + + hue_degrees = _sanitize_degrees(hue_degrees) + hue_radians = math.radians(hue_degrees) + # Y is in 0-100 range (same scale as internal linear RGB in the solver) + y = lstar_to_y(tone) + + # Try to find exact solution + exact = HctSolver._find_result_by_j(hue_radians, chroma, y) + if exact is not None: + return exact + + # Fall back to bisection - find max chroma that preserves hue + linrgb = HctSolver._bisect_to_limit(y, hue_radians) + + return ( + _delinearize(linrgb[0] / 100.0), + _delinearize(linrgb[1] / 100.0), + _delinearize(linrgb[2] / 100.0), + ) + + +def rgb_to_xyz(r: int, g: int, b: int) -> tuple[float, float, float]: + """Convert sRGB to CIE XYZ.""" + linear_r = _linearize(r) + linear_g = _linearize(g) + linear_b = _linearize(b) + xyz = _matrix_multiply(SRGB_TO_XYZ, [linear_r, linear_g, linear_b]) + return (xyz[0] * 100, xyz[1] * 100, xyz[2] * 100) + + +def xyz_to_rgb(x: float, y: float, z: float) -> tuple[int, int, int]: + """Convert CIE XYZ to sRGB.""" + linear = _matrix_multiply(XYZ_TO_SRGB, [x / 100, y / 100, z / 100]) + return (_delinearize(linear[0]), _delinearize(linear[1]), _delinearize(linear[2])) + + +def y_to_lstar(y: float) -> float: + """Convert XYZ Y component to L* (CIELAB lightness / HCT Tone).""" + if y <= 0: + return 0.0 + y_normalized = y / 100.0 + if y_normalized <= 0.008856: + return 903.2962962962963 * y_normalized + return 116.0 * math.pow(y_normalized, 1.0 / 3.0) - 16.0 + + +def lstar_to_y(lstar: float) -> float: + """Convert L* (Tone) to XYZ Y component.""" + if lstar <= 0: + return 0.0 + if lstar > 100: + lstar = 100.0 + if lstar <= 8.0: + return lstar / 903.2962962962963 * 100.0 + fy = (lstar + 16.0) / 116.0 + return fy * fy * fy * 100.0 + + +def argb_to_int(r: int, g: int, b: int) -> int: + """Convert RGB to ARGB integer (alpha = 255).""" + return (255 << 24) | (r << 16) | (g << 8) | b + + +def int_to_rgb(argb: int) -> tuple[int, int, int]: + """Convert ARGB integer to RGB tuple.""" + return ((argb >> 16) & 0xFF, (argb >> 8) & 0xFF, argb & 0xFF) + + +class Cam16: + """CAM16 color appearance model representation.""" + + def __init__(self, hue: float, chroma: float, j: float, q: float, + m: float, s: float, jstar: float, astar: float, bstar: float): + self.hue = hue + self.chroma = chroma + self.j = j # Lightness + self.q = q # Brightness + self.m = m # Colorfulness + self.s = s # Saturation + self.jstar = jstar # CAM16-UCS J* + self.astar = astar # CAM16-UCS a* + self.bstar = bstar # CAM16-UCS b* + + @classmethod + def from_rgb(cls, r: int, g: int, b: int) -> 'Cam16': + """Create CAM16 from sRGB values.""" + x, y, z = rgb_to_xyz(r, g, b) + + r_c = 0.401288 * x + 0.650173 * y - 0.051461 * z + g_c = -0.250268 * x + 1.204414 * y + 0.045854 * z + b_c = -0.002079 * x + 0.048952 * y + 0.953127 * z + + r_d = ViewingConditions.RGB_D[0] * r_c + g_d = ViewingConditions.RGB_D[1] * g_c + b_d = ViewingConditions.RGB_D[2] * b_c + + r_af = math.pow(ViewingConditions.fl * abs(r_d) / 100.0, 0.42) + g_af = math.pow(ViewingConditions.fl * abs(g_d) / 100.0, 0.42) + b_af = math.pow(ViewingConditions.fl * abs(b_d) / 100.0, 0.42) + + r_a = _signum(r_d) * 400.0 * r_af / (r_af + 27.13) + g_a = _signum(g_d) * 400.0 * g_af / (g_af + 27.13) + b_a = _signum(b_d) * 400.0 * b_af / (b_af + 27.13) + + a = (11.0 * r_a + -12.0 * g_a + b_a) / 11.0 + b = (r_a + g_a - 2.0 * b_a) / 9.0 + + hue_radians = math.atan2(b, a) + hue = math.degrees(hue_radians) + if hue < 0: + hue += 360.0 + + u = (20.0 * r_a + 20.0 * g_a + 21.0 * b_a) / 20.0 + p2 = (40.0 * r_a + 20.0 * g_a + b_a) / 20.0 + ac = p2 * ViewingConditions.nbb + + j = 100.0 * math.pow(ac / ViewingConditions.aw, ViewingConditions.c * ViewingConditions.z) + q = (4.0 / ViewingConditions.c) * math.sqrt(j / 100.0) * (ViewingConditions.aw + 4.0) * ViewingConditions.fl_root + + hue_prime = hue + 360.0 if hue < 20.14 else hue + e_hue = 0.25 * (math.cos(math.radians(hue_prime) + 2.0) + 3.8) + + t = 50000.0 / 13.0 * ViewingConditions.nc * ViewingConditions.ncb * e_hue * math.sqrt(a * a + b * b) / (u + 0.305) + alpha = math.pow(t, 0.9) * math.pow(1.64 - math.pow(0.29, ViewingConditions.n), 0.73) + chroma = alpha * math.sqrt(j / 100.0) + + m = chroma * ViewingConditions.fl_root + s = 50.0 * math.sqrt((ViewingConditions.c * alpha) / (ViewingConditions.aw + 4.0)) + + jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j) + mstar = 1.0 / 0.0228 * math.log(1.0 + 0.0228 * m) if m > 0 else 0 + astar = mstar * math.cos(hue_radians) + bstar = mstar * math.sin(hue_radians) + + return cls(hue, chroma, j, q, m, s, jstar, astar, bstar) + + @classmethod + def from_jch(cls, j: float, chroma: float, hue: float) -> 'Cam16': + """Create CAM16 from J (lightness), chroma, and hue.""" + q = (4.0 / ViewingConditions.c) * math.sqrt(j / 100.0) * (ViewingConditions.aw + 4.0) * ViewingConditions.fl_root + m = chroma * ViewingConditions.fl_root + alpha = chroma / math.sqrt(j / 100.0) if j > 0 else 0 + s = 50.0 * math.sqrt((ViewingConditions.c * alpha) / (ViewingConditions.aw + 4.0)) + + hue_radians = math.radians(hue) + jstar = (1.0 + 100.0 * 0.007) * j / (1.0 + 0.007 * j) + mstar = 1.0 / 0.0228 * math.log(1.0 + 0.0228 * m) if m > 0 else 0 + astar = mstar * math.cos(hue_radians) + bstar = mstar * math.sin(hue_radians) + + return cls(hue, chroma, j, q, m, s, jstar, astar, bstar) + + def to_rgb(self) -> tuple[int, int, int]: + """Convert CAM16 back to sRGB.""" + if self.chroma == 0 or self.j == 0: + y = lstar_to_y(self.j) + return xyz_to_rgb(y, y, y) + + hue_radians = math.radians(self.hue) + + alpha = self.chroma / math.sqrt(self.j / 100.0) if self.j > 0 else 0 + t = math.pow(alpha / math.pow(1.64 - math.pow(0.29, ViewingConditions.n), 0.73), 1.0 / 0.9) + + hue_prime = self.hue + 360.0 if self.hue < 20.14 else self.hue + e_hue = 0.25 * (math.cos(math.radians(hue_prime) + 2.0) + 3.8) + + ac = ViewingConditions.aw * math.pow(self.j / 100.0, 1.0 / (ViewingConditions.c * ViewingConditions.z)) + p1 = 50000.0 / 13.0 * ViewingConditions.nc * ViewingConditions.ncb * e_hue + p2 = ac / ViewingConditions.nbb + + gamma = 23.0 * (p2 + 0.305) * t / (23.0 * p1 + 11.0 * t * math.cos(hue_radians) + 108.0 * t * math.sin(hue_radians)) + + a = gamma * math.cos(hue_radians) + b = gamma * math.sin(hue_radians) + + r_a = (460.0 * p2 + 451.0 * a + 288.0 * b) / 1403.0 + g_a = (460.0 * p2 - 891.0 * a - 261.0 * b) / 1403.0 + b_a = (460.0 * p2 - 220.0 * a - 6300.0 * b) / 1403.0 + + def reverse_adapt(adapted: float) -> float: + abs_adapted = abs(adapted) + base = max(0, 27.13 * abs_adapted / (400.0 - abs_adapted)) + return _signum(adapted) * 100.0 / ViewingConditions.fl * math.pow(base, 1.0 / 0.42) + + r_c = reverse_adapt(r_a) / ViewingConditions.RGB_D[0] + g_c = reverse_adapt(g_a) / ViewingConditions.RGB_D[1] + b_c = reverse_adapt(b_a) / ViewingConditions.RGB_D[2] + + x = 1.8620678 * r_c - 1.0112547 * g_c + 0.1491867 * b_c + y = 0.3875265 * r_c + 0.6214474 * g_c - 0.0089739 * b_c + z = -0.0158415 * r_c - 0.0344156 * g_c + 1.0502571 * b_c + + return xyz_to_rgb(x, y, z) + + +class Hct: + """ + HCT (Hue, Chroma, Tone) color representation. + + Material Design 3's perceptual color space combining: + - Hue: CAM16 hue (0-360) + - Chroma: CAM16 chroma (colorfulness, typically 0-120+) + - Tone: CIELAB L* lightness (0-100) + """ + + def __init__(self, hue: float, chroma: float, tone: float): + self._hue = hue % 360.0 + self._chroma = max(0.0, chroma) + self._tone = max(0.0, min(100.0, tone)) + self._argb: int | None = None + + @property + def hue(self) -> float: + return self._hue + + @property + def chroma(self) -> float: + return self._chroma + + @property + def tone(self) -> float: + return self._tone + + @classmethod + def from_rgb(cls, r: int, g: int, b: int) -> 'Hct': + """Create HCT from sRGB values.""" + cam = Cam16.from_rgb(r, g, b) + _, y, _ = rgb_to_xyz(r, g, b) + tone = y_to_lstar(y) + return cls(cam.hue, cam.chroma, tone) + + @classmethod + def from_argb(cls, argb: int) -> 'Hct': + """Create HCT from ARGB integer.""" + r, g, b = int_to_rgb(argb) + return cls.from_rgb(r, g, b) + + def to_rgb(self) -> tuple[int, int, int]: + """Convert HCT to sRGB, solving for the color.""" + return self._solve_to_rgb(self._hue, self._chroma, self._tone) + + def to_argb(self) -> int: + """Convert HCT to ARGB integer.""" + if self._argb is None: + r, g, b = self.to_rgb() + self._argb = argb_to_int(r, g, b) + return self._argb + + def to_hex(self) -> str: + """Convert HCT to hex string.""" + r, g, b = self.to_rgb() + return f"#{r:02x}{g:02x}{b:02x}" + + @staticmethod + def _solve_to_rgb(hue: float, chroma: float, tone: float) -> tuple[int, int, int]: + """ + Solve for RGB given HCT values using the Material HctSolver. + + This uses proper gamut mapping that preserves hue exactly. + When the requested chroma is out of gamut, it finds the maximum + achievable chroma while maintaining the exact target hue. + """ + return HctSolver.solve_to_rgb(hue, chroma, tone) + + def set_hue(self, hue: float) -> 'Hct': + """Return new HCT with different hue.""" + return Hct(hue, self._chroma, self._tone) + + def set_chroma(self, chroma: float) -> 'Hct': + """Return new HCT with different chroma.""" + return Hct(self._hue, chroma, self._tone) + + def set_tone(self, tone: float) -> 'Hct': + """Return new HCT with different tone.""" + return Hct(self._hue, self._chroma, tone) + + +class TemperatureCache: + """ + Color temperature analysis for finding harmonious colors. + + Based on Material Color Utilities - calculates relative warmth of colors + and finds analogous colors based on temperature similarity. + """ + + def __init__(self, input_hct: Hct): + self.input = input_hct + self._hcts_by_temp: list[Hct] | None = None + self._hcts_by_hue: list[Hct] | None = None + self._temps_by_hct: dict[tuple[float, float, float], float] | None = None + self._input_relative_temp: float | None = None + self._complement: Hct | None = None + + @staticmethod + def raw_temperature(hct: Hct) -> float: + """ + Calculate raw temperature of a color using Ou-Woodcock-Wright algorithm. + + Based on material-colors Rust implementation. + Uses LAB a* and b* to determine warm-cool factor. + Values below 0 are cool, above 0 are warm. + """ + # Convert HCT to RGB then to LAB + rgb = hct.to_rgb() + x, y, z = rgb_to_xyz(rgb[0], rgb[1], rgb[2]) + + # XYZ to LAB + def f(t: float) -> float: + delta = 6.0 / 29.0 + if t > delta ** 3: + return t ** (1.0 / 3.0) + return t / (3 * delta * delta) + 4.0 / 29.0 + + xn, yn, zn = 95.047, 100.0, 108.883 # D65 reference + lab_a = 500.0 * (f(x / xn) - f(y / yn)) + lab_b = 200.0 * (f(y / yn) - f(z / zn)) + + # Calculate LAB hue and chroma + lab_hue = math.degrees(math.atan2(lab_b, lab_a)) + if lab_hue < 0: + lab_hue += 360.0 + lab_chroma = math.hypot(lab_a, lab_b) + + # Ou-Woodcock-Wright formula for temperature + # temp = -0.5 + 0.02 * chroma^1.07 * cos(toRadians(hue - 50)) + hue_rad = math.radians((lab_hue - 50.0) % 360.0) + return -0.5 + 0.02 * (lab_chroma ** 1.07) * math.cos(hue_rad) + + def _get_hcts_by_hue(self) -> list[Hct]: + """Generate HCT colors at regular hue intervals.""" + if self._hcts_by_hue is not None: + return self._hcts_by_hue + + hcts = [] + for hue in range(360): + color_at_hue = Hct(float(hue), self.input.chroma, self.input.tone) + hcts.append(color_at_hue) + + self._hcts_by_hue = hcts + return hcts + + def _get_temps_by_hct(self) -> dict[tuple[float, float, float], float]: + """Cache temperatures for all hue variants.""" + if self._temps_by_hct is not None: + return self._temps_by_hct + + hcts = self._get_hcts_by_hue() + temps = {} + for hct in hcts: + key = (hct.hue, hct.chroma, hct.tone) + temps[key] = self.raw_temperature(hct) + + self._temps_by_hct = temps + return temps + + def _get_hcts_by_temp(self) -> list[Hct]: + """Get HCT colors sorted by temperature.""" + if self._hcts_by_temp is not None: + return self._hcts_by_temp + + hcts = list(self._get_hcts_by_hue()) + temps = self._get_temps_by_hct() + hcts.sort(key=lambda h: temps[(h.hue, h.chroma, h.tone)]) + + self._hcts_by_temp = hcts + return hcts + + def _relative_temperature(self, hct: Hct) -> float: + """ + Calculate relative temperature (0-1) based on position in temperature-sorted list. + """ + temps = self._get_temps_by_hct() + hcts_by_temp = self._get_hcts_by_temp() + + key = (hct.hue, hct.chroma, hct.tone) + if key in temps: + raw = temps[key] + else: + raw = self.raw_temperature(hct) + + # Find position in sorted list + coldest = self.raw_temperature(hcts_by_temp[0]) + warmest = self.raw_temperature(hcts_by_temp[-1]) + + if warmest == coldest: + return 0.5 + + return (raw - coldest) / (warmest - coldest) + + def _input_relative_temperature_value(self) -> float: + """Get relative temperature of the input color.""" + if self._input_relative_temp is None: + self._input_relative_temp = self._relative_temperature(self.input) + return self._input_relative_temp + + def complement(self) -> Hct: + """ + Find the complement: color with opposite temperature. + """ + if self._complement is not None: + return self._complement + + input_temp = self._input_relative_temperature_value() + hcts_by_temp = self._get_hcts_by_temp() + temps = self._get_temps_by_hct() + + # Target is opposite temperature + target_temp = 1.0 - input_temp + + # Find closest match + best_hct = hcts_by_temp[0] + best_diff = float('inf') + + for hct in hcts_by_temp: + key = (hct.hue, hct.chroma, hct.tone) + raw = temps.get(key, self.raw_temperature(hct)) + rel = self._relative_temperature(hct) + diff = abs(rel - target_temp) + if diff < best_diff: + best_diff = diff + best_hct = hct + + self._complement = best_hct + return best_hct + + def analogous(self, count: int | None = None, divisions: int | None = None) -> list[Hct]: + """ + Find analogous colors based on temperature. + + Uses material-colors algorithm: + 1. Build a list of all `divisions` colors at equal temperature steps + 2. Pick `count` colors from this list, centered around the input + + Args: + count: Number of colors to return (default 5) + divisions: How many divisions of the temperature range (default 12) + + Returns: + List of HCT colors including the input, spread by temperature. + """ + if count is None: + count = 5 + if divisions is None: + divisions = 12 + + hcts_by_hue = self._get_hcts_by_hue() + start_hue = round(self.input.hue) % 360 + start_hct = hcts_by_hue[start_hue] + + # Calculate total absolute temperature delta around the color wheel + last_temp = self._relative_temperature(start_hct) + absolute_total_temp_delta = 0.0 + + for i in range(360): + hue = (start_hue + i) % 360 + hct = hcts_by_hue[hue] + temp = self._relative_temperature(hct) + temp_delta = abs(temp - last_temp) + last_temp = temp + absolute_total_temp_delta += temp_delta + + # Build list of all colors at equal temperature steps + temp_step = absolute_total_temp_delta / divisions + all_colors: list[Hct] = [start_hct] + total_temp_delta = 0.0 + last_temp = self._relative_temperature(start_hct) + hue_addend = 1 + + while len(all_colors) < divisions and hue_addend <= 360: + hue = (start_hue + hue_addend) % 360 + hct = hcts_by_hue[hue] + temp = self._relative_temperature(hct) + temp_delta = abs(temp - last_temp) + total_temp_delta += temp_delta + + desired_total = len(all_colors) * temp_step + + # Add this hue until its temperature is insufficient + while total_temp_delta >= desired_total and len(all_colors) < divisions: + all_colors.append(hct) + desired_total = (len(all_colors) + 1) * temp_step + + last_temp = temp + hue_addend += 1 + + # Fill remaining slots if needed + while len(all_colors) < divisions: + all_colors.append(all_colors[-1] if all_colors else start_hct) + + # Build final answer list centered around input + answers: list[Hct] = [self.input] + + # Counter-clockwise (negative indices) + increase_hue_count = int((count - 1) // 2) + for i in range(1, increase_hue_count + 1): + index = (-i) % len(all_colors) + answers.insert(0, all_colors[index]) + + # Clockwise (positive indices) + decrease_hue_count = count - increase_hue_count - 1 + for i in range(1, decrease_hue_count + 1): + index = i % len(all_colors) + answers.append(all_colors[index]) + + return answers + + +def fix_if_disliked(hct: Hct) -> Hct: + """ + Fix colors in the "disliked" hue range (yellow-green). + + These colors often look muddy or unpleasant. If detected, + shift the hue slightly to improve appearance. + """ + # Disliked range: roughly 80-110 degrees (yellow-green) + if hct.hue >= 80.0 and hct.hue <= 110.0 and hct.chroma > 16.0: + # Shift towards warmer yellow or cooler green + new_hue = 75.0 if hct.hue < 95.0 else 115.0 + return Hct(new_hue, hct.chroma, hct.tone) + return hct + + +class TonalPalette: + """ + A palette of tones for a single hue and chroma. + + Material Design 3 uses specific tone values for different UI elements. + """ + + def __init__(self, hue: float, chroma: float): + self.hue = hue + self.chroma = chroma + self._cache: dict[int, int] = {} + + @classmethod + def from_hct(cls, hct: Hct) -> 'TonalPalette': + """Create TonalPalette from HCT color.""" + return cls(hct.hue, hct.chroma) + + @classmethod + def from_rgb(cls, r: int, g: int, b: int) -> 'TonalPalette': + """Create TonalPalette from RGB color.""" + hct = Hct.from_rgb(r, g, b) + return cls(hct.hue, hct.chroma) + + def tone(self, t: int) -> int: + """Get ARGB color at the specified tone (0-100).""" + if t not in self._cache: + hct = Hct(self.hue, self.chroma, float(t)) + self._cache[t] = hct.to_argb() + return self._cache[t] + + def get_rgb(self, t: int) -> tuple[int, int, int]: + """Get RGB color at the specified tone.""" + return int_to_rgb(self.tone(t)) + + def get_hex(self, t: int) -> str: + """Get hex color at the specified tone.""" + r, g, b = self.get_rgb(t) + return f"#{r:02x}{g:02x}{b:02x}" diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/image.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/image.py new file mode 100644 index 0000000..7e0c31a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/image.py @@ -0,0 +1,366 @@ +""" +Image reading utilities for PNG and JPEG files. + +This module provides functions for extracting RGB pixels from image files +without external dependencies (except ImageMagick for fallback). +""" + +import struct +import zlib +from pathlib import Path + +# Type alias +RGB = tuple[int, int, int] + + +class ImageReadError(Exception): + """Raised when image cannot be read or parsed.""" + pass + + +def read_png(path: Path) -> list[RGB]: + """ + Parse a PNG file and extract RGB pixels. + + Supports 8-bit RGB and RGBA color types (most common for wallpapers). + Uses zlib for IDAT decompression and handles PNG filters. + """ + with open(path, 'rb') as f: + data = f.read() + + # Verify PNG signature + if data[:8] != b'\x89PNG\r\n\x1a\n': + raise ImageReadError("Invalid PNG signature") + + pos = 8 + width = 0 + height = 0 + bit_depth = 0 + color_type = 0 + idat_chunks: list[bytes] = [] + + while pos < len(data): + # Read chunk length and type + chunk_len = struct.unpack('>I', data[pos:pos+4])[0] + chunk_type = data[pos+4:pos+8] + chunk_data = data[pos+8:pos+8+chunk_len] + pos += 12 + chunk_len # length + type + data + crc + + if chunk_type == b'IHDR': + width = struct.unpack('>I', chunk_data[0:4])[0] + height = struct.unpack('>I', chunk_data[4:8])[0] + bit_depth = chunk_data[8] + color_type = chunk_data[9] + + if bit_depth != 8: + raise ImageReadError(f"Unsupported bit depth: {bit_depth}") + if color_type not in (2, 6): # RGB or RGBA + raise ImageReadError(f"Unsupported color type: {color_type}") + + elif chunk_type == b'IDAT': + idat_chunks.append(chunk_data) + + elif chunk_type == b'IEND': + break + + if not idat_chunks or width == 0: + raise ImageReadError("Missing image data") + + # Decompress all IDAT chunks + compressed = b''.join(idat_chunks) + raw_data = zlib.decompress(compressed) + + # Calculate bytes per pixel and row + bpp = 3 if color_type == 2 else 4 # RGB or RGBA + stride = width * bpp + 1 # +1 for filter byte + + pixels: list[RGB] = [] + prev_row: list[int] = [0] * (width * bpp) + + for y in range(height): + row_start = y * stride + filter_type = raw_data[row_start] + row_data = list(raw_data[row_start + 1:row_start + stride]) + + # Apply PNG filter reconstruction + unfiltered = _png_unfilter(row_data, prev_row, bpp, filter_type) + prev_row = unfiltered + + # Extract RGB values (skip alpha if present) + for x in range(width): + idx = x * bpp + r, g, b = unfiltered[idx], unfiltered[idx+1], unfiltered[idx+2] + pixels.append((r, g, b)) + + return pixels + + +def _png_unfilter( + row: list[int], + prev_row: list[int], + bpp: int, + filter_type: int +) -> list[int]: + """Apply PNG filter reconstruction.""" + result = [0] * len(row) + + for i in range(len(row)): + x = row[i] + a = result[i - bpp] if i >= bpp else 0 + b = prev_row[i] + c = prev_row[i - bpp] if i >= bpp else 0 + + if filter_type == 0: # None + result[i] = x + elif filter_type == 1: # Sub + result[i] = (x + a) & 0xFF + elif filter_type == 2: # Up + result[i] = (x + b) & 0xFF + elif filter_type == 3: # Average + result[i] = (x + (a + b) // 2) & 0xFF + elif filter_type == 4: # Paeth + result[i] = (x + _paeth_predictor(a, b, c)) & 0xFF + else: + raise ImageReadError(f"Unknown PNG filter type: {filter_type}") + + return result + + +def _paeth_predictor(a: int, b: int, c: int) -> int: + """Paeth predictor for PNG filter reconstruction.""" + p = a + b - c + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) + + if pa <= pb and pa <= pc: + return a + elif pb <= pc: + return b + return c + + +def read_jpeg(path: Path) -> list[RGB]: + """ + Parse a JPEG file and extract RGB pixels. + + Supports baseline (SOF0), extended (SOF1), and progressive (SOF2) JPEG. + This is a simplified decoder that extracts dimensions then samples colors. + """ + with open(path, 'rb') as f: + data = f.read() + + # Verify JPEG signature (SOI marker) + if data[:2] != b'\xff\xd8': + raise ImageReadError("Invalid JPEG signature") + + pos = 2 + width = 0 + height = 0 + + # SOF markers that contain image dimensions + # SOF0=Baseline, SOF1=Extended, SOF2=Progressive, SOF3=Lossless + # SOF5-7=Differential variants, SOF9-11=Arithmetic coding variants + sof_markers = {0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, + 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF} + + # Standalone markers (no length field) + standalone_markers = {0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, # RST0-7 + 0xD8, # SOI + 0xD9, # EOI + 0x01} # TEM + + while pos < len(data) - 1: + # Find next marker + if data[pos] != 0xFF: + pos += 1 + continue + + # Skip padding 0xFF bytes + while pos < len(data) and data[pos] == 0xFF: + pos += 1 + + if pos >= len(data): + break + + marker = data[pos] + pos += 1 + + # Check for SOF marker (contains dimensions) + if marker in sof_markers: + if pos + 7 <= len(data): + # Skip segment length (2 bytes), precision (1 byte) + height = struct.unpack('>H', data[pos+3:pos+5])[0] + width = struct.unpack('>H', data[pos+5:pos+7])[0] + break + + # End of image + if marker == 0xD9: + break + + # Skip segment data for markers with length field + if marker not in standalone_markers and marker != 0x00: + if pos + 2 <= len(data): + seg_len = struct.unpack('>H', data[pos:pos+2])[0] + pos += seg_len + + if width == 0 or height == 0: + raise ImageReadError("Could not parse JPEG dimensions") + + # Since full JPEG decoding is extremely complex without external libraries, + # we fall back to sampling the raw data for color approximation. + return _sample_jpeg_colors(data, width, height) + + +def _sample_jpeg_colors(data: bytes, width: int, height: int) -> list[RGB]: + """ + Sample colors from JPEG data without full decoding. + + This is a rough approximation that samples byte triplets from the + compressed data. Not accurate, but provides some color information. + For accurate results, use ImageMagick via read_image(). + """ + # Sample every Nth byte triplet from the image data + # Skip headers and look for image data after SOS marker + pixels: list[RGB] = [] + step = max(1, len(data) // (width * height // 16)) + + for i in range(0, len(data) - 2, step): + r, g, b = data[i], data[i+1], data[i+2] + # Filter out obviously non-image data (markers, etc.) + if not (r == 0xFF and g in (0xD8, 0xD9, 0xE0, 0xE1)): + pixels.append((r, g, b)) + + return pixels if pixels else [(128, 128, 128)] + + +def _read_image_imagemagick(path: Path, resize_filter: str = "Triangle") -> list[RGB]: + """ + Read image using ImageMagick's convert command. + + Converts image to PPM format (trivial to parse) and extracts RGB pixels. + This method works accurately for any image format ImageMagick supports. + """ + import subprocess + + # Use magick or convert command + # -depth 8: 8 bits per channel + # -resize: downsample for performance (we don't need full resolution for color extraction) + # ppm: output as PPM format (easy to parse) + + # Resize to 112x112 to match matugen's color extraction + # Use -filter Triangle (bilinear) for M3 schemes to match matugen's FilterType::Triangle default + # Use -filter Box for k-means schemes (sharper, preserves distinct color regions) + # Use -depth 8 -colorspace sRGB -strip to reduce variance between HDRI/non-HDRI builds + resize_spec = "112x112!" + + try: + # Try 'magick' first (ImageMagick 7+), fallback to 'convert' (ImageMagick 6) + try: + result = subprocess.run( + ['magick', str(path), '-filter', resize_filter, '-resize', resize_spec, + '-depth', '8', '-colorspace', 'sRGB', '-strip', 'ppm:-'], + capture_output=True, + check=True + ) + except FileNotFoundError: + result = subprocess.run( + ['convert', str(path), '-filter', resize_filter, '-resize', resize_spec, + '-depth', '8', '-colorspace', 'sRGB', '-strip', 'ppm:-'], + capture_output=True, + check=True + ) + except subprocess.CalledProcessError as e: + raise ImageReadError(f"ImageMagick failed: {e.stderr.decode()}") + except FileNotFoundError: + raise ImageReadError("ImageMagick not found. Please install imagemagick.") + + ppm_data = result.stdout + return _parse_ppm(ppm_data) + + +def _parse_ppm(data: bytes) -> list[RGB]: + """ + Parse PPM (Portable Pixmap) binary format. + + PPM P6 format: + P6 + width height + maxval + + """ + pos = 0 + tokens: list[str] = [] + + # Read header tokens (need 4: P6, width, height, maxval) + while len(tokens) < 4 and pos < len(data): + # Skip whitespace + while pos < len(data) and data[pos:pos+1] in (b' ', b'\t', b'\n', b'\r'): + pos += 1 + + # Skip comments + if pos < len(data) and data[pos:pos+1] == b'#': + while pos < len(data) and data[pos:pos+1] != b'\n': + pos += 1 + continue + + # Read token + token_start = pos + while pos < len(data) and data[pos:pos+1] not in (b' ', b'\t', b'\n', b'\r', b'#'): + pos += 1 + + if pos > token_start: + tokens.append(data[token_start:pos].decode('ascii')) + + if len(tokens) < 4 or tokens[0] != 'P6': + raise ImageReadError(f"Invalid PPM format: {tokens}") + + width = int(tokens[1]) + height = int(tokens[2]) + maxval = int(tokens[3]) + + # Skip exactly one whitespace character after maxval (per PPM spec) + if pos < len(data) and data[pos:pos+1] in (b' ', b'\t', b'\n', b'\r'): + pos += 1 + + pixel_data = data[pos:] + + # Parse RGB triplets + pixels: list[RGB] = [] + scale = 255.0 / maxval if maxval != 255 else 1.0 + + for i in range(0, min(len(pixel_data), width * height * 3), 3): + if i + 2 < len(pixel_data): + r = int(pixel_data[i] * scale) + g = int(pixel_data[i + 1] * scale) + b = int(pixel_data[i + 2] * scale) + pixels.append((r, g, b)) + + if not pixels: + raise ImageReadError("No pixels extracted from PPM data") + + return pixels + + +def read_image(path: Path, resize_filter: str = "Triangle") -> list[RGB]: + """ + Read an image file and return its pixels as RGB tuples. + + Uses ImageMagick for accurate color extraction from any format. + Falls back to native PNG parsing if ImageMagick is unavailable. + + Args: + path: Path to the image file. + resize_filter: ImageMagick resize filter. "Triangle" for M3 schemes + (matches matugen), "Box" for k-means schemes. + """ + suffix = path.suffix.lower() + + # Try ImageMagick first (works for any format) + try: + return _read_image_imagemagick(path, resize_filter) + except ImageReadError: + # Fall back to native parsing for PNG + if suffix == '.png': + return read_png(path) + raise diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/material.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/material.py new file mode 100644 index 0000000..9bc318a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/material.py @@ -0,0 +1,540 @@ +""" +Material Design 3 color scheme implementation. + +This module provides scheme classes for generating MD3 color schemes +from a source color using the HCT color space. + +Supported schemes (matching Matugen): +- SchemeTonalSpot: Default Android 12-13 scheme, mid-vibrancy +- SchemeFruitSalad: Bold/playful with -50° hue rotation +- SchemeRainbow: Chromatic accents with grayscale neutrals +- SchemeContent: Preserves source color's chroma +""" + +from .hct import Hct, TonalPalette, TemperatureCache, fix_if_disliked + + +# ============================================================================= +# Tone Values (shared across all schemes) +# ============================================================================= + +# Tone values for Material Design 3 (dark theme) +DARK_TONES = { + 'primary': 80, + 'on_primary': 20, + 'primary_container': 30, + 'on_primary_container': 90, + 'secondary': 80, + 'on_secondary': 20, + 'secondary_container': 30, + 'on_secondary_container': 90, + 'tertiary': 80, + 'on_tertiary': 20, + 'tertiary_container': 30, + 'on_tertiary_container': 90, + 'error': 80, + 'on_error': 20, + 'error_container': 30, + 'on_error_container': 90, + 'surface': 6, + 'on_surface': 90, + 'surface_variant': 30, + 'on_surface_variant': 80, + 'surface_container_lowest': 4, + 'surface_container_low': 10, + 'surface_container': 12, + 'surface_container_high': 17, + 'surface_container_highest': 22, + 'outline': 60, + 'outline_variant': 30, + 'shadow': 0, + 'scrim': 0, + 'inverse_surface': 90, + 'inverse_on_surface': 20, + 'inverse_primary': 40, +} + +# Tone values for Material Design 3 (light theme) +LIGHT_TONES = { + 'primary': 40, + 'on_primary': 100, + 'primary_container': 90, + 'on_primary_container': 10, + 'secondary': 40, + 'on_secondary': 100, + 'secondary_container': 90, + 'on_secondary_container': 10, + 'tertiary': 40, + 'on_tertiary': 100, + 'tertiary_container': 90, + 'on_tertiary_container': 10, + 'error': 40, + 'on_error': 100, + 'error_container': 90, + 'on_error_container': 10, + 'surface': 98, + 'on_surface': 10, + 'surface_variant': 90, + 'on_surface_variant': 30, + 'surface_container_lowest': 100, + 'surface_container_low': 96, + 'surface_container': 94, + 'surface_container_high': 92, + 'surface_container_highest': 90, + 'outline': 50, + 'outline_variant': 80, + 'shadow': 0, + 'scrim': 0, + 'inverse_surface': 20, + 'inverse_on_surface': 95, + 'inverse_primary': 80, +} + +# Monochrome scheme uses different tone values (from material-colors library) +# Primary/tertiary get special treatment for higher contrast in grayscale +MONOCHROME_DARK_TONES = { + **DARK_TONES, + 'primary': 100, # White (was 80) + 'on_primary': 10, # Near-black (was 20) + 'primary_container': 85, # Light gray (was 30) + 'on_primary_container': 0, # Black (was 90) + 'tertiary': 90, # Light gray (was 80) + 'on_tertiary': 10, # Near-black (was 20) + 'tertiary_container': 60, # Mid gray (was 30) + 'on_tertiary_container': 0, # Black (was 90) + 'secondary_container': 30, # Same as normal +} + +MONOCHROME_LIGHT_TONES = { + **LIGHT_TONES, + 'primary': 0, # Black (was 40) + 'on_primary': 90, # Light gray (was 100) + 'primary_container': 25, # Dark gray (was 90) + 'on_primary_container': 100, # White (was 10) + 'tertiary': 25, # Dark gray (was 40) + 'on_tertiary': 90, # Light gray (was 100) + 'tertiary_container': 49, # Mid gray (was 90) + 'on_tertiary_container': 100, # White (was 10) + 'secondary_container': 90, # Same as normal +} + + +# ============================================================================= +# Base Scheme Class +# ============================================================================= + +class _BaseScheme: + """Base class for all Material Design 3 schemes.""" + + # Error palette is the same for all schemes + error_palette: TonalPalette + + def __init__(self, source_color: Hct): + """Initialize with source color. Subclasses must set palettes.""" + self.source = source_color + self.error_palette = TonalPalette(25.0, 84.0) # Material red + + @classmethod + def from_rgb(cls, r: int, g: int, b: int) -> '_BaseScheme': + """Create scheme from RGB color.""" + return cls(Hct.from_rgb(r, g, b)) + + @classmethod + def from_hex(cls, hex_color: str) -> '_BaseScheme': + """Create scheme from hex color string.""" + hex_color = hex_color.lstrip('#') + r = int(hex_color[0:2], 16) + g = int(hex_color[2:4], 16) + b = int(hex_color[4:6], 16) + return cls.from_rgb(r, g, b) + + def get_dark_scheme(self) -> dict[str, str]: + """Generate dark theme color dictionary.""" + return self._generate_scheme(is_dark=True) + + def get_light_scheme(self) -> dict[str, str]: + """Generate light theme color dictionary.""" + return self._generate_scheme(is_dark=False) + + def _generate_scheme(self, is_dark: bool) -> dict[str, str]: + """Generate scheme with appropriate tone values.""" + tones = DARK_TONES if is_dark else LIGHT_TONES + + scheme = { + # Primary colors + 'primary': self.primary_palette.get_hex(tones['primary']), + 'on_primary': self.primary_palette.get_hex(tones['on_primary']), + 'primary_container': self.primary_palette.get_hex(tones['primary_container']), + 'on_primary_container': self.primary_palette.get_hex(tones['on_primary_container']), + + # Surface tint (same as primary, used for M3 elevation tinting) + 'surface_tint': self.primary_palette.get_hex(tones['primary']), + + # Secondary colors + 'secondary': self.secondary_palette.get_hex(tones['secondary']), + 'on_secondary': self.secondary_palette.get_hex(tones['on_secondary']), + 'secondary_container': self.secondary_palette.get_hex(tones['secondary_container']), + 'on_secondary_container': self.secondary_palette.get_hex(tones['on_secondary_container']), + + # Tertiary colors + 'tertiary': self.tertiary_palette.get_hex(tones['tertiary']), + 'on_tertiary': self.tertiary_palette.get_hex(tones['on_tertiary']), + 'tertiary_container': self.tertiary_palette.get_hex(tones['tertiary_container']), + 'on_tertiary_container': self.tertiary_palette.get_hex(tones['on_tertiary_container']), + + # Error colors + 'error': self.error_palette.get_hex(tones['error']), + 'on_error': self.error_palette.get_hex(tones['on_error']), + 'error_container': self.error_palette.get_hex(tones['error_container']), + 'on_error_container': self.error_palette.get_hex(tones['on_error_container']), + + # Surface colors + 'surface': self.neutral_palette.get_hex(tones['surface']), + 'on_surface': self.neutral_palette.get_hex(tones['on_surface']), + 'surface_variant': self.neutral_variant_palette.get_hex(tones['surface_variant']), + 'on_surface_variant': self.neutral_variant_palette.get_hex(tones['on_surface_variant']), + + # Surface containers + 'surface_container_lowest': self.neutral_palette.get_hex(tones['surface_container_lowest']), + 'surface_container_low': self.neutral_palette.get_hex(tones['surface_container_low']), + 'surface_container': self.neutral_palette.get_hex(tones['surface_container']), + 'surface_container_high': self.neutral_palette.get_hex(tones['surface_container_high']), + 'surface_container_highest': self.neutral_palette.get_hex(tones['surface_container_highest']), + + # Outline and other + 'outline': self.neutral_variant_palette.get_hex(tones['outline']), + 'outline_variant': self.neutral_variant_palette.get_hex(tones['outline_variant']), + 'shadow': self.neutral_palette.get_hex(tones['shadow']), + 'scrim': self.neutral_palette.get_hex(tones['scrim']), + + # Inverse colors + 'inverse_surface': self.neutral_palette.get_hex(tones['inverse_surface']), + 'inverse_on_surface': self.neutral_palette.get_hex(tones['inverse_on_surface']), + 'inverse_primary': self.primary_palette.get_hex(tones['inverse_primary']), + + # Background (alias for surface) + 'background': self.neutral_palette.get_hex(tones['surface']), + 'on_background': self.neutral_palette.get_hex(tones['on_surface']), + + # Surface dim and bright + 'surface_dim': self.neutral_palette.get_hex(87 if not is_dark else 6), + 'surface_bright': self.neutral_palette.get_hex(98 if not is_dark else 24), + + # Fixed colors - consistent across light/dark modes (MD3 spec) + 'primary_fixed': self.primary_palette.get_hex(90), + 'primary_fixed_dim': self.primary_palette.get_hex(80), + 'on_primary_fixed': self.primary_palette.get_hex(10), + 'on_primary_fixed_variant': self.primary_palette.get_hex(30), + + 'secondary_fixed': self.secondary_palette.get_hex(90), + 'secondary_fixed_dim': self.secondary_palette.get_hex(80), + 'on_secondary_fixed': self.secondary_palette.get_hex(10), + 'on_secondary_fixed_variant': self.secondary_palette.get_hex(30), + + 'tertiary_fixed': self.tertiary_palette.get_hex(90), + 'tertiary_fixed_dim': self.tertiary_palette.get_hex(80), + 'on_tertiary_fixed': self.tertiary_palette.get_hex(10), + 'on_tertiary_fixed_variant': self.tertiary_palette.get_hex(30), + } + + return scheme + + +# ============================================================================= +# Scheme Implementations +# ============================================================================= + +class SchemeTonalSpot(_BaseScheme): + """ + Tonal Spot scheme - the default Android 12-13 Material You scheme. + + Uses fixed chroma values for consistent, harmonious palettes: + - Primary: source hue, chroma 48 + - Secondary: source hue, chroma 16 + - Tertiary: hue +60°, chroma 24 + - Neutrals: low chroma (tinted with source hue) + """ + + def __init__(self, source_color: Hct): + super().__init__(source_color) + + # Primary: source hue with fixed chroma 48 + self.primary_palette = TonalPalette(source_color.hue, 48.0) + + # Secondary: source hue with lower chroma 16 + self.secondary_palette = TonalPalette(source_color.hue, 16.0) + + # Tertiary: 60° hue rotation with chroma 24 + tertiary_hue = (source_color.hue + 60.0) % 360.0 + self.tertiary_palette = TonalPalette(tertiary_hue, 24.0) + + # Neutral: source hue with very low chroma (tinted grays) + self.neutral_palette = TonalPalette(source_color.hue, 4.0) + + # Neutral variant: slightly more chroma for contrast + self.neutral_variant_palette = TonalPalette(source_color.hue, 8.0) + + +class SchemeFruitSalad(_BaseScheme): + """ + Fruit Salad scheme - bold, playful theme with hue rotation. + + Designed for expressive, colorful themes: + - Primary: hue -50°, chroma 48 + - Secondary: hue -50°, chroma 36 + - Tertiary: source hue (original), chroma 36 + - Neutrals: tinted (chroma 10-16) + """ + + def __init__(self, source_color: Hct): + super().__init__(source_color) + + # Rotate hue by -50° for primary and secondary + rotated_hue = (source_color.hue - 50.0) % 360.0 + + # Primary: rotated hue with chroma 48 + self.primary_palette = TonalPalette(rotated_hue, 48.0) + + # Secondary: rotated hue with chroma 36 + self.secondary_palette = TonalPalette(rotated_hue, 36.0) + + # Tertiary: original source hue with chroma 36 + self.tertiary_palette = TonalPalette(source_color.hue, 36.0) + + # Neutral: source hue with higher chroma (tinted) + self.neutral_palette = TonalPalette(source_color.hue, 10.0) + + # Neutral variant: even more tinted + self.neutral_variant_palette = TonalPalette(source_color.hue, 16.0) + + +class SchemeRainbow(_BaseScheme): + """ + Rainbow scheme - chromatic accents with grayscale neutrals. + + Same structure as Tonal Spot but with pure grayscale neutrals: + - Primary: source hue, chroma 48 + - Secondary: source hue, chroma 16 + - Tertiary: hue +60°, chroma 24 + - Neutrals: pure grayscale (chroma 0) + """ + + def __init__(self, source_color: Hct): + super().__init__(source_color) + + # Primary: source hue with fixed chroma 48 + self.primary_palette = TonalPalette(source_color.hue, 48.0) + + # Secondary: source hue with lower chroma 16 + self.secondary_palette = TonalPalette(source_color.hue, 16.0) + + # Tertiary: 60° hue rotation with chroma 24 + tertiary_hue = (source_color.hue + 60.0) % 360.0 + self.tertiary_palette = TonalPalette(tertiary_hue, 24.0) + + # Neutral: pure grayscale (chroma 0) + self.neutral_palette = TonalPalette(0.0, 0.0) + + # Neutral variant: also grayscale + self.neutral_variant_palette = TonalPalette(0.0, 0.0) + + +class SchemeContent(_BaseScheme): + """ + Content scheme - preserves source color's chroma. + + This is the Material Design 3 "content" scheme that preserves the source + color's characteristics while creating harmonious palettes: + - Primary: source hue and chroma (full preservation) + - Secondary: same hue, reduced chroma: max(chroma - 32, chroma * 0.5) + - Tertiary: analogous color from temperature analysis (warm-cool harmony) + - Neutrals: low chroma (chroma / 8, tinted with source hue) + """ + + def __init__(self, source_color: Hct): + super().__init__(source_color) + + # Primary: preserve source color's hue and chroma (full preservation) + self.primary_palette = TonalPalette(source_color.hue, source_color.chroma) + + # Secondary: same hue, reduced chroma + # Formula from matugen: max(chroma - 32, chroma * 0.5) + secondary_chroma = max(source_color.chroma - 32.0, source_color.chroma * 0.5) + self.secondary_palette = TonalPalette(source_color.hue, secondary_chroma) + + # Tertiary: use analogous color from temperature analysis + # Get 3 analogous colors with 6 divisions, pick the last one (most different) + temp_cache = TemperatureCache(source_color) + analogous_colors = temp_cache.analogous(3, 6) + tertiary_hct = fix_if_disliked(analogous_colors[-1]) + self.tertiary_palette = TonalPalette.from_hct(tertiary_hct) + + # Neutral: source hue, low chroma (chroma / 8) + neutral_chroma = source_color.chroma / 8.0 + self.neutral_palette = TonalPalette(source_color.hue, neutral_chroma) + + # Neutral variant: slightly more chroma (chroma / 8 + 4) + neutral_variant_chroma = (source_color.chroma / 8.0) + 4.0 + self.neutral_variant_palette = TonalPalette(source_color.hue, neutral_variant_chroma) + + +class SchemeMonochrome(_BaseScheme): + """ + Material Design 3 Monochrome scheme. + + All color palettes use chroma=0.0, producing a pure grayscale theme. + Only the error color retains saturation for accessibility. + + Uses special tone mappings (different from other M3 schemes) for higher + contrast in grayscale - e.g., primary is tone 100 (white) in dark mode. + + Palette configuration: + - Primary: chroma 0.0 (grayscale) + - Secondary: chroma 0.0 (grayscale) + - Tertiary: chroma 0.0 (grayscale) + - Neutral: chroma 0.0 (grayscale) + - Neutral variant: chroma 0.0 (grayscale) + - Error: hue 25°, chroma 84 (vibrant red) + """ + + def __init__(self, source_color: Hct): + super().__init__(source_color) + + # All palettes use chroma=0 (grayscale) + # Source hue is preserved but irrelevant at chroma 0 + self.primary_palette = TonalPalette(source_color.hue, 0.0) + self.secondary_palette = TonalPalette(source_color.hue, 0.0) + self.tertiary_palette = TonalPalette(source_color.hue, 0.0) + self.neutral_palette = TonalPalette(source_color.hue, 0.0) + self.neutral_variant_palette = TonalPalette(source_color.hue, 0.0) + + # Error palette keeps vibrant red for accessibility + self.error_palette = TonalPalette(25.0, 84.0) + + def _generate_scheme(self, is_dark: bool) -> dict[str, str]: + """Generate scheme with monochrome-specific tone values.""" + # Monochrome uses different tones for higher contrast in grayscale + tones = MONOCHROME_DARK_TONES if is_dark else MONOCHROME_LIGHT_TONES + + scheme = { + # Primary colors + 'primary': self.primary_palette.get_hex(tones['primary']), + 'on_primary': self.primary_palette.get_hex(tones['on_primary']), + 'primary_container': self.primary_palette.get_hex(tones['primary_container']), + 'on_primary_container': self.primary_palette.get_hex(tones['on_primary_container']), + + # Surface tint (same as primary, used for M3 elevation tinting) + 'surface_tint': self.primary_palette.get_hex(tones['primary']), + + # Secondary colors + 'secondary': self.secondary_palette.get_hex(tones['secondary']), + 'on_secondary': self.secondary_palette.get_hex(tones['on_secondary']), + 'secondary_container': self.secondary_palette.get_hex(tones['secondary_container']), + 'on_secondary_container': self.secondary_palette.get_hex(tones['on_secondary_container']), + + # Tertiary colors + 'tertiary': self.tertiary_palette.get_hex(tones['tertiary']), + 'on_tertiary': self.tertiary_palette.get_hex(tones['on_tertiary']), + 'tertiary_container': self.tertiary_palette.get_hex(tones['tertiary_container']), + 'on_tertiary_container': self.tertiary_palette.get_hex(tones['on_tertiary_container']), + + # Error colors + 'error': self.error_palette.get_hex(tones['error']), + 'on_error': self.error_palette.get_hex(tones['on_error']), + 'error_container': self.error_palette.get_hex(tones['error_container']), + 'on_error_container': self.error_palette.get_hex(tones['on_error_container']), + + # Surface colors + 'surface': self.neutral_palette.get_hex(tones['surface']), + 'on_surface': self.neutral_palette.get_hex(tones['on_surface']), + 'surface_variant': self.neutral_variant_palette.get_hex(tones['surface_variant']), + 'on_surface_variant': self.neutral_variant_palette.get_hex(tones['on_surface_variant']), + + # Surface containers + 'surface_container_lowest': self.neutral_palette.get_hex(tones['surface_container_lowest']), + 'surface_container_low': self.neutral_palette.get_hex(tones['surface_container_low']), + 'surface_container': self.neutral_palette.get_hex(tones['surface_container']), + 'surface_container_high': self.neutral_palette.get_hex(tones['surface_container_high']), + 'surface_container_highest': self.neutral_palette.get_hex(tones['surface_container_highest']), + + # Outline and other + 'outline': self.neutral_variant_palette.get_hex(tones['outline']), + 'outline_variant': self.neutral_variant_palette.get_hex(tones['outline_variant']), + 'shadow': self.neutral_palette.get_hex(tones['shadow']), + 'scrim': self.neutral_palette.get_hex(tones['scrim']), + + # Inverse colors + 'inverse_surface': self.neutral_palette.get_hex(tones['inverse_surface']), + 'inverse_on_surface': self.neutral_palette.get_hex(tones['inverse_on_surface']), + 'inverse_primary': self.primary_palette.get_hex(tones['inverse_primary']), + + # Background (same as surface in MD3) + 'background': self.neutral_palette.get_hex(tones['surface']), + 'on_background': self.neutral_palette.get_hex(tones['on_surface']), + + # Surface dim and bright + 'surface_dim': self.neutral_palette.get_hex(tones['surface']), + 'surface_bright': self.neutral_palette.get_hex(tones['surface_container_highest'] + 5), + + # Fixed colors + 'primary_fixed': self.primary_palette.get_hex(90), + 'primary_fixed_dim': self.primary_palette.get_hex(80), + 'on_primary_fixed': self.primary_palette.get_hex(10), + 'on_primary_fixed_variant': self.primary_palette.get_hex(30), + 'secondary_fixed': self.secondary_palette.get_hex(90), + 'secondary_fixed_dim': self.secondary_palette.get_hex(80), + 'on_secondary_fixed': self.secondary_palette.get_hex(10), + 'on_secondary_fixed_variant': self.secondary_palette.get_hex(30), + 'tertiary_fixed': self.tertiary_palette.get_hex(90), + 'tertiary_fixed_dim': self.tertiary_palette.get_hex(80), + 'on_tertiary_fixed': self.tertiary_palette.get_hex(10), + 'on_tertiary_fixed_variant': self.tertiary_palette.get_hex(30), + } + + return scheme + + +# Backward compatibility alias +MaterialScheme = SchemeContent + + +# ============================================================================= +# Helper Functions +# ============================================================================= + +def harmonize_color(design_color: Hct, source_color: Hct, amount: float = 0.5) -> Hct: + """ + Shift a design color's hue towards a source color's hue. + + Used to make custom colors feel more cohesive with the theme. + + Args: + design_color: The color to adjust + source_color: The reference color to harmonize towards + amount: How much to shift (0-1, default 0.5) + + Returns: + Harmonized HCT color + """ + diff = _hue_difference(source_color.hue, design_color.hue) + rotation = min(diff * amount, 15.0) # Max 15° rotation + if _shorter_rotation(source_color.hue, design_color.hue) < 0: + rotation = -rotation + new_hue = (design_color.hue + rotation) % 360.0 + return Hct(new_hue, design_color.chroma, design_color.tone) + + +def _hue_difference(hue1: float, hue2: float) -> float: + """Calculate the absolute difference between two hues.""" + diff = abs(hue1 - hue2) + return min(diff, 360.0 - diff) + + +def _shorter_rotation(from_hue: float, to_hue: float) -> float: + """Calculate the shorter rotation direction between hues.""" + diff = to_hue - from_hue + if diff > 180.0: + return diff - 360.0 + elif diff < -180.0: + return diff + 360.0 + return diff diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/palette.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/palette.py new file mode 100644 index 0000000..2d8d507 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/palette.py @@ -0,0 +1,672 @@ +""" +Palette extraction using K-means clustering. + +This module provides functions for extracting dominant colors from images +using perceptual color distance calculations and k-means clustering. +""" + +import math + +from .color import Color, rgb_to_hsl, hsl_to_rgb, hue_distance, rgb_to_lab, lab_to_rgb, lab_distance +from .hct import Cam16, Hct + +# Type aliases +RGB = tuple[int, int, int] +HSL = tuple[float, float, float] +LAB = tuple[float, float, float] + + +def downsample_pixels(pixels: list[RGB], factor: int = 4) -> list[RGB]: + """ + Downsample pixels for faster processing. + + Takes every Nth pixel to reduce dataset size while maintaining + color distribution characteristics. + """ + if factor <= 1: + return pixels + + # Calculate step based on factor squared (for 2D image) + step = factor * factor + return pixels[::step] + + +def kmeans_cluster( + colors: list[RGB], + k: int = 5, + iterations: int = 10 +) -> list[tuple[RGB, RGB, int]]: + """ + Perform K-means clustering on colors in Lab color space. + + Lab space is perceptually uniform, matching matugen's approach. + Returns list of (centroid_rgb, representative_rgb, cluster_size) tuples, + sorted by cluster size. + + - centroid_rgb: averaged color from the cluster (smoother, blended) + - representative_rgb: actual image pixel closest to centroid + """ + if len(colors) < k: + # Not enough colors, return what we have (same color for centroid and representative) + unique = list(set(colors)) + return [(c, c, colors.count(c)) for c in unique[:k]] + + # Convert to Lab for perceptual clustering (like matugen's WSMeans) + colors_lab = [rgb_to_lab(*c) for c in colors] + + # Deterministic initialization: pick evenly spaced colors from sorted list + # Sort by L (lightness) first for better spread + sorted_indices = sorted(range(len(colors_lab)), key=lambda i: colors_lab[i][0]) + step = len(sorted_indices) // k + centroids = [colors_lab[sorted_indices[i * step]] for i in range(k)] + + # K-means iterations + assignments = [0] * len(colors_lab) + for _ in range(iterations): + # Assign colors to nearest centroid + for idx, color in enumerate(colors_lab): + min_dist = float('inf') + min_cluster = 0 + for i, centroid in enumerate(centroids): + dist = lab_distance(color, centroid) + if dist < min_dist: + min_dist = dist + min_cluster = i + assignments[idx] = min_cluster + + # Update centroids (simple mean in Lab space) + new_centroids = [] + for i in range(k): + cluster_colors = [colors_lab[j] for j in range(len(colors_lab)) if assignments[j] == i] + if cluster_colors: + avg_L = sum(c[0] for c in cluster_colors) / len(cluster_colors) + avg_a = sum(c[1] for c in cluster_colors) / len(cluster_colors) + avg_b = sum(c[2] for c in cluster_colors) / len(cluster_colors) + new_centroids.append((avg_L, avg_a, avg_b)) + else: + new_centroids.append(centroids[i]) + + centroids = new_centroids + + # Final assignment and count, also find representative pixel (closest to centroid) + cluster_counts = [0] * k + cluster_representatives: list[tuple[RGB, float]] = [(colors[0], float('inf'))] * k + + for idx, color_lab in enumerate(colors_lab): + cluster_idx = assignments[idx] + cluster_counts[cluster_idx] += 1 + + # Track the pixel closest to the centroid as the representative + dist = lab_distance(color_lab, centroids[cluster_idx]) + if dist < cluster_representatives[cluster_idx][1]: + cluster_representatives[cluster_idx] = (colors[idx], dist) + + # Return both centroid (averaged) and representative (actual pixel) colors + results = [] + for i in range(k): + if cluster_counts[i] > 0: + # Convert Lab centroid back to RGB + centroid_rgb = lab_to_rgb(*centroids[i]) + representative_rgb = cluster_representatives[i][0] + results.append((centroid_rgb, representative_rgb, cluster_counts[i])) + + # Sort by cluster size (most common first) + results.sort(key=lambda x: -x[2]) + + return results + + +def _score_colors_chroma( + colors_with_counts: list[tuple[RGB, int]], +) -> list[tuple[Color, float]]: + """ + Score colors prioritizing chroma (vibrancy) over area coverage. + + Uses count^0.3 weighting so saturated colors win even with small area. + Used for "vibrant" mode to find the most eye-catching colors. + + Args: + colors_with_counts: List of (RGB, count) tuples from clustering + + Returns: + List of (Color, score) tuples, sorted by score descending + """ + result_colors = [] + for rgb, count in colors_with_counts: + color = Color.from_rgb(rgb) + try: + hct = color.to_hct() + + # Chroma contribution - prefer colorful colors + chroma_score = hct.chroma + + # Tone penalty - prefer mid-tones (40-60 is ideal) + if hct.tone < 20: + tone_penalty = (20 - hct.tone) * 2 + elif hct.tone > 80: + tone_penalty = (hct.tone - 80) * 1.5 + elif hct.tone < 40: + tone_penalty = (40 - hct.tone) * 0.5 + elif hct.tone > 60: + tone_penalty = (hct.tone - 60) * 0.3 + else: + tone_penalty = 0 + + # Hue penalty - slight penalty for yellow-green hues + if 80 < hct.hue < 110: + hue_penalty = 5 + else: + hue_penalty = 0 + + # Combined score: chroma minus penalties, balanced with count + # Using count^0.3 so chroma dominates while still considering area + score = (chroma_score - tone_penalty - hue_penalty) * (count ** 0.3) + result_colors.append((color, score)) + except (ValueError, ZeroDivisionError): + result_colors.append((color, 0.0)) + + result_colors.sort(key=lambda x: -x[1]) + return result_colors + + +def _hue_to_family(hue: float) -> int: + """ + Map hue to perceptual color family. + + Uses non-uniform ranges that match human color perception: + - 0: RED (330-30°, wraps around) + - 1: ORANGE (30-60°) + - 2: YELLOW (60-105°) + - 3: GREEN (105-190°, includes green-leaning teal) + - 4: BLUE (190-270°, includes cyan) + - 5: PURPLE (270-330°) + """ + if hue >= 330 or hue < 30: + return 0 # RED + elif hue < 60: + return 1 # ORANGE + elif hue < 105: + return 2 # YELLOW + elif hue < 190: + return 3 # GREEN (includes green-leaning teal) + elif hue < 270: + return 4 # BLUE (includes cyan) + else: + return 5 # PURPLE + + +def _score_colors_count( + colors_with_counts: list[tuple[RGB, int]], +) -> list[tuple[Color, float]]: + """ + Score colors prioritizing pixel count (area coverage) by hue family. + + Groups colors into perceptual hue families, sums counts per family, + then picks the dominant family. This is more faithful to human perception + where we see "green" as a category, not individual shades. + + Args: + colors_with_counts: List of (RGB, count) tuples from clustering + + Returns: + List of (Color, score) tuples, sorted by family dominance then count + """ + MIN_CHROMA = 10.0 # Filter out near-gray colors + + # First pass: collect colorful colors and group by hue family + hue_families: dict[int, list[tuple[Color, float, float, int]]] = {} # family -> [(color, hue, chroma, count), ...] + + for rgb, count in colors_with_counts: + color = Color.from_rgb(rgb) + try: + hct = color.to_hct() + if hct.chroma >= MIN_CHROMA: + family = _hue_to_family(hct.hue) + if family not in hue_families: + hue_families[family] = [] + hue_families[family].append((color, hct.hue, hct.chroma, count)) + except (ValueError, ZeroDivisionError): + pass + + # If no colorful colors found, fall back to all colors + if not hue_families: + result = [] + for rgb, count in colors_with_counts: + color = Color.from_rgb(rgb) + result.append((color, float(count))) + result.sort(key=lambda x: -x[1]) + return result + + # Calculate total count per hue family + family_totals: list[tuple[int, int]] = [] + for family, colors in hue_families.items(): + total = sum(c[3] for c in colors) + family_totals.append((family, total)) + + # Sort families by total count (dominant family first) + family_totals.sort(key=lambda x: -x[1]) + + # Build result: colors from dominant families first, sorted by count within each family + result_colors = [] + for family, _ in family_totals: + family_colors = hue_families[family] + # Sort by count descending, chroma as tiebreaker + family_colors.sort(key=lambda x: (-x[3], -x[2])) + for color, hue, chroma, count in family_colors: + # Score encodes family rank + count for proper ordering + family_rank = next(i for i, (f, _) in enumerate(family_totals) if f == family) + score = (len(family_totals) - family_rank) * 1000000 + count * 1000 + chroma + result_colors.append((color, score)) + + result_colors.sort(key=lambda x: -x[1]) + return result_colors + + +def _family_center_hue(family: int) -> float: + """Get the center hue for a family index.""" + # Family centers based on _hue_to_family ranges: + # 0: RED (330-30°, wraps) -> center 0° + # 1: ORANGE (30-60°) -> center 45° + # 2: YELLOW (60-105°) -> center 82.5° + # 3: GREEN (105-190°) -> center 147.5° + # 4: BLUE (190-270°) -> center 230° + # 5: PURPLE (270-330°) -> center 300° + centers = [0.0, 45.0, 82.5, 147.5, 230.0, 300.0] + return centers[family] + + +def _circular_hue_diff(h1: float, h2: float) -> float: + """Calculate circular hue difference (0-180).""" + diff = abs(h1 - h2) + return min(diff, 360.0 - diff) + + +def _score_colors_dysfunctional( + colors_with_counts: list[tuple[RGB, int]], +) -> list[tuple[Color, float]]: + """ + Score colors prioritizing the 2nd most dominant hue family. + + Like count scoring but skips the dominant family (and any families + too close to it) to pick a visually distinct secondary color. + + Args: + colors_with_counts: List of (RGB, count) tuples from clustering + + Returns: + List of (Color, score) tuples, sorted by family dominance then count + """ + MIN_CHROMA = 10.0 # Filter out near-gray colors + MIN_HUE_DISTANCE = 45.0 # Minimum hue distance from dominant family + MIN_COUNT_RATIO = 0.02 # Distant family must have at least 2% of total colorful pixels + + # First pass: collect colorful colors and group by hue family + hue_families: dict[int, list[tuple[Color, float, float, int]]] = {} # family -> [(color, hue, chroma, count), ...] + + for rgb, count in colors_with_counts: + color = Color.from_rgb(rgb) + try: + hct = color.to_hct() + if hct.chroma >= MIN_CHROMA: + family = _hue_to_family(hct.hue) + if family not in hue_families: + hue_families[family] = [] + hue_families[family].append((color, hct.hue, hct.chroma, count)) + except (ValueError, ZeroDivisionError): + pass + + # If no colorful colors found, fall back to all colors + if not hue_families: + result = [] + for rgb, count in colors_with_counts: + color = Color.from_rgb(rgb) + result.append((color, float(count))) + result.sort(key=lambda x: -x[1]) + return result + + # Calculate total count per hue family + family_totals: list[tuple[int, int]] = [] + for family, colors in hue_families.items(): + total = sum(c[3] for c in colors) + family_totals.append((family, total)) + + # Sort families by total count (dominant family first) + family_totals.sort(key=lambda x: -x[1]) + + # Find the dominant family and its center hue + dominant_family, dominant_count = family_totals[0] + dominant_center = _family_center_hue(dominant_family) + total_colorful_pixels = sum(count for _, count in family_totals) + min_count = total_colorful_pixels * MIN_COUNT_RATIO + + # Find families that are far enough from the dominant one AND have enough pixels + distant_families = [] + close_families = [dominant_family] + for family, count in family_totals[1:]: + family_center = _family_center_hue(family) + hue_diff = _circular_hue_diff(dominant_center, family_center) + if hue_diff >= MIN_HUE_DISTANCE and count >= min_count: + # Get max chroma in this family - we want families with vibrant colors + max_chroma = max(c[2] for c in hue_families[family]) + distant_families.append((family, count, hue_diff, max_chroma)) + else: + close_families.append(family) + + # Build result: colors from distant families first + result_colors = [] + + # Sort distant families by weighted score: hue_distance * max_chroma + # This balances visual distinctness (hue distance) with color quality (chroma) + # A family that's far away AND has good colors beats one that's close with great colors + distant_families.sort(key=lambda x: -(x[2] * x[3])) + + for family, _, _, _ in distant_families: + family_colors = hue_families[family] + # Sort by chroma descending - we want the most vibrant color from this family + # Count is tiebreaker to avoid picking tiny noise clusters + family_colors.sort(key=lambda x: (-x[2], -x[3])) + for color, hue, chroma, count in family_colors: + # Score encodes family rank + chroma for proper ordering + # Chroma is primary (we want vibrant), count is tiebreaker + family_rank = next(i for i, (f, _, _, _) in enumerate(distant_families) if f == family) + score = (len(distant_families) - family_rank) * 1000000 + chroma * 1000 + count + result_colors.append((color, score)) + + # Add colors from close families (including dominant) at lower priority + for family in close_families: + family_colors = hue_families[family] + family_colors.sort(key=lambda x: (-x[3], -x[2])) + for color, hue, chroma, count in family_colors: + # Lower score than all distant-family colors + score = count * 1000 + chroma + result_colors.append((color, score)) + + result_colors.sort(key=lambda x: -x[1]) + return result_colors + + +def _score_colors_muted( + colors_with_counts: list[tuple[RGB, int]], +) -> list[tuple[Color, float]]: + """ + Score colors for muted mode - pure pixel count without chroma filtering. + + Unlike count scoring which filters to chroma >= 10, this accepts all colors + including grayscale. Designed for monochrome/monotonal wallpapers where + the dominant color may have very low or zero saturation. + + Args: + colors_with_counts: List of (RGB, count) tuples from clustering + + Returns: + List of (Color, score) tuples, sorted by count descending + """ + result = [] + for rgb, count in colors_with_counts: + color = Color.from_rgb(rgb) + result.append((color, float(count))) + + result.sort(key=lambda x: -x[1]) + return result + + +def _score_colors_population( + colors_with_counts: list[tuple[RGB, int]], + total_pixels: int +) -> list[tuple[Color, float]]: + """ + Score colors using Material Design's Score algorithm. + + This matches matugen's scoring approach exactly: + - Build per-hue population histogram (360 buckets) + - Calculate "excited proportions" (±15° hue window sum) + - Score: proportion * 100 * 0.7 + (chroma - 48) * weight + - Filter by chroma >= 5 and proportion >= 1% + - Deduplicate by maximizing hue distance + + Args: + colors_with_counts: List of (RGB, count) tuples from clustering + total_pixels: Total number of pixels in the sample + + Returns: + List of (Color, score) tuples, sorted by score descending + """ + # Constants matching Material Score + TARGET_CHROMA = 48.0 + WEIGHT_PROPORTION = 0.7 + WEIGHT_CHROMA_ABOVE = 0.3 + WEIGHT_CHROMA_BELOW = 0.1 + CUTOFF_CHROMA = 5.0 + CUTOFF_EXCITED_PROPORTION = 0.01 + + # Build per-hue population histogram (360 buckets) + hue_population = [0] * 360 + population_sum = 0 + + colors_hct: list[tuple[Color, Hct, int]] = [] + for rgb, count in colors_with_counts: + try: + color = Color.from_rgb(rgb) + hct = color.to_hct() + hue_bucket = int(hct.hue) % 360 + hue_population[hue_bucket] += count + population_sum += count + colors_hct.append((color, hct, count)) + except (ValueError, ZeroDivisionError): + continue + + if not colors_hct or population_sum == 0: + # Fallback: return colors without scoring + result = [] + for rgb, count in colors_with_counts: + color = Color.from_rgb(rgb) + result.append((color, float(count))) + return sorted(result, key=lambda x: -x[1]) + + # Calculate "excited proportions" - sum of proportions in ±15° hue window + hue_excited_proportions = [0.0] * 360 + for hue in range(360): + proportion = hue_population[hue] / population_sum + # Spread to neighboring hues (±15°, so 30° total window) + for offset in range(-14, 16): + neighbor_hue = (hue + offset) % 360 + hue_excited_proportions[neighbor_hue] += proportion + + # Score each color + scored_hcts: list[tuple[Color, Hct, float]] = [] + for color, hct, count in colors_hct: + hue_bucket = int(hct.hue) % 360 + proportion = hue_excited_proportions[hue_bucket] + + # Filter by chroma and proportion + if hct.chroma < CUTOFF_CHROMA: + continue + if proportion <= CUTOFF_EXCITED_PROPORTION: + continue + + # Proportion score (70% weight) + proportion_score = proportion * 100.0 * WEIGHT_PROPORTION + + # Chroma score: (chroma - target) * weight + # This gives bonus for high chroma, penalty for low chroma + if hct.chroma < TARGET_CHROMA: + chroma_weight = WEIGHT_CHROMA_BELOW + else: + chroma_weight = WEIGHT_CHROMA_ABOVE + chroma_score = (hct.chroma - TARGET_CHROMA) * chroma_weight + + score = proportion_score + chroma_score + scored_hcts.append((color, hct, score)) + + if not scored_hcts: + # Fallback if filtering removed everything + result = [] + for rgb, count in colors_with_counts: + color = Color.from_rgb(rgb) + result.append((color, float(count))) + return sorted(result, key=lambda x: -x[1]) + + # Sort by score descending + scored_hcts.sort(key=lambda x: -x[2]) + + # Deduplicate by hue distance - pick colors maximizing hue diversity + # Start at 90° minimum distance, decrease to 15° if needed + chosen_colors: list[tuple[Color, float]] = [] + + for min_hue_diff in range(90, 14, -1): + chosen_colors.clear() + for color, hct, score in scored_hcts: + # Check if this hue is far enough from all chosen colors + is_far_enough = True + for chosen_color, _ in chosen_colors: + chosen_hct = chosen_color.to_hct() + if hue_distance(hct.hue, chosen_hct.hue) < min_hue_diff: + is_far_enough = False + break + + if is_far_enough: + chosen_colors.append((color, score)) + + # Stop if we have enough colors (4 is Material default) + if len(chosen_colors) >= 4: + break + + # If we found enough colors, stop decreasing threshold + if len(chosen_colors) >= 4: + break + + # If deduplication yielded nothing, fall back to top scored + if not chosen_colors: + chosen_colors = [(c, s) for c, h, s in scored_hcts[:4]] + + return chosen_colors + + +def extract_palette( + pixels: list[RGB], + k: int = 5, + scoring: str = "population" +) -> list[Color]: + """ + Extract K dominant colors from pixel data. + + Args: + pixels: List of RGB tuples + k: Number of colors to extract + scoring: Scoring method: + - "population": matugen-like, representative colors (M3 schemes) + - "chroma": vibrant, chroma-prioritized with centroid averaging + - "count": area-dominant, picks by pixel count (faithful mode) + - "dysfunctional": picks 2nd most dominant color family + - "muted": like count but without chroma filtering (monochrome wallpapers) + + Returns: + List of Color objects, sorted by score + """ + # Downsample for performance + sampled = downsample_pixels(pixels, factor=4) + total_sampled = len(sampled) + + # For population scoring, we need many clusters then score/filter them + # For chroma scoring, fewer clusters work fine + if scoring == "population": + # Use more clusters for Material scoring (like matugen's 128-256) + cluster_count = min(128, max(k * 10, len(set(sampled)) // 10)) + # Don't pre-filter for population scoring - let the Score algorithm filter + # This matches matugen which quantizes all pixels, then filters in scoring + filtered = sampled + elif scoring == "count": + # Faithful mode: many clusters to capture color diversity, no pre-filtering + # Scoring will filter to colorful colors and pick by count + cluster_count = 48 + filtered = sampled + elif scoring == "dysfunctional": + # Dysfunctional mode: same as count but picks 2nd dominant family + cluster_count = 48 + filtered = sampled + elif scoring == "muted": + # Muted mode: similar to count but accepts low-chroma colors + # For monochrome/monotonal wallpapers + cluster_count = 24 + filtered = sampled + else: + # Vibrant mode: more clusters to capture high-chroma colors that might + # otherwise get averaged away, with colorfulness pre-filter + cluster_count = 20 + # Filter to colorful pixels for smoother averaged results + filtered = [] + for p in sampled: + try: + cam = Cam16.from_rgb(p[0], p[1], p[2]) + if cam.chroma >= 5.0: + filtered.append(p) + except (ValueError, ZeroDivisionError): + continue + + if len(filtered) < cluster_count * 2: + filtered = sampled + + # Cluster - returns (centroid_rgb, representative_rgb, count) tuples + clusters = kmeans_cluster(filtered, k=cluster_count) + + # Score colors based on method + # - chroma: centroid colors (averaged, smoother - vibrant mode) + # - count: representative pixels by area dominance (faithful mode) + # - muted: like count but accepts low/zero chroma (monochrome wallpapers) + # - population: representative colors with Material scoring (M3 schemes) + if scoring == "chroma": + # Use centroid colors for vibrant mode (smoother, blended) + colors_for_scoring = [(c[0], c[2]) for c in clusters] + scored = _score_colors_chroma(colors_for_scoring) + elif scoring == "count": + # Use representative colors with count scoring (faithful mode) + colors_for_scoring = [(c[1], c[2]) for c in clusters] + scored = _score_colors_count(colors_for_scoring) + elif scoring == "dysfunctional": + # Use representative colors with dysfunctional scoring (2nd dominant family) + colors_for_scoring = [(c[1], c[2]) for c in clusters] + scored = _score_colors_dysfunctional(colors_for_scoring) + elif scoring == "muted": + # Use representative colors with muted scoring (no chroma filter) + colors_for_scoring = [(c[1], c[2]) for c in clusters] + scored = _score_colors_muted(colors_for_scoring) + else: + # Use representative colors for M3 schemes + colors_for_scoring = [(c[1], c[2]) for c in clusters] + scored = _score_colors_population(colors_for_scoring, total_sampled) + + # Extract colors + final_colors = [c[0] for c in scored] + + # Ensure we have enough colors by deriving from primary using HCT + while len(final_colors) < k: + if not final_colors: + final_colors.append(Color.from_hex("#6750A4")) + continue + + primary = final_colors[0] + primary_hct = primary.to_hct() + offset = len(final_colors) * 60.0 + new_hct = Hct((primary_hct.hue + offset) % 360.0, primary_hct.chroma, primary_hct.tone) + final_colors.append(Color.from_hct(new_hct)) + + return final_colors[:k] + + +def find_error_color(palette: list[Color]) -> Color: + """ + Find or generate an error color (red-biased). + + Looks for existing red in palette, otherwise returns a default. + """ + # Look for a red-ish color in the palette + for color in palette: + h, s, l = color.to_hsl() + # Red hues: 0-30 or 330-360 + if (h <= 30 or h >= 330) and s > 0.4 and 0.3 < l < 0.7: + return color + + # Default error red + return Color.from_hex("#FD4663") + + diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/quantizer.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/quantizer.py new file mode 100644 index 0000000..2432ff2 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/quantizer.py @@ -0,0 +1,815 @@ +""" +Wu and WSMeans quantizer implementations matching material-color-utilities. + +Wu implements Xiaolin Wu's color quantization algorithm from Graphics Gems II (1991). +WSMeans refines Wu's output via weighted k-means in Lab space (QuantizerCelebi pipeline). +Together they match the QuantizerCelebi pipeline used by matugen/material-color-utilities. +""" + +from typing import Dict, List, Tuple + +from .color import rgb_to_lab, lab_to_rgb + +# Constants matching material-color-utilities +INDEX_BITS = 5 +SIDE_LENGTH = 33 # (1 << INDEX_BITS) + 1 +TOTAL_SIZE = 35937 # SIDE_LENGTH^3 + +# Direction constants +DIR_RED = 0 +DIR_GREEN = 1 +DIR_BLUE = 2 + + +class Box: + """Represents a box in RGB color space.""" + __slots__ = ('r0', 'r1', 'g0', 'g1', 'b0', 'b1', 'vol') + + def __init__(self): + self.r0 = 0 + self.r1 = 0 + self.g0 = 0 + self.g1 = 0 + self.b0 = 0 + self.b1 = 0 + self.vol = 0 + + +def _get_index(r: int, g: int, b: int) -> int: + """Calculate 3D array index from RGB coordinates.""" + return (r << (INDEX_BITS * 2)) + (r << (INDEX_BITS + 1)) + r + (g << INDEX_BITS) + g + b + + +def _argb_from_rgb(r: int, g: int, b: int) -> int: + """Convert RGB to ARGB integer format.""" + return (255 << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF) + + +def _rgb_from_argb(argb: int) -> Tuple[int, int, int]: + """Extract RGB from ARGB integer.""" + return ((argb >> 16) & 0xFF, (argb >> 8) & 0xFF, argb & 0xFF) + + +class QuantizerWu: + """ + Wu color quantizer implementation. + + Divides image pixels into clusters by recursively cutting an RGB cube, + based on the weight of pixels in each area of the cube. + """ + + def __init__(self): + self.weights: List[int] = [] + self.moments_r: List[int] = [] + self.moments_g: List[int] = [] + self.moments_b: List[int] = [] + self.moments: List[float] = [] + self.cubes: List[Box] = [] + + def quantize(self, pixels: List[int], max_colors: int) -> List[int]: + """ + Quantize pixels to a reduced color palette. + + Args: + pixels: List of colors in ARGB integer format + max_colors: Maximum number of colors to return + + Returns: + List of colors in ARGB format + """ + self._construct_histogram(pixels) + self._compute_moments() + result_count = self._create_boxes(max_colors) + return self._create_result(result_count) + + def _construct_histogram(self, pixels: List[int]): + """Build histogram of pixel colors.""" + self.weights = [0] * TOTAL_SIZE + self.moments_r = [0] * TOTAL_SIZE + self.moments_g = [0] * TOTAL_SIZE + self.moments_b = [0] * TOTAL_SIZE + self.moments = [0.0] * TOTAL_SIZE + + # Count pixels by color + count_by_color: Dict[int, int] = {} + for pixel in pixels: + # Only count fully opaque pixels + if (pixel >> 24) & 0xFF == 255: + count_by_color[pixel] = count_by_color.get(pixel, 0) + 1 + + bits_to_remove = 8 - INDEX_BITS + for pixel, count in count_by_color.items(): + red = (pixel >> 16) & 0xFF + green = (pixel >> 8) & 0xFF + blue = pixel & 0xFF + + i_r = (red >> bits_to_remove) + 1 + i_g = (green >> bits_to_remove) + 1 + i_b = (blue >> bits_to_remove) + 1 + index = _get_index(i_r, i_g, i_b) + + self.weights[index] += count + self.moments_r[index] += count * red + self.moments_g[index] += count * green + self.moments_b[index] += count * blue + self.moments[index] += count * (red * red + green * green + blue * blue) + + def _compute_moments(self): + """Compute cumulative moments for efficient volume calculations.""" + for r in range(1, SIDE_LENGTH): + area = [0] * SIDE_LENGTH + area_r = [0] * SIDE_LENGTH + area_g = [0] * SIDE_LENGTH + area_b = [0] * SIDE_LENGTH + area2 = [0.0] * SIDE_LENGTH + + for g in range(1, SIDE_LENGTH): + line = 0 + line_r = 0 + line_g = 0 + line_b = 0 + line2 = 0.0 + + for b in range(1, SIDE_LENGTH): + index = _get_index(r, g, b) + line += self.weights[index] + line_r += self.moments_r[index] + line_g += self.moments_g[index] + line_b += self.moments_b[index] + line2 += self.moments[index] + + area[b] += line + area_r[b] += line_r + area_g[b] += line_g + area_b[b] += line_b + area2[b] += line2 + + prev_index = _get_index(r - 1, g, b) + self.weights[index] = self.weights[prev_index] + area[b] + self.moments_r[index] = self.moments_r[prev_index] + area_r[b] + self.moments_g[index] = self.moments_g[prev_index] + area_g[b] + self.moments_b[index] = self.moments_b[prev_index] + area_b[b] + self.moments[index] = self.moments[prev_index] + area2[b] + + def _create_boxes(self, max_colors: int) -> int: + """Create color boxes by recursive cutting.""" + self.cubes = [Box() for _ in range(max_colors)] + volume_variance = [0.0] * max_colors + + # Initialize first box to cover entire color space + self.cubes[0].r1 = SIDE_LENGTH - 1 + self.cubes[0].g1 = SIDE_LENGTH - 1 + self.cubes[0].b1 = SIDE_LENGTH - 1 + + generated_color_count = max_colors + next_box = 0 + i = 1 + + while i < max_colors: + if self._cut(self.cubes[next_box], self.cubes[i]): + volume_variance[next_box] = ( + self._variance(self.cubes[next_box]) + if self.cubes[next_box].vol > 1 else 0.0 + ) + volume_variance[i] = ( + self._variance(self.cubes[i]) + if self.cubes[i].vol > 1 else 0.0 + ) + else: + volume_variance[next_box] = 0.0 + i -= 1 + + # Find box with maximum variance + next_box = 0 + temp = volume_variance[0] + for j in range(1, i + 1): + if volume_variance[j] > temp: + temp = volume_variance[j] + next_box = j + + if temp <= 0.0: + generated_color_count = i + 1 + break + + i += 1 + + return generated_color_count + + def _create_result(self, color_count: int) -> List[int]: + """Extract final colors from boxes.""" + colors = [] + for i in range(color_count): + cube = self.cubes[i] + weight = self._volume(cube, self.weights) + if weight > 0: + r = int(self._volume(cube, self.moments_r) / weight) + g = int(self._volume(cube, self.moments_g) / weight) + b = int(self._volume(cube, self.moments_b) / weight) + color = _argb_from_rgb(r, g, b) + colors.append(color) + return colors + + def _variance(self, cube: Box) -> float: + """Calculate variance within a box.""" + dr = self._volume(cube, self.moments_r) + dg = self._volume(cube, self.moments_g) + db = self._volume(cube, self.moments_b) + + xx = ( + self.moments[_get_index(cube.r1, cube.g1, cube.b1)] + - self.moments[_get_index(cube.r1, cube.g1, cube.b0)] + - self.moments[_get_index(cube.r1, cube.g0, cube.b1)] + + self.moments[_get_index(cube.r1, cube.g0, cube.b0)] + - self.moments[_get_index(cube.r0, cube.g1, cube.b1)] + + self.moments[_get_index(cube.r0, cube.g1, cube.b0)] + + self.moments[_get_index(cube.r0, cube.g0, cube.b1)] + - self.moments[_get_index(cube.r0, cube.g0, cube.b0)] + ) + + hypotenuse = dr * dr + dg * dg + db * db + volume = self._volume(cube, self.weights) + if volume == 0: + return 0.0 + return xx - hypotenuse / volume + + def _cut(self, one: Box, two: Box) -> bool: + """Cut a box into two boxes along the optimal axis.""" + whole_r = self._volume(one, self.moments_r) + whole_g = self._volume(one, self.moments_g) + whole_b = self._volume(one, self.moments_b) + whole_w = self._volume(one, self.weights) + + max_r_cut, max_r = self._maximize( + one, DIR_RED, one.r0 + 1, one.r1, whole_r, whole_g, whole_b, whole_w + ) + max_g_cut, max_g = self._maximize( + one, DIR_GREEN, one.g0 + 1, one.g1, whole_r, whole_g, whole_b, whole_w + ) + max_b_cut, max_b = self._maximize( + one, DIR_BLUE, one.b0 + 1, one.b1, whole_r, whole_g, whole_b, whole_w + ) + + if max_r >= max_g and max_r >= max_b: + if max_r_cut < 0: + return False + direction = DIR_RED + cut_location = max_r_cut + elif max_g >= max_r and max_g >= max_b: + direction = DIR_GREEN + cut_location = max_g_cut + else: + direction = DIR_BLUE + cut_location = max_b_cut + + two.r1 = one.r1 + two.g1 = one.g1 + two.b1 = one.b1 + + if direction == DIR_RED: + one.r1 = cut_location + two.r0 = one.r1 + two.g0 = one.g0 + two.b0 = one.b0 + elif direction == DIR_GREEN: + one.g1 = cut_location + two.r0 = one.r0 + two.g0 = one.g1 + two.b0 = one.b0 + else: # DIR_BLUE + one.b1 = cut_location + two.r0 = one.r0 + two.g0 = one.g0 + two.b0 = one.b1 + + one.vol = (one.r1 - one.r0) * (one.g1 - one.g0) * (one.b1 - one.b0) + two.vol = (two.r1 - two.r0) * (two.g1 - two.g0) * (two.b1 - two.b0) + return True + + def _maximize( + self, + cube: Box, + direction: int, + first: int, + last: int, + whole_r: int, + whole_g: int, + whole_b: int, + whole_w: int, + ) -> Tuple[int, float]: + """Find the optimal cut position along an axis.""" + bottom_r = self._bottom(cube, direction, self.moments_r) + bottom_g = self._bottom(cube, direction, self.moments_g) + bottom_b = self._bottom(cube, direction, self.moments_b) + bottom_w = self._bottom(cube, direction, self.weights) + + max_val = 0.0 + cut = -1 + + for i in range(first, last): + half_r = bottom_r + self._top(cube, direction, i, self.moments_r) + half_g = bottom_g + self._top(cube, direction, i, self.moments_g) + half_b = bottom_b + self._top(cube, direction, i, self.moments_b) + half_w = bottom_w + self._top(cube, direction, i, self.weights) + + if half_w == 0: + continue + + temp = (half_r * half_r + half_g * half_g + half_b * half_b) / half_w + + half_r = whole_r - half_r + half_g = whole_g - half_g + half_b = whole_b - half_b + half_w = whole_w - half_w + + if half_w == 0: + continue + + temp += (half_r * half_r + half_g * half_g + half_b * half_b) / half_w + + if temp > max_val: + max_val = temp + cut = i + + return cut, max_val + + def _volume(self, cube: Box, moment: List) -> int: + """Calculate volume sum using inclusion-exclusion.""" + return ( + moment[_get_index(cube.r1, cube.g1, cube.b1)] + - moment[_get_index(cube.r1, cube.g1, cube.b0)] + - moment[_get_index(cube.r1, cube.g0, cube.b1)] + + moment[_get_index(cube.r1, cube.g0, cube.b0)] + - moment[_get_index(cube.r0, cube.g1, cube.b1)] + + moment[_get_index(cube.r0, cube.g1, cube.b0)] + + moment[_get_index(cube.r0, cube.g0, cube.b1)] + - moment[_get_index(cube.r0, cube.g0, cube.b0)] + ) + + def _bottom(self, cube: Box, direction: int, moment: List) -> int: + """Calculate bottom sum for maximize.""" + if direction == DIR_RED: + return ( + -moment[_get_index(cube.r0, cube.g1, cube.b1)] + + moment[_get_index(cube.r0, cube.g1, cube.b0)] + + moment[_get_index(cube.r0, cube.g0, cube.b1)] + - moment[_get_index(cube.r0, cube.g0, cube.b0)] + ) + elif direction == DIR_GREEN: + return ( + -moment[_get_index(cube.r1, cube.g0, cube.b1)] + + moment[_get_index(cube.r1, cube.g0, cube.b0)] + + moment[_get_index(cube.r0, cube.g0, cube.b1)] + - moment[_get_index(cube.r0, cube.g0, cube.b0)] + ) + else: # DIR_BLUE + return ( + -moment[_get_index(cube.r1, cube.g1, cube.b0)] + + moment[_get_index(cube.r1, cube.g0, cube.b0)] + + moment[_get_index(cube.r0, cube.g1, cube.b0)] + - moment[_get_index(cube.r0, cube.g0, cube.b0)] + ) + + def _top(self, cube: Box, direction: int, position: int, moment: List) -> int: + """Calculate top sum for maximize.""" + if direction == DIR_RED: + return ( + moment[_get_index(position, cube.g1, cube.b1)] + - moment[_get_index(position, cube.g1, cube.b0)] + - moment[_get_index(position, cube.g0, cube.b1)] + + moment[_get_index(position, cube.g0, cube.b0)] + ) + elif direction == DIR_GREEN: + return ( + moment[_get_index(cube.r1, position, cube.b1)] + - moment[_get_index(cube.r1, position, cube.b0)] + - moment[_get_index(cube.r0, position, cube.b1)] + + moment[_get_index(cube.r0, position, cube.b0)] + ) + else: # DIR_BLUE + return ( + moment[_get_index(cube.r1, cube.g1, position)] + - moment[_get_index(cube.r1, cube.g0, position)] + - moment[_get_index(cube.r0, cube.g1, position)] + + moment[_get_index(cube.r0, cube.g0, position)] + ) + + +def quantize_wu(pixels: List[Tuple[int, int, int]], max_colors: int = 128) -> Dict[int, int]: + """ + Quantize RGB pixels using Wu algorithm. + + Args: + pixels: List of (R, G, B) tuples + max_colors: Maximum colors to extract + + Returns: + Dictionary mapping ARGB colors to pixel counts + """ + # Convert RGB tuples to ARGB integers + argb_pixels = [_argb_from_rgb(r, g, b) for r, g, b in pixels] + + # Run Wu quantizer + quantizer = QuantizerWu() + result_colors = quantizer.quantize(argb_pixels, max_colors) + + # Build color to count mapping in box order (matching Rust's IndexMap insertion order) + # Wu returns colors with count 0; WSMeans uses only the keys as starting clusters + color_to_count: Dict[int, int] = {c: 0 for c in result_colors} + + return color_to_count + + +# ============================================================================= +# WSMeans Quantizer - weighted k-means refinement in Lab space +# ============================================================================= + +# Mask for 48-bit LCG state +_LCG_MASK = (1 << 48) - 1 + + +class _Random: + """LCG matching Java's java.util.Random / material-color-utilities.""" + + def __init__(self, seed: int): + self._seed = (seed ^ 0x5DEECE66D) & _LCG_MASK + + def _next(self, bits: int) -> int: + self._seed = (self._seed * 0x5DEECE66D + 0xB) & _LCG_MASK + # Unsigned right shift: treat as unsigned 48-bit, shift, return as signed 32-bit + val = self._seed >> (48 - bits) + # Convert to signed 32-bit int to match Java behavior + if val >= (1 << 31): + val -= (1 << 32) + return val + + def next_range(self, range_val: int) -> int: + if (range_val & -range_val) == range_val: + # Power of 2 + return (range_val * self._next(31)) >> 31 + while True: + bits = self._next(31) + val = bits % range_val + if bits - val + (range_val - 1) >= 0: + return val + + +def _lab_distance_squared(a: Tuple[float, float, float], b: Tuple[float, float, float]) -> float: + """Squared Euclidean distance in Lab space (no sqrt).""" + dL = a[0] - b[0] + da = a[1] - b[1] + db = a[2] - b[2] + return dL * dL + da * da + db * db + + +def quantize_wsmeans( + pixels: List[Tuple[int, int, int]], + max_colors: int, + starting_clusters: List[int], +) -> Dict[int, int]: + """ + Refine quantized colors via weighted k-means in Lab space. + + Port of QuantizerWsmeans from material-colors-0.4.2 Rust crate. + + Args: + pixels: List of (R, G, B) tuples (original image pixels) + max_colors: Maximum number of colors + starting_clusters: List of ARGB colors from Wu quantizer + + Returns: + Dictionary mapping ARGB colors to pixel counts + """ + # Deduplicate pixels, build count map and Lab points + pixel_to_count: Dict[int, int] = {} + unique_pixels: List[int] = [] # ARGB values in insertion order + points: List[Tuple[float, float, float]] = [] # Lab coordinates + + for r, g, b in pixels: + argb = _argb_from_rgb(r, g, b) + if argb in pixel_to_count: + pixel_to_count[argb] += 1 + else: + unique_pixels.append(argb) + points.append(rgb_to_lab(r, g, b)) + pixel_to_count[argb] = 1 + + cluster_count = min(max_colors, len(points)) + if cluster_count == 0: + return {} + + # Convert starting clusters from ARGB to Lab + clusters: List[Tuple[float, float, float]] = [] + for argb in starting_clusters: + cr, cg, cb = _rgb_from_argb(argb) + clusters.append(rgb_to_lab(cr, cg, cb)) + + # Fill remaining clusters with actual image pixels using seeded LCG + additional_needed = cluster_count - len(clusters) + if additional_needed > 0: + rng = _Random(0x42688) + indices: List[int] = [] + for _ in range(additional_needed): + index = rng.next_range(len(points)) + while index in indices: + index = rng.next_range(len(points)) + indices.append(index) + for index in indices: + clusters.append(points[index]) + + # Initialize assignments + cluster_indices = [i % cluster_count for i in range(len(points))] + + # Distance matrix and sorted index matrix + distance_to_index_matrix: List[List[List]] = [ + [[0.0, j] for j in range(cluster_count)] + for _ in range(cluster_count) + ] + pixel_count_sums = [0] * cluster_count + + for iteration in range(10): + points_moved = 0 + + # Compute inter-cluster distance matrix + for i in range(cluster_count): + for j in range(i + 1, cluster_count): + dist = _lab_distance_squared(clusters[i], clusters[j]) + distance_to_index_matrix[j][i][0] = dist + distance_to_index_matrix[j][i][1] = i + distance_to_index_matrix[i][j][0] = dist + distance_to_index_matrix[i][j][1] = j + + # Sort row by distance + distance_to_index_matrix[i].sort(key=lambda x: x[0]) + + # Assignment step: find nearest cluster for each point + for i in range(len(points)): + point = points[i] + prev_idx = cluster_indices[i] + prev_dist = _lab_distance_squared(point, clusters[prev_idx]) + + min_dist = prev_dist + new_idx = -1 + + for j in range(cluster_count): + # Triangle inequality: skip if inter-cluster dist >= 4 * current dist + if distance_to_index_matrix[prev_idx][j][0] >= 4.0 * prev_dist: + continue + + dist = _lab_distance_squared(point, clusters[j]) + if dist < min_dist: + min_dist = dist + new_idx = j + + if new_idx != -1: + points_moved += 1 + cluster_indices[i] = new_idx + + # Early stop + if points_moved == 0 and iteration > 0: + break + + # Update step: compute new centroids as weighted mean in Lab space + component_l = [0.0] * cluster_count + component_a = [0.0] * cluster_count + component_b = [0.0] * cluster_count + for k in range(cluster_count): + pixel_count_sums[k] = 0 + + for i in range(len(points)): + cidx = cluster_indices[i] + pt = points[i] + count = pixel_to_count[unique_pixels[i]] + pixel_count_sums[cidx] += count + component_l[cidx] += pt[0] * count + component_a[cidx] += pt[1] * count + component_b[cidx] += pt[2] * count + + for i in range(cluster_count): + count = pixel_count_sums[i] + if count == 0: + clusters[i] = (0.0, 0.0, 0.0) + else: + clusters[i] = ( + component_l[i] / count, + component_a[i] / count, + component_b[i] / count, + ) + + # Build result: convert cluster centroids from Lab to ARGB with populations + cluster_argbs: List[int] = [] + cluster_populations: List[int] = [] + + for i in range(cluster_count): + count = pixel_count_sums[i] + if count == 0: + continue + + lab = clusters[i] + cr, cg, cb = lab_to_rgb(lab[0], lab[1], lab[2]) + argb = _argb_from_rgb(cr, cg, cb) + + if argb in cluster_argbs: + continue + + cluster_argbs.append(argb) + cluster_populations.append(count) + + color_to_count: Dict[int, int] = {} + for i in range(len(cluster_argbs)): + color_to_count[cluster_argbs[i]] = cluster_populations[i] + + return color_to_count + + +# ============================================================================= +# Score Algorithm - ranks colors for UI theme suitability +# ============================================================================= + +# Score constants matching material-color-utilities +TARGET_CHROMA = 48.0 +WEIGHT_PROPORTION = 0.7 +WEIGHT_CHROMA_ABOVE = 0.3 +WEIGHT_CHROMA_BELOW = 0.1 +CUTOFF_CHROMA = 5.0 +CUTOFF_EXCITED_PROPORTION = 0.01 +FALLBACK_COLOR_ARGB = 0xFF4285F4 # Google Blue + + +def _sanitize_degrees(degrees: float) -> int: + """Sanitize degrees to 0-359 range.""" + return int(degrees) % 360 + + +def _difference_degrees(a: float, b: float) -> float: + """Calculate the shortest distance between two angles.""" + diff = abs(a - b) + return min(diff, 360.0 - diff) + + +def score_colors( + color_to_population: Dict[int, int], + desired: int = 4, + fallback_color: int = FALLBACK_COLOR_ARGB, + filter_colors: bool = True, +) -> List[int]: + """ + Rank colors based on suitability for UI themes. + + Given a map of colors to population counts, removes unsuitable colors + and ranks the rest based on chroma and proportion. + + Args: + color_to_population: Dict mapping ARGB colors to pixel counts + desired: Maximum number of colors to return + fallback_color: Color to return if no suitable colors found + filter_colors: Whether to filter out low-chroma/low-proportion colors + + Returns: + List of ARGB colors sorted by suitability (best first) + """ + # Import here to avoid circular dependency + from .hct import Cam16, Hct + + # Build HCT colors and hue population histogram + colors_hct: List[Tuple[int, Hct]] = [] + hue_population = [0] * 360 + population_sum = 0 + + for argb, population in color_to_population.items(): + r = (argb >> 16) & 0xFF + g = (argb >> 8) & 0xFF + b = argb & 0xFF + + try: + hct = Hct.from_rgb(r, g, b) + colors_hct.append((argb, hct)) + hue = _sanitize_degrees(hct.hue) + hue_population[hue] += population + population_sum += population + except (ValueError, ZeroDivisionError): + continue + + if not colors_hct or population_sum == 0: + return [fallback_color] + + # Calculate "excited proportions" - sum of proportions in ±15° hue window + hue_excited_proportions = [0.0] * 360 + for hue in range(360): + proportion = hue_population[hue] / population_sum + for offset in range(-14, 16): + neighbor_hue = _sanitize_degrees(hue + offset) + hue_excited_proportions[neighbor_hue] += proportion + + # Score each color + scored_hct: List[Tuple[int, Hct, float]] = [] + for argb, hct in colors_hct: + hue = _sanitize_degrees(round(hct.hue)) + proportion = hue_excited_proportions[hue] + + # Filter by chroma and proportion + if filter_colors: + if hct.chroma < CUTOFF_CHROMA: + continue + if proportion <= CUTOFF_EXCITED_PROPORTION: + continue + + # Proportion score (70% weight) + proportion_score = proportion * 100.0 * WEIGHT_PROPORTION + + # Chroma score + if hct.chroma < TARGET_CHROMA: + chroma_weight = WEIGHT_CHROMA_BELOW + else: + chroma_weight = WEIGHT_CHROMA_ABOVE + chroma_score = (hct.chroma - TARGET_CHROMA) * chroma_weight + + score = proportion_score + chroma_score + scored_hct.append((argb, hct, score)) + + if not scored_hct: + return [fallback_color] + + # Sort by score descending + scored_hct.sort(key=lambda x: -x[2]) + + # Deduplicate by hue distance - maximize hue diversity + # Start at 90° (max for 4 colors), decrease to 15° minimum + chosen_colors: List[Tuple[int, Hct]] = [] + + for diff_degrees in range(90, 14, -1): + chosen_colors.clear() + for argb, hct, score in scored_hct: + # Check if this hue is far enough from all chosen colors + is_duplicate = False + for chosen_argb, chosen_hct in chosen_colors: + if _difference_degrees(hct.hue, chosen_hct.hue) < diff_degrees: + is_duplicate = True + break + + if not is_duplicate: + chosen_colors.append((argb, hct)) + + if len(chosen_colors) >= desired: + break + + if len(chosen_colors) >= desired: + break + + if not chosen_colors: + return [fallback_color] + + return [argb for argb, hct in chosen_colors] + + +def extract_source_color( + pixels: List[Tuple[int, int, int]], + fallback_color: int = FALLBACK_COLOR_ARGB, +) -> int: + """ + Extract the primary source color from image pixels. + + Uses Wu + WSMeans quantizer (QuantizerCelebi) + Score algorithm matching + matugen/material-color-utilities. + + Args: + pixels: List of (R, G, B) tuples + fallback_color: Color to return if extraction fails + + Returns: + Source color in ARGB format + """ + from .hct import Cam16 + + if not pixels: + return fallback_color + + # Quantize using Wu + WSMeans (QuantizerCelebi pipeline like matugen) + wu_result = quantize_wu(pixels, max_colors=128) + starting_clusters = list(wu_result.keys()) + color_to_count = quantize_wsmeans(pixels, 128, starting_clusters) + + # Filter out low-chroma colors before scoring (like matugen) + filtered = {} + for argb, count in color_to_count.items(): + r = (argb >> 16) & 0xFF + g = (argb >> 8) & 0xFF + b = argb & 0xFF + try: + cam = Cam16.from_rgb(r, g, b) + if cam.chroma >= 5.0: + filtered[argb] = count + except (ValueError, ZeroDivisionError): + continue + + if not filtered: + filtered = color_to_count + + # Score and rank colors + ranked = score_colors(filtered, desired=4, fallback_color=fallback_color) + + return ranked[0] if ranked else fallback_color + + +def source_color_to_rgb(argb: int) -> Tuple[int, int, int]: + """Convert ARGB integer to RGB tuple.""" + return _rgb_from_argb(argb) diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/renderer.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/renderer.py new file mode 100644 index 0000000..bb900c3 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/renderer.py @@ -0,0 +1,1181 @@ +""" +Template rendering for Matugen compatibility. + +This module provides the TemplateRenderer class for processing template files +using the {{colors.name.mode.format}} syntax compatible with Matugen. + +Supports: +- Pipe filters: {{ colors.primary.dark.hex | set_alpha 0.5 | grayscale }} +- For loops: <* for name, value in colors *> ... <* endfor *> +- If/else: <* if {{ expr }} *> ... <* else *> ... <* endif *> +- Loop variables: loop.index, loop.first, loop.last +- Color filters: grayscale, invert, set_alpha, set_lightness, set_hue, + set_saturation, set_red, set_green, set_blue, lighten, darken, + saturate, desaturate, auto_lightness, blend, harmonize, to_color +- String filters: replace, lower_case, camel_case, pascal_case, + snake_case, kebab_case +- Custom colors: [config.custom_colors] in TOML config generates + {name}, on_{name}, {name}_container, on_{name}_container tokens +""" + +import re +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Optional, Union + +try: + import tomllib +except ImportError: + tomllib = None + +from .color import Color, find_closest_color +from .hct import Hct + + +# --- Node Types for the template AST --- + +@dataclass +class TextNode: + text: str + +@dataclass +class ForNode: + variables: list[str] + iterable: str + body: list + +@dataclass +class IfNode: + condition_expr: str + negated: bool + then_body: list + else_body: list = field(default_factory=list) + + +# --- Variable Scope Stack --- + +class VariableScope: + """Stack-based variable scope for loop variables.""" + + def __init__(self): + self._scopes: list[dict[str, Any]] = [] + + def push(self, bindings: Optional[dict[str, Any]] = None): + self._scopes.append(bindings or {}) + + def pop(self): + if self._scopes: + self._scopes.pop() + + def set(self, name: str, value: Any): + if self._scopes: + self._scopes[-1][name] = value + + def get(self, name: str) -> Optional[Any]: + """Look up variable, searching from innermost scope outward.""" + for scope in reversed(self._scopes): + if name in scope: + return scope[name] + return None + + @property + def is_active(self) -> bool: + return len(self._scopes) > 0 + + +# --- Known color format types --- + +KNOWN_FORMATS = frozenset({ + "hex", "hex_stripped", "rgb", "rgba", "hsl", "hsla", + "red", "green", "blue", "alpha", "hue", "saturation", "lightness", +}) + + +class TemplateRenderer: + """ + Renders templates using the generated theme colors. + Compatible with Matugen-style {{colors.name.mode.format}} tags. + + Supports filters via pipe syntax: + {{ colors.primary.dark.hex | grayscale }} + {{ colors.primary.dark.rgba | set_alpha 0.5 }} + + Supports block constructs: + <* for name, value in colors *> ... <* endfor *> + <* if {{ loop.first }} *> ... <* else *> ... <* endif *> + + Theme data uses snake_case keys (e.g., 'primary', 'surface_container'). + """ + + # Aliases for custom/legacy keys + COLOR_ALIASES = { + "hover": "surface_container_high", + "on_hover": "on_surface", + } + + # Supported color filters and their argument requirements + SUPPORTED_FILTERS = { + # No arguments + "grayscale": 0, + "invert": 0, + # One argument (float) + "set_alpha": 1, + "set_lightness": 1, + "set_hue": 1, + "set_saturation": 1, + "set_red": 1, + "set_green": 1, + "set_blue": 1, + "lighten": 1, + "darken": 1, + "saturate": 1, + "desaturate": 1, + "auto_lightness": 1, + } + + # Filters that take a hex color argument (+ optional numeric) + COLOR_ARG_FILTERS = {"blend", "harmonize"} + + # Regex for block delimiters: <* ... *> + _BLOCK_RE = re.compile(r'<\*(.*?)\*>', re.DOTALL) + + # Regex for expression tags: {{ ... }} + _EXPR_RE = re.compile(r"\{\{([^}\n]+?)\}\}") + + def __init__(self, theme_data: dict[str, dict[str, str]], verbose: bool = True, default_mode: str = "dark", image_path: Optional[str] = None, scheme_type: str = "content"): + self.theme_data = theme_data + self.closest_color = "" + self.verbose = verbose + self.default_mode = default_mode + self.image_path = image_path + self.scheme_type = scheme_type + self._current_file: Optional[str] = None + self._error_count = 0 + self._colors_map: Optional[dict[str, dict[str, str]]] = None + + def _log_error(self, message: str, line_hint: str = ""): + """Log an error to stderr.""" + self._error_count += 1 + prefix = f"[{self._current_file}] " if self._current_file else "" + hint = f" near '{line_hint}'" if line_hint else "" + print(f"Template error: {prefix}{message}{hint}", file=sys.stderr) + + def _log_warning(self, message: str): + """Log a warning to stderr.""" + if self.verbose: + prefix = f"[{self._current_file}] " if self._current_file else "" + print(f"Template warning: {prefix}{message}", file=sys.stderr) + + # --- Colors Map --- + + def _build_colors_map(self) -> dict[str, dict[str, str]]: + """Build iterable colors map: {name: {mode: hex_value, ...}}.""" + if self._colors_map is not None: + return self._colors_map + + colors_map: dict[str, dict[str, str]] = {} + all_names: set[str] = set() + for mode_data in self.theme_data.values(): + all_names.update(mode_data.keys()) + + for name in sorted(all_names): + color_modes: dict[str, str] = {} + for mode, mode_data in self.theme_data.items(): + if name in mode_data: + color_modes[mode] = mode_data[name] + # Add "default" as alias for self.default_mode + if self.default_mode in color_modes: + color_modes["default"] = color_modes[self.default_mode] + colors_map[name] = color_modes + + self._colors_map = colors_map + return colors_map + + # --- Palette Support --- + + def _get_palette_entries(self, palette_name: str) -> list[dict[str, str]]: + """Get tonal palette entries for iteration.""" + from .hct import Hct, TonalPalette + + TONES = [0, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 95, 98, 99, 100] + + palette_color_map = { + "primary": "primary", + "secondary": "secondary", + "tertiary": "tertiary", + "error": "error", + "neutral": "surface", + "neutral_variant": "surface_variant", + } + + color_name = palette_color_map.get(palette_name) + if not color_name: + self._log_warning(f"Unknown palette: {palette_name}") + return [] + + mode_data = self.theme_data.get(self.default_mode, {}) + hex_color = mode_data.get(color_name) + if not hex_color: + return [] + + color = Color.from_hex(hex_color) + palette = TonalPalette.from_rgb(color.r, color.g, color.b) + + entries = [] + for tone in TONES: + tone_hex = palette.get_hex(tone) + # Each entry is a color modes dict (same hex for all modes in palette context) + entries.append({"default": tone_hex, "dark": tone_hex, "light": tone_hex}) + + return entries + + # --- Block Parser --- + + def _parse_template(self, text: str) -> list: + """Parse template text into a tree of nodes.""" + tokens = self._tokenize(text) + nodes, _ = self._parse_nodes(tokens, 0) + return nodes + + def _tokenize(self, text: str) -> list[Union[str, tuple[str, str]]]: + """Split text into raw text strings and ('block', content) tuples. + + Standalone block tags (only content on their line) consume the entire + line including the trailing newline, matching matugen's behavior. + """ + tokens = [] + last_end = 0 + + for match in self._BLOCK_RE.finditer(text): + start = match.start() + end = match.end() + + # Check if this block tag is standalone on its line: + # preceded only by whitespace (after newline or start), followed by optional whitespace + newline + line_start = text.rfind('\n', last_end, start) + line_start = line_start + 1 if line_start != -1 else (last_end if last_end <= start else 0) + before_on_line = text[line_start:start] + + if before_on_line.strip() == '': + # Check what follows the block tag + after_end = end + while after_end < len(text) and text[after_end] in (' ', '\t'): + after_end += 1 + if after_end < len(text) and text[after_end] == '\n': + # Standalone: consume leading whitespace and trailing newline + start = line_start + end = after_end + 1 + elif after_end == len(text): + # At end of file, still standalone + start = line_start + end = after_end + + # Text before the block + if start > last_end: + tokens.append(text[last_end:start]) + # The block command + tokens.append(("block", match.group(1).strip())) + last_end = end + + # Remaining text after last block + if last_end < len(text): + tokens.append(text[last_end:]) + + return tokens + + def _parse_nodes(self, tokens: list, pos: int, stop_keywords: Optional[set[str]] = None) -> tuple[list, int]: + """Parse tokens into nodes, stopping at specified keywords. + + Returns (nodes, position_after_stop_keyword). + """ + nodes = [] + stop_keywords = stop_keywords or set() + + while pos < len(tokens): + token = tokens[pos] + + if isinstance(token, str): + # Raw text + if token: + nodes.append(TextNode(token)) + pos += 1 + else: + # Block command + _, cmd = token + + # Check if this is a stop keyword + if any(cmd.startswith(kw) for kw in stop_keywords): + return nodes, pos + + if cmd.startswith("for "): + node, pos = self._parse_for(tokens, pos) + nodes.append(node) + elif cmd.startswith("if "): + node, pos = self._parse_if(tokens, pos) + nodes.append(node) + else: + # Unknown block command - treat as text + self._log_warning(f"Unknown block command: {cmd}") + pos += 1 + + return nodes, pos + + def _parse_for(self, tokens: list, pos: int) -> tuple[ForNode, int]: + """Parse a for loop block starting at pos.""" + _, cmd = tokens[pos] + pos += 1 + + # Parse: for var1, var2 in iterable + for_match = re.match(r'for\s+(.+?)\s+in\s+(.+)$', cmd) + if not for_match: + self._log_error(f"Invalid for syntax: {cmd}") + return ForNode([], "", []), pos + + vars_str, iterable = for_match.groups() + variables = [v.strip() for v in vars_str.split(',')] + iterable = iterable.strip() + + # Parse body until endfor + body, pos = self._parse_nodes(tokens, pos, stop_keywords={"endfor"}) + + # Skip past 'endfor' + if pos < len(tokens): + pos += 1 + + return ForNode(variables, iterable, body), pos + + def _parse_if(self, tokens: list, pos: int) -> tuple[IfNode, int]: + """Parse an if/else/endif block starting at pos.""" + _, cmd = tokens[pos] + pos += 1 + + # Parse: if [not] {{ expr }} + negated = False + condition_part = cmd[3:].strip() # Remove 'if ' + + if condition_part.startswith("not "): + negated = True + condition_part = condition_part[4:].strip() + + # Extract expression from {{ ... }} if present + expr_match = re.match(r'\{\{(.+?)\}\}', condition_part) + if expr_match: + condition_expr = expr_match.group(1).strip() + else: + condition_expr = condition_part + + # Parse then body until else or endif + then_body, pos = self._parse_nodes(tokens, pos, stop_keywords={"else", "endif"}) + + else_body = [] + if pos < len(tokens): + _, stop_cmd = tokens[pos] + if stop_cmd.strip() == "else": + pos += 1 + # Parse else body until endif + else_body, pos = self._parse_nodes(tokens, pos, stop_keywords={"endif"}) + + # Skip past 'endif' + if pos < len(tokens): + pos += 1 + + return IfNode(condition_expr, negated, then_body, else_body), pos + + # --- Node Evaluation --- + + def _evaluate_nodes(self, nodes: list, scope: VariableScope) -> str: + """Evaluate a list of nodes with the given variable scope.""" + parts = [] + for node in nodes: + if isinstance(node, TextNode): + parts.append(self._resolve_text(node.text, scope)) + elif isinstance(node, ForNode): + parts.append(self._evaluate_for(node, scope)) + elif isinstance(node, IfNode): + parts.append(self._evaluate_if(node, scope)) + return ''.join(parts) + + def _evaluate_for(self, node: ForNode, scope: VariableScope) -> str: + """Evaluate a for loop node.""" + iterable = self._resolve_iterable(node.iterable, scope) + if not iterable: + return "" + + results = [] + total = len(iterable) + + for index, item in enumerate(iterable): + loop_meta = { + "index": index, + "first": index == 0, + "last": index == total - 1, + } + + scope.push({"loop": loop_meta}) + + if isinstance(item, tuple) and len(item) == 2: + # Map iteration: (key, value) + if len(node.variables) >= 2: + scope.set(node.variables[0], item[0]) + scope.set(node.variables[1], item[1]) + elif len(node.variables) == 1: + scope.set(node.variables[0], item[0]) + else: + # Array or range iteration + if node.variables: + scope.set(node.variables[0], item) + + results.append(self._evaluate_nodes(node.body, scope)) + scope.pop() + + return ''.join(results) + + def _evaluate_if(self, node: IfNode, scope: VariableScope) -> str: + """Evaluate an if/else node.""" + condition_value = self._resolve_expression(node.condition_expr, scope) + is_truthy = self._is_truthy(condition_value) + + if node.negated: + is_truthy = not is_truthy + + if is_truthy: + return self._evaluate_nodes(node.then_body, scope) + else: + return self._evaluate_nodes(node.else_body, scope) + + def _is_truthy(self, value: Any) -> bool: + """Determine if a value is truthy.""" + if value is None: + return False + if isinstance(value, bool): + return value + if isinstance(value, (int, float)): + return value != 0 + s = str(value).strip().lower() + return s not in ("", "false", "0", "none") + + # --- Iterable Resolution --- + + def _resolve_iterable(self, iterable_expr: str, scope: VariableScope) -> list: + """Resolve an iterable expression to a list of items.""" + # Range: "0..10" or "-5..5" + range_match = re.match(r'^(-?\d+)\.\.(-?\d+)$', iterable_expr) + if range_match: + start, end = int(range_match.group(1)), int(range_match.group(2)) + return list(range(start, end)) + + # Colors map: "colors" + if iterable_expr == "colors": + colors_map = self._build_colors_map() + return list(colors_map.items()) + + # Palette: "palettes.primary" etc. + if iterable_expr.startswith("palettes."): + palette_name = iterable_expr.split('.', 1)[1] + entries = self._get_palette_entries(palette_name) + # Return as list of dicts (each entry is a color modes dict) + return entries + + # Check scope for iterable variable + val = scope.get(iterable_expr) + if val is not None: + if isinstance(val, dict): + return list(val.items()) + if isinstance(val, (list, tuple)): + return list(val) + + self._log_warning(f"Unknown iterable: {iterable_expr}") + return [] + + # --- Expression Resolution --- + + def _resolve_text(self, text: str, scope: VariableScope) -> str: + """Resolve all {{ expr }} tags in a text segment.""" + def replace(match): + expr = match.group(1).strip() + return str(self._resolve_expression(expr, scope)) + + return self._EXPR_RE.sub(replace, text) + + def _resolve_expression(self, expr: str, scope: VariableScope) -> Any: + """Resolve an expression, checking scope variables first, then colors.""" + # Split by pipe for filters + parts = self._split_pipes(expr) + + if not parts: + return "" + + base = parts[0].strip() + filters = [p.strip() for p in parts[1:]] + + # Handle {{mode}} tag - resolves to current theme mode + if base == "mode": + result_str = self.default_mode + for filter_str in filters: + result_str = self._apply_string_or_color_filter(result_str, filter_str, expr) + return result_str + + # Try scope resolution first + resolved = self._resolve_from_scope(base, scope) + if resolved is not None: + result_str = str(resolved) + for filter_str in filters: + result_str = self._apply_string_or_color_filter(result_str, filter_str, expr) + return result_str + + # Handle {{image}} tag - resolves to source image path + if base == 'image': + result_str = self.image_path or "" + for filter_str in filters: + result_str = self._apply_string_or_color_filter(result_str, filter_str, expr) + return result_str + + # Fall back to colors.name.mode.format parsing + if base.startswith('colors.'): + return self._process_color_expression(base, filters, expr) + + # Unknown expression - return as-is + return f"{{{{{expr}}}}}" + + def _split_pipes(self, expr: str) -> list[str]: + """Split expression by pipe, respecting quoted strings.""" + parts = [] + current = [] + in_quotes = False + quote_char = None + + for char in expr: + if char in ('"', "'") and not in_quotes: + in_quotes = True + quote_char = char + current.append(char) + elif char == quote_char and in_quotes: + in_quotes = False + quote_char = None + current.append(char) + elif char == '|' and not in_quotes: + parts.append(''.join(current)) + current = [] + else: + current.append(char) + + if current: + parts.append(''.join(current)) + + return parts + + def _resolve_from_scope(self, base: str, scope: VariableScope) -> Optional[Any]: + """Resolve a dotted path from scope variables.""" + if not scope.is_active: + return None + + parts = base.split('.') + first = parts[0] + + val = scope.get(first) + if val is None: + return None + + # Navigate dotted path + for i, part in enumerate(parts[1:], 1): + if isinstance(val, dict) and part in val: + val = val[part] + elif isinstance(val, str) and val.startswith('#') and part in KNOWN_FORMATS: + # It's a hex color string and remaining part is a format specifier + color = Color.from_hex(val) + return self._format_color(color, part) + else: + return None + + # If the final value is a hex color string, return it as-is + return val + + def _process_color_expression(self, base: str, filters: list[str], raw_expr: str) -> str: + """Process a colors.name.mode.format expression with optional filters.""" + base_match = re.match(r'^colors\.([a-z_0-9]+)\.([a-z_0-9]+)\.([a-z_0-9]+)$', base) + + if not base_match: + self._log_error(f"Invalid syntax '{base}'. Expected: colors...", raw_expr) + return f"{{{{{raw_expr}}}}}" + + color_name, mode, format_type = base_match.groups() + + hex_color = self._get_hex_color(color_name, mode) + if not hex_color: + return f"{{{{UNKNOWN:{color_name}.{mode}}}}}" + + color = Color.from_hex(hex_color) + + # Apply color filters + for filter_str in filters: + filter_name, arg = self._parse_filter(filter_str) + if filter_name: + if filter_name == "replace": + # Replace works on the formatted string, apply after formatting + formatted = self._format_color(color, format_type) + return self._apply_replace(formatted, arg, raw_expr) + elif filter_name in self.COLOR_ARG_FILTERS: + hex_result = self._apply_color_arg_filter(color.to_hex(), filter_name, arg, raw_expr) + color = Color.from_hex(hex_result) + elif filter_name == "to_color": + pass # Already a color, no-op + elif filter_name in self.SUPPORTED_FILTERS: + color = self._apply_filter(color, filter_name, arg, raw_expr) + elif filter_name in ("lower_case", "camel_case", "pascal_case", "snake_case", "kebab_case"): + # String case filters apply to formatted output + formatted = self._format_color(color, format_type) + return self._apply_string_or_color_filter(formatted, filter_str, raw_expr) + else: + self._log_warning(f"Unknown filter '{filter_name}'") + + return self._format_color(color, format_type) + + # --- Color Access --- + + def _get_hex_color(self, color_name: str, mode: str) -> Optional[str]: + """Get raw hex color value for a color name and mode.""" + key = self.COLOR_ALIASES.get(color_name, color_name) + + if mode == "default": + mode_data = self.theme_data.get(self.default_mode) or self.theme_data.get("dark") or self.theme_data.get("light") + else: + mode_data = self.theme_data.get(mode) + + if not mode_data: + self._log_error(f"Unknown mode '{mode}'", f"colors.{color_name}.{mode}") + return None + + hex_color = mode_data.get(key) + if not hex_color: + self._log_error(f"Unknown color '{key}'", f"colors.{color_name}.{mode}") + return None + + return hex_color + + def _format_color(self, color: Color, format_type: str) -> str: + """Format a Color object to the requested format string.""" + if format_type == "hex": + return color.to_hex() + elif format_type == "hex_stripped": + return color.to_hex().lstrip('#') + elif format_type == "rgb": + return f"rgb({color.r}, {color.g}, {color.b})" + elif format_type == "rgba": + alpha = getattr(color, 'alpha', 1.0) + return f"rgba({color.r}, {color.g}, {color.b}, {alpha})" + elif format_type == "hsl": + h, s, l = color.to_hsl() + return f"hsl({int(h)}, {int(s * 100)}%, {int(l * 100)}%)" + elif format_type == "hsla": + h, s, l = color.to_hsl() + alpha = getattr(color, 'alpha', 1.0) + return f"hsla({int(h)}, {int(s * 100)}%, {int(l * 100)}%, {alpha})" + elif format_type == "hue": + h, _, _ = color.to_hsl() + return str(int(h)) + elif format_type == "saturation": + _, s, _ = color.to_hsl() + return str(int(s * 100)) + elif format_type == "lightness": + _, _, l = color.to_hsl() + return str(int(l * 100)) + elif format_type == "red": + return str(color.r) + elif format_type == "green": + return str(color.g) + elif format_type == "blue": + return str(color.b) + elif format_type == "alpha": + return str(getattr(color, 'alpha', 1.0)) + else: + self._log_error(f"Unknown format '{format_type}'") + return color.to_hex() + + # --- Filters --- + + def _parse_filter(self, filter_str: str) -> tuple[str, Optional[str]]: + """Parse a filter string into (name, argument). + + Supports both syntaxes: + | set_alpha 0.5 (space-separated) + | set_alpha: 0.5 (colon-separated, matugen-style) + | replace: "_", "-" (colon with quoted args) + """ + filter_str = filter_str.strip() + + # Check for colon syntax: name: args + colon_match = re.match(r'^([a-z_]+)\s*:\s*(.+)$', filter_str) + if colon_match: + return colon_match.group(1), colon_match.group(2).strip() + + # Space syntax: name arg + if ' ' in filter_str: + name, arg = filter_str.split(None, 1) + return name.strip(), arg.strip() + + return filter_str, None + + def _apply_string_or_color_filter(self, value: str, filter_str: str, raw_expr: str) -> str: + """Apply a filter to a string value (may be a color hex or plain string).""" + name, arg = self._parse_filter(filter_str) + + if name == "replace": + return self._apply_replace(value, arg, raw_expr) + + # String case transforms (work on any string) + if name == "lower_case": + return value.lower() + if name == "camel_case": + return self._to_camel_case(value) + if name == "pascal_case": + return self._to_pascal_case(value) + if name == "snake_case": + return self._to_snake_case(value) + if name == "kebab_case": + return self._to_kebab_case(value) + + # to_color: treat value as color string (pass-through, validates hex) + if name == "to_color": + if value.startswith('#') and len(value) in (7, 9): + return value + self._log_error(f"to_color: value '{value}' is not a valid hex color", raw_expr) + return value + + # Color-arg filters (blend, harmonize) - need hex color argument + if name in self.COLOR_ARG_FILTERS: + if not (value.startswith('#') and len(value) == 7): + self._log_error(f"Filter '{name}' requires a hex color value", raw_expr) + return value + return self._apply_color_arg_filter(value, name, arg, raw_expr) + + # Try as color filter if value looks like a hex color + if name in self.SUPPORTED_FILTERS and value.startswith('#') and len(value) == 7: + color = Color.from_hex(value) + color = self._apply_filter(color, name, arg, raw_expr) + return color.to_hex() + + self._log_warning(f"Cannot apply filter '{name}' to non-color value") + return value + + def _apply_color_arg_filter(self, value: str, name: str, arg: Optional[str], raw_expr: str) -> str: + """Apply blend or harmonize filter (takes hex color argument).""" + if not arg: + self._log_error(f"Filter '{name}' requires a color argument", raw_expr) + return value + + # Parse arguments: "#hexcolor", amount (for blend) or just "#hexcolor" (for harmonize) + # Support both quoted and unquoted hex + hex_match = re.match(r'["\']?(#[0-9a-fA-F]{6})["\']?\s*(?:,\s*(.+))?', arg) + if not hex_match: + self._log_error(f"Filter '{name}' requires a hex color argument, got '{arg}'", raw_expr) + return value + + target_hex = hex_match.group(1) + extra_arg = hex_match.group(2) + + src = Color.from_hex(value) + target = Color.from_hex(target_hex) + + src_hct = Hct.from_rgb(src.r, src.g, src.b) + target_hct = Hct.from_rgb(target.r, target.g, target.b) + + if name == "blend": + # Blend hue in HCT space by amount, keep source chroma/tone + if not extra_arg: + self._log_error("blend filter requires amount argument: blend: \"#hex\", 0.5", raw_expr) + return value + try: + amount = float(extra_arg.strip().strip('"\'')) + except ValueError: + self._log_error(f"blend amount must be numeric, got '{extra_arg}'", raw_expr) + return value + amount = max(0.0, min(1.0, amount)) + + # Shortest arc hue interpolation + diff = target_hct.hue - src_hct.hue + if diff > 180.0: + diff -= 360.0 + elif diff < -180.0: + diff += 360.0 + new_hue = (src_hct.hue + diff * amount) % 360.0 + result_hct = Hct(new_hue, src_hct.chroma, src_hct.tone) + + elif name == "harmonize": + # M3 harmonize: rotate hue toward target by min(diff * 0.5, 15°) + diff = target_hct.hue - src_hct.hue + if diff > 180.0: + diff -= 360.0 + elif diff < -180.0: + diff += 360.0 + max_rotation = 15.0 + rotation = min(abs(diff) * 0.5, max_rotation) + if diff < 0: + rotation = -rotation + new_hue = (src_hct.hue + rotation) % 360.0 + result_hct = Hct(new_hue, src_hct.chroma, src_hct.tone) + else: + return value + + r, g, b = result_hct.to_rgb() + return Color(r, g, b).to_hex() + + @staticmethod + def _split_words(s: str) -> list[str]: + """Split a string into words for case conversion.""" + # Handle camelCase/PascalCase boundaries + s = re.sub(r'([a-z])([A-Z])', r'\1_\2', s) + # Split on non-alphanumeric + return [w for w in re.split(r'[^a-zA-Z0-9]+', s) if w] + + def _to_camel_case(self, s: str) -> str: + words = self._split_words(s) + if not words: + return s + return words[0].lower() + ''.join(w.capitalize() for w in words[1:]) + + def _to_pascal_case(self, s: str) -> str: + words = self._split_words(s) + if not words: + return s + return ''.join(w.capitalize() for w in words) + + def _to_snake_case(self, s: str) -> str: + words = self._split_words(s) + return '_'.join(w.lower() for w in words) + + def _to_kebab_case(self, s: str) -> str: + words = self._split_words(s) + return '-'.join(w.lower() for w in words) + + def _apply_replace(self, value: str, args: Optional[str], raw_expr: str) -> str: + """Apply the replace filter: | replace: "search", "replacement" """ + if not args: + self._log_error("replace filter requires arguments", raw_expr) + return value + + # Parse quoted arguments: "search", "replacement" + match = re.match(r'"([^"]*?)"\s*,\s*"([^"]*?)"', args) + if match: + find_str, replace_str = match.groups() + return value.replace(find_str, replace_str) + + # Try single-quoted: 'search', 'replacement' + match = re.match(r"'([^']*?)'\s*,\s*'([^']*?)'", args) + if match: + find_str, replace_str = match.groups() + return value.replace(find_str, replace_str) + + self._log_error(f"replace filter syntax: replace: \"find\", \"replacement\"", raw_expr) + return value + + def _apply_filter(self, color: Color, filter_name: str, arg: Optional[str], raw_expr: str) -> Color: + """Apply a single color filter.""" + if filter_name not in self.SUPPORTED_FILTERS: + supported = ", ".join(sorted(self.SUPPORTED_FILTERS.keys())) + self._log_error(f"Unknown filter '{filter_name}'. Supported: {supported}", raw_expr) + return color + + expected_args = self.SUPPORTED_FILTERS[filter_name] + + if expected_args > 0 and arg is None: + self._log_error(f"Filter '{filter_name}' requires an argument", raw_expr) + return color + if expected_args == 0 and arg is not None: + self._log_warning(f"Filter '{filter_name}' ignores argument '{arg}'") + + num_arg = None + if expected_args > 0: + try: + num_arg = float(arg) + except (ValueError, TypeError): + self._log_error(f"Filter '{filter_name}' requires numeric argument, got '{arg}'", raw_expr) + return color + + h, s, l = color.to_hsl() + + if filter_name == "grayscale": + gray = int(0.299 * color.r + 0.587 * color.g + 0.114 * color.b) + result = Color(gray, gray, gray) + elif filter_name == "invert": + result = Color(255 - color.r, 255 - color.g, 255 - color.b) + elif filter_name == "set_alpha": + result = Color(color.r, color.g, color.b) + result.alpha = max(0.0, min(1.0, num_arg)) + elif filter_name == "set_lightness": + new_l = max(0.0, min(1.0, num_arg / 100.0)) + result = Color.from_hsl(h, s, new_l) + elif filter_name == "set_hue": + new_h = num_arg % 360 + result = Color.from_hsl(new_h, s, l) + elif filter_name == "set_saturation": + new_s = max(0.0, min(1.0, num_arg / 100.0)) + result = Color.from_hsl(h, new_s, l) + elif filter_name == "lighten": + new_l = max(0.0, min(1.0, l + num_arg / 100.0)) + result = Color.from_hsl(h, s, new_l) + elif filter_name == "darken": + new_l = max(0.0, min(1.0, l - num_arg / 100.0)) + result = Color.from_hsl(h, s, new_l) + elif filter_name == "saturate": + new_s = max(0.0, min(1.0, s + num_arg / 100.0)) + result = Color.from_hsl(h, new_s, l) + elif filter_name == "desaturate": + new_s = max(0.0, min(1.0, s - num_arg / 100.0)) + result = Color.from_hsl(h, new_s, l) + elif filter_name == "auto_lightness": + # If lightness < 50%, lighten; otherwise darken (push toward mid-lightness) + if l < 0.5: + new_l = max(0.0, min(1.0, l + num_arg / 100.0)) + else: + new_l = max(0.0, min(1.0, l - num_arg / 100.0)) + result = Color.from_hsl(h, s, new_l) + elif filter_name == "set_red": + result = Color(max(0, min(255, int(num_arg))), color.g, color.b) + elif filter_name == "set_green": + result = Color(color.r, max(0, min(255, int(num_arg))), color.b) + elif filter_name == "set_blue": + result = Color(color.r, color.g, max(0, min(255, int(num_arg)))) + else: + result = color + + if hasattr(color, 'alpha') and not hasattr(result, 'alpha'): + result.alpha = color.alpha + + return result + + # --- Main Render Methods --- + + def render(self, template_text: str) -> str: + """Render a template string, processing blocks and expressions.""" + self._error_count = 0 + + # Parse template into node tree + nodes = self._parse_template(template_text) + + # Evaluate with empty scope + scope = VariableScope() + result = self._evaluate_nodes(nodes, scope) + + if self.closest_color: + result = self._substitute_closest_color(result) + + # Process escape sequences (matugen-compatible) + result = result.replace('\\\\', '\\') + + if self._error_count > 0: + print(f"Template rendering completed with {self._error_count} error(s)", file=sys.stderr) + + return result + + def render_file(self, input_path: Path, output_path: Path) -> bool: + """Render a template file to an output path. + + Returns True if successful, False if skipped due to errors. + """ + self._current_file = str(input_path) + success = False + try: + template_text = input_path.read_text() + rendered_text = self.render(template_text) + + if self._error_count > 0: + print(f"Skipping {output_path}: template has {self._error_count} error(s)", file=sys.stderr) + else: + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(rendered_text) + success = True + except FileNotFoundError: + self._log_error(f"Template file not found: {input_path}") + except PermissionError: + self._log_error(f"Permission denied: {output_path}") + except Exception as e: + self._log_error(f"Unexpected error: {e}") + finally: + self._current_file = None + return success + + # --- Custom Colors --- + + # Standard M3 tone mappings for primary color group + _CUSTOM_COLOR_TONES = { + "dark": { + "color": 80, + "on_color": 20, + "color_container": 30, + "on_color_container": 90, + }, + "light": { + "color": 40, + "on_color": 100, + "color_container": 90, + "on_color_container": 10, + }, + } + + def _apply_custom_colors(self, custom_colors: dict[str, Any]): + """Generate and merge custom color tokens into theme_data. + + Matches matugen's [config.custom_colors] behavior: + - Each custom color generates 6 tokens per mode + - Optional blend (harmonize toward source color) + - Uses the same scheme type as the main theme for palette generation + - Tokens: {name}_source, {name}_value, {name}, on_{name}, + {name}_container, on_{name}_container + """ + from .hct import Hct, TonalPalette + from .material import ( + SchemeTonalSpot, SchemeFruitSalad, SchemeRainbow, + SchemeContent, SchemeMonochrome + ) + + SCHEME_CLASSES = { + "tonal-spot": SchemeTonalSpot, + "content": SchemeContent, + "fruit-salad": SchemeFruitSalad, + "rainbow": SchemeRainbow, + "monochrome": SchemeMonochrome, + } + scheme_class = SCHEME_CLASSES.get(self.scheme_type, SchemeContent) + + # Get source color for harmonization (primary from default mode) + source_hex = None + mode_data = self.theme_data.get(self.default_mode, {}) + source_hex = mode_data.get("primary") or mode_data.get("source_color") + + for name, config in custom_colors.items(): + # Parse config: either a string "#hex" or {color: "#hex", blend: bool} + if isinstance(config, str): + color_hex = config + blend = True + elif isinstance(config, dict): + color_hex = config.get("color", "") + blend = config.get("blend", True) + else: + self._log_warning(f"Invalid custom_color config for '{name}': {config}") + continue + + # Validate hex color + if not (color_hex.startswith('#') and len(color_hex) == 7): + self._log_error(f"Custom color '{name}' has invalid hex: '{color_hex}'") + continue + + original_color = Color.from_hex(color_hex) + + # Optionally harmonize toward source color + if blend and source_hex: + src = Color.from_hex(color_hex) + target = Color.from_hex(source_hex) + src_hct = Hct.from_rgb(src.r, src.g, src.b) + target_hct = Hct.from_rgb(target.r, target.g, target.b) + + # M3 harmonize: rotate hue toward target by min(diff * 0.5, 15°) + diff = target_hct.hue - src_hct.hue + if diff > 180.0: + diff -= 360.0 + elif diff < -180.0: + diff += 360.0 + max_rotation = 15.0 + rotation = min(abs(diff) * 0.5, max_rotation) + if diff < 0: + rotation = -rotation + new_hue = (src_hct.hue + rotation) % 360.0 + harmonized_hct = Hct(new_hue, src_hct.chroma, src_hct.tone) + r, g, b = harmonized_hct.to_rgb() + palette_color = Color(r, g, b) + else: + palette_color = original_color + + # Generate palette using the scheme class (matches matugen behavior) + palette_hct = Hct.from_rgb(palette_color.r, palette_color.g, palette_color.b) + scheme = scheme_class(palette_hct) + palette = scheme.primary_palette + + # Generate tokens for each mode + for mode in self.theme_data: + tones = self._CUSTOM_COLOR_TONES.get(mode) + if not tones: + continue + + # Source/value tokens (always the original unharmonized color) + self.theme_data[mode][f"{name}_source"] = original_color.to_hex() + self.theme_data[mode][f"{name}_value"] = original_color.to_hex() + + # Primary role tokens from the tonal palette + self.theme_data[mode][name] = palette.get_hex(tones["color"]) + self.theme_data[mode][f"on_{name}"] = palette.get_hex(tones["on_color"]) + self.theme_data[mode][f"{name}_container"] = palette.get_hex(tones["color_container"]) + self.theme_data[mode][f"on_{name}_container"] = palette.get_hex(tones["on_color_container"]) + + # Invalidate colors map cache so new colors appear in iterations + self._colors_map = None + + def _substitute_closest_color(self, text: str) -> str: + """Substitute {{closest_color}} in text.""" + return re.sub(r"\{\{\s*closest_color\s*\}\}", self.closest_color, text) + + def process_config_file(self, config_path: Path): + """Process Matugen TOML configuration file.""" + if not tomllib: + print("Error: tomllib module not available (requires Python 3.11+)", file=sys.stderr) + return + + try: + with open(config_path, "rb") as f: + data = tomllib.load(f) + + # Apply custom colors before rendering templates + config_section = data.get("config", {}) + custom_colors = config_section.get("custom_colors") + if custom_colors: + self._apply_custom_colors(custom_colors) + + templates = data.get("templates", {}) + for name, template in templates.items(): + input_path = template.get("input_path") + output_path = template.get("output_path") + + if not input_path or not output_path: + print(f"Warning: Template '{name}' missing input_path or output_path", file=sys.stderr) + continue + + # Handle closest_color if configured (matugen-compatible) + # Reset for each template to avoid state pollution between templates + self.closest_color = "" + colors_to_compare = template.get("colors_to_compare") + compare_to = template.get("compare_to") + + if colors_to_compare and compare_to: + rendered_compare_to = self.render(compare_to) + self.closest_color = find_closest_color(rendered_compare_to, colors_to_compare) + + self.render_file(Path(input_path).expanduser(), Path(output_path).expanduser()) + + # Execute pre_hook if specified + pre_hook = template.get("pre_hook") + if pre_hook: + import subprocess + if self.closest_color: + pre_hook = self._substitute_closest_color(pre_hook) + pre_hook = self.render(pre_hook) + try: + subprocess.run(pre_hook, shell=True, check=False) + except Exception as e: + print(f"Error running pre_hook for {name}: {e}", file=sys.stderr) + + # Execute post_hook if specified + post_hook = template.get("post_hook") + if post_hook: + import subprocess + if self.closest_color: + post_hook = self._substitute_closest_color(post_hook) + post_hook = self.render(post_hook) + try: + subprocess.run(post_hook, shell=True, check=False) + except Exception as e: + print(f"Error running post_hook for {name}: {e}", file=sys.stderr) + + except FileNotFoundError: + print(f"Error: Config file not found: {config_path}", file=sys.stderr) + except Exception as e: + print(f"Error processing config file {config_path}: {e}", file=sys.stderr) diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/scheme.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/scheme.py new file mode 100644 index 0000000..6ea0d62 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/scheme.py @@ -0,0 +1,351 @@ +""" +Predefined scheme expansion - Convert 14-color schemes to full palette. + +This module expands predefined color schemes (like Tokyo-Night) from their +14 core colors to the full 48-color palette used by templates. + +Input format (14 colors): + mPrimary, mOnPrimary, mSecondary, mOnSecondary, mTertiary, mOnTertiary, + mError, mOnError, mSurface, mOnSurface, mSurfaceVariant, mOnSurfaceVariant, + mOutline, mHover + +Output: Full 48-color palette matching generate_theme() output. +""" + +from typing import Literal + +from .color import Color +from .contrast import ensure_contrast + +ThemeMode = Literal["dark", "light"] + + +def _hex_to_color(hex_str: str) -> Color: + """Convert hex string to Color object.""" + hex_str = hex_str.lstrip("#") + r = int(hex_str[0:2], 16) + g = int(hex_str[2:4], 16) + b = int(hex_str[4:6], 16) + return Color(r, g, b) + + +def _make_container_dark(base: Color) -> Color: + """Generate container color for dark mode.""" + h, s, l = base.to_hsl() + return Color.from_hsl(h, min(s + 0.15, 1.0), max(l - 0.35, 0.15)) + + +def _make_container_light(base: Color) -> Color: + """Generate container color for light mode.""" + h, s, l = base.to_hsl() + return Color.from_hsl(h, max(s - 0.20, 0.30), min(l + 0.35, 0.85)) + + +def _make_fixed_dark(base: Color) -> tuple[Color, Color]: + """Generate fixed and fixed_dim colors for dark mode.""" + h, s, _ = base.to_hsl() + fixed = Color.from_hsl(h, max(s, 0.70), 0.85) + fixed_dim = Color.from_hsl(h, max(s, 0.65), 0.75) + return fixed, fixed_dim + + +def _make_fixed_light(base: Color) -> tuple[Color, Color]: + """Generate fixed and fixed_dim colors for light mode.""" + h, s, _ = base.to_hsl() + fixed = Color.from_hsl(h, max(s, 0.70), 0.40) + fixed_dim = Color.from_hsl(h, max(s, 0.65), 0.30) + return fixed, fixed_dim + + +def _interpolate_color(c1: Color, c2: Color, t: float) -> Color: + """Interpolate between two colors. t=0 returns c1, t=1 returns c2.""" + r = int(c1.r + (c2.r - c1.r) * t) + g = int(c1.g + (c2.g - c1.g) * t) + b = int(c1.b + (c2.b - c1.b) * t) + return Color(max(0, min(255, r)), max(0, min(255, g)), max(0, min(255, b))) + + +def expand_predefined_scheme(scheme_data: dict[str, str], mode: ThemeMode) -> dict[str, str]: + """ + Expand 14-color predefined scheme to full 48-color palette. + + Args: + scheme_data: Dictionary with keys like mPrimary, mSecondary, etc. + mode: "dark" or "light" + + Returns: + Dictionary with all 48 color names mapped to hex values. + """ + is_dark = mode == "dark" + + # Parse input colors + primary = _hex_to_color(scheme_data["mPrimary"]) + on_primary = _hex_to_color(scheme_data["mOnPrimary"]) + secondary = _hex_to_color(scheme_data["mSecondary"]) + on_secondary = _hex_to_color(scheme_data["mOnSecondary"]) + tertiary = _hex_to_color(scheme_data["mTertiary"]) + on_tertiary = _hex_to_color(scheme_data["mOnTertiary"]) + error = _hex_to_color(scheme_data["mError"]) + on_error = _hex_to_color(scheme_data["mOnError"]) + surface = _hex_to_color(scheme_data["mSurface"]) + on_surface = _hex_to_color(scheme_data["mOnSurface"]) + surface_variant = _hex_to_color(scheme_data["mSurfaceVariant"]) + on_surface_variant = _hex_to_color(scheme_data["mOnSurfaceVariant"]) + outline_raw = _hex_to_color(scheme_data["mOutline"]) + shadow = _hex_to_color(scheme_data.get("mShadow", scheme_data["mSurface"])) + + # Generate container colors + if is_dark: + primary_container = _make_container_dark(primary) + secondary_container = _make_container_dark(secondary) + tertiary_container = _make_container_dark(tertiary) + error_container = _make_container_dark(error) + else: + primary_container = _make_container_light(primary) + secondary_container = _make_container_light(secondary) + tertiary_container = _make_container_light(tertiary) + error_container = _make_container_light(error) + + # Generate "on container" colors with proper contrast + primary_h, primary_s, _ = primary.to_hsl() + secondary_h, secondary_s, _ = secondary.to_hsl() + tertiary_h, tertiary_s, _ = tertiary.to_hsl() + error_h, error_s, _ = error.to_hsl() + + if is_dark: + # Light text on dark containers + on_primary_container = ensure_contrast( + Color.from_hsl(primary_h, primary_s, 0.90), primary_container, 4.5 + ) + on_secondary_container = ensure_contrast( + Color.from_hsl(secondary_h, secondary_s, 0.90), secondary_container, 4.5 + ) + on_tertiary_container = ensure_contrast( + Color.from_hsl(tertiary_h, tertiary_s, 0.90), tertiary_container, 4.5 + ) + on_error_container = ensure_contrast( + Color.from_hsl(error_h, error_s, 0.90), error_container, 4.5 + ) + else: + # Dark text on light containers + on_primary_container = ensure_contrast( + Color.from_hsl(primary_h, primary_s, 0.15), primary_container, 4.5 + ) + on_secondary_container = ensure_contrast( + Color.from_hsl(secondary_h, secondary_s, 0.15), secondary_container, 4.5 + ) + on_tertiary_container = ensure_contrast( + Color.from_hsl(tertiary_h, tertiary_s, 0.15), tertiary_container, 4.5 + ) + on_error_container = ensure_contrast( + Color.from_hsl(error_h, error_s, 0.15), error_container, 4.5 + ) + + # Generate fixed colors + if is_dark: + primary_fixed, primary_fixed_dim = _make_fixed_dark(primary) + secondary_fixed, secondary_fixed_dim = _make_fixed_dark(secondary) + tertiary_fixed, tertiary_fixed_dim = _make_fixed_dark(tertiary) + else: + primary_fixed, primary_fixed_dim = _make_fixed_light(primary) + secondary_fixed, secondary_fixed_dim = _make_fixed_light(secondary) + tertiary_fixed, tertiary_fixed_dim = _make_fixed_light(tertiary) + + # Generate "on fixed" colors + if is_dark: + on_primary_fixed = ensure_contrast( + Color.from_hsl(primary_h, 0.15, 0.15), primary_fixed, 4.5 + ) + on_primary_fixed_variant = ensure_contrast( + Color.from_hsl(primary_h, 0.15, 0.20), primary_fixed_dim, 4.5 + ) + on_secondary_fixed = ensure_contrast( + Color.from_hsl(secondary_h, 0.15, 0.15), secondary_fixed, 4.5 + ) + on_secondary_fixed_variant = ensure_contrast( + Color.from_hsl(secondary_h, 0.15, 0.20), secondary_fixed_dim, 4.5 + ) + on_tertiary_fixed = ensure_contrast( + Color.from_hsl(tertiary_h, 0.15, 0.15), tertiary_fixed, 4.5 + ) + on_tertiary_fixed_variant = ensure_contrast( + Color.from_hsl(tertiary_h, 0.15, 0.20), tertiary_fixed_dim, 4.5 + ) + else: + on_primary_fixed = ensure_contrast( + Color.from_hsl(primary_h, 0.15, 0.90), primary_fixed, 4.5 + ) + on_primary_fixed_variant = ensure_contrast( + Color.from_hsl(primary_h, 0.15, 0.85), primary_fixed_dim, 4.5 + ) + on_secondary_fixed = ensure_contrast( + Color.from_hsl(secondary_h, 0.15, 0.90), secondary_fixed, 4.5 + ) + on_secondary_fixed_variant = ensure_contrast( + Color.from_hsl(secondary_h, 0.15, 0.85), secondary_fixed_dim, 4.5 + ) + on_tertiary_fixed = ensure_contrast( + Color.from_hsl(tertiary_h, 0.15, 0.90), tertiary_fixed, 4.5 + ) + on_tertiary_fixed_variant = ensure_contrast( + Color.from_hsl(tertiary_h, 0.15, 0.85), tertiary_fixed_dim, 4.5 + ) + + # Generate surface containers using mSurfaceVariant as the middle container + # This respects the scheme author's color choices + surface_h, surface_s, surface_l = surface.to_hsl() + sv_h, sv_s, sv_l = surface_variant.to_hsl() + + # surface_container = mSurfaceVariant (direct assignment) + surface_container = surface_variant + + if is_dark: + # Dark mode: surface is darkest, surface_variant is the middle container + # Lower containers interpolate between surface and surface_variant + surface_container_lowest = _interpolate_color(surface, surface_variant, 0.2) + surface_container_low = _interpolate_color(surface, surface_variant, 0.5) + # Higher containers go beyond surface_variant (lighter) + surface_container_high = Color.from_hsl(sv_h, sv_s, min(sv_l + 0.04, 0.40)) + surface_container_highest = Color.from_hsl(sv_h, sv_s, min(sv_l + 0.08, 0.45)) + # Dim is darker than surface, bright is lighter than highest container + surface_dim = Color.from_hsl(surface_h, surface_s, max(surface_l - 0.04, 0.02)) + surface_bright = Color.from_hsl(sv_h, sv_s, min(sv_l + 0.12, 0.50)) + else: + # Light mode: surface is lightest, surface_variant is the middle container + # Lower containers interpolate between surface and surface_variant + surface_container_lowest = _interpolate_color(surface, surface_variant, 0.2) + surface_container_low = _interpolate_color(surface, surface_variant, 0.5) + # Higher containers go beyond surface_variant (darker) + surface_container_high = Color.from_hsl(sv_h, sv_s, max(sv_l - 0.04, 0.60)) + surface_container_highest = Color.from_hsl(sv_h, sv_s, max(sv_l - 0.08, 0.55)) + # Dim is darker than highest, bright is lighter than surface + surface_dim = Color.from_hsl(sv_h, sv_s, max(sv_l - 0.12, 0.50)) + surface_bright = Color.from_hsl(surface_h, surface_s, min(surface_l + 0.03, 0.98)) + + # Ensure outline has sufficient contrast against surface (3:1 minimum for UI) + outline = ensure_contrast(outline_raw, surface, 3.0) + + # Generate outline variant + outline_h, outline_s, outline_l = outline.to_hsl() + if is_dark: + outline_variant = Color.from_hsl(outline_h, outline_s, max(outline_l - 0.15, 0.1)) + else: + outline_variant = Color.from_hsl(outline_h, outline_s, min(outline_l + 0.15, 0.9)) + + # Scrim is always black + scrim = Color(0, 0, 0) + + # Inverse colors + if is_dark: + inverse_surface = Color.from_hsl(surface_h, 0.08, 0.90) + inverse_on_surface = Color.from_hsl(surface_h, 0.05, 0.15) + inverse_primary = Color.from_hsl(primary_h, max(primary_s * 0.8, 0.5), 0.40) + else: + inverse_surface = Color.from_hsl(surface_h, 0.08, 0.15) + inverse_on_surface = Color.from_hsl(surface_h, 0.05, 0.90) + inverse_primary = Color.from_hsl(primary_h, max(primary_s * 0.8, 0.5), 0.70) + + # Background is same as surface in MD3 + background = surface + on_background = on_surface + + return { + # Primary + "primary": primary.to_hex(), + "on_primary": on_primary.to_hex(), + "primary_container": primary_container.to_hex(), + "on_primary_container": on_primary_container.to_hex(), + "primary_fixed": primary_fixed.to_hex(), + "primary_fixed_dim": primary_fixed_dim.to_hex(), + "on_primary_fixed": on_primary_fixed.to_hex(), + "on_primary_fixed_variant": on_primary_fixed_variant.to_hex(), + # Secondary + "secondary": secondary.to_hex(), + "on_secondary": on_secondary.to_hex(), + "secondary_container": secondary_container.to_hex(), + "on_secondary_container": on_secondary_container.to_hex(), + "secondary_fixed": secondary_fixed.to_hex(), + "secondary_fixed_dim": secondary_fixed_dim.to_hex(), + "on_secondary_fixed": on_secondary_fixed.to_hex(), + "on_secondary_fixed_variant": on_secondary_fixed_variant.to_hex(), + # Tertiary + "tertiary": tertiary.to_hex(), + "on_tertiary": on_tertiary.to_hex(), + "tertiary_container": tertiary_container.to_hex(), + "on_tertiary_container": on_tertiary_container.to_hex(), + "tertiary_fixed": tertiary_fixed.to_hex(), + "tertiary_fixed_dim": tertiary_fixed_dim.to_hex(), + "on_tertiary_fixed": on_tertiary_fixed.to_hex(), + "on_tertiary_fixed_variant": on_tertiary_fixed_variant.to_hex(), + # Error + "error": error.to_hex(), + "on_error": on_error.to_hex(), + "error_container": error_container.to_hex(), + "on_error_container": on_error_container.to_hex(), + # Surface + "surface": surface.to_hex(), + "on_surface": on_surface.to_hex(), + "surface_variant": surface_variant.to_hex(), + "on_surface_variant": on_surface_variant.to_hex(), + "surface_dim": surface_dim.to_hex(), + "surface_bright": surface_bright.to_hex(), + # Surface containers + "surface_container_lowest": surface_container_lowest.to_hex(), + "surface_container_low": surface_container_low.to_hex(), + "surface_container": surface_container.to_hex(), + "surface_container_high": surface_container_high.to_hex(), + "surface_container_highest": surface_container_highest.to_hex(), + # Outline and other + "outline": outline.to_hex(), + "outline_variant": outline_variant.to_hex(), + "shadow": shadow.to_hex(), + "scrim": scrim.to_hex(), + # Inverse + "inverse_surface": inverse_surface.to_hex(), + "inverse_on_surface": inverse_on_surface.to_hex(), + "inverse_primary": inverse_primary.to_hex(), + # Background + "background": background.to_hex(), + "on_background": on_background.to_hex(), + } + + +def inject_terminal_colors(result: dict[str, str], scheme_mode_data: dict) -> dict[str, str]: + """Flatten scheme's terminal section into template-ready color keys. + + Adds keys like terminal_foreground, terminal_normal_black, terminal_bright_red, etc. + so predefined terminal templates can reference them as + {{colors.terminal_foreground.default.hex_stripped}}. + + Args: + result: Expanded color palette dict to augment. + scheme_mode_data: Raw scheme JSON mode data (e.g., scheme_data["dark"]). + + Returns: + The same result dict with terminal_ keys added. + """ + terminal = scheme_mode_data.get("terminal") + if not terminal: + return result + + # Map of JSON keys to flattened key names + direct_keys = { + "foreground": "terminal_foreground", + "background": "terminal_background", + "cursor": "terminal_cursor", + "cursorText": "terminal_cursor_text", + "selectionFg": "terminal_selection_fg", + "selectionBg": "terminal_selection_bg", + } + + for json_key, flat_key in direct_keys.items(): + if json_key in terminal: + result[flat_key] = terminal[json_key] + + # ANSI normal/bright color groups + for group in ("normal", "bright"): + if group in terminal: + for name, hex_val in terminal[group].items(): + result[f"terminal_{group}_{name}"] = hex_val + + return result diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/theme.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/theme.py new file mode 100644 index 0000000..871dfc8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/lib/theme.py @@ -0,0 +1,878 @@ +""" +Theme generation functions for Material and Normal modes. + +This module provides functions for generating complete color themes +from a color palette, supporting both Material Design 3 and a more +vibrant "wallust-style" theme. + +Supported scheme types: +- tonal-spot: Default Android 12-13 scheme (recommended) +- fruit-salad: Bold/playful with hue rotation +- rainbow: Chromatic accents with grayscale neutrals +- monochrome: Pure grayscale M3 scheme (chroma = 0) +- vibrant: Prioritizes the most saturated colors regardless of area +- faithful: Prioritizes dominant colors by area coverage +- muted: Preserves hue but caps saturation low (for monochrome wallpapers) +""" + +from typing import Literal + +from .color import Color, shift_hue, hue_distance, adjust_surface +from .contrast import ensure_contrast +from .material import SchemeTonalSpot, SchemeFruitSalad, SchemeRainbow, SchemeContent, SchemeMonochrome +from .palette import find_error_color + +# Type aliases +ThemeMode = Literal["dark", "light"] +SchemeType = Literal["tonal-spot", "fruit-salad", "rainbow", "content", "monochrome", "vibrant", "faithful", "muted"] + +# Map scheme type strings to classes +SCHEME_CLASSES = { + "tonal-spot": SchemeTonalSpot, + "fruit-salad": SchemeFruitSalad, + "rainbow": SchemeRainbow, + "content": SchemeContent, + "monochrome": SchemeMonochrome, + # "vibrant", "faithful", and "muted" use generate_*_* functions, not a scheme class +} + + +def generate_material_dark(palette: list[Color], scheme_type: str = "tonal-spot") -> dict[str, str]: + """ + Generate Material Design 3 dark theme from palette using HCT color space. + + Args: + palette: List of extracted colors (primary color is index 0) + scheme_type: One of "tonal-spot", "fruit-salad", "rainbow" + + Returns: + Dictionary of color token names to hex values + """ + primary = palette[0] if palette else Color(255, 245, 155) + + # Get the appropriate scheme class + scheme_class = SCHEME_CLASSES.get(scheme_type, SchemeTonalSpot) + scheme = scheme_class.from_rgb(primary.r, primary.g, primary.b) + return scheme.get_dark_scheme() + + +def generate_material_light(palette: list[Color], scheme_type: str = "tonal-spot") -> dict[str, str]: + """ + Generate Material Design 3 light theme from palette using HCT color space. + + Args: + palette: List of extracted colors (primary color is index 0) + scheme_type: One of "tonal-spot", "fruit-salad", "rainbow" + + Returns: + Dictionary of color token names to hex values + """ + primary = palette[0] if palette else Color(93, 101, 245) + + # Get the appropriate scheme class + scheme_class = SCHEME_CLASSES.get(scheme_type, SchemeTonalSpot) + scheme = scheme_class.from_rgb(primary.r, primary.g, primary.b) + return scheme.get_light_scheme() + + +def generate_normal_dark(palette: list[Color]) -> dict[str, str]: + """ + Generate wallust-style dark theme from palette. + + More vibrant than Material - uses palette colors directly and keeps + surfaces saturated with the primary hue. Outputs same keys as Material. + """ + # Use extracted colors directly (wallust style) + # But check if colors are distinct enough - if not, derive from primary + primary = palette[0] if palette else Color(255, 245, 155) + primary_h, primary_s, primary_l = primary.to_hsl() + + # Secondary: use palette[1] only if hue is >30° different, otherwise derive + MIN_HUE_DISTANCE = 30 + if len(palette) > 1: + sec_h, _, _ = palette[1].to_hsl() + if hue_distance(primary_h, sec_h) > MIN_HUE_DISTANCE: + secondary = palette[1] + else: + # Colors too similar - shift hue by 30° to stay in same color family + secondary = shift_hue(primary, 30) + else: + secondary = shift_hue(primary, 30) + + # Tertiary: use palette[2] only if hue is >30° different from both primary and secondary + if len(palette) > 2: + ter_h, _, _ = palette[2].to_hsl() + sec_h, _, _ = secondary.to_hsl() + if hue_distance(primary_h, ter_h) > MIN_HUE_DISTANCE and hue_distance(sec_h, ter_h) > MIN_HUE_DISTANCE: + tertiary = palette[2] + else: + # Colors too similar - shift hue by 60° from primary to stay closer to original + tertiary = shift_hue(primary, 60) + else: + tertiary = shift_hue(primary, 60) + + error = find_error_color(palette) + + # Keep colors vibrant - preserve saturation + h, s, l = primary.to_hsl() + primary_adjusted = Color.from_hsl(h, max(s, 0.7), max(l, 0.65)) + + h, s, l = secondary.to_hsl() + secondary_adjusted = Color.from_hsl(h, max(s, 0.6), max(l, 0.60)) + + h, s, l = tertiary.to_hsl() + tertiary_adjusted = Color.from_hsl(h, max(s, 0.5), max(l, 0.60)) + + # Container colors - darker, more saturated versions of accent colors + def make_container_dark(base: Color) -> Color: + h, s, l = base.to_hsl() + return Color.from_hsl(h, min(s + 0.15, 1.0), max(l - 0.35, 0.15)) + + primary_container = make_container_dark(primary_adjusted) + secondary_container = make_container_dark(secondary_adjusted) + tertiary_container = make_container_dark(tertiary_adjusted) + error_container = make_container_dark(error) + + # Surface: COLORFUL dark - a deep, saturated version of primary + # Heuristic: Shift Cyan (160-200) slightly towards Blue (+10) to avoid "Teal" look + surface_hue, s, _ = palette[0].to_hsl() + if 160 <= surface_hue <= 200: + surface_hue = (surface_hue + 10) % 360 + + # Reduce saturation for warm hues (red/orange/yellow) - they feel overwhelming as surfaces + # Warm hues: 0-60 and 300-360 + if surface_hue < 60 or surface_hue > 300: + surface_saturation_cap = 0.35 # More desaturated for warm colors + elif 60 <= surface_hue < 120: + surface_saturation_cap = 0.50 # Moderate for yellow-greens + else: + surface_saturation_cap = 0.90 # Keep cool colors vibrant + + base_surface = Color.from_hsl(surface_hue, min(s, surface_saturation_cap), 0.5) + + # Preserving saturation (up to the cap) to be true to primary color + surface = adjust_surface(base_surface, surface_saturation_cap, 0.12) + surface_variant = adjust_surface(base_surface, min(0.80, surface_saturation_cap), 0.16) + + # Surface containers - progressive lightness for visual hierarchy (keep primary hue) + surface_container_lowest = adjust_surface(base_surface, 0.85, 0.06) + surface_container_low = adjust_surface(base_surface, 0.85, 0.10) + surface_container = adjust_surface(base_surface, 0.70, 0.20) + surface_container_high = adjust_surface(base_surface, 0.75, 0.18) + surface_container_highest = adjust_surface(base_surface, 0.70, 0.22) + + # Text colors - desaturated + text_h, _, _ = palette[0].to_hsl() + base_on_surface = Color.from_hsl(text_h, 0.05, 0.95) + on_surface = ensure_contrast(base_on_surface, surface, 4.5) + + base_on_surface_variant = Color.from_hsl(text_h, 0.05, 0.70) + on_surface_variant = ensure_contrast(base_on_surface_variant, surface_variant, 4.5) + + outline = ensure_contrast(adjust_surface(palette[0], 0.10, 0.30), surface, 3.0) + outline_variant = ensure_contrast(adjust_surface(palette[0], 0.10, 0.40), surface, 3.0) + + # Contrasting foregrounds - dark text on bright accent colors + dark_fg = Color.from_hsl(palette[0].to_hsl()[0], 0.20, 0.12) # Darker for better contrast + on_primary = ensure_contrast(dark_fg, primary_adjusted, 7.0) # Higher contrast target + on_secondary = ensure_contrast(dark_fg, secondary_adjusted, 7.0) + on_tertiary = ensure_contrast(dark_fg, tertiary_adjusted, 7.0) + on_error = ensure_contrast(dark_fg, error, 7.0) + + # "On" colors for containers - light text on dark containers, tinted with respective color + # Explicitly prefer_light=True since containers in dark mode are dark + on_primary_container = ensure_contrast(Color.from_hsl(primary_h, primary_s, 0.90), primary_container, 4.5, prefer_light=True) + sec_h, sec_s, _ = secondary.to_hsl() + on_secondary_container = ensure_contrast(Color.from_hsl(sec_h, sec_s, 0.90), secondary_container, 4.5, prefer_light=True) + ter_h, ter_s, _ = tertiary.to_hsl() + on_tertiary_container = ensure_contrast(Color.from_hsl(ter_h, ter_s, 0.90), tertiary_container, 4.5, prefer_light=True) + err_h, err_s, _ = error.to_hsl() + on_error_container = ensure_contrast(Color.from_hsl(err_h, err_s, 0.90), error_container, 4.5, prefer_light=True) + + # Shadow and scrim + shadow = surface + scrim = Color(0, 0, 0) # Pure black + + # Inverse colors - for inverted surfaces (light surface on dark theme) + inv_h = palette[0].to_hsl()[0] + inverse_surface = Color.from_hsl(inv_h, 0.08, 0.90) + inverse_on_surface = Color.from_hsl(inv_h, 0.05, 0.15) + inverse_primary = Color.from_hsl(primary_h, max(primary_s * 0.8, 0.5), 0.40) + + # Background aliases (same as surface in MD3) + background = surface + on_background = on_surface + + # Fixed colors - high-chroma accents consistent across light/dark + # In dark mode: lighter versions of accent colors + def make_fixed_dark(base: Color) -> tuple[Color, Color]: + h, s, _ = base.to_hsl() + fixed = Color.from_hsl(h, max(s, 0.70), 0.85) # Light, saturated + fixed_dim = Color.from_hsl(h, max(s, 0.65), 0.75) # Slightly darker + return fixed, fixed_dim + + primary_fixed, primary_fixed_dim = make_fixed_dark(primary_adjusted) + secondary_fixed, secondary_fixed_dim = make_fixed_dark(secondary_adjusted) + tertiary_fixed, tertiary_fixed_dim = make_fixed_dark(tertiary_adjusted) + + # "On" colors for fixed - dark text on light fixed colors + on_primary_fixed = ensure_contrast(Color.from_hsl(primary_h, 0.15, 0.15), primary_fixed, 4.5) + on_primary_fixed_variant = ensure_contrast(Color.from_hsl(primary_h, 0.15, 0.20), primary_fixed_dim, 4.5) + on_secondary_fixed = ensure_contrast(Color.from_hsl(secondary.to_hsl()[0], 0.15, 0.15), secondary_fixed, 4.5) + on_secondary_fixed_variant = ensure_contrast(Color.from_hsl(secondary.to_hsl()[0], 0.15, 0.20), secondary_fixed_dim, 4.5) + on_tertiary_fixed = ensure_contrast(Color.from_hsl(tertiary.to_hsl()[0], 0.15, 0.15), tertiary_fixed, 4.5) + on_tertiary_fixed_variant = ensure_contrast(Color.from_hsl(tertiary.to_hsl()[0], 0.15, 0.20), tertiary_fixed_dim, 4.5) + + # Surface dim - darker than surface for dimmed areas + surface_dim = adjust_surface(base_surface, 0.85, 0.08) + # Surface bright - lighter than surface + surface_bright = adjust_surface(base_surface, 0.75, 0.24) + + return { + # Primary + "primary": primary_adjusted.to_hex(), + "on_primary": on_primary.to_hex(), + "primary_container": primary_container.to_hex(), + "on_primary_container": on_primary_container.to_hex(), + "primary_fixed": primary_fixed.to_hex(), + "primary_fixed_dim": primary_fixed_dim.to_hex(), + "on_primary_fixed": on_primary_fixed.to_hex(), + "on_primary_fixed_variant": on_primary_fixed_variant.to_hex(), + "surface_tint": primary_adjusted.to_hex(), + # Secondary + "secondary": secondary_adjusted.to_hex(), + "on_secondary": on_secondary.to_hex(), + "secondary_container": secondary_container.to_hex(), + "on_secondary_container": on_secondary_container.to_hex(), + "secondary_fixed": secondary_fixed.to_hex(), + "secondary_fixed_dim": secondary_fixed_dim.to_hex(), + "on_secondary_fixed": on_secondary_fixed.to_hex(), + "on_secondary_fixed_variant": on_secondary_fixed_variant.to_hex(), + # Tertiary + "tertiary": tertiary_adjusted.to_hex(), + "on_tertiary": on_tertiary.to_hex(), + "tertiary_container": tertiary_container.to_hex(), + "on_tertiary_container": on_tertiary_container.to_hex(), + "tertiary_fixed": tertiary_fixed.to_hex(), + "tertiary_fixed_dim": tertiary_fixed_dim.to_hex(), + "on_tertiary_fixed": on_tertiary_fixed.to_hex(), + "on_tertiary_fixed_variant": on_tertiary_fixed_variant.to_hex(), + # Error + "error": error.to_hex(), + "on_error": on_error.to_hex(), + "error_container": error_container.to_hex(), + "on_error_container": on_error_container.to_hex(), + # Surface + "surface": surface.to_hex(), + "on_surface": on_surface.to_hex(), + "surface_variant": surface_variant.to_hex(), + "on_surface_variant": on_surface_variant.to_hex(), + "surface_dim": surface_dim.to_hex(), + "surface_bright": surface_bright.to_hex(), + # Surface containers + "surface_container_lowest": surface_container_lowest.to_hex(), + "surface_container_low": surface_container_low.to_hex(), + "surface_container": surface_container.to_hex(), + "surface_container_high": surface_container_high.to_hex(), + "surface_container_highest": surface_container_highest.to_hex(), + # Outline and other + "outline": outline.to_hex(), + "outline_variant": outline_variant.to_hex(), + "shadow": shadow.to_hex(), + "scrim": scrim.to_hex(), + # Inverse + "inverse_surface": inverse_surface.to_hex(), + "inverse_on_surface": inverse_on_surface.to_hex(), + "inverse_primary": inverse_primary.to_hex(), + # Background + "background": background.to_hex(), + "on_background": on_background.to_hex(), + } + + +def generate_normal_light(palette: list[Color]) -> dict[str, str]: + """ + Generate wallust-style light theme from palette. + + More vibrant than Material - uses palette colors directly and keeps + surfaces saturated with the primary hue. Outputs same keys as Material. + """ + # Use extracted colors directly, but check if distinct enough + primary = palette[0] if palette else Color(93, 101, 245) + primary_h, _, _ = primary.to_hsl() + + # Secondary: use palette[1] only if hue is >30° different + MIN_HUE_DISTANCE = 30 + if len(palette) > 1: + sec_h, _, _ = palette[1].to_hsl() + if hue_distance(primary_h, sec_h) > MIN_HUE_DISTANCE: + secondary = palette[1] + else: + secondary = shift_hue(primary, 30) + else: + secondary = shift_hue(primary, 30) + + # Tertiary: use palette[2] only if hue is >30° different from both + if len(palette) > 2: + ter_h, _, _ = palette[2].to_hsl() + sec_h, _, _ = secondary.to_hsl() + if hue_distance(primary_h, ter_h) > MIN_HUE_DISTANCE and hue_distance(sec_h, ter_h) > MIN_HUE_DISTANCE: + tertiary = palette[2] + else: + tertiary = shift_hue(primary, 60) + else: + tertiary = shift_hue(primary, 60) + + error = find_error_color(palette) + + # Keep colors vibrant - darken for visibility on light bg + # Clamp lightness to [0.25, 0.45] so colors are never near-black nor washed out + h, s, l = primary.to_hsl() + primary_adjusted = Color.from_hsl(h, max(s, 0.7), max(min(l, 0.45), 0.25)) + + h, s, l = secondary.to_hsl() + secondary_adjusted = Color.from_hsl(h, max(s, 0.6), max(min(l, 0.40), 0.22)) + + h, s, l = tertiary.to_hsl() + tertiary_adjusted = Color.from_hsl(h, max(s, 0.5), max(min(l, 0.35), 0.20)) + + # Container colors - lighter, less saturated versions of accent colors for light mode + def make_container_light(base: Color) -> Color: + h, s, l = base.to_hsl() + return Color.from_hsl(h, max(s - 0.20, 0.30), min(l + 0.35, 0.85)) + + primary_container = make_container_light(primary_adjusted) + secondary_container = make_container_light(secondary_adjusted) + tertiary_container = make_container_light(tertiary_adjusted) + error_container = make_container_light(error) + + # Surface: COLORFUL light - a pastel, saturated version of primary + # Preserving saturation (up to 0.9) to be true to primary color + surface = adjust_surface(palette[0], 0.90, 0.90) + surface_variant = adjust_surface(palette[0], 0.80, 0.78) # Darker than surface + + # Surface containers - progressive darkening for light mode (keep primary hue) + surface_container_lowest = adjust_surface(palette[0], 0.85, 0.96) # Lightest + surface_container_low = adjust_surface(palette[0], 0.85, 0.92) + surface_container = adjust_surface(palette[0], 0.80, 0.86) + surface_container_high = adjust_surface(palette[0], 0.75, 0.84) + surface_container_highest = adjust_surface(palette[0], 0.70, 0.80) # Darkest + + # Foreground colors - tinted with primary hue + text_h, _, _ = palette[0].to_hsl() + base_on_surface = Color.from_hsl(text_h, 0.05, 0.10) + on_surface = ensure_contrast(base_on_surface, surface, 4.5) + + base_on_surface_variant = Color.from_hsl(text_h, 0.05, 0.35) + on_surface_variant = ensure_contrast(base_on_surface_variant, surface_variant, 4.5) + + # Contrasting foregrounds - light text on dark accent colors + light_fg = Color.from_hsl(text_h, 0.1, 0.98) # Brighter for better contrast + on_primary = ensure_contrast(light_fg, primary_adjusted, 7.0) # Higher contrast target + on_secondary = ensure_contrast(light_fg, secondary_adjusted, 7.0) + on_tertiary = ensure_contrast(light_fg, tertiary_adjusted, 7.0) + on_error = ensure_contrast(light_fg, error, 7.0) + + # "On" colors for containers - dark text on light containers, tinted with respective color + # Explicitly prefer_light=False since containers in light mode are light + primary_h, primary_s, _ = primary.to_hsl() + on_primary_container = ensure_contrast(Color.from_hsl(primary_h, primary_s, 0.15), primary_container, 4.5, prefer_light=False) + sec_h, sec_s, _ = secondary.to_hsl() + on_secondary_container = ensure_contrast(Color.from_hsl(sec_h, sec_s, 0.15), secondary_container, 4.5, prefer_light=False) + ter_h, ter_s, _ = tertiary.to_hsl() + on_tertiary_container = ensure_contrast(Color.from_hsl(ter_h, ter_s, 0.15), tertiary_container, 4.5, prefer_light=False) + err_h, err_s, _ = error.to_hsl() + on_error_container = ensure_contrast(Color.from_hsl(err_h, err_s, 0.15), error_container, 4.5, prefer_light=False) + + # Fixed colors - high-chroma accents consistent across light/dark + # In light mode: darker versions of accent colors + def make_fixed_light(base: Color) -> tuple[Color, Color]: + h, s, _ = base.to_hsl() + fixed = Color.from_hsl(h, max(s, 0.70), 0.40) # Darker, saturated + fixed_dim = Color.from_hsl(h, max(s, 0.65), 0.30) # Even darker + return fixed, fixed_dim + + primary_fixed, primary_fixed_dim = make_fixed_light(primary_adjusted) + secondary_fixed, secondary_fixed_dim = make_fixed_light(secondary_adjusted) + tertiary_fixed, tertiary_fixed_dim = make_fixed_light(tertiary_adjusted) + + # "On" colors for fixed - light text on dark fixed colors + on_primary_fixed = ensure_contrast(Color.from_hsl(primary_h, 0.15, 0.90), primary_fixed, 4.5) + on_primary_fixed_variant = ensure_contrast(Color.from_hsl(primary_h, 0.15, 0.85), primary_fixed_dim, 4.5) + on_secondary_fixed = ensure_contrast(Color.from_hsl(secondary.to_hsl()[0], 0.15, 0.90), secondary_fixed, 4.5) + on_secondary_fixed_variant = ensure_contrast(Color.from_hsl(secondary.to_hsl()[0], 0.15, 0.85), secondary_fixed_dim, 4.5) + on_tertiary_fixed = ensure_contrast(Color.from_hsl(tertiary.to_hsl()[0], 0.15, 0.90), tertiary_fixed, 4.5) + on_tertiary_fixed_variant = ensure_contrast(Color.from_hsl(tertiary.to_hsl()[0], 0.15, 0.85), tertiary_fixed_dim, 4.5) + + # Surface dim - slightly darker than surface + surface_dim = adjust_surface(palette[0], 0.85, 0.82) + # Surface bright - brighter than surface + surface_bright = adjust_surface(palette[0], 0.90, 0.95) + + # Outline uses primary hue, more saturated + surface_h, surface_s, _ = palette[0].to_hsl() + outline = ensure_contrast(Color.from_hsl(surface_h, max(surface_s * 0.4, 0.25), 0.65), surface, 3.0) + outline_variant = ensure_contrast(Color.from_hsl(surface_h, max(surface_s * 0.3, 0.20), 0.75), surface, 3.0) + shadow = Color.from_hsl(surface_h, max(surface_s * 0.3, 0.15), 0.80) + scrim = Color(0, 0, 0) # Pure black + + # Inverse colors - for inverted surfaces (dark surface on light theme) + inverse_surface = Color.from_hsl(surface_h, 0.08, 0.15) + inverse_on_surface = Color.from_hsl(surface_h, 0.05, 0.90) + inverse_primary = Color.from_hsl(primary_h, max(primary_s * 0.8, 0.5), 0.70) + + # Background aliases (same as surface in MD3) + background = surface + on_background = on_surface + + return { + # Primary + "primary": primary_adjusted.to_hex(), + "on_primary": on_primary.to_hex(), + "primary_container": primary_container.to_hex(), + "on_primary_container": on_primary_container.to_hex(), + "primary_fixed": primary_fixed.to_hex(), + "primary_fixed_dim": primary_fixed_dim.to_hex(), + "on_primary_fixed": on_primary_fixed.to_hex(), + "on_primary_fixed_variant": on_primary_fixed_variant.to_hex(), + "surface_tint": primary_adjusted.to_hex(), + # Secondary + "secondary": secondary_adjusted.to_hex(), + "on_secondary": on_secondary.to_hex(), + "secondary_container": secondary_container.to_hex(), + "on_secondary_container": on_secondary_container.to_hex(), + "secondary_fixed": secondary_fixed.to_hex(), + "secondary_fixed_dim": secondary_fixed_dim.to_hex(), + "on_secondary_fixed": on_secondary_fixed.to_hex(), + "on_secondary_fixed_variant": on_secondary_fixed_variant.to_hex(), + # Tertiary + "tertiary": tertiary_adjusted.to_hex(), + "on_tertiary": on_tertiary.to_hex(), + "tertiary_container": tertiary_container.to_hex(), + "on_tertiary_container": on_tertiary_container.to_hex(), + "tertiary_fixed": tertiary_fixed.to_hex(), + "tertiary_fixed_dim": tertiary_fixed_dim.to_hex(), + "on_tertiary_fixed": on_tertiary_fixed.to_hex(), + "on_tertiary_fixed_variant": on_tertiary_fixed_variant.to_hex(), + # Error + "error": error.to_hex(), + "on_error": on_error.to_hex(), + "error_container": error_container.to_hex(), + "on_error_container": on_error_container.to_hex(), + # Surface + "surface": surface.to_hex(), + "on_surface": on_surface.to_hex(), + "surface_variant": surface_variant.to_hex(), + "on_surface_variant": on_surface_variant.to_hex(), + "surface_dim": surface_dim.to_hex(), + "surface_bright": surface_bright.to_hex(), + # Surface containers + "surface_container_lowest": surface_container_lowest.to_hex(), + "surface_container_low": surface_container_low.to_hex(), + "surface_container": surface_container.to_hex(), + "surface_container_high": surface_container_high.to_hex(), + "surface_container_highest": surface_container_highest.to_hex(), + # Outline and other + "outline": outline.to_hex(), + "outline_variant": outline_variant.to_hex(), + "shadow": shadow.to_hex(), + "scrim": scrim.to_hex(), + # Inverse + "inverse_surface": inverse_surface.to_hex(), + "inverse_on_surface": inverse_on_surface.to_hex(), + "inverse_primary": inverse_primary.to_hex(), + # Background + "background": background.to_hex(), + "on_background": on_background.to_hex(), + } + + +def generate_muted_dark(palette: list[Color]) -> dict[str, str]: + """ + Generate muted dark theme from palette. + + Designed for monochrome/monotonal wallpapers - preserves the dominant hue + but caps saturation to very low values for a subtle, understated look. + Outputs same keys as Material for compatibility. + """ + # Use primary color's hue but with very low saturation + primary = palette[0] if palette else Color(128, 128, 128) + primary_h, primary_s, primary_l = primary.to_hsl() + + # Derive secondary and tertiary with subtle hue shifts (monochromatic feel) + # Much smaller shifts than normal mode since we want cohesion + secondary = shift_hue(primary, 15) + tertiary = shift_hue(primary, 30) + error = find_error_color(palette) + + # Cap saturation low - this is the key difference from normal mode + MUTED_SAT_PRIMARY = 0.15 + MUTED_SAT_SECONDARY = 0.12 + MUTED_SAT_TERTIARY = 0.10 + MUTED_SAT_SURFACE = 0.08 + + h, s, l = primary.to_hsl() + primary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), max(l, 0.65)) + + h, s, l = secondary.to_hsl() + secondary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_SECONDARY), max(l, 0.60)) + + h, s, l = tertiary.to_hsl() + tertiary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_TERTIARY), max(l, 0.60)) + + # Container colors - darker, slightly saturated versions + def make_container_dark(base: Color) -> Color: + h, s, l = base.to_hsl() + return Color.from_hsl(h, min(s + 0.05, MUTED_SAT_PRIMARY), max(l - 0.35, 0.15)) + + primary_container = make_container_dark(primary_adjusted) + secondary_container = make_container_dark(secondary_adjusted) + tertiary_container = make_container_dark(tertiary_adjusted) + error_container = make_container_dark(error) + + # Surface: very low saturation, preserving hue for subtle tint + surface_hue = primary_h + base_surface = Color.from_hsl(surface_hue, MUTED_SAT_SURFACE, 0.5) + + surface = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.12) + surface_variant = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.16) + + # Surface containers - progressive lightness with minimal saturation + surface_container_lowest = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.06) + surface_container_low = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.10) + surface_container = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.20) + surface_container_high = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.18) + surface_container_highest = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.22) + + # Text colors - near-neutral with slight hue tint + base_on_surface = Color.from_hsl(primary_h, 0.03, 0.95) + on_surface = ensure_contrast(base_on_surface, surface, 4.5) + + base_on_surface_variant = Color.from_hsl(primary_h, 0.03, 0.70) + on_surface_variant = ensure_contrast(base_on_surface_variant, surface_variant, 4.5) + + outline = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.30), surface, 3.0) + outline_variant = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.40), surface, 3.0) + + # Contrasting foregrounds + dark_fg = Color.from_hsl(primary_h, 0.10, 0.12) + on_primary = ensure_contrast(dark_fg, primary_adjusted, 7.0) + on_secondary = ensure_contrast(dark_fg, secondary_adjusted, 7.0) + on_tertiary = ensure_contrast(dark_fg, tertiary_adjusted, 7.0) + on_error = ensure_contrast(dark_fg, error, 7.0) + + # "On" colors for containers + on_primary_container = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.90), primary_container, 4.5, prefer_light=True) + sec_h, _, _ = secondary.to_hsl() + on_secondary_container = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.90), secondary_container, 4.5, prefer_light=True) + ter_h, _, _ = tertiary.to_hsl() + on_tertiary_container = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.90), tertiary_container, 4.5, prefer_light=True) + err_h, _, _ = error.to_hsl() + on_error_container = ensure_contrast(Color.from_hsl(err_h, 0.05, 0.90), error_container, 4.5, prefer_light=True) + + # Shadow and scrim + shadow = surface + scrim = Color(0, 0, 0) + + # Inverse colors + inverse_surface = Color.from_hsl(primary_h, 0.05, 0.90) + inverse_on_surface = Color.from_hsl(primary_h, 0.03, 0.15) + inverse_primary = Color.from_hsl(primary_h, min(primary_s * 0.5, MUTED_SAT_PRIMARY), 0.40) + + # Background aliases + background = surface + on_background = on_surface + + # Fixed colors - still muted + def make_fixed_dark(base: Color) -> tuple[Color, Color]: + h, s, _ = base.to_hsl() + fixed = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), 0.85) + fixed_dim = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), 0.75) + return fixed, fixed_dim + + primary_fixed, primary_fixed_dim = make_fixed_dark(primary_adjusted) + secondary_fixed, secondary_fixed_dim = make_fixed_dark(secondary_adjusted) + tertiary_fixed, tertiary_fixed_dim = make_fixed_dark(tertiary_adjusted) + + # "On" colors for fixed + on_primary_fixed = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.15), primary_fixed, 4.5) + on_primary_fixed_variant = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.20), primary_fixed_dim, 4.5) + on_secondary_fixed = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.15), secondary_fixed, 4.5) + on_secondary_fixed_variant = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.20), secondary_fixed_dim, 4.5) + on_tertiary_fixed = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.15), tertiary_fixed, 4.5) + on_tertiary_fixed_variant = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.20), tertiary_fixed_dim, 4.5) + + # Surface dim and bright + surface_dim = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.08) + surface_bright = adjust_surface(base_surface, MUTED_SAT_SURFACE, 0.24) + + return { + # Primary + "primary": primary_adjusted.to_hex(), + "on_primary": on_primary.to_hex(), + "primary_container": primary_container.to_hex(), + "on_primary_container": on_primary_container.to_hex(), + "primary_fixed": primary_fixed.to_hex(), + "primary_fixed_dim": primary_fixed_dim.to_hex(), + "on_primary_fixed": on_primary_fixed.to_hex(), + "on_primary_fixed_variant": on_primary_fixed_variant.to_hex(), + "surface_tint": primary_adjusted.to_hex(), + # Secondary + "secondary": secondary_adjusted.to_hex(), + "on_secondary": on_secondary.to_hex(), + "secondary_container": secondary_container.to_hex(), + "on_secondary_container": on_secondary_container.to_hex(), + "secondary_fixed": secondary_fixed.to_hex(), + "secondary_fixed_dim": secondary_fixed_dim.to_hex(), + "on_secondary_fixed": on_secondary_fixed.to_hex(), + "on_secondary_fixed_variant": on_secondary_fixed_variant.to_hex(), + # Tertiary + "tertiary": tertiary_adjusted.to_hex(), + "on_tertiary": on_tertiary.to_hex(), + "tertiary_container": tertiary_container.to_hex(), + "on_tertiary_container": on_tertiary_container.to_hex(), + "tertiary_fixed": tertiary_fixed.to_hex(), + "tertiary_fixed_dim": tertiary_fixed_dim.to_hex(), + "on_tertiary_fixed": on_tertiary_fixed.to_hex(), + "on_tertiary_fixed_variant": on_tertiary_fixed_variant.to_hex(), + # Error + "error": error.to_hex(), + "on_error": on_error.to_hex(), + "error_container": error_container.to_hex(), + "on_error_container": on_error_container.to_hex(), + # Surface + "surface": surface.to_hex(), + "on_surface": on_surface.to_hex(), + "surface_variant": surface_variant.to_hex(), + "on_surface_variant": on_surface_variant.to_hex(), + "surface_dim": surface_dim.to_hex(), + "surface_bright": surface_bright.to_hex(), + # Surface containers + "surface_container_lowest": surface_container_lowest.to_hex(), + "surface_container_low": surface_container_low.to_hex(), + "surface_container": surface_container.to_hex(), + "surface_container_high": surface_container_high.to_hex(), + "surface_container_highest": surface_container_highest.to_hex(), + # Outline and other + "outline": outline.to_hex(), + "outline_variant": outline_variant.to_hex(), + "shadow": shadow.to_hex(), + "scrim": scrim.to_hex(), + # Inverse + "inverse_surface": inverse_surface.to_hex(), + "inverse_on_surface": inverse_on_surface.to_hex(), + "inverse_primary": inverse_primary.to_hex(), + # Background + "background": background.to_hex(), + "on_background": on_background.to_hex(), + } + + +def generate_muted_light(palette: list[Color]) -> dict[str, str]: + """ + Generate muted light theme from palette. + + Designed for monochrome/monotonal wallpapers - preserves the dominant hue + but caps saturation to very low values for a subtle, understated look. + Outputs same keys as Material for compatibility. + """ + primary = palette[0] if palette else Color(128, 128, 128) + primary_h, primary_s, _ = primary.to_hsl() + + # Derive secondary and tertiary with subtle hue shifts + secondary = shift_hue(primary, 15) + tertiary = shift_hue(primary, 30) + error = find_error_color(palette) + + # Cap saturation low + MUTED_SAT_PRIMARY = 0.15 + MUTED_SAT_SECONDARY = 0.12 + MUTED_SAT_TERTIARY = 0.10 + MUTED_SAT_SURFACE = 0.08 + + h, s, l = primary.to_hsl() + primary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), min(l, 0.45)) + + h, s, l = secondary.to_hsl() + secondary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_SECONDARY), min(l, 0.40)) + + h, s, l = tertiary.to_hsl() + tertiary_adjusted = Color.from_hsl(h, min(s, MUTED_SAT_TERTIARY), min(l, 0.35)) + + # Container colors - lighter, less saturated + def make_container_light(base: Color) -> Color: + h, s, l = base.to_hsl() + return Color.from_hsl(h, max(s - 0.05, 0.05), min(l + 0.35, 0.85)) + + primary_container = make_container_light(primary_adjusted) + secondary_container = make_container_light(secondary_adjusted) + tertiary_container = make_container_light(tertiary_adjusted) + error_container = make_container_light(error) + + # Surface: very low saturation, preserving hue for subtle tint + surface = adjust_surface(primary, MUTED_SAT_SURFACE, 0.90) + surface_variant = adjust_surface(primary, MUTED_SAT_SURFACE, 0.78) + + # Surface containers - progressive darkening with minimal saturation + surface_container_lowest = adjust_surface(primary, MUTED_SAT_SURFACE, 0.96) + surface_container_low = adjust_surface(primary, MUTED_SAT_SURFACE, 0.92) + surface_container = adjust_surface(primary, MUTED_SAT_SURFACE, 0.86) + surface_container_high = adjust_surface(primary, MUTED_SAT_SURFACE, 0.84) + surface_container_highest = adjust_surface(primary, MUTED_SAT_SURFACE, 0.80) + + # Text colors - near-neutral with slight hue tint + base_on_surface = Color.from_hsl(primary_h, 0.03, 0.10) + on_surface = ensure_contrast(base_on_surface, surface, 4.5) + + base_on_surface_variant = Color.from_hsl(primary_h, 0.03, 0.35) + on_surface_variant = ensure_contrast(base_on_surface_variant, surface_variant, 4.5) + + # Contrasting foregrounds + light_fg = Color.from_hsl(primary_h, 0.05, 0.98) + on_primary = ensure_contrast(light_fg, primary_adjusted, 7.0) + on_secondary = ensure_contrast(light_fg, secondary_adjusted, 7.0) + on_tertiary = ensure_contrast(light_fg, tertiary_adjusted, 7.0) + on_error = ensure_contrast(light_fg, error, 7.0) + + # "On" colors for containers + on_primary_container = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.15), primary_container, 4.5, prefer_light=False) + sec_h, _, _ = secondary.to_hsl() + on_secondary_container = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.15), secondary_container, 4.5, prefer_light=False) + ter_h, _, _ = tertiary.to_hsl() + on_tertiary_container = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.15), tertiary_container, 4.5, prefer_light=False) + err_h, _, _ = error.to_hsl() + on_error_container = ensure_contrast(Color.from_hsl(err_h, 0.05, 0.15), error_container, 4.5, prefer_light=False) + + # Fixed colors - still muted + def make_fixed_light(base: Color) -> tuple[Color, Color]: + h, s, _ = base.to_hsl() + fixed = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), 0.40) + fixed_dim = Color.from_hsl(h, min(s, MUTED_SAT_PRIMARY), 0.30) + return fixed, fixed_dim + + primary_fixed, primary_fixed_dim = make_fixed_light(primary_adjusted) + secondary_fixed, secondary_fixed_dim = make_fixed_light(secondary_adjusted) + tertiary_fixed, tertiary_fixed_dim = make_fixed_light(tertiary_adjusted) + + # "On" colors for fixed + on_primary_fixed = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.90), primary_fixed, 4.5) + on_primary_fixed_variant = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.85), primary_fixed_dim, 4.5) + on_secondary_fixed = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.90), secondary_fixed, 4.5) + on_secondary_fixed_variant = ensure_contrast(Color.from_hsl(sec_h, 0.05, 0.85), secondary_fixed_dim, 4.5) + on_tertiary_fixed = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.90), tertiary_fixed, 4.5) + on_tertiary_fixed_variant = ensure_contrast(Color.from_hsl(ter_h, 0.05, 0.85), tertiary_fixed_dim, 4.5) + + # Surface dim and bright + surface_dim = adjust_surface(primary, MUTED_SAT_SURFACE, 0.82) + surface_bright = adjust_surface(primary, MUTED_SAT_SURFACE, 0.95) + + # Outline + outline = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.65), surface, 3.0) + outline_variant = ensure_contrast(Color.from_hsl(primary_h, 0.05, 0.75), surface, 3.0) + shadow = Color.from_hsl(primary_h, 0.05, 0.80) + scrim = Color(0, 0, 0) + + # Inverse colors + inverse_surface = Color.from_hsl(primary_h, 0.05, 0.15) + inverse_on_surface = Color.from_hsl(primary_h, 0.03, 0.90) + inverse_primary = Color.from_hsl(primary_h, min(primary_s * 0.5, MUTED_SAT_PRIMARY), 0.70) + + # Background aliases + background = surface + on_background = on_surface + + return { + # Primary + "primary": primary_adjusted.to_hex(), + "on_primary": on_primary.to_hex(), + "primary_container": primary_container.to_hex(), + "on_primary_container": on_primary_container.to_hex(), + "primary_fixed": primary_fixed.to_hex(), + "primary_fixed_dim": primary_fixed_dim.to_hex(), + "on_primary_fixed": on_primary_fixed.to_hex(), + "on_primary_fixed_variant": on_primary_fixed_variant.to_hex(), + "surface_tint": primary_adjusted.to_hex(), + # Secondary + "secondary": secondary_adjusted.to_hex(), + "on_secondary": on_secondary.to_hex(), + "secondary_container": secondary_container.to_hex(), + "on_secondary_container": on_secondary_container.to_hex(), + "secondary_fixed": secondary_fixed.to_hex(), + "secondary_fixed_dim": secondary_fixed_dim.to_hex(), + "on_secondary_fixed": on_secondary_fixed.to_hex(), + "on_secondary_fixed_variant": on_secondary_fixed_variant.to_hex(), + # Tertiary + "tertiary": tertiary_adjusted.to_hex(), + "on_tertiary": on_tertiary.to_hex(), + "tertiary_container": tertiary_container.to_hex(), + "on_tertiary_container": on_tertiary_container.to_hex(), + "tertiary_fixed": tertiary_fixed.to_hex(), + "tertiary_fixed_dim": tertiary_fixed_dim.to_hex(), + "on_tertiary_fixed": on_tertiary_fixed.to_hex(), + "on_tertiary_fixed_variant": on_tertiary_fixed_variant.to_hex(), + # Error + "error": error.to_hex(), + "on_error": on_error.to_hex(), + "error_container": error_container.to_hex(), + "on_error_container": on_error_container.to_hex(), + # Surface + "surface": surface.to_hex(), + "on_surface": on_surface.to_hex(), + "surface_variant": surface_variant.to_hex(), + "on_surface_variant": on_surface_variant.to_hex(), + "surface_dim": surface_dim.to_hex(), + "surface_bright": surface_bright.to_hex(), + # Surface containers + "surface_container_lowest": surface_container_lowest.to_hex(), + "surface_container_low": surface_container_low.to_hex(), + "surface_container": surface_container.to_hex(), + "surface_container_high": surface_container_high.to_hex(), + "surface_container_highest": surface_container_highest.to_hex(), + # Outline and other + "outline": outline.to_hex(), + "outline_variant": outline_variant.to_hex(), + "shadow": shadow.to_hex(), + "scrim": scrim.to_hex(), + # Inverse + "inverse_surface": inverse_surface.to_hex(), + "inverse_on_surface": inverse_on_surface.to_hex(), + "inverse_primary": inverse_primary.to_hex(), + # Background + "background": background.to_hex(), + "on_background": on_background.to_hex(), + } + + +def generate_theme( + palette: list[Color], + mode: ThemeMode, + scheme_type: str = "tonal-spot" +) -> dict[str, str]: + """ + Generate theme for specified mode and scheme type. + + Args: + palette: List of extracted colors + mode: "dark" or "light" + scheme_type: One of "tonal-spot", "fruit-salad", "rainbow", "vibrant", "faithful", "dysfunctional", "muted" + + Returns: + Dictionary of color token names to hex values + """ + # Handle vibrant/faithful/dysfunctional modes (use generate_normal_* functions) + # All three use same theme generation, but different color extraction (handled in palette.py) + if scheme_type in ("vibrant", "faithful", "dysfunctional"): + if mode == "dark": + return generate_normal_dark(palette) + return generate_normal_light(palette) + + # Handle muted mode (low saturation, monochrome wallpapers) + if scheme_type == "muted": + if mode == "dark": + return generate_muted_dark(palette) + return generate_muted_light(palette) + + # All other schemes use Material Design 3 generation + if mode == "dark": + return generate_material_dark(palette, scheme_type) + return generate_material_light(palette, scheme_type) diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/migrate-colorschemes.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/migrate-colorschemes.py new file mode 100644 index 0000000..0f87043 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/migrate-colorschemes.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +import os +import json +import sys +import urllib.request +import urllib.parse +from pathlib import Path + +# Registry URL for color schemes +REGISTRY_URL = "https://raw.githubusercontent.com/noctalia-dev/noctalia-colorschemes/main/registry.json" +RAW_BASE_URL = "https://raw.githubusercontent.com/noctalia-dev/noctalia-colorschemes/main/" + +def is_valid_format(data): + """Check if the scheme data has the new terminal format.""" + for variant in ['dark', 'light']: + if variant in data: + v_data = data[variant] + if isinstance(v_data, dict) and 'terminal' in v_data: + term = v_data['terminal'] + if isinstance(term, dict) and 'normal' in term: + if isinstance(term['normal'], dict) and 'black' in term['normal']: + return True + return False + +def get_registry(): + """Fetch the remote registry to get correct paths for schemes.""" + try: + with urllib.request.urlopen(REGISTRY_URL) as response: + return json.loads(response.read().decode()) + except Exception as e: + print(f"Error fetching registry: {e}") + return None + +def migrate(config_dir): + colorschemes_dir = Path(config_dir) / "colorschemes" + if not colorschemes_dir.exists(): + return + + registry = get_registry() + if not registry: + return + + # Map name to path from registry + theme_map = {t['name']: t['path'] for t in registry.get('themes', [])} + + for scheme_dir in colorschemes_dir.iterdir(): + if not scheme_dir.is_dir(): + continue + + scheme_name = scheme_dir.name + json_file = scheme_dir / f"{scheme_name}.json" + + if not json_file.exists(): + continue + + try: + with open(json_file, 'r') as f: + data = json.load(f) + except Exception: + continue + + if not is_valid_format(data): + print(f"Scheme '{scheme_name}' has old format. Attempting to redownload...") + + # Use registry path if available, otherwise fallback to name + remote_path = theme_map.get(scheme_name, scheme_name) + + # Encode URL parts to handle spaces and special characters + encoded_path = urllib.parse.quote(remote_path) + encoded_name = urllib.parse.quote(scheme_name) + remote_url = f"{RAW_BASE_URL}{encoded_path}/{encoded_name}.json" + + try: + with urllib.request.urlopen(remote_url) as response: + new_data = json.loads(response.read().decode()) + with open(json_file, 'w') as f: + json.dump(new_data, f, indent=2) + + print(f"Successfully migrated '{scheme_name}'") + except Exception as e: + print(f"Failed to migrate '{scheme_name}': {e}") + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: migrate-colorschemes.py ") + sys.exit(1) + + migrate(sys.argv[1]) diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/template-processor.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/template-processor.py new file mode 100644 index 0000000..f7bad69 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/template-processor.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python3 +""" +Noctalia's Template processor - Wallpaper-based color extraction and theme generation. + +A CLI tool that extracts dominant colors from wallpaper images and generates palettes with optional templating. + +Supported scheme types: +- tonal-spot: Default Android 12-13 Material You scheme (recommended) +- content: Preserves source color's chroma with temperature-based tertiary (matugen default) +- fruit-salad: Bold/playful with -50° hue rotation +- rainbow: Chromatic accents with grayscale neutrals +- monochrome: Pure grayscale M3 scheme (chroma = 0, only error has color) +- vibrant: Prioritizes the most saturated colors regardless of area coverage +- faithful: Prioritizes dominant colors by area, what you see is what you get +- dysfunctional: Like faithful but picks the 2nd most dominant color family +- muted: Preserves hue but caps saturation low (for monochrome/monotonal wallpapers) + +Usage: + python3 template-processor.py IMAGE_OR_JSON [OPTIONS] + +Options: + --scheme-type Scheme type: tonal-spot (default), content, fruit-salad, rainbow, monochrome, vibrant, faithful, dysfunctional, muted + --dark Generate dark theme only + --light Generate light theme only + --both Generate both themes (default) + -o, --output Write JSON output to file (stdout if omitted) + -r, --render Render a template (input_path:output_path) + -c, --config Path to TOML configuration file with template definitions + --mode Theme mode: dark or light + +Input: + Can be an image file (PNG/JPG) or a JSON color palette file. + +Example: + python3 template-processor.py ~/wallpaper.png --scheme-type tonal-spot + python3 template-processor.py ~/wallpaper.png --scheme-type fruit-salad --dark + python3 template-processor.py ~/wallpaper.jpg --dark -o theme.json + python3 template-processor.py ~/wallpaper.png -r template.txt:output.txt + python3 template-processor.py ~/wallpaper.png -c config.toml --mode dark + +Author: Noctalia Team +License: MIT +""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +# Import from lib package +from lib import ( + read_image, ImageReadError, extract_palette, generate_theme, + TemplateRenderer, expand_predefined_scheme, + extract_source_color, source_color_to_rgb, Color, +) +from lib.scheme import inject_terminal_colors + + +def parse_args() -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser( + prog='template-processor', + description='Extract color palettes from wallpapers and generate themes', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python3 template-processor.py wallpaper.png # tonal-spot (default), both themes + python3 template-processor.py wallpaper.png --scheme-type content --dark # content scheme, dark only + python3 template-processor.py wallpaper.jpg --dark -o theme.json # output to file + python3 template-processor.py wallpaper.png -r template.txt:output.txt # render template + python3 template-processor.py wallpaper.png -c config.toml --mode dark # render config, dark only + """ + ) + + parser.add_argument( + 'image', + type=Path, + nargs='?', + help='Path to wallpaper image (PNG/JPG) or JSON color palette (not required if --scheme is used)' + ) + + # Scheme type selection + parser.add_argument( + '--scheme-type', + choices=['tonal-spot', 'content', 'fruit-salad', 'rainbow', 'monochrome', 'vibrant', 'faithful', 'dysfunctional', 'muted'], + default='tonal-spot', + help='Color scheme type (default: tonal-spot)' + ) + + # Theme mode (mutually exclusive) + mode_group = parser.add_mutually_exclusive_group() + mode_group.add_argument( + '--dark', + action='store_true', + help='Generate dark theme only' + ) + mode_group.add_argument( + '--light', + action='store_true', + help='Generate light theme only' + ) + mode_group.add_argument( + '--both', + action='store_true', + default=True, + help='Generate both dark and light themes (default)' + ) + + parser.add_argument( + '--output', '-o', + type=Path, + help='Write JSON output to file (stdout if omitted)' + ) + + parser.add_argument( + '--render', '-r', + action='append', + help='Render a template (input_path:output_path)' + ) + + parser.add_argument( + '--config', '-c', + type=Path, + help='Path to TOML configuration file with template definitions' + ) + parser.add_argument( + '--mode', + choices=['dark', 'light'], + help='Theme mode: dark or light' + ) + + parser.add_argument( + '--scheme', + type=Path, + help='Path to predefined scheme JSON file (bypasses image extraction)' + ) + + parser.add_argument( + '--default-mode', + choices=['dark', 'light'], + default='dark', + help='Theme mode to use for "default" in templates (default: dark)' + ) + + return parser.parse_args() + + +def main() -> int: + """Main entry point.""" + args = parse_args() + + # Initialize result dictionary + result: dict[str, dict[str, str]] = {} + + # Determine mode from arguments + if args.mode == 'dark': + modes = ["dark"] + elif args.mode == 'light': + modes = ["light"] + elif args.dark: + modes = ["dark"] + elif args.light: + modes = ["light"] + else: + modes = ["dark", "light"] + + # Path 1: Predefined scheme (--scheme flag) + if args.scheme: + if not args.scheme.exists(): + print(f"Error: Scheme file not found: {args.scheme}", file=sys.stderr) + return 1 + + try: + with open(args.scheme, 'r') as f: + scheme_data = json.load(f) + + # Scheme format: {"dark": {"mPrimary": "#...", ...}, "light": {...}} + # or single mode: {"mPrimary": "#...", ...} + for mode in modes: + if mode in scheme_data: + # Multi-mode format + result[mode] = expand_predefined_scheme(scheme_data[mode], mode) + inject_terminal_colors(result[mode], scheme_data[mode]) + elif "mPrimary" in scheme_data: + # Single-mode format - use same colors for requested mode + result[mode] = expand_predefined_scheme(scheme_data, mode) + inject_terminal_colors(result[mode], scheme_data) + else: + print(f"Error: Invalid scheme format - missing '{mode}' or 'mPrimary'", file=sys.stderr) + return 1 + + except json.JSONDecodeError as e: + print(f"Error parsing scheme JSON: {e}", file=sys.stderr) + return 1 + except KeyError as e: + print(f"Error: Missing required color in scheme: {e}", file=sys.stderr) + return 1 + except Exception as e: + print(f"Error processing scheme: {e}", file=sys.stderr) + return 1 + + # Path 2: Image-based extraction (default) + else: + # Validate image argument is provided + if args.image is None: + print("Error: Image path is required (unless --scheme is used)", file=sys.stderr) + return 1 + + # Validate image path + if not args.image.exists(): + print(f"Error: Image not found: {args.image}", file=sys.stderr) + return 1 + + # Check if input is a JSON palette (Predefined Color Scheme) + if args.image.suffix.lower() == '.json': + try: + with open(args.image, 'r') as f: + input_data = json.load(f) + + # Expect {"colors": ...} or direct dict + colors_data = input_data.get("colors", input_data) + + # Flatten QML-style object structure if needed + # structure: key -> { default: { hex: "#..." } } or key -> "#..." + flat_colors = {} + for k, v in colors_data.items(): + if isinstance(v, dict) and 'default' in v and 'hex' in v['default']: + flat_colors[k] = v['default']['hex'] + elif isinstance(v, str): + flat_colors[k] = v + else: + # Best effort fallback + flat_colors[k] = str(v) + + # Assign to requested modes + for mode in modes: + result[mode] = flat_colors + + except Exception as e: + print(f"Error reading JSON palette: {e}", file=sys.stderr) + return 1 + else: + # Standard Image Extraction + if not args.image.is_file(): + print(f"Error: Not a file: {args.image}", file=sys.stderr) + return 1 + + # Determine scheme type + scheme_type = args.scheme_type + + # M3 schemes use Triangle filter (matches matugen), others use Box + # (sharper downscale preserves distinct color regions for k-means) + m3_schemes = {"tonal-spot", "content", "fruit-salad", "rainbow", "monochrome"} + resize_filter = "Triangle" if scheme_type in m3_schemes else "Box" + + try: + pixels = read_image(args.image, resize_filter) + except ImageReadError as e: + print(f"Error reading image: {e}", file=sys.stderr) + return 1 + except Exception as e: + print(f"Unexpected error reading image: {e}", file=sys.stderr) + return 1 + + # Extract palette based on scheme type: + # - M3 schemes (tonal-spot, fruit-salad, rainbow, content): Use Wu quantizer + Score + # This matches matugen's color extraction exactly + # - vibrant: Use k-means clustering for colorful/blended colors + # - faithful: Use Wu quantizer for primary (dominant by area), k-means for accents + # - dysfunctional: Like faithful but picks 2nd most dominant color family + # - muted: Like count but without chroma filtering (for monochrome wallpapers) + if scheme_type == "vibrant": + # K-means with chroma scoring for vibrant, blended colors + palette = extract_palette(pixels, k=5, scoring="chroma") + elif scheme_type == "faithful": + # K-means with count scoring - picks dominant color by area coverage + # This ensures primary reflects what you actually see in the image + palette = extract_palette(pixels, k=5, scoring="count") + elif scheme_type == "dysfunctional": + # K-means with dysfunctional scoring - picks 2nd most dominant color family + # For when the dominant color is not what you want as primary + palette = extract_palette(pixels, k=5, scoring="dysfunctional") + elif scheme_type == "muted": + # K-means with muted scoring - accepts low/zero chroma colors + # For monochrome/monotonal wallpapers where dominant color has low saturation + palette = extract_palette(pixels, k=5, scoring="muted") + else: + # Wu quantizer + Score algorithm (matches matugen) + source_argb = extract_source_color(pixels) + r, g, b = source_color_to_rgb(source_argb) + palette = [Color(r, g, b)] + + if not palette: + print("Error: Could not extract colors from image", file=sys.stderr) + return 1 + + # Generate theme for each mode + for mode in modes: + result[mode] = generate_theme(palette, mode, scheme_type) + + # Output JSON + json_output = json.dumps(result, indent=2) + + if args.output: + try: + args.output.write_text(json_output) + print(f"Theme written to: {args.output}", file=sys.stderr) + except IOError as e: + print(f"Error writing output: {e}", file=sys.stderr) + return 1 + elif not args.render and not args.config: + print(json_output) + + # Process templates + if args.render or args.config: + image_path = str(args.image) if args.image else None + renderer = TemplateRenderer(result, default_mode=args.default_mode, image_path=image_path, scheme_type=args.scheme_type) + + if args.render: + for render_spec in args.render: + if ':' not in render_spec: + print(f"Error: Invalid render spec (must be input:output): {render_spec}", file=sys.stderr) + continue + + input_str, output_str = render_spec.split(':', 1) + input_path = Path(input_str).expanduser() + output_path = Path(output_str).expanduser() + + if not input_path.exists(): + print(f"Error: Template not found: {input_path}", file=sys.stderr) + continue + + renderer.render_file(input_path, output_path) + + if args.config: + if not args.config.exists(): + print(f"Error: Config file not found: {args.config}", file=sys.stderr) + else: + renderer.process_config_file(args.config) + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/vscode-helper.py b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/vscode-helper.py new file mode 100644 index 0000000..722092a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Scripts/python/src/theming/vscode-helper.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# Finds all installed Noctalia theme extensions for VSCode/VSCodium. + +import sys +from pathlib import Path + + +def find_all_noctalia_themes(extensions_dir: Path, prefix: str) -> list[str]: + # Bail early if the extensions directory doesn't exist + if not extensions_dir.is_dir(): + return [] + # Collect all directories matching the extension prefix + candidates = [d for d in extensions_dir.iterdir() if d.is_dir() and d.name.startswith(prefix)] + # Return theme file paths for all matching extensions + return [str(d / "themes" / "NoctaliaTheme-color-theme.json") for d in candidates] + + +if __name__ == "__main__": + # Resolve ~ in the provided extensions directory path + extensions_dir = Path(sys.argv[1]).expanduser() + prefix = sys.argv[2] if len(sys.argv) > 2 else "noctalia.noctaliatheme-" + + # Print the resolved paths to stdout for the QML Process to capture + results = find_all_noctalia_themes(extensions_dir, prefix) + if results: + for path in results: + print(path) + else: + print(f"No matching extension found in {extensions_dir}", file=sys.stderr) + sys.exit(1) \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Compositor/CompositorService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Compositor/CompositorService.qml new file mode 100644 index 0000000..62b6232 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Compositor/CompositorService.qml @@ -0,0 +1,665 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.Control +import qs.Services.UI + +Singleton { + id: root + + // Compositor detection + property bool isHyprland: false + property bool isNiri: false + property bool isSway: false + property bool isMango: false + property bool isLabwc: false + property bool isExtWorkspace: false + property bool isScroll: false + + // Generic workspace and window data + property ListModel workspaces: ListModel {} + property ListModel windows: ListModel {} + property int focusedWindowIndex: -1 + + // Display scale data + property var displayScales: ({}) + property bool displayScalesLoaded: false + + // Overview state (Niri-specific, defaults to false for other compositors) + property bool overviewActive: false + + // Global workspaces flag (workspaces shared across all outputs) + // True for LabWC (stacking compositor), false for tiling WMs with per-output workspaces + property bool globalWorkspaces: false + + // Generic events + signal workspaceChanged + signal activeWindowChanged + signal windowListChanged + + // Backend service loader + property var backend: null + + Component.onCompleted: { + // Load display scales from ShellState + Qt.callLater(() => { + if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { + loadDisplayScalesFromState(); + } + }); + + detectCompositor(); + } + + Connections { + target: typeof ShellState !== 'undefined' ? ShellState : null + function onIsLoadedChanged() { + if (ShellState.isLoaded) { + loadDisplayScalesFromState(); + } + } + } + + function detectCompositor() { + const hyprlandSignature = Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE"); + const niriSocket = Quickshell.env("NIRI_SOCKET"); + const swaySock = Quickshell.env("SWAYSOCK"); + const currentDesktop = Quickshell.env("XDG_CURRENT_DESKTOP"); + const labwcPid = Quickshell.env("LABWC_PID"); + + // Check for MangoWC using XDG_CURRENT_DESKTOP environment variable + // MangoWC sets XDG_CURRENT_DESKTOP=mango + if (currentDesktop && currentDesktop.toLowerCase().includes("mango")) { + isHyprland = false; + isNiri = false; + isSway = false; + isMango = true; + isLabwc = false; + isExtWorkspace = false; + backendLoader.sourceComponent = mangoComponent; + } else if (labwcPid && labwcPid.length > 0) { + isHyprland = false; + isNiri = false; + isSway = false; + isMango = false; + isLabwc = true; + isExtWorkspace = false; + backendLoader.sourceComponent = labwcComponent; + Logger.i("CompositorService", "Detected LabWC with PID: " + labwcPid); + } else if (niriSocket && niriSocket.length > 0) { + isHyprland = false; + isNiri = true; + isSway = false; + isMango = false; + isLabwc = false; + isExtWorkspace = false; + backendLoader.sourceComponent = niriComponent; + } else if (hyprlandSignature && hyprlandSignature.length > 0) { + isHyprland = true; + isNiri = false; + isSway = false; + isMango = false; + isLabwc = false; + isExtWorkspace = false; + backendLoader.sourceComponent = hyprlandComponent; + } else if (swaySock && swaySock.length > 0) { + isHyprland = false; + isNiri = false; + isSway = true; + isMango = false; + isLabwc = false; + isExtWorkspace = false; + isScroll = currentDesktop && currentDesktop.toLowerCase().includes("scroll"); + backendLoader.sourceComponent = swayComponent; + } else { + // Always fallback to ext-workspace-v1 + isHyprland = false; + isNiri = false; + isSway = false; + isMango = false; + isLabwc = false; + isExtWorkspace = true; + backendLoader.sourceComponent = extWorkspaceComponent; + Logger.i("CompositorService", "Using generic ext-workspace backend (no recognized compositor env)"); + } + } + + Loader { + id: backendLoader + onLoaded: { + if (item) { + if (isScroll) { + item.msgCommand = "scrollmsg"; + } + root.backend = item; + setupBackendConnections(); + backend.initialize(); + } + } + } + + // Load display scales from ShellState + function loadDisplayScalesFromState() { + try { + const cached = ShellState.getDisplay(); + if (cached && Object.keys(cached).length > 0) { + displayScales = cached; + displayScalesLoaded = true; + Logger.d("CompositorService", "Loaded display scales from ShellState"); + } else { + // Migration is now handled in Settings.qml + displayScalesLoaded = true; + } + } catch (error) { + Logger.e("CompositorService", "Failed to load display scales:", error); + displayScalesLoaded = true; + } + } + + // Hyprland backend component + Component { + id: hyprlandComponent + HyprlandService { + id: hyprlandBackend + } + } + + // Niri backend component + Component { + id: niriComponent + NiriService { + id: niriBackend + } + } + + // Sway backend component + Component { + id: swayComponent + SwayService { + id: swayBackend + } + } + + // Mango backend component + Component { + id: mangoComponent + MangoService { + id: mangoBackend + } + } + + // Labwc backend component + Component { + id: labwcComponent + LabwcService { + id: labwcBackend + } + } + + // Generic ext-workspace (WindowManager) when compositor env is unknown + Component { + id: extWorkspaceComponent + ExtWorkspaceService { + id: extWorkspaceBackend + } + } + + function setupBackendConnections() { + if (!backend) + return; + + // Connect backend signals to facade signals + backend.workspaceChanged.connect(() => { + // Sync workspaces when they change + syncWorkspaces(); + // Forward the signal + workspaceChanged(); + }); + + backend.activeWindowChanged.connect(() => { + // Only sync focus state, not entire window list + syncFocusedWindow(); + // Forward the signal + activeWindowChanged(); + }); + + backend.windowListChanged.connect(() => { + syncWindows(); + }); + + // Property bindings - use automatic property change signal + backend.focusedWindowIndexChanged.connect(() => { + focusedWindowIndex = backend.focusedWindowIndex; + }); + + // Overview state (Niri-specific) + if (backend.overviewActiveChanged) { + backend.overviewActiveChanged.connect(() => { + overviewActive = backend.overviewActive; + }); + } + + // Initial sync + syncWorkspaces(); + syncWindows(); + focusedWindowIndex = backend.focusedWindowIndex; + if (backend.overviewActive !== undefined) { + overviewActive = backend.overviewActive; + } + if (backend.globalWorkspaces !== undefined) { + globalWorkspaces = backend.globalWorkspaces; + } + } + + function syncWorkspaces() { + workspaces.clear(); + const ws = backend.workspaces; + for (var i = 0; i < ws.count; i++) { + workspaces.append(ws.get(i)); + } + // Emit signal to notify listeners that workspace list has been updated + workspacesChanged(); + } + + function syncWindows() { + windows.clear(); + const ws = backend.windows; + for (var i = 0; i < ws.length; i++) { + windows.append(ws[i]); + } + // Emit signal to notify listeners that window list has been updated + windowListChanged(); + } + + // Sync only the focused window state, not the entire window list + function syncFocusedWindow() { + const newIndex = backend.focusedWindowIndex; + + // Update isFocused flags by syncing from backend + for (var i = 0; i < windows.count && i < backend.windows.length; i++) { + const backendFocused = backend.windows[i].isFocused; + if (windows.get(i).isFocused !== backendFocused) { + windows.setProperty(i, "isFocused", backendFocused); + } + } + + focusedWindowIndex = newIndex; + } + + // Update display scales from backend + function updateDisplayScales() { + if (!backend || !backend.queryDisplayScales) { + Logger.w("CompositorService", "Backend does not support display scale queries"); + return; + } + + backend.queryDisplayScales(); + } + + // Called by backend when display scales are ready + function onDisplayScalesUpdated(scales) { + displayScales = scales; + saveDisplayScalesToCache(); + Logger.d("CompositorService", "Display scales updated"); + } + + // Save display scales to cache + function saveDisplayScalesToCache() { + try { + ShellState.setDisplay(displayScales); + Logger.d("CompositorService", "Saved display scales to ShellState"); + } catch (error) { + Logger.e("CompositorService", "Failed to save display scales:", error); + } + } + + // Public function to get scale for a specific display + function getDisplayScale(displayName) { + if (!displayName || !displayScales[displayName]) { + return 1.0; + } + return displayScales[displayName].scale || 1.0; + } + + // Public function to get all display info for a specific display + function getDisplayInfo(displayName) { + if (!displayName || !displayScales[displayName]) { + return null; + } + return displayScales[displayName]; + } + + // Get focused window + function getFocusedWindow() { + if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.count) { + return windows.get(focusedWindowIndex); + } + return null; + } + + // Get focused screen from compositor + function getFocusedScreen() { + if (backend && backend.getFocusedScreen) { + return backend.getFocusedScreen(); + } + return null; + } + + // Get focused window title + function getFocusedWindowTitle() { + if (focusedWindowIndex >= 0 && focusedWindowIndex < windows.count) { + var title = windows.get(focusedWindowIndex).title; + if (title !== undefined) { + title = title.replace(/(\r\n|\n|\r)/g, ""); + } + return title || ""; + } + return ""; + } + + // Get clean app name from appId + // Extracts the last segment from reverse domain notation (e.g., "org.kde.dolphin" -> "Dolphin") + // Falls back to title if appId is empty + function getCleanAppName(appId, fallbackTitle) { + var name = (appId || "").split(".").pop() || fallbackTitle || "Unknown"; + return name.charAt(0).toUpperCase() + name.slice(1); + } + + function getWindowsForWorkspace(workspaceId) { + var windowsInWs = []; + for (var i = 0; i < windows.count; i++) { + var window = windows.get(i); + if (window.workspaceId === workspaceId) { + // Snapshot to plain JS object so callers never hold live ListModel + // proxies that become invalid when syncWindows() clears the model. + windowsInWs.push({ + id: window.id, + title: window.title, + appId: window.appId, + isFocused: window.isFocused, + workspaceId: window.workspaceId, + handle: window.handle + }); + } + } + return windowsInWs; + } + + // Generic workspace switching + function switchToWorkspace(workspace) { + if (backend && backend.switchToWorkspace) { + backend.switchToWorkspace(workspace); + } else { + Logger.w("Compositor", "No backend available for workspace switching"); + } + } + + // Scrollable workspace content (Niri) + function scrollWorkspaceContent(direction) { + if (backend && backend.scrollWorkspaceContent) { + backend.scrollWorkspaceContent(direction); + } + } + + // Get current workspace + function getCurrentWorkspace() { + for (var i = 0; i < workspaces.count; i++) { + const ws = workspaces.get(i); + if (ws.isFocused) { + return ws; + } + } + return null; + } + + // Get active workspaces + function getActiveWorkspaces() { + const activeWorkspaces = []; + for (var i = 0; i < workspaces.count; i++) { + const ws = workspaces.get(i); + if (ws.isActive) { + activeWorkspaces.push(ws); + } + } + return activeWorkspaces; + } + + // Set focused window + function focusWindow(window) { + if (backend && backend.focusWindow) { + backend.focusWindow(window); + } else { + Logger.w("Compositor", "No backend available for window focus"); + } + } + + // Close window + function closeWindow(window) { + if (backend && backend.closeWindow) { + backend.closeWindow(window); + } else { + Logger.w("Compositor", "No backend available for window closing"); + } + } + + // Spawn command + function spawn(command) { + // Ensure command is a proper JS array (QML lists can behave unexpectedly in some contexts) + const cmdArray = Array.isArray(command) ? command : (command && typeof command === "object" && command.length !== undefined) ? Array.from(command) : [command]; + + Logger.d("CompositorService", `Spawning: ${cmdArray.join(" ")}`); + if (backend && backend.spawn) { + backend.spawn(cmdArray); + } else { + try { + Quickshell.execDetached(cmdArray); + } catch (e) { + Logger.e("CompositorService", "Failed to execute detached:", e); + } + } + } + + // Session management helper for custom commands + function getCustomCommand(action) { + const powerOptions = Settings.data.sessionMenu.powerOptions || []; + for (let i = 0; i < powerOptions.length; i++) { + const option = powerOptions[i]; + if (option.action === action && option.enabled && option.command && option.command.trim() !== "") { + return option.command.trim(); + } + } + return ""; + } + + function executeSessionAction(action, defaultCommand) { + const customCommand = getCustomCommand(action); + if (customCommand) { + Logger.i("Compositor", `Executing custom command for action: ${action} Command: ${customCommand}`); + Quickshell.execDetached(["sh", "-c", customCommand]); + return true; + } + return false; + } + + // Session management + function logout() { + Logger.i("Compositor", "Logout requested"); + if (executeSessionAction("logout")) + return; + + if (backend && backend.logout) { + backend.logout(); + } else { + Logger.w("Compositor", "No backend available for logout"); + } + } + + function shutdown() { + Logger.i("Compositor", "Shutdown requested"); + if (executeSessionAction("shutdown")) + return; + + HooksService.executeSessionHook("shutdown", () => { + Quickshell.execDetached(["sh", "-c", "systemctl poweroff || loginctl poweroff"]); + }); + } + + function reboot() { + Logger.i("Compositor", "Reboot requested"); + if (executeSessionAction("reboot")) + return; + + HooksService.executeSessionHook("reboot", () => { + Quickshell.execDetached(["sh", "-c", "systemctl reboot || loginctl reboot"]); + }); + } + + function userspaceReboot() { + Logger.i("Compositor", "Userspace reboot requested"); + if (executeSessionAction("userspaceReboot")) + return; + + HooksService.executeSessionHook("userspaceReboot", () => { + Quickshell.execDetached(["sh", "-c", "systemctl soft-reboot"]); + }); + } + + function rebootToUefi() { + Logger.i("Compositor", "Reboot to UEFI firmware requested requested"); + if (executeSessionAction("rebootToUefi")) + return; + + HooksService.executeSessionHook("rebootToUefi", () => { + Quickshell.execDetached(["sh", "-c", "systemctl reboot --firmware-setup || loginctl reboot --firmware-setup"]); + }); + } + + function turnOffMonitors() { + Logger.i("Compositor", "Turn off monitors requested"); + if (backend && backend.turnOffMonitors) { + backend.turnOffMonitors(); + } else { + Logger.w("Compositor", "No backend available for turnOffMonitors"); + } + } + + function turnOnMonitors() { + Logger.i("Compositor", "Turn on monitors requested"); + if (backend && backend.turnOnMonitors) { + backend.turnOnMonitors(); + } else { + Logger.w("Compositor", "No backend available for turnOnMonitors"); + } + } + + function suspend() { + Logger.i("Compositor", "Suspend requested"); + if (executeSessionAction("suspend")) + return; + + Quickshell.execDetached(["sh", "-c", "systemctl suspend || loginctl suspend"]); + } + + function lock() { + Logger.i("Compositor", "LockScreen requested"); + if (executeSessionAction("lock")) + return; + + if (PanelService && PanelService.lockScreen) { + PanelService.lockScreen.active = true; + } + } + + function hibernate() { + Logger.i("Compositor", "Hibernate requested"); + if (executeSessionAction("hibernate")) + return; + + Quickshell.execDetached(["sh", "-c", "systemctl hibernate || loginctl hibernate"]); + } + + function cycleKeyboardLayout() { + if (backend && backend.cycleKeyboardLayout) { + backend.cycleKeyboardLayout(); + } + } + + property int lockAndSuspendCheckCount: 0 + + function lockAndSuspend() { + Logger.i("Compositor", "Lock and suspend requested"); + + // if a custom lock command exists, execute it and suspend without wait + if (executeSessionAction("lock")) { + suspend(); + return; + } + + // If already locked, suspend immediately + if (PanelService && PanelService.lockScreen && PanelService.lockScreen.active) { + Logger.i("Compositor", "Screen already locked, suspending"); + suspend(); + return; + } + + // Lock the screen first + try { + if (PanelService && PanelService.lockScreen) { + PanelService.lockScreen.active = true; + lockAndSuspendCheckCount = 0; + + // Wait for lock screen to be confirmed active before suspending + lockAndSuspendTimer.start(); + } else { + Logger.w("Compositor", "Lock screen not available, suspending without lock"); + suspend(); + } + } catch (e) { + Logger.w("Compositor", "Failed to activate lock screen before suspend: " + e); + suspend(); + } + } + + Timer { + id: lockAndSuspendTimer + interval: 100 + repeat: true + running: false + + onTriggered: { + lockAndSuspendCheckCount++; + + // Check if lock screen is now active + if (PanelService && PanelService.lockScreen && PanelService.lockScreen.active) { + // Verify the lock screen component is loaded + if (PanelService.lockScreen.item) { + Logger.i("Compositor", "Lock screen confirmed active, suspending"); + stop(); + lockAndSuspendCheckCount = 0; + suspend(); + } else { + // Lock screen is active but component not loaded yet, wait a bit more + if (lockAndSuspendCheckCount > 20) { + // Max 2 seconds wait + Logger.w("Compositor", "Lock screen active but component not loaded, suspending anyway"); + stop(); + lockAndSuspendCheckCount = 0; + suspend(); + } + } + } else { + // Lock screen not active yet, keep checking + if (lockAndSuspendCheckCount > 30) { + // Max 3 seconds wait + Logger.w("Compositor", "Lock screen failed to activate, suspending anyway"); + stop(); + lockAndSuspendCheckCount = 0; + suspend(); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Compositor/ExtWorkspaceService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Compositor/ExtWorkspaceService.qml new file mode 100644 index 0000000..1ed4551 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Compositor/ExtWorkspaceService.qml @@ -0,0 +1,300 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.WindowManager +import qs.Commons + +// Generic ext-workspace-v1 + toplevel handling +Item { + id: root + + property ListModel workspaces: ListModel {} + property var windows: [] + property int focusedWindowIndex: -1 + property var trackedToplevels: new Set() + + property bool globalWorkspaces: false + + property var nativeWorkspaceMap: ({}) + property var connectedWorkspaces: ({}) + + signal workspaceChanged + signal activeWindowChanged + signal windowListChanged + signal displayScalesChanged + + function initialize() { + updateWindows(); + connectWorkspaceSignals(); + syncWorkspaces(); + Logger.i("ExtWorkspaceService", "Service started (generic ext-workspace-v1)"); + } + + Connections { + target: WindowManager + + function onWindowsetsChanged() { + root.connectWorkspaceSignals(); + Qt.callLater(root.syncWorkspaces); + } + + function onWindowsetProjectionsChanged() { + Qt.callLater(root.syncWorkspaces); + } + } + + Timer { + interval: 500 + running: true + repeat: false + onTriggered: { + if (WindowManager.windowsets.length > 0) { + root.connectWorkspaceSignals(); + root.syncWorkspaces(); + } + } + } + + function connectWorkspaceSignals() { + const nativeWs = WindowManager.windowsets; + const newConnected = {}; + + for (const ws of nativeWs) { + const key = ws.id || ws.toString(); + newConnected[key] = true; + + if (connectedWorkspaces[key]) + continue; + + ws.activeChanged.connect(() => { + Qt.callLater(root.syncWorkspaces); + }); + + ws.urgentChanged.connect(() => { + Qt.callLater(root.syncWorkspaces); + }); + + ws.shouldDisplayChanged.connect(() => { + Qt.callLater(root.syncWorkspaces); + }); + + ws.nameChanged.connect(() => { + Qt.callLater(root.syncWorkspaces); + }); + } + + connectedWorkspaces = newConnected; + } + + function syncWorkspaces() { + const nativeWs = WindowManager.windowsets; + + workspaces.clear(); + nativeWorkspaceMap = {}; + + /* Per-output (projection) index: compositors expose one workspace group per + * monitor with names "1"…"9". A single global idx produced 10–18 on the + * second head because all windowsets were merged into one list. */ + const perOutputNextIdx = {}; + + for (const ws of nativeWs) { + if (!ws.shouldDisplay) { + continue; + } + + let outputName = ""; + if (ws.projection) { + const projScreens = ws.projection.screens; + if (projScreens && projScreens.length > 0) { + outputName = projScreens[0].name || ""; + } + } + + const groupKey = outputName || "_"; + let idx; + const numericName = ws.name && /^\d+$/.test(String(ws.name)) ? parseInt(ws.name, 10) : NaN; + if (!isNaN(numericName) && numericName >= 1) { + idx = numericName; + } else { + if (perOutputNextIdx[groupKey] === undefined) { + perOutputNextIdx[groupKey] = 1; + } + idx = perOutputNextIdx[groupKey]++; + } + + const wsEntry = { + "id": ws.id || idx.toString(), + "idx": idx, + "name": ws.name || ("Workspace " + idx), + "output": outputName, + "isFocused": ws.active, + "isActive": true, + "isUrgent": ws.urgent, + "isOccupied": false, + "oid": ws.id || idx.toString() + }; + + workspaces.append(wsEntry); + nativeWorkspaceMap[wsEntry.id] = ws; + } + + updateWindowWorkspaces(); + workspaceChanged(); + } + + function updateWindowWorkspaces() { + let activeId = ""; + for (let i = 0; i < workspaces.count; i++) { + const ws = workspaces.get(i); + if (ws.isFocused) { + activeId = ws.id; + break; + } + } + + for (let i = 0; i < windows.length; i++) { + if (activeId) { + windows[i].workspaceId = activeId; + } + } + windowListChanged(); + } + + Connections { + target: ToplevelManager.toplevels + function onValuesChanged() { + updateWindows(); + } + } + + function connectToToplevel(toplevel) { + if (!toplevel) + return; + + toplevel.activatedChanged.connect(() => { + Qt.callLater(onToplevelActivationChanged); + }); + + toplevel.titleChanged.connect(() => { + Qt.callLater(updateWindows); + }); + } + + function onToplevelActivationChanged() { + updateWindows(); + activeWindowChanged(); + } + + function updateWindows() { + const newWindows = []; + const toplevels = ToplevelManager.toplevels?.values || []; + + let focusedIdx = -1; + let idx = 0; + + let activeId = ""; + for (let i = 0; i < workspaces.count; i++) { + const ws = workspaces.get(i); + if (ws.isFocused) { + activeId = ws.id; + break; + } + } + + for (const toplevel of toplevels) { + if (!toplevel) + continue; + + if (!trackedToplevels.has(toplevel)) { + connectToToplevel(toplevel); + trackedToplevels.add(toplevel); + } + + const output = (toplevel.screens && toplevel.screens.length > 0) ? (toplevel.screens[0].name || "") : ""; + + const windowId = (toplevel.appId || "") + ":" + idx; + + newWindows.push({ + "id": windowId, + "appId": toplevel.appId || "", + "title": toplevel.title || "", + "output": output, + "workspaceId": activeId || "1", + "isFocused": toplevel.activated || false, + "toplevel": toplevel + }); + + if (toplevel.activated) { + focusedIdx = idx; + } + idx++; + } + windows = newWindows; + focusedWindowIndex = focusedIdx; + + windowListChanged(); + } + + function focusWindow(window) { + if (window.toplevel && typeof window.toplevel.activate === "function") { + window.toplevel.activate(); + } + } + + function closeWindow(window) { + if (window.toplevel && typeof window.toplevel.close === "function") { + window.toplevel.close(); + } + } + + function switchToWorkspace(workspace) { + const nativeWs = nativeWorkspaceMap[workspace.id] || nativeWorkspaceMap[workspace.oid]; + if (nativeWs && nativeWs.canActivate) { + nativeWs.activate(); + } else { + Logger.w("ExtWorkspaceService", "Cannot activate workspace: " + (workspace.name || workspace.id)); + } + } + + function turnOffMonitors() { + try { + Quickshell.execDetached(["wlr-randr", "--off"]); + } catch (e) { + Logger.e("ExtWorkspaceService", "Failed to turn off monitors:", e); + } + } + + function turnOnMonitors() { + try { + Quickshell.execDetached(["wlr-randr", "--on"]); + } catch (e) { + Logger.e("ExtWorkspaceService", "Failed to turn on monitors:", e); + } + } + + function logout() { + const sid = Quickshell.env("XDG_SESSION_ID"); + try { + if (sid && sid.length > 0) { + Quickshell.execDetached(["loginctl", "terminate-session", sid]); + } else { + Logger.w("ExtWorkspaceService", "logout: XDG_SESSION_ID unset; use session menu custom command or compositor-specific backend"); + } + } catch (e) { + Logger.e("ExtWorkspaceService", "Failed to logout:", e); + } + } + + function cycleKeyboardLayout() { + Logger.w("ExtWorkspaceService", "Keyboard layout cycling not supported"); + } + + function queryDisplayScales() { + Logger.w("ExtWorkspaceService", "Display scale queries not supported via ToplevelManager"); + } + + function getFocusedScreen() { + return null; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Compositor/HyprlandService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Compositor/HyprlandService.qml new file mode 100644 index 0000000..19cd804 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Compositor/HyprlandService.qml @@ -0,0 +1,615 @@ +import QtQuick +import Quickshell +import Quickshell.Hyprland +import Quickshell.Io +import qs.Commons +import qs.Services.Keyboard + +Item { + id: root + + // Properties that match the facade interface + property ListModel workspaces: ListModel {} + property var windows: [] + property int focusedWindowIndex: -1 + + // Signals that match the facade interface + signal workspaceChanged + signal activeWindowChanged + signal windowListChanged + signal displayScalesChanged + + // Hyprland-specific properties + property bool initialized: false + property var workspaceCache: ({}) + property var windowCache: ({}) + + // Debounce timer for window updates + Timer { + id: updateTimer + interval: 50 + repeat: false + onTriggered: safeUpdate() + } + + // Deferred via Qt.callLater to coalesce workspace updates: onRawEvent calls + // refreshWorkspaces() which triggers onValuesChanged synchronously in the + // same call stack — without deferral the ListModel gets cleared+repopulated + // twice per event. Qt.callLater deduplicates by function identity. + function _deferredWorkspaceUpdate() { + safeUpdateWorkspaces(); + workspaceChanged(); + } + + // Initialization + function initialize() { + if (initialized) + return; + try { + Hyprland.refreshWorkspaces(); + Hyprland.refreshToplevels(); + Qt.callLater(() => { + safeUpdateWorkspaces(); + safeUpdateWindows(); + queryDisplayScales(); + queryKeyboardLayout(); + }); + initialized = true; + Logger.i("HyprlandService", "Service started"); + } catch (e) { + Logger.e("HyprlandService", "Failed to initialize:", e); + } + } + + // Query display scales + function queryDisplayScales() { + hyprlandMonitorsProcess.running = true; + } + + // Hyprland monitors process for display scale detection + Process { + id: hyprlandMonitorsProcess + running: false + command: ["hyprctl", "monitors", "-j"] + + property string accumulatedOutput: "" + + stdout: SplitParser { + onRead: function (line) { + // Accumulate lines instead of parsing each one + hyprlandMonitorsProcess.accumulatedOutput += line; + } + } + + onExited: function (exitCode) { + if (exitCode !== 0 || !accumulatedOutput) { + Logger.e("HyprlandService", "Failed to query monitors, exit code:", exitCode); + accumulatedOutput = ""; + return; + } + + try { + const monitorsData = JSON.parse(accumulatedOutput); + const scales = {}; + + for (const monitor of monitorsData) { + if (monitor.name) { + scales[monitor.name] = { + "name": monitor.name, + "scale": monitor.scale || 1.0, + "width": monitor.width || 0, + "height": monitor.height || 0, + "refresh_rate": monitor.refreshRate || 0, + "x": monitor.x || 0, + "y": monitor.y || 0, + "active_workspace": monitor.activeWorkspace ? monitor.activeWorkspace.id : -1, + "vrr": monitor.vrr || false, + "focused": monitor.focused || false + }; + } + } + + // Notify CompositorService (it will emit displayScalesChanged) + if (CompositorService && CompositorService.onDisplayScalesUpdated) { + CompositorService.onDisplayScalesUpdated(scales); + } + } catch (e) { + Logger.e("HyprlandService", "Failed to parse monitors:", e); + } finally { + // Clear accumulated output for next query + accumulatedOutput = ""; + } + } + } + + function queryKeyboardLayout() { + hyprlandDevicesProcess.running = true; + } + // Hyprland devices process for keyboard layout detection + Process { + id: hyprlandDevicesProcess + running: false + command: ["hyprctl", "devices", "-j"] + + property string accumulatedOutput: "" + + stdout: SplitParser { + onRead: function (line) { + // Accumulate lines instead of parsing each one + hyprlandDevicesProcess.accumulatedOutput += line; + } + } + + onExited: function (exitCode) { + if (exitCode !== 0 || !accumulatedOutput) { + Logger.e("HyprlandService", "Failed to query devices, exit code:", exitCode); + accumulatedOutput = ""; + return; + } + + try { + const devicesData = JSON.parse(accumulatedOutput); + for (const keyboard of devicesData.keyboards) { + if (keyboard.main) { + const layoutName = keyboard.active_keymap; + KeyboardLayoutService.setCurrentLayout(layoutName); + Logger.d("HyprlandService", "Keyboard layout switched:", layoutName); + } + } + } catch (e) { + Logger.e("HyprlandService", "Failed to parse devices:", e); + } finally { + // Clear accumulated output for next query + accumulatedOutput = ""; + } + } + } + + // Safe update wrapper + function safeUpdate() { + safeUpdateWindows(); + safeUpdateWorkspaces(); + workspaceChanged(); + windowListChanged(); + } + + // Safe workspace update + function safeUpdateWorkspaces() { + try { + workspaces.clear(); + workspaceCache = {}; + + if (!Hyprland.workspaces || !Hyprland.workspaces.values) { + return; + } + + const hlWorkspaces = Hyprland.workspaces.values; + const occupiedIds = getOccupiedWorkspaceIds(); + + for (var i = 0; i < hlWorkspaces.length; i++) { + const ws = hlWorkspaces[i]; + if (ws.name && ws.name.startsWith("special:")) + continue; + + const wsData = { + "id": ws.id, + "idx": ws.id, + "name": ws.name || "", + "output": (ws.monitor && ws.monitor.name) ? ws.monitor.name : "", + "isActive": ws.active === true, + "isFocused": ws.focused === true, + "isUrgent": ws.urgent === true, + "isOccupied": occupiedIds[ws.id] === true + }; + + workspaceCache[ws.id] = wsData; + workspaces.append(wsData); + } + } catch (e) { + Logger.e("HyprlandService", "Error updating workspaces:", e); + } + } + + // Get occupied workspace IDs safely + function getOccupiedWorkspaceIds() { + const occupiedIds = {}; + + try { + if (!Hyprland.toplevels || !Hyprland.toplevels.values) { + return occupiedIds; + } + + const hlToplevels = Hyprland.toplevels.values; + for (var i = 0; i < hlToplevels.length; i++) { + const toplevel = hlToplevels[i]; + if (!toplevel) + continue; + try { + const wsId = toplevel.workspace ? toplevel.workspace.id : null; + if (wsId !== null && wsId !== undefined) { + occupiedIds[wsId] = true; + } + } catch (e) + + // Ignore individual toplevel errors + {} + } + } catch (e) + + // Return empty if we can't determine occupancy + {} + + return occupiedIds; + } + + // Safe window update + function safeUpdateWindows() { + try { + const windowsList = []; + windowCache = {}; + + if (!Hyprland.toplevels || !Hyprland.toplevels.values) { + windows = []; + focusedWindowIndex = -1; + return; + } + + const hlToplevels = Hyprland.toplevels.values; + let focusedWindowId = null; + + // Get active workspaces to filter focus + const activeWorkspaceIds = {}; + if (Hyprland.workspaces && Hyprland.workspaces.values) { + const hlWorkspaces = Hyprland.workspaces.values; + for (var j = 0; j < hlWorkspaces.length; j++) { + if (hlWorkspaces[j].active) { + activeWorkspaceIds[hlWorkspaces[j].id] = true; + } + } + } + + for (var i = 0; i < hlToplevels.length; i++) { + const toplevel = hlToplevels[i]; + if (!toplevel) + continue; + const windowData = extractWindowData(toplevel); + if (windowData) { + // If the window claims to be focused, verify it's on an active workspace + if (windowData.isFocused) { + if (!activeWorkspaceIds[windowData.workspaceId]) { + windowData.isFocused = false; + } + } + + // Normalize to a plain, backend-independent window object + const normalized = { + "id": windowData.id ? String(windowData.id) : "", + "title": windowData.title ? String(windowData.title) : "", + "appId": windowData.appId ? String(windowData.appId) : "", + "workspaceId": (typeof windowData.workspaceId === "number" && !isNaN(windowData.workspaceId)) ? windowData.workspaceId : -1, + "isFocused": windowData.isFocused === true, + "output": windowData.output ? String(windowData.output) : "", + "x": (typeof windowData.x === "number" && !isNaN(windowData.x)) ? windowData.x : 0, + "y": (typeof windowData.y === "number" && !isNaN(windowData.y)) ? windowData.y : 0 + }; + + windowsList.push(normalized); + windowCache[normalized.id] = normalized; + + if (normalized.isFocused) { + focusedWindowId = normalized.id; + } + } + } + + windows = toSortedWindowList(windowsList); + + // Resolve focused index from sorted list (order changes after sort) + let newFocusedIndex = -1; + if (focusedWindowId) { + for (let k = 0; k < windows.length; k++) { + if (windows[k].id === focusedWindowId) { + newFocusedIndex = k; + break; + } + } + } + + if (newFocusedIndex !== focusedWindowIndex) { + focusedWindowIndex = newFocusedIndex; + activeWindowChanged(); + } + } catch (e) { + Logger.e("HyprlandService", "Error updating windows:", e); + } + } + + // Extract window data safely from a toplevel + function extractWindowData(toplevel) { + if (!toplevel) + return null; + + try { + // Safely extract properties + const windowId = safeGetProperty(toplevel, "address", ""); + if (!windowId) + return null; + + const appId = getAppId(toplevel); + const title = getAppTitle(toplevel); + const wsId = toplevel.workspace ? toplevel.workspace.id : null; + const focused = toplevel.activated === true; + const output = toplevel.monitor?.name || ""; + + // Extract position + let x = 0; + let y = 0; + try { + const ipcData = toplevel.lastIpcObject; + if (ipcData && ipcData.at) { + x = ipcData.at[0]; + y = ipcData.at[1]; + } else if (typeof toplevel.x !== 'undefined') { + x = toplevel.x; + y = toplevel.y; + } + } catch (e) {} + + // Normalize coordinates to safe numeric values + const safeX = (typeof x === "number" && !isNaN(x)) ? x : 0; + const safeY = (typeof y === "number" && !isNaN(y)) ? y : 0; + + return { + "id": windowId, + "title": title, + "appId": appId, + "workspaceId": wsId || -1, + "isFocused": focused, + "output": output, + "x": safeX, + "y": safeY + }; + } catch (e) { + return null; + } + } + + function toSortedWindowList(windowList) { + return windowList.sort((a, b) => { + // Sort by workspace first (just in case they are mixed) + if (a.workspaceId !== b.workspaceId) { + return a.workspaceId - b.workspaceId; + } + // Then sort by X position (left to right) + if (a.x !== b.x) { + return a.x - b.x; + } + // Then sort by Y position (top to bottom) + if (a.y !== b.y) { + return a.y - b.y; + } + // Fallback to Window ID mapping + return a.id.localeCompare(b.id); + }); + } + + function getAppTitle(toplevel) { + try { + var title = toplevel.wayland.title; + if (title) + return title; + } catch (e) {} + + return safeGetProperty(toplevel, "title", ""); + } + + function getAppId(toplevel) { + if (!toplevel) + return ""; + + var appId = ""; + + // Try the wayland object first! + // From my (Lemmy) testing it works fine so we could probably get rid of all the other attempts below. + // Leaving them in for now, just in case... + try { + appId = toplevel.wayland.appId; + if (appId) + return appId; + } catch (e) {} + + // Try direct properties + appId = safeGetProperty(toplevel, "class", ""); + if (appId) + return appId; + + appId = safeGetProperty(toplevel, "initialClass", ""); + if (appId) + return appId; + + appId = safeGetProperty(toplevel, "appId", ""); + if (appId) + return appId; + + // Try lastIpcObject + try { + const ipcData = toplevel.lastIpcObject; + if (ipcData) { + return String(ipcData.class || ipcData.initialClass || ipcData.appId || ipcData.wm_class || ""); + } + } catch (e) {} + + return ""; + } + + // Safe property getter + function safeGetProperty(obj, prop, defaultValue) { + try { + const value = obj[prop]; + if (value !== undefined && value !== null) { + return String(value); + } + } catch (e) + + // Property access failed + {} + return defaultValue; + } + + function handleActiveLayoutEvent(ev) { + try { + let beforeParenthesis; + const parenthesisPos = ev.lastIndexOf('('); + + if (parenthesisPos === -1) { + beforeParenthesis = ev; + } else { + beforeParenthesis = ev.substring(0, parenthesisPos); + } + + const layoutNameStart = beforeParenthesis.lastIndexOf(',') + 1; + const layoutName = ev.substring(layoutNameStart); + + // Ignore bogus "error" layout reported by virtual keyboards (e.g. wtype) + if (layoutName.toLowerCase() === "error") { + Logger.d("HyprlandService", "Ignoring bogus 'error' layout from activelayout event"); + return; + } + + KeyboardLayoutService.setCurrentLayout(layoutName); + Logger.d("HyprlandService", "Keyboard layout switched:", layoutName); + } catch (e) { + Logger.e("HyprlandService", "Error handling activelayout:", e); + } + } + + // Connections to Hyprland + Connections { + target: Hyprland.workspaces + enabled: initialized + function onValuesChanged() { + Qt.callLater(_deferredWorkspaceUpdate); + } + } + + Connections { + target: Hyprland.toplevels + enabled: initialized + function onValuesChanged() { + updateTimer.restart(); + } + } + + Connections { + target: Hyprland + enabled: initialized + function onRawEvent(event) { + Hyprland.refreshWorkspaces(); + Hyprland.refreshToplevels(); + // Workspace and window updates are deferred — refreshWorkspaces()/ + // refreshToplevels() trigger onValuesChanged which also calls + // Qt.callLater, so the deduplication coalesces into one update. + Qt.callLater(_deferredWorkspaceUpdate); + updateTimer.restart(); + + const monitorsEvents = ["configreloaded", "monitoradded", "monitorremoved", "monitoraddedv2", "monitorremovedv2"]; + + if (monitorsEvents.includes(event.name)) { + Qt.callLater(queryDisplayScales); + } + + if (event.name == "activelayout") { + handleActiveLayoutEvent(event.data); + } + } + } + + // Public functions + function switchToWorkspace(workspace) { + try { + if (workspace.name) { + Hyprland.dispatch(`workspace ${workspace.name}`); + return; + } + Hyprland.dispatch(`workspace ${workspace.idx}`); + } catch (e) { + Logger.e("HyprlandService", "Failed to switch workspace:", e); + } + } + + function focusWindow(window) { + try { + if (!window || !window.id) { + Logger.w("HyprlandService", "Invalid window object for focus"); + return; + } + + const windowId = window.id.toString(); + Hyprland.dispatch(`focuswindow address:0x${windowId}`); + Hyprland.dispatch(`alterzorder top,address:0x${windowId}`); // Bring the focused window to the top (essential for Float Mode) + } catch (e) { + Logger.e("HyprlandService", "Failed to switch window:", e); + } + } + + function closeWindow(window) { + try { + Hyprland.dispatch(`killwindow address:0x${window.id}`); + } catch (e) { + Logger.e("HyprlandService", "Failed to close window:", e); + } + } + + function turnOffMonitors() { + try { + Quickshell.execDetached(["hyprctl", "dispatch", "dpms", "off"]); + } catch (e) { + Logger.e("HyprlandService", "Failed to turn off monitors:", e); + } + } + + function turnOnMonitors() { + try { + Quickshell.execDetached(["hyprctl", "dispatch", "dpms", "on"]); + } catch (e) { + Logger.e("HyprlandService", "Failed to turn on monitors:", e); + } + } + + function logout() { + try { + Quickshell.execDetached(["hyprctl", "dispatch", "exit"]); + } catch (e) { + Logger.e("HyprlandService", "Failed to logout:", e); + } + } + + function cycleKeyboardLayout() { + try { + Quickshell.execDetached(["hyprctl", "switchxkblayout", "all", "next"]); + } catch (e) { + Logger.e("HyprlandService", "Failed to cycle keyboard layout:", e); + } + } + + function getFocusedScreen() { + const hyprMon = Hyprland.focusedMonitor; + if (hyprMon) { + const monitorName = hyprMon.name; + for (let i = 0; i < Quickshell.screens.length; i++) { + if (Quickshell.screens[i].name === monitorName) { + return Quickshell.screens[i]; + } + } + } + return null; + } + + function spawn(command) { + try { + Quickshell.execDetached(["hyprctl", "dispatch", "--", "exec"].concat(command)); + } catch (e) { + Logger.e("HyprlandService", "Failed to spawn command:", e); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Compositor/LabwcService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Compositor/LabwcService.qml new file mode 100644 index 0000000..fe24a8b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Compositor/LabwcService.qml @@ -0,0 +1,300 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.WindowManager +import qs.Commons + +Item { + id: root + + property ListModel workspaces: ListModel {} + property var windows: [] + property int focusedWindowIndex: -1 + property var trackedToplevels: new Set() + + // LabWC typically has global workspaces (shared across all outputs) + property bool globalWorkspaces: true + + // Map from native workspace id to the native Workspace object for activation + property var nativeWorkspaceMap: ({}) + + // Set of workspace objects we've already connected signals to + property var connectedWorkspaces: ({}) + + signal workspaceChanged + signal activeWindowChanged + signal windowListChanged + signal displayScalesChanged + + function initialize() { + updateWindows(); + connectWorkspaceSignals(); + syncWorkspaces(); + Logger.i("LabwcService", "Service started (ext-workspace-v1)"); + } + + // Watch for windowsets being added/removed + Connections { + target: WindowManager + + function onWindowsetsChanged() { + root.connectWorkspaceSignals(); + Qt.callLater(root.syncWorkspaces); + } + + function onWindowsetProjectionsChanged() { + Qt.callLater(root.syncWorkspaces); + } + } + + // Re-check windowsets after a short delay - the Wayland protocol data + // may arrive after init and the changed signal can be missed + Timer { + interval: 500 + running: true + repeat: false + onTriggered: { + if (WindowManager.windowsets.length > 0) { + root.connectWorkspaceSignals(); + root.syncWorkspaces(); + } + } + } + + // Connect to property change signals on each native windowset object + function connectWorkspaceSignals() { + const nativeWs = WindowManager.windowsets; + const newConnected = {}; + + for (const ws of nativeWs) { + const key = ws.id || ws.toString(); + newConnected[key] = true; + + if (connectedWorkspaces[key]) + continue; + + ws.activeChanged.connect(() => { + Qt.callLater(root.syncWorkspaces); + }); + + ws.urgentChanged.connect(() => { + Qt.callLater(root.syncWorkspaces); + }); + + ws.shouldDisplayChanged.connect(() => { + Qt.callLater(root.syncWorkspaces); + }); + + ws.nameChanged.connect(() => { + Qt.callLater(root.syncWorkspaces); + }); + } + + connectedWorkspaces = newConnected; + } + + function syncWorkspaces() { + const nativeWs = WindowManager.windowsets; + + workspaces.clear(); + nativeWorkspaceMap = {}; + + let idx = 1; + + for (const ws of nativeWs) { + // Skip hidden workspaces (shouldDisplay = false means hidden) + if (!ws.shouldDisplay) { + continue; + } + + // Find which outputs this windowset's projection spans + let outputName = ""; + if (ws.projection) { + const projScreens = ws.projection.screens; + if (projScreens && projScreens.length > 0) { + outputName = projScreens[0].name || ""; + } + } + + const wsEntry = { + "id": ws.id || idx.toString(), + "idx": idx, + "name": ws.name || ("Workspace " + idx), + "output": outputName, + "isFocused": ws.active, + "isActive": true, + "isUrgent": ws.urgent, + "isOccupied": false, + "oid": ws.id || idx.toString() + }; + + workspaces.append(wsEntry); + nativeWorkspaceMap[wsEntry.id] = ws; + + idx++; + } + + // Update windows with workspace info + updateWindowWorkspaces(); + workspaceChanged(); + } + + function updateWindowWorkspaces() { + // ext-workspace-v1 doesn't provide window-to-workspace mapping + // Assign all windows to the active workspace + let activeId = ""; + for (let i = 0; i < workspaces.count; i++) { + const ws = workspaces.get(i); + if (ws.isFocused) { + activeId = ws.id; + break; + } + } + + for (let i = 0; i < windows.length; i++) { + if (activeId) { + windows[i].workspaceId = activeId; + } + } + windowListChanged(); + } + + Connections { + target: ToplevelManager.toplevels + function onValuesChanged() { + updateWindows(); + } + } + + function connectToToplevel(toplevel) { + if (!toplevel) + return; + + toplevel.activatedChanged.connect(() => { + Qt.callLater(onToplevelActivationChanged); + }); + + toplevel.titleChanged.connect(() => { + Qt.callLater(updateWindows); + }); + } + + function onToplevelActivationChanged() { + updateWindows(); + activeWindowChanged(); + } + + function updateWindows() { + const newWindows = []; + const toplevels = ToplevelManager.toplevels?.values || []; + + let focusedIdx = -1; + let idx = 0; + + // Find active workspace id + let activeId = ""; + for (let i = 0; i < workspaces.count; i++) { + const ws = workspaces.get(i); + if (ws.isFocused) { + activeId = ws.id; + break; + } + } + + for (const toplevel of toplevels) { + if (!toplevel) + continue; + + if (!trackedToplevels.has(toplevel)) { + connectToToplevel(toplevel); + trackedToplevels.add(toplevel); + } + + // Get output name from toplevel's screen list + const output = (toplevel.screens && toplevel.screens.length > 0) ? (toplevel.screens[0].name || "") : ""; + + // Use appId + title as a stable id since Toplevel has no address property + const windowId = (toplevel.appId || "") + ":" + idx; + + newWindows.push({ + "id": windowId, + "appId": toplevel.appId || "", + "title": toplevel.title || "", + "output": output, + "workspaceId": activeId || "1", + "isFocused": toplevel.activated || false, + "toplevel": toplevel + }); + + if (toplevel.activated) { + focusedIdx = idx; + } + idx++; + } + windows = newWindows; + focusedWindowIndex = focusedIdx; + + windowListChanged(); + } + + function focusWindow(window) { + if (window.toplevel && typeof window.toplevel.activate === "function") { + window.toplevel.activate(); + } + } + + function closeWindow(window) { + if (window.toplevel && typeof window.toplevel.close === "function") { + window.toplevel.close(); + } + } + + function switchToWorkspace(workspace) { + // Find the native Workspace object and activate it directly + const nativeWs = nativeWorkspaceMap[workspace.id] || nativeWorkspaceMap[workspace.oid]; + if (nativeWs && nativeWs.canActivate) { + nativeWs.activate(); + } else { + Logger.w("LabwcService", "Cannot activate workspace: " + (workspace.name || workspace.id)); + } + } + + function turnOffMonitors() { + try { + Quickshell.execDetached(["wlr-randr", "--off"]); + } catch (e) { + Logger.e("LabwcService", "Failed to turn off monitors:", e); + } + } + + function turnOnMonitors() { + try { + Quickshell.execDetached(["wlr-randr", "--on"]); + } catch (e) { + Logger.e("LabwcService", "Failed to turn on monitors:", e); + } + } + + function logout() { + try { + // Exit labwc by sending SIGTERM to $LABWC_PID or using --exit flag + Quickshell.execDetached(["sh", "-c", "labwc --exit || kill -s SIGTERM $LABWC_PID"]); + } catch (e) { + Logger.e("LabwcService", "Failed to logout:", e); + } + } + + function cycleKeyboardLayout() { + Logger.w("LabwcService", "Keyboard layout cycling not supported"); + } + + function queryDisplayScales() { + Logger.w("LabwcService", "Display scale queries not supported via ToplevelManager"); + } + + function getFocusedScreen() { + // de-activated until proper testing + return null; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Compositor/MangoService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Compositor/MangoService.qml new file mode 100644 index 0000000..39c34b1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Compositor/MangoService.qml @@ -0,0 +1,491 @@ +import QtQuick +import Quickshell +import Quickshell.DWL +import Quickshell.Io +import Quickshell.Wayland +import qs.Commons +import qs.Services.Keyboard + +Item { + id: root + + // ===== PUBLIC INTERFACE (CompositorService compatibility) ===== + + property ListModel workspaces: ListModel {} + property var windows: [] + property int focusedWindowIndex: -1 + property bool initialized: false + + signal workspaceChanged + signal activeWindowChanged + signal windowListChanged + signal displayScalesChanged + + // ===== MANGOSERVICE-SPECIFIC PROPERTIES ===== + + property string selectedMonitor: "" + property string currentLayoutSymbol: "" + + // ===== INTERNAL STATE ===== + + QtObject { + id: internal + + // Window-to-tag persistence: Map + property var windowTagMap: ({}) + + // Window-to-output persistence: Map + property var windowOutputMap: ({}) + + // Toplevel-to-ID mapping: Map + property var toplevelIdMap: new Map() + property int windowIdCounter: 0 + + // Output name to index mapping for unique workspace IDs + property var outputIndices: ({}) + property int outputCounter: 0 + + // Monitor scales: Map + property var monitorScales: ({}) + + // Window signature for change detection + property string lastWindowSignature: "" + + // Scale regex + readonly property var scalePattern: /^(\S+)\s+scale_factor\s+(\d+(?:\.\d+)?)$/ + + // Get all screen names mapped to their DwlIpcOutput + function getOutputMap() { + const map = {}; + const screens = Quickshell.screens; + for (let i = 0; i < screens.length; i++) { + const name = screens[i].name; + const dwlOutput = DwlIpc.outputForName(name); + if (dwlOutput) { + map[name] = dwlOutput; + } + } + return map; + } + + // ===== REBUILD WORKSPACES FROM DWL ===== + + function rebuildWorkspaces() { + if (!DwlIpc.available) { + return; + } + + const outputMap = getOutputMap(); + const workspaceList = []; + + for (const outputName in outputMap) { + const output = outputMap[outputName]; + + // Assign stable index to output + if (internal.outputIndices[outputName] === undefined) { + internal.outputIndices[outputName] = internal.outputCounter++; + } + const outputIdx = internal.outputIndices[outputName]; + + // Track selected monitor and layout + if (output.active) { + root.selectedMonitor = outputName; + root.currentLayoutSymbol = output.layoutSymbol; + } + + const tags = output.tags; + for (let ti = 0; ti < tags.length; ti++) { + const tag = tags[ti]; + const tagId = tag.index + 1; // DwlTag.index is zero-based, our IDs are 1-based + const uniqueId = outputIdx * 100 + tagId; + + workspaceList.push({ + id: uniqueId, + idx: tagId, + name: tagId.toString(), + output: outputName, + isActive: tag.active, + isFocused: tag.active && output.active, + isUrgent: tag.urgent, + isOccupied: tag.clientCount > 0 + }); + } + } + + // Sort by unique ID + workspaceList.sort((a, b) => a.id - b.id); + + root.workspaces.clear(); + for (let k = 0; k < workspaceList.length; k++) { + root.workspaces.append(workspaceList[k]); + } + + root.workspaceChanged(); + } + + // ===== UPDATE WINDOWS ===== + + function updateWindows() { + if (!ToplevelManager.toplevels || !DwlIpc.available) { + return; + } + + const outputMap = getOutputMap(); + const toplevels = ToplevelManager.toplevels.values; + const windowList = []; + let newFocusedIdx = -1; + const currentWindows = new Set(); + + // Build per-output state from DWL + // Map + // Always populated (activeTagId needed for visible-window inference), + // title/appId may be empty if no window is focused. + const outputState = {}; + for (const outputName in outputMap) { + const output = outputMap[outputName]; + + // Find active tag for this output + let activeTagId = 1; + const tags = output.tags; + for (let ti = 0; ti < tags.length; ti++) { + if (tags[ti].active) { + activeTagId = tags[ti].index + 1; + break; + } + } + + outputState[outputName] = { + title: output.title || "", + appId: output.appId || "", + activeTagId: activeTagId + }; + + // Ensure output index exists + if (internal.outputIndices[outputName] === undefined) { + internal.outputIndices[outputName] = internal.outputCounter++; + } + } + + for (let i = 0; i < toplevels.length; i++) { + const toplevel = toplevels[i]; + if (!toplevel || toplevel.outliers) { + continue; + } + + const appId = toplevel.appId || toplevel.wayland?.appId || ""; + const title = toplevel.title || toplevel.wayland?.title || ""; + const isFocused = toplevel.activated; + + // Get or assign a stable ID + let windowId; + if (internal.toplevelIdMap.has(toplevel)) { + windowId = internal.toplevelIdMap.get(toplevel); + } else { + windowId = `win-${internal.windowIdCounter++}`; + internal.toplevelIdMap.set(toplevel, windowId); + } + + currentWindows.add(windowId); + + // Determine output + let outputName; + + // Priority 1: Focused window matched to DWL output metadata + if (isFocused && (title || appId)) { + for (const oName in outputState) { + const os = outputState[oName]; + if ((os.title || os.appId) && title === os.title && appId === os.appId) { + outputName = oName; + internal.windowOutputMap[windowId] = oName; + break; + } + } + } + + // Priority 2: Remembered output + if (!outputName && internal.windowOutputMap[windowId]) { + outputName = internal.windowOutputMap[windowId]; + } + + // Priority 3: toplevel.screens (wlr-foreign-toplevel visible screens) + if (!outputName && toplevel.screens && toplevel.screens.length > 0) { + outputName = toplevel.screens[0].name; + } + + // Fallback: selected monitor + if (!outputName) { + outputName = root.selectedMonitor || "DP-1"; + } + + // Determine tag + let tagId = null; + + const os = outputState[outputName]; + if (isFocused && os && !os.consumed && (os.title || os.appId) && title === os.title && appId === os.appId) { + // Focused window: assign to the active tag from DWL metadata + tagId = os.activeTagId; + internal.windowTagMap[windowId] = tagId; + // Consume so a second toplevel with identical title+appId cannot also claim focus + os.consumed = true; + } else if (internal.windowTagMap[windowId] !== undefined) { + // Previously seen window: use remembered tag + tagId = internal.windowTagMap[windowId]; + } + + if (tagId === null) { + // DWL only reports the focused window per output, so we can't + // determine the tag for unfocused windows until they gain focus. + continue; + } + + // Convert to unique workspace ID + const outputIdx = internal.outputIndices[outputName]; + if (outputIdx === undefined) { + Logger.e("MangoService", "No output index for", outputName); + continue; + } + const workspaceId = outputIdx * 100 + tagId; + + windowList.push({ + id: `${outputName}:${appId}:${title}:${i}`, + title: title, + appId: appId, + class: appId, + workspaceId: workspaceId, + isFocused: isFocused, + output: outputName, + handle: toplevel, + fullscreen: toplevel.fullscreen || false, + floating: toplevel.maximized === false && toplevel.fullscreen === false + }); + + if (isFocused) { + newFocusedIdx = windowList.length - 1; + } + } + + // Clean up stale window tracking + if (Object.keys(internal.windowTagMap).length > toplevels.length + 20) { + const newTagMap = {}; + const newOutputMap = {}; + for (const windowId of currentWindows) { + if (internal.windowTagMap[windowId] !== undefined) { + newTagMap[windowId] = internal.windowTagMap[windowId]; + } + if (internal.windowOutputMap[windowId] !== undefined) { + newOutputMap[windowId] = internal.windowOutputMap[windowId]; + } + } + internal.windowTagMap = newTagMap; + internal.windowOutputMap = newOutputMap; + } + + // Check if window list changed + const signature = JSON.stringify(windowList.map(w => w.id + w.workspaceId + w.isFocused)); + if (signature !== internal.lastWindowSignature) { + internal.lastWindowSignature = signature; + root.windows = windowList; + root.windowListChanged(); + } + + if (newFocusedIdx !== root.focusedWindowIndex) { + root.focusedWindowIndex = newFocusedIdx; + root.activeWindowChanged(); + } + } + + // ===== PROCESS SCALES ===== + + function processScales(output) { + const lines = output.trim().split('\n'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + const match = line.match(scalePattern); + + if (match) { + const outputName = match[1]; + const scale = parseFloat(match[2]); + internal.monitorScales[outputName] = scale; + } + } + + const scalesMap = {}; + for (const name in internal.monitorScales) { + scalesMap[name] = { + name: name, + scale: internal.monitorScales[name] || 1.0, + width: 0, + height: 0, + x: 0, + y: 0 + }; + } + + if (CompositorService && CompositorService.onDisplayScalesUpdated) { + CompositorService.onDisplayScalesUpdated(scalesMap); + } + + root.displayScalesChanged(); + } + } + + // ===== DWL CONNECTIONS ===== + + // React to DWL frame events on each output (atomic state updates) + Instantiator { + model: DwlIpc.outputs + delegate: Connections { + required property DwlIpcOutput modelData + target: modelData + + function onFrame() { + internal.rebuildWorkspaces(); + internal.updateWindows(); + } + + function onKbLayoutChanged() { + if (KeyboardLayoutService) { + KeyboardLayoutService.setCurrentLayout(modelData.kbLayout); + } + } + } + } + + // ===== PROCESSES ===== + + // Scale query (mmsg -g -A) - DWL doesn't provide scale info + property QtObject _scaleQuery: Process { + id: scaleQuery + command: ["mmsg", "-g", "-A"] + + property string buffer: "" + + stdout: SplitParser { + onRead: line => { + scaleQuery.buffer += line + "\n"; + } + } + + onExited: code => { + if (code === 0) { + internal.processScales(scaleQuery.buffer); + scaleQuery.buffer = ""; + } + } + } + + // ===== TOPLEVEL MANAGER CONNECTION ===== + + property QtObject _toplevelConnection: Connections { + target: ToplevelManager.toplevels + + function onValuesChanged() { + internal.updateWindows(); + } + } + + // ===== PUBLIC FUNCTIONS ===== + + function initialize() { + if (initialized) { + return; + } + + Logger.i("MangoService", "Initializing MangoWC/DWL compositor integration (DWL protocol)"); + + // Query display scales (only thing still needing mmsg) + scaleQuery.running = true; + + // Initial build from DWL state + if (DwlIpc.available && DwlIpc.outputs.length > 0) { + internal.rebuildWorkspaces(); + internal.updateWindows(); + } + + initialized = true; + } + + function queryDisplayScales() { + scaleQuery.running = true; + } + + function switchToWorkspace(workspace) { + const tagId = workspace.idx || workspace.id || 1; + const outputName = workspace.output || root.selectedMonitor || ""; + + // Use DWL protocol to switch tags + const dwlOutput = DwlIpc.outputForName(outputName); + if (dwlOutput) { + dwlOutput.setTags(1 << (tagId - 1)); // tagId is 1-based, bitmask is 0-based + } else { + // Fallback to mmsg + const cmd = ["mmsg", "-s", "-t", tagId.toString()]; + if (outputName && Object.keys(internal.monitorScales).length > 1) { + cmd.push("-o", outputName); + } + Quickshell.execDetached(cmd); + } + } + + function focusWindow(window) { + if (window && window.handle) { + window.handle.activate(); + } else if (window.workspaceId) { + switchToWorkspace({ + id: window.workspaceId, + output: window.output + }); + } + } + + function closeWindow(window) { + if (window && window.handle) { + window.handle.close(); + } else { + Quickshell.execDetached(["mmsg", "-s", "-d", "killclient"]); + } + } + + function turnOffMonitors() { + const screens = Quickshell.screens; + const cmds = []; + for (let i = 0; i < screens.length; i++) { + cmds.push("mmsg -s -d disable_monitor," + screens[i].name); + } + if (cmds.length > 0) { + Quickshell.execDetached(["sh", "-c", cmds.join(" && ")]); + } + } + + function turnOnMonitors() { + const screens = Quickshell.screens; + const cmds = []; + for (let i = 0; i < screens.length; i++) { + cmds.push("mmsg -s -d enable_monitor," + screens[i].name); + } + if (cmds.length > 0) { + Quickshell.execDetached(["sh", "-c", cmds.join(" && ")]); + } + } + + function logout() { + Quickshell.execDetached(["mmsg", "-s", "-q"]); + } + + function cycleKeyboardLayout() { + Logger.w("MangoService", "Keyboard layout cycling not supported"); + } + + function getFocusedScreen() { + return null; + } + + function spawn(command) { + try { + Quickshell.execDetached(["mmsg", "-s", "-d", "spawn_shell," + command.join(" ")]); + } catch (e) { + Logger.e("MangoService", "Failed to spawn command:", e); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Compositor/NiriService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Compositor/NiriService.qml new file mode 100644 index 0000000..a74efb0 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Compositor/NiriService.qml @@ -0,0 +1,294 @@ +import QtQuick +import Quickshell +import Quickshell.Niri +import qs.Commons +import qs.Services.Keyboard + +Item { + id: root + + property int floatingWindowPosition: Number.MAX_SAFE_INTEGER + + property ListModel workspaces: ListModel {} + property var windows: [] + property int focusedWindowIndex: -1 + + property bool overviewActive: false + + property var keyboardLayouts: [] + + signal workspaceChanged + signal activeWindowChanged + signal windowListChanged + signal displayScalesChanged + + property var outputCache: ({}) + property var workspaceCache: ({}) + + function initialize() { + Niri.refreshOutputs(); + Niri.refreshWorkspaces(); + Niri.refreshWindows(); + + Qt.callLater(() => { + safeUpdateOutputs(); + safeUpdateWorkspaces(); + safeUpdateWindows(); + queryDisplayScales(); + }); + + Logger.i("NiriService", "Service started"); + } + + // Connections to the C++ Niri IPC module + Connections { + target: Niri + function onWorkspacesUpdated() { + safeUpdateWorkspaces(); + workspaceChanged(); + } + function onWindowsUpdated() { + safeUpdateWindows(); + windowListChanged(); + activeWindowChanged(); + } + function onOutputsUpdated() { + safeUpdateOutputs(); + queryDisplayScales(); + } + function onOverviewActiveChanged() { + overviewActive = Niri.overviewActive; + } + function onKeyboardLayoutsChanged() { + keyboardLayouts = Niri.keyboardLayoutNames; + const layoutName = Niri.currentKeyboardLayoutName; + if (layoutName) { + KeyboardLayoutService.setCurrentLayout(layoutName); + } + Logger.d("NiriService", "Keyboard layouts changed:", keyboardLayouts.toString()); + } + function onKeyboardLayoutSwitched() { + const layoutName = Niri.currentKeyboardLayoutName; + if (layoutName) { + KeyboardLayoutService.setCurrentLayout(layoutName); + } + Logger.d("NiriService", "Keyboard layout switched:", layoutName); + } + } + + function safeUpdateOutputs() { + const niriOutputs = Niri.outputs.values; + outputCache = {}; + + for (var i = 0; i < niriOutputs.length; i++) { + const output = niriOutputs[i]; + outputCache[output.name] = { + "name": output.name, + "connected": output.connected, + "scale": output.scale, + "width": output.width, + "height": output.height, + "x": output.x, + "y": output.y, + "physical_width": output.physicalWidth, + "physical_height": output.physicalHeight, + "refresh_rate": output.refreshRate, + "vrr_supported": output.vrrSupported, + "vrr_enabled": output.vrrEnabled, + "transform": output.transform + }; + } + } + + function safeUpdateWorkspaces() { + const niriWorkspaces = Niri.workspaces.values; + workspaceCache = {}; + + const workspacesList = []; + for (var i = 0; i < niriWorkspaces.length; i++) { + const ws = niriWorkspaces[i]; + const wsData = { + "id": ws.id, + "idx": ws.idx, + "name": ws.name, + "output": ws.output, + "isFocused": ws.focused, + "isActive": ws.active, + "isUrgent": ws.urgent, + "isOccupied": ws.occupied + }; + workspacesList.push(wsData); + workspaceCache[ws.id] = wsData; + } + + // Workspaces come pre-sorted from C++ (by output then idx) + workspaces.clear(); + for (var j = 0; j < workspacesList.length; j++) { + workspaces.append(workspacesList[j]); + } + } + + function getWindowOutput(win) { + for (var i = 0; i < workspaces.count; i++) { + if (workspaces.get(i).id === win.workspaceId) { + return workspaces.get(i).output; + } + } + return null; + } + + function toSortedWindowList(windowList) { + return windowList.map(win => { + const workspace = workspaceCache[win.workspaceId]; + const output = (workspace && workspace.output) ? outputCache[workspace.output] : null; + + return { + window: win, + workspaceIdx: workspace ? workspace.idx : 0, + outputX: output ? output.x : 0, + outputY: output ? output.y : 0 + }; + }).sort((a, b) => { + // Sort by output position first + if (a.outputX !== b.outputX) { + return a.outputX - b.outputX; + } + if (a.outputY !== b.outputY) { + return a.outputY - b.outputY; + } + // Then by workspace index + if (a.workspaceIdx !== b.workspaceIdx) { + return a.workspaceIdx - b.workspaceIdx; + } + // Then by window position + if (a.window.position.x !== b.window.position.x) { + return a.window.position.x - b.window.position.x; + } + if (a.window.position.y !== b.window.position.y) { + return a.window.position.y - b.window.position.y; + } + // Finally by window ID to ensure consistent ordering + return a.window.id - b.window.id; + }).map(info => info.window); + } + + function safeUpdateWindows() { + const niriWindows = Niri.windows.values; + const windowsList = []; + + for (var i = 0; i < niriWindows.length; i++) { + const win = niriWindows[i]; + windowsList.push({ + "id": win.id, + "title": win.title || "", + "appId": win.appId || "", + "workspaceId": win.workspaceId || -1, + "isFocused": win.focused, + "output": win.output || getWindowOutput(win) || "", + "position": { + "x": win.isFloating ? floatingWindowPosition : win.positionX, + "y": win.isFloating ? floatingWindowPosition : win.positionY + } + }); + } + + windows = toSortedWindowList(windowsList); + safeUpdateFocusedWindow(); + } + + function safeUpdateFocusedWindow() { + focusedWindowIndex = -1; + for (var i = 0; i < windows.length; i++) { + if (windows[i].isFocused) { + focusedWindowIndex = i; + break; + } + } + } + + function queryDisplayScales() { + if (CompositorService && CompositorService.onDisplayScalesUpdated) { + CompositorService.onDisplayScalesUpdated(outputCache); + } + } + + function switchToWorkspace(workspace) { + try { + Niri.dispatch(["focus-workspace", workspace.idx.toString()]); + } catch (e) { + Logger.e("NiriService", "Failed to switch workspace:", e); + } + } + + function scrollWorkspaceContent(direction) { + try { + var action = direction < 0 ? "focus-column-left" : "focus-column-right"; + Niri.dispatch([action]); + } catch (e) { + Logger.e("NiriService", "Failed to scroll workspace content:", e); + } + } + + function focusWindow(window) { + try { + Niri.dispatch(["focus-window", "--id", window.id.toString()]); + } catch (e) { + Logger.e("NiriService", "Failed to switch window:", e); + } + } + + function closeWindow(window) { + try { + Niri.dispatch(["close-window", "--id", window.id.toString()]); + } catch (e) { + Logger.e("NiriService", "Failed to close window:", e); + } + } + + function turnOffMonitors() { + try { + Niri.dispatch(["power-off-monitors"]); + } catch (e) { + Logger.e("NiriService", "Failed to turn off monitors:", e); + } + } + + function turnOnMonitors() { + try { + Niri.dispatch(["power-on-monitors"]); + } catch (e) { + Logger.e("NiriService", "Failed to turn on monitors:", e); + } + } + + function logout() { + try { + Niri.dispatch(["quit", "--skip-confirmation"]); + } catch (e) { + Logger.e("NiriService", "Failed to logout:", e); + } + } + + function cycleKeyboardLayout() { + try { + Niri.dispatch(["switch-layout", "next"]); + } catch (e) { + Logger.e("NiriService", "Failed to cycle keyboard layout:", e); + } + } + + function getFocusedScreen() { + // On niri the code below only works when you have an actual app selected on that screen. + return null; + } + + function spawn(command) { + try { + const niriArgs = ["spawn", "--"].concat(command); + Logger.d("NiriService", "Calling niri spawn: niri msg action " + niriArgs.join(" ")); + Niri.dispatch(niriArgs); + } catch (e) { + Logger.e("NiriService", "Failed to spawn command:", e); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Compositor/SwayService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Compositor/SwayService.qml new file mode 100644 index 0000000..397c114 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Compositor/SwayService.qml @@ -0,0 +1,593 @@ +import QtQuick +import Quickshell +import Quickshell.I3 +import Quickshell.Io +import Quickshell.Wayland +import qs.Commons +import qs.Services.Keyboard + +Item { + id: root + + // Configurable IPC command name (overridden to "scrollmsg" for Scroll) + property string msgCommand: "swaymsg" + + // Properties that match the facade interface + property ListModel workspaces: ListModel {} + property var windows: [] + property int focusedWindowIndex: -1 + + // Signals that match the facade interface + signal workspaceChanged + signal activeWindowChanged + signal windowListChanged + signal displayScalesChanged + + // I3-specific properties + property bool initialized: false + + // Cache for window-to-workspace mapping + property var windowWorkspaceMap: ({}) + + // Track window usage counts per workspace to handle duplicates + property var windowUsageCountsPerWorkspace: ({}) + + // Debounce timer for updates + Timer { + id: updateTimer + interval: 50 + repeat: false + onTriggered: safeUpdate() + } + + // Initialization + function initialize() { + if (initialized) + return; + try { + I3.refreshWorkspaces(); + Qt.callLater(() => { + safeUpdateWorkspaces(); + queryWindowWorkspaces(); + queryDisplayScales(); + queryKeyboardLayout(); + }); + initialized = true; + Logger.i("SwayService", "Service started"); + } catch (e) { + Logger.e("SwayService", "Failed to initialize:", e); + } + } + + // Query window-to-workspace mapping via IPC + function queryWindowWorkspaces() { + swayTreeProcess.running = true; + } + + // Sway tree process for getting window workspace information + Process { + id: swayTreeProcess + running: false + command: [msgCommand, "-t", "get_tree", "-r"] + + property string accumulatedOutput: "" + + stdout: SplitParser { + onRead: function (line) { + swayTreeProcess.accumulatedOutput += line; + } + } + + onExited: function (exitCode) { + if (exitCode !== 0 || !accumulatedOutput) { + Logger.e("SwayService", "Failed to query tree, exit code:", exitCode); + accumulatedOutput = ""; + return; + } + + try { + const treeData = JSON.parse(accumulatedOutput); + const newMap = {}; + const workspaceWindows = {}; // Track windows per workspace + + // Recursively find all windows and their workspaces + function traverseTree(node, workspaceNum) { + if (!node) + return; + + // If this is a workspace node, update the workspace number + if (node.type === "workspace" && node.num !== undefined) { + workspaceNum = node.num; + if (!workspaceWindows[workspaceNum]) { + workspaceWindows[workspaceNum] = []; + } + } + + // If this is a regular or floating container with app_id/class (i.e., a window) + if ((node.type === "con" || node.type === "floating_con") && (node.app_id || node.window_properties)) { + const appId = node.app_id || (node.window_properties ? node.window_properties.class : null); + const title = node.name || ""; + const id = node.id; + + if (appId && workspaceNum !== undefined && workspaceNum >= 0) { + // Store window info for this workspace + workspaceWindows[workspaceNum].push({ + appId: appId, + title: title, + id: id + }); + } + } + + // Traverse children + if (node.nodes && node.nodes.length > 0) { + for (const child of node.nodes) { + traverseTree(child, workspaceNum); + } + } + + // Traverse floating nodes + if (node.floating_nodes && node.floating_nodes.length > 0) { + for (const child of node.floating_nodes) { + traverseTree(child, workspaceNum); + } + } + } + + traverseTree(treeData, -1); + + // Now build the map with workspace-specific keys + for (const wsNum in workspaceWindows) { + const windows = workspaceWindows[wsNum]; + const appTitleCounts = {}; // Count occurrences of each appId:title in this workspace + + for (const win of windows) { + const baseKey = `${win.appId}:${win.title}`; + + // Track how many times we've seen this appId:title combo in this workspace + if (!appTitleCounts[baseKey]) { + appTitleCounts[baseKey] = 0; + } + const occurrence = appTitleCounts[baseKey]; + appTitleCounts[baseKey]++; + + // Create unique key with workspace and occurrence index + const uniqueKey = `ws${wsNum}:${baseKey}[${occurrence}]`; + newMap[uniqueKey] = parseInt(wsNum); + + // Also store by ID if available (most reliable) + if (win.id) { + newMap[`id:${win.id}`] = parseInt(wsNum); + } + } + } + + windowWorkspaceMap = newMap; + + // Update windows with new workspace information + Qt.callLater(safeUpdateWindows); + } catch (e) { + Logger.e("SwayService", "Failed to parse tree:", e); + } finally { + accumulatedOutput = ""; + } + } + } + + // Query display scales + function queryDisplayScales() { + swayOutputsProcess.running = true; + } + + // Sway outputs process for display scale detection + Process { + id: swayOutputsProcess + running: false + command: [msgCommand, "-t", "get_outputs", "-r"] + + property string accumulatedOutput: "" + + stdout: SplitParser { + onRead: function (line) { + swayOutputsProcess.accumulatedOutput += line; + } + } + + onExited: function (exitCode) { + if (exitCode !== 0 || !accumulatedOutput) { + Logger.e("SwayService", "Failed to query outputs, exit code:", exitCode); + accumulatedOutput = ""; + return; + } + + try { + const outputsData = JSON.parse(accumulatedOutput); + const scales = {}; + + for (const output of outputsData) { + if (output.name) { + scales[output.name] = { + "name": output.name, + "scale": output.scale || 1.0, + "width": output.current_mode ? output.current_mode.width : 0, + "height": output.current_mode ? output.current_mode.height : 0, + "refresh_rate": output.current_mode ? output.current_mode.refresh : 0, + "x": output.rect ? output.rect.x : 0, + "y": output.rect ? output.rect.y : 0, + "active": output.active || false, + "focused": output.focused || false, + "current_workspace": output.current_workspace || "" + }; + } + } + + // Notify CompositorService (it will emit displayScalesChanged) + if (CompositorService && CompositorService.onDisplayScalesUpdated) { + CompositorService.onDisplayScalesUpdated(scales); + } + } catch (e) { + Logger.e("SwayService", "Failed to parse outputs:", e); + } finally { + // Clear accumulated output for next query + accumulatedOutput = ""; + } + } + } + + function queryKeyboardLayout() { + swayInputsProcess.running = true; + } + // Sway inputs process for keyboard layout detection + Process { + id: swayInputsProcess + running: false + command: [msgCommand, "-t", "get_inputs", "-r"] + + property string accumulatedOutput: "" + + stdout: SplitParser { + onRead: function (line) { + // Accumulate lines instead of parsing each one + swayInputsProcess.accumulatedOutput += line; + } + } + + onExited: function (exitCode) { + if (exitCode !== 0 || !accumulatedOutput) { + Logger.e("SwayService", "Failed to query inputs, exit code:", exitCode); + accumulatedOutput = ""; + return; + } + + try { + const inputsData = JSON.parse(accumulatedOutput); + for (const input of inputsData) { + if (input.type == "keyboard") { + const layoutName = input.xkb_active_layout_name; + KeyboardLayoutService.setCurrentLayout(layoutName); + Logger.d("SwayService", "Keyboard layout switched:", layoutName); + break; + } + } + } catch (e) { + Logger.e("SwayService", "Failed to parse inputs:", e); + } finally { + // Clear accumulated output for next query + accumulatedOutput = ""; + } + } + } + + // Safe update wrapper + function safeUpdate() { + queryWindowWorkspaces(); + safeUpdateWorkspaces(); + } + + // Safe workspace update + function safeUpdateWorkspaces() { + try { + workspaces.clear(); + + if (!I3.workspaces || !I3.workspaces.values) { + return; + } + + const hlWorkspaces = I3.workspaces.values; + + for (var i = 0; i < hlWorkspaces.length; i++) { + const ws = hlWorkspaces[i]; + if (!ws || ws.id < 1) + continue; + const wsData = { + "id": i, + "idx": ws.num, + "name": ws.name || "", + "output": (ws.monitor && ws.monitor.name) ? ws.monitor.name : "", + "isActive": ws.active === true, + "isFocused": ws.focused === true, + "isUrgent": ws.urgent === true, + "isOccupied": true, + "handle": ws + }; + + workspaces.append(wsData); + } + } catch (e) { + Logger.e("SwayService", "Error updating workspaces:", e); + } + } + + // Safe window update + function safeUpdateWindows() { + try { + const windowsList = []; + + // Reset usage counts per workspace before processing windows + windowUsageCountsPerWorkspace = {}; + + if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values) { + windows = []; + focusedWindowIndex = -1; + windowListChanged(); + return; + } + + const hlToplevels = ToplevelManager.toplevels.values; + let newFocusedIndex = -1; + + for (var i = 0; i < hlToplevels.length; i++) { + const toplevel = hlToplevels[i]; + if (!toplevel) + continue; + const windowData = extractWindowData(toplevel); + if (windowData) { + windowsList.push(windowData); + + if (windowData.isFocused) { + newFocusedIndex = windowsList.length - 1; + } + } + } + + windows = windowsList; + + if (newFocusedIndex !== focusedWindowIndex) { + focusedWindowIndex = newFocusedIndex; + activeWindowChanged(); + } + + windowListChanged(); + } catch (e) { + Logger.e("SwayService", "Error updating windows:", e); + } + } + + // Extract window data safely from a toplevel + function extractWindowData(toplevel) { + if (!toplevel) + return null; + + try { + // Safely extract properties + const appId = getAppId(toplevel); + const title = safeGetProperty(toplevel, "title", ""); + const focused = toplevel.activated === true; + + // Try to find workspace ID from our cached map by trying all workspaces + let workspaceId = -1; + let foundWorkspaceNum = -1; + + // Build base key for this window + const baseKey = `${appId}:${title}`; + + // Try to find this window in any workspace + for (var i = 0; i < workspaces.count; i++) { + const ws = workspaces.get(i); + if (!ws) + continue; + + const wsNum = ws.idx; + + // Initialize usage count for this workspace if needed + if (!windowUsageCountsPerWorkspace[wsNum]) { + windowUsageCountsPerWorkspace[wsNum] = {}; + } + + // Get current usage count for this appId:title in this workspace + if (!windowUsageCountsPerWorkspace[wsNum][baseKey]) { + windowUsageCountsPerWorkspace[wsNum][baseKey] = 0; + } + + const occurrence = windowUsageCountsPerWorkspace[wsNum][baseKey]; + const uniqueKey = `ws${wsNum}:${baseKey}[${occurrence}]`; + + // Check if this key exists in our map + if (windowWorkspaceMap[uniqueKey] !== undefined) { + foundWorkspaceNum = windowWorkspaceMap[uniqueKey]; + workspaceId = ws.id; + + // Increment the usage count for this workspace + windowUsageCountsPerWorkspace[wsNum][baseKey]++; + break; + } + } + + return { + "title": title, + "appId": appId, + "isFocused": focused, + "workspaceId": workspaceId, + "handle": toplevel + }; + } catch (e) { + return null; + } + } + + function getAppId(toplevel) { + if (!toplevel) + return ""; + + return toplevel.appId; + } + + // Safe property getter + function safeGetProperty(obj, prop, defaultValue) { + try { + const value = obj[prop]; + if (value !== undefined && value !== null) { + return String(value); + } + } catch (e) + + // Property access failed + {} + return defaultValue; + } + + function handleInputEvent(ev) { + try { + const eventData = JSON.parse(ev); + if (eventData.change == "xkb_layout" && eventData.input != null) { + const input = eventData.input; + if (input.type == "keyboard" && input.xkb_active_layout_name != null) { + const layoutName = input.xkb_active_layout_name; + KeyboardLayoutService.setCurrentLayout(layoutName); + Logger.d("SwayService", "Keyboard layout switched:", layoutName); + } + } + } catch (e) { + Logger.e("SwayService", "Error handling input event:", e); + } + } + + // Connections to I3 + Connections { + target: I3.workspaces + enabled: initialized + function onValuesChanged() { + safeUpdateWorkspaces(); + workspaceChanged(); + } + } + + Connections { + target: ToplevelManager + enabled: initialized + function onActiveToplevelChanged() { + updateTimer.restart(); + } + } + + // Some programs change title of window dependent on content + Connections { + target: ToplevelManager ? ToplevelManager.activeToplevel : null + enabled: initialized + function onTitleChanged() { + updateTimer.restart(); + } + } + + Connections { + target: I3 + enabled: initialized + function onRawEvent(event) { + safeUpdateWorkspaces(); + workspaceChanged(); + updateTimer.restart(); + + if (event.type === "output") { + Qt.callLater(queryDisplayScales); + } + } + } + + I3IpcListener { + subscriptions: ["input"] + onIpcEvent: function (event) { + handleInputEvent(event.data); + } + } + + // Public functions + function switchToWorkspace(workspace) { + try { + workspace.handle.activate(); + } catch (e) { + Logger.e("SwayService", "Failed to switch workspace:", e); + } + } + + function focusWindow(window) { + try { + window.handle.activate(); + } catch (e) { + Logger.e("SwayService", "Failed to switch window:", e); + } + } + + function closeWindow(window) { + try { + window.handle.close(); + } catch (e) { + Logger.e("SwayService", "Failed to close window:", e); + } + } + + function turnOffMonitors() { + try { + Quickshell.execDetached([msgCommand, "output", "*", "dpms", "off"]); + } catch (e) { + Logger.e("SwayService", "Failed to turn off monitors:", e); + } + } + + function turnOnMonitors() { + try { + Quickshell.execDetached([msgCommand, "output", "*", "dpms", "on"]); + } catch (e) { + Logger.e("SwayService", "Failed to turn on monitors:", e); + } + } + + function logout() { + try { + Quickshell.execDetached([msgCommand, "exit"]); + } catch (e) { + Logger.e("SwayService", "Failed to logout:", e); + } + } + + function cycleKeyboardLayout() { + try { + Quickshell.execDetached([msgCommand, "input", "type:keyboard", "xkb_switch_layout", "next"]); + } catch (e) { + Logger.e("SwayService", "Failed to cycle keyboard layout:", e); + } + } + + function getFocusedScreen() { + // de-activated until proper testing + return null; + + // const i3Mon = I3.focusedMonitor; + // if (i3Mon) { + // const monitorName = i3Mon.name; + // for (let i = 0; i < Quickshell.screens.length; i++) { + // if (Quickshell.screens[i].name === monitorName) { + // return Quickshell.screens[i]; + // } + // } + // } + // return null; + } + + function spawn(command) { + try { + Quickshell.execDetached([msgCommand, "exec", "--"].concat(command)); + } catch (e) { + Logger.e("SwayService", "Failed to spawn command:", e); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Control/CurrentScreenDetector.qml b/fedora/.config/quickshell/noctalia-shell/Services/Control/CurrentScreenDetector.qml new file mode 100644 index 0000000..8ab719e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Control/CurrentScreenDetector.qml @@ -0,0 +1,163 @@ +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services.Compositor + +/** +* Detects which screen the cursor is currently on by creating a temporary +* invisible PanelWindow. Use withCurrentScreen() to get the screen asynchronously. +* +* Usage: +* CurrentScreenDetector { +* id: screenDetector +* } +* +* function doSomething() { +* screenDetector.withCurrentScreen(function(screen) { +* // screen is the ShellScreen where cursor is +* }) +* } +*/ +Item { + id: root + + // Pending callback to execute once screen is detected + property var pendingCallback: null + property bool pendingSkipBarCheck: false + + // Detected screen + property var detectedScreen: null + + // Signal emitted when screen is detected from the PanelWindow + signal screenDetected(var detectedScreen) + + onScreenDetected: function (detectedScreen) { + root.detectedScreen = detectedScreen; + screenDetectorDebounce.restart(); + } + + /** + * Find a fallback screen that has a bar configured. + * Prioritizes the screen at position 0x0 (likely the primary screen). + */ + function findScreenWithBar(): var { + const monitors = Settings.data.bar.monitors || []; + let primaryCandidate = null; + let firstWithBar = null; + + for (let i = 0; i < Quickshell.screens.length; i++) { + const s = Quickshell.screens[i]; + const hasBar = monitors.length === 0 || monitors.includes(s.name); + + if (hasBar) { + // Check if this is at 0x0 (primary position) + if (s.x === 0 && s.y === 0) { + primaryCandidate = s; + } + // Track first screen with bar as fallback + if (!firstWithBar) { + firstWithBar = s; + } + } + } + + // Prefer primary (0x0), then first with bar, then just first screen + return primaryCandidate || firstWithBar || Quickshell.screens[0]; + } + + /** + * Execute callback with the screen where the cursor currently is. + * On single-monitor setups, executes immediately. + * On multi-monitor setups, briefly opens an invisible window to detect the screen. + */ + function withCurrentScreen(callback: var, skipBarCheck: bool) { + if (root.pendingCallback) { + Logger.w("CurrentScreenDetector", "Another detection is pending, ignoring new call"); + return; + } + + // Single monitor setup can execute immediately + if (Quickshell.screens.length === 1) { + callback(Quickshell.screens[0]); + return; + } + + // Try compositor-specific focused monitor detection first + let screen = CompositorService.getFocusedScreen(); + + if (screen) { + // Apply the bar check if configured (skip for overlay launcher etc.) + if (!skipBarCheck && !Settings.data.general.allowPanelsOnScreenWithoutBar) { + const monitors = Settings.data.bar.monitors || []; + const hasBar = monitors.length === 0 || monitors.includes(screen.name); + if (!hasBar) { + screen = findScreenWithBar(); + } + } + Logger.d("CurrentScreenDetector", "Using compositor-detected screen:", screen.name); + callback(screen); + return; + } + + // Fallback: Multi-monitor setup needs async detection via invisible PanelWindow + root.detectedScreen = null; + root.pendingCallback = callback; + root.pendingSkipBarCheck = !!skipBarCheck; + screenDetectorLoader.active = true; + } + + Timer { + id: screenDetectorDebounce + running: false + interval: 40 + onTriggered: { + Logger.d("CurrentScreenDetector", "Screen debounced to:", root.detectedScreen?.name || "null"); + + // Execute pending callback if any + if (root.pendingCallback) { + if (!root.pendingSkipBarCheck && !Settings.data.general.allowPanelsOnScreenWithoutBar) { + // If we explicitly disabled panels on screen without bar, check if bar is configured + // for this screen, and fallback to primary screen if necessary + var monitors = Settings.data.bar.monitors || []; + const hasBar = monitors.length === 0 || monitors.includes(root.detectedScreen?.name); + if (!hasBar) { + root.detectedScreen = findScreenWithBar(); + } + } + + Logger.d("CurrentScreenDetector", "Executing callback on screen:", root.detectedScreen.name); + // Store callback locally and clear pendingCallback first to prevent deadlock + // if the callback throws an error + var callback = root.pendingCallback; + root.pendingCallback = null; + root.pendingSkipBarCheck = false; + try { + callback(root.detectedScreen); + } catch (e) { + Logger.e("CurrentScreenDetector", "Callback failed:", e); + } + } + + // Clean up + screenDetectorLoader.active = false; + } + } + + // Invisible dummy PanelWindow to detect which screen should receive the action + Loader { + id: screenDetectorLoader + active: false + + sourceComponent: PanelWindow { + implicitWidth: 0 + implicitHeight: 0 + color: "transparent" + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "noctalia-screen-detector" + mask: Region {} + + onScreenChanged: root.screenDetected(screen) + } + } + } diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Control/CustomButtonIPCService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Control/CustomButtonIPCService.qml new file mode 100644 index 0000000..85c5e8b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Control/CustomButtonIPCService.qml @@ -0,0 +1,318 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.UI + +Singleton { + id: root + + // Must be called at startup to force singleton instantiation, + // which registers the IpcHandler with the IPC system. + function init() { + Logger.i("CustomButtonIPCService", "Service started"); + } + + // Registry to store references to active custom buttons by their user-defined identifier + property var customButtonRegistry: ({}) + + // Register a custom button instance + function registerButton(button) { + if (!button || !button.ipcIdentifier) { + Logger.w("CustomButtonIPCService", "Cannot register button without ipcIdentifier"); + return false; + } + + customButtonRegistry[button.ipcIdentifier] = button; + Logger.d("CustomButtonIPCService", `Registered button with identifier: ${button.ipcIdentifier}`); + return true; + } + + // Unregister a custom button instance + function unregisterButton(button) { + if (!button || !button.ipcIdentifier) { + return false; + } + + if (customButtonRegistry[button.ipcIdentifier] === button) { + delete customButtonRegistry[button.ipcIdentifier]; + Logger.d("CustomButtonIPCService", `Unregistered button with identifier: ${button.ipcIdentifier}`); + return true; + } + return false; + } + + // Find a button by identifier + function findButton(identifier) { + return customButtonRegistry[identifier] || null; + } + + // Find button config from Settings for when the live widget is not loaded + function findButtonConfig(identifier) { + var screens = Quickshell.screens; + for (var i = 0; i < screens.length; i++) { + var widgets = Settings.getBarWidgetsForScreen(screens[i].name); + var config = _searchWidgetsForIdentifier(widgets, identifier); + if (config) + return config; + } + // Also check global widgets as a final fallback + var globalConfig = _searchWidgetsForIdentifier(Settings.data.bar.widgets, identifier); + if (globalConfig) + return globalConfig; + return null; + } + + function _searchWidgetsForIdentifier(widgets, identifier) { + var sections = ["left", "center", "right"]; + for (var s = 0; s < sections.length; s++) { + var list = widgets[sections[s]]; + if (!list) + continue; + for (var j = 0; j < list.length; j++) { + var w = list[j]; + if (w.id === "CustomButton" && w.ipcIdentifier === identifier) { + return w; + } + } + } + return null; + } + + // Resolve a command property from config with fallback to widgetMetadata defaults + function resolveCommand(config, prop) { + if (config[prop]) + return config[prop]; + var meta = BarWidgetRegistry.widgetMetadata["CustomButton"]; + return meta ? (meta[prop] || "") : ""; + } + + // Substitute $delta expressions in a command string + function substituteWheelDelta(command, delta) { + var normalizedDelta = delta > 0 ? 1 : -1; + return command.replace(/\$delta([+\-*/]\d+)?/g, function (match, operation) { + if (operation) { + try { + var operator = operation.charAt(0); + var operand = parseInt(operation.substring(1)); + var result; + switch (operator) { + case '+': + result = normalizedDelta + operand; + break; + case '-': + result = normalizedDelta - operand; + break; + case '*': + result = normalizedDelta * operand; + break; + case '/': + result = Math.floor(normalizedDelta / operand); + break; + default: + result = normalizedDelta; + } + return result.toString(); + } catch (e) { + return normalizedDelta.toString(); + } + } else { + return normalizedDelta.toString(); + } + }); + } + + // IpcHandler for custom button commands using short alias 'cb' + IpcHandler { + target: "cb" + + // Handle left click: cb left "identifier" + function left(identifier: string) { + const button = findButton(identifier); + if (button) { + if (button.leftClickExec || button.textCommand) { + button.clicked(); + Logger.i("CustomButtonIPCService", `Triggered left click on button '${identifier}'`); + } else { + Logger.w("CustomButtonIPCService", `Button '${identifier}' has no left click action configured`); + } + return; + } + + // Fallback: read from Settings + const config = findButtonConfig(identifier); + if (!config) { + Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`); + return; + } + const cmd = resolveCommand(config, "leftClickExec"); + if (cmd) { + Quickshell.execDetached(["sh", "-lc", cmd]); + Logger.i("CustomButtonIPCService", `Triggered left click on button '${identifier}' (from settings)`); + } else { + Logger.w("CustomButtonIPCService", `Button '${identifier}' has no left click action configured`); + } + } + + // Handle right click: cb right "identifier" + function right(identifier: string) { + const button = findButton(identifier); + if (button) { + if (button.rightClickExec) { + button.rightClicked(); + Logger.i("CustomButtonIPCService", `Triggered right click on button '${identifier}'`); + } else { + Logger.w("CustomButtonIPCService", `Button '${identifier}' has no right click action configured`); + } + return; + } + + const config = findButtonConfig(identifier); + if (!config) { + Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`); + return; + } + const cmd = resolveCommand(config, "rightClickExec"); + if (cmd) { + Quickshell.execDetached(["sh", "-lc", cmd]); + Logger.i("CustomButtonIPCService", `Triggered right click on button '${identifier}' (from settings)`); + } else { + Logger.w("CustomButtonIPCService", `Button '${identifier}' has no right click action configured`); + } + } + + // Handle middle click: cb middle "identifier" + function middle(identifier: string) { + const button = findButton(identifier); + if (button) { + if (button.middleClickExec) { + button.middleClicked(); + Logger.i("CustomButtonIPCService", `Triggered middle click on button '${identifier}'`); + } else { + Logger.w("CustomButtonIPCService", `Button '${identifier}' has no middle click action configured`); + } + return; + } + + const config = findButtonConfig(identifier); + if (!config) { + Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`); + return; + } + const cmd = resolveCommand(config, "middleClickExec"); + if (cmd) { + Quickshell.execDetached(["sh", "-lc", cmd]); + Logger.i("CustomButtonIPCService", `Triggered middle click on button '${identifier}' (from settings)`); + } else { + Logger.w("CustomButtonIPCService", `Button '${identifier}' has no middle click action configured`); + } + } + + // Handle wheel up: cb up "identifier" + function up(identifier: string) { + const button = findButton(identifier); + if (button) { + if (button.wheelMode === "separate" && button.wheelUpExec) { + button.wheeled(1); + Logger.i("CustomButtonIPCService", `Triggered wheel up on button '${identifier}'`); + } else { + Logger.w("CustomButtonIPCService", `Button '${identifier}' has no separate wheel up action configured or is not in separate mode`); + } + return; + } + + const config = findButtonConfig(identifier); + if (!config) { + Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`); + return; + } + const mode = config.wheelMode || BarWidgetRegistry.widgetMetadata["CustomButton"].wheelMode; + const cmd = resolveCommand(config, "wheelUpExec"); + if (mode === "separate" && cmd) { + const resolved = substituteWheelDelta(cmd, 1); + Quickshell.execDetached(["sh", "-lc", resolved]); + Logger.i("CustomButtonIPCService", `Triggered wheel up on button '${identifier}' (from settings)`); + } else { + Logger.w("CustomButtonIPCService", `Button '${identifier}' has no separate wheel up action configured or is not in separate mode`); + } + } + + // Handle wheel down: cb down "identifier" + function down(identifier: string) { + const button = findButton(identifier); + if (button) { + if (button.wheelMode === "separate" && button.wheelDownExec) { + button.wheeled(-1); + Logger.i("CustomButtonIPCService", `Triggered wheel down on button '${identifier}'`); + } else { + Logger.w("CustomButtonIPCService", `Button '${identifier}' has no separate wheel down action configured or is not in separate mode`); + } + return; + } + + const config = findButtonConfig(identifier); + if (!config) { + Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`); + return; + } + const mode = config.wheelMode || BarWidgetRegistry.widgetMetadata["CustomButton"].wheelMode; + const cmd = resolveCommand(config, "wheelDownExec"); + if (mode === "separate" && cmd) { + const resolved = substituteWheelDelta(cmd, -1); + Quickshell.execDetached(["sh", "-lc", resolved]); + Logger.i("CustomButtonIPCService", `Triggered wheel down on button '${identifier}' (from settings)`); + } else { + Logger.w("CustomButtonIPCService", `Button '${identifier}' has no separate wheel down action configured or is not in separate mode`); + } + } + + // Handle wheel action: cb wheel "identifier" + function wheel(identifier: string) { + const button = findButton(identifier); + if (button) { + if (button.wheelMode === "unified" && button.wheelExec) { + button.wheeled(1); + Logger.i("CustomButtonIPCService", `Triggered wheel action on button '${identifier}'`); + } else { + Logger.w("CustomButtonIPCService", `Button '${identifier}' has no unified wheel action configured or is not in unified mode`); + } + return; + } + + const config = findButtonConfig(identifier); + if (!config) { + Logger.w("CustomButtonIPCService", `Button with identifier '${identifier}' not found`); + return; + } + const mode = config.wheelMode || BarWidgetRegistry.widgetMetadata["CustomButton"].wheelMode; + const cmd = resolveCommand(config, "wheelExec"); + if (mode === "unified" && cmd) { + const resolved = substituteWheelDelta(cmd, 1); + Quickshell.execDetached(["sh", "-lc", resolved]); + Logger.i("CustomButtonIPCService", `Triggered wheel action on button '${identifier}' (from settings)`); + } else { + Logger.w("CustomButtonIPCService", `Button '${identifier}' has no unified wheel action configured or is not in unified mode`); + } + } + + // Handle refresh: cb refresh "identifier" + function refresh(identifier: string) { + const button = findButton(identifier); + if (!button) { + Logger.w("CustomButtonIPCService", `Button '${identifier}' is not currently loaded — refresh requires a live widget instance`); + return; + } + + if (button.textCommand && button.textCommand.length > 0 && !button.textStream) { + button.runTextCommand(); + Logger.i("CustomButtonIPCService", `Triggered refresh (text command) on button '${identifier}'`); + } else if (button.textStream) { + Logger.w("CustomButtonIPCService", `Button '${identifier}' uses streaming, manual refresh disabled`); + } else { + Logger.w("CustomButtonIPCService", `Button '${identifier}' has no text command to refresh`); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Control/HooksService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Control/HooksService.qml new file mode 100644 index 0000000..c11c8c7 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Control/HooksService.qml @@ -0,0 +1,326 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.Power +import qs.Services.Theming +import qs.Services.UI + +Singleton { + id: root + + // Hook connections for automatic script execution + Connections { + target: Settings.data.colorSchemes + function onDarkModeChanged() { + executeDarkModeHook(Settings.data.colorSchemes.darkMode); + } + } + + // Pending wallpaper hook when waiting for color generation + property var pendingWallpaperHook: null + + Connections { + target: WallpaperService + function onWallpaperChanged(screenName, path) { + // Check if we need to wait for color generation + if (Settings.data.colorSchemes.useWallpaperColors) { + var effectiveMonitor = Settings.data.colorSchemes.monitorForColors; + if (effectiveMonitor === "" || effectiveMonitor === undefined) { + effectiveMonitor = Screen.name; + } + + if (screenName === effectiveMonitor) { + // Store pending hook and wait for colors to be generated + root.pendingWallpaperHook = { + path: path, + screenName: screenName + }; + return; + } + } + // No color generation, execute immediately + executeWallpaperHook(path, screenName); + } + } + + Connections { + target: TemplateProcessor + function onColorsGenerated() { + // Execute pending wallpaper hook after colors are ready + if (root.pendingWallpaperHook) { + const hook = root.pendingWallpaperHook; + root.pendingWallpaperHook = null; + executeWallpaperHook(hook.path, hook.screenName); + } + executeColorGenerationHook(); + } + } + + // Track lock screen state for unlock hook + property bool wasLocked: false + + Connections { + target: PanelService + function onLockScreenChanged() { + if (PanelService.lockScreen) { + lockScreenActiveConnection.target = PanelService.lockScreen; + } + } + } + + Connections { + id: lockScreenActiveConnection + target: PanelService.lockScreen + function onActiveChanged() { + // Detect lock: was unlocked, now locked + if (!wasLocked && PanelService.lockScreen.active) { + executeLockHook(); + } + // Detect unlock: was locked, now not locked + if (wasLocked && !PanelService.lockScreen.active) { + executeUnlockHook(); + } + wasLocked = PanelService.lockScreen.active; + } + } + + // Track performance mode state for hooks + property bool wasPerformanceModeEnabled: false + + Connections { + target: PowerProfileService + function onNoctaliaPerformanceModeChanged() { + const isEnabled = PowerProfileService.noctaliaPerformanceMode; + + // Detect enabled: was disabled, now enabled + if (!wasPerformanceModeEnabled && isEnabled) { + executePerformanceModeEnabledHook(); + } + // Detect disabled: was enabled, now disabled + if (wasPerformanceModeEnabled && !isEnabled) { + executePerformanceModeDisabledHook(); + } + wasPerformanceModeEnabled = isEnabled; + } + } + + // Execute wallpaper change hook + function executeWallpaperHook(wallpaperPath, screenName) { + if (!Settings.data.hooks?.enabled) { + return; + } + + const script = Settings.data.hooks?.wallpaperChange; + if (!script || script === "") { + return; + } + + try { + const theme = Settings.data.colorSchemes.darkMode ? "dark" : "light"; + let command = script.replace(/\$1/g, wallpaperPath); + command = command.replace(/\$2/g, screenName || ""); + command = command.replace(/\$3/g, theme); + Quickshell.execDetached(["sh", "-lc", command]); + Logger.d("HooksService", `Executed wallpaper hook: ${command}`); + } catch (e) { + Logger.e("HooksService", `Failed to execute wallpaper hook: ${e}`); + } + } + + // Execute dark mode change hook + function executeDarkModeHook(isDarkMode) { + if (!Settings.data.hooks?.enabled) { + return; + } + + const script = Settings.data.hooks?.darkModeChange; + if (!script || script === "") { + return; + } + + try { + const command = script.replace(/\$1/g, isDarkMode ? "true" : "false"); + Quickshell.execDetached(["sh", "-lc", command]); + Logger.d("HooksService", `Executed dark mode hook: ${command}`); + } catch (e) { + Logger.e("HooksService", `Failed to execute dark mode hook: ${e}`); + } + } + + // Execute screen lock hook + function executeLockHook() { + if (!Settings.data.hooks?.enabled) { + return; + } + + const script = Settings.data.hooks?.screenLock; + if (!script || script === "") { + return; + } + + try { + // Pass "lock" as $1 via shell arguments so the script receives it + Quickshell.execDetached(["sh", "-lc", script, "lock-hook", "lock"]); + Logger.d("HooksService", `Executed screen lock hook: ${script}`); + } catch (e) { + Logger.e("HooksService", `Failed to execute screen lock hook: ${e}`); + } + } + + // Execute screen unlock hook + function executeUnlockHook() { + if (!Settings.data.hooks?.enabled) { + return; + } + + const script = Settings.data.hooks?.screenUnlock; + if (!script || script === "") { + return; + } + + try { + // Pass "unlock" as $1 via shell arguments so the script receives it + Quickshell.execDetached(["sh", "-lc", script, "unlock-hook", "unlock"]); + Logger.d("HooksService", `Executed screen unlock hook: ${script}`); + } catch (e) { + Logger.e("HooksService", `Failed to execute screen unlock hook: ${e}`); + } + } + + // Execute performance mode enabled hook + function executePerformanceModeEnabledHook() { + if (!Settings.data.hooks?.enabled) { + return; + } + + const script = Settings.data.hooks?.performanceModeEnabled; + if (!script || script === "") { + return; + } + + try { + Quickshell.execDetached(["sh", "-lc", script]); + } catch (e) { + Logger.e("HooksService", `Failed to execute performance mode enabled hook: ${e}`); + } + } + + // Execute performance mode disabled hook + function executePerformanceModeDisabledHook() { + if (!Settings.data.hooks?.enabled) { + return; + } + + const script = Settings.data.hooks?.performanceModeDisabled; + if (!script || script === "") { + return; + } + + try { + Quickshell.execDetached(["sh", "-lc", script]); + } catch (e) { + Logger.e("HooksService", `Failed to execute performance mode disabled hook: ${e}`); + } + } + + // Execute color generation hook + function executeColorGenerationHook() { + if (!Settings.data.hooks?.enabled) { + return; + } + + const script = Settings.data.hooks?.colorGeneration; + if (!script || script === "") { + return; + } + + try { + const theme = Settings.data.colorSchemes.darkMode ? "dark" : "light"; + const command = script.replace(/\$1/g, theme); + Quickshell.execDetached(["sh", "-lc", command]); + Logger.d("HooksService", `Executed color generation hook: ${command}`); + } catch (e) { + Logger.e("HooksService", `Failed to execute color generation hook: ${e}`); + } + } + + // Blocking power hook infrastructure + property var pendingPowerCallback: null + + Process { + id: powerHookProcess + onExited: (exitCode, exitStatus) => { + if (exitCode !== 0) { + Logger.w("HooksService", `Power hook failed with exit code ${exitCode}`); + } + + if (pendingPowerCallback !== null) { + const callback = pendingPowerCallback; + pendingPowerCallback = null; + callback(); + } + } + } + + function runPowerHook(script, callback) { + pendingPowerCallback = callback; + powerHookProcess.command = ["sh", "-lc", script]; + powerHookProcess.running = true; + } + + function executeSessionHook(action, callback) { + if (!Settings.data.hooks?.enabled) { + callback(); + + return; + } + + const script = Settings.data.hooks?.session; + if (!script) { + callback(); + + return; + } + + Logger.i("HooksService", `Executing session hook for ${action}`); + runPowerHook(`${script} ${action}`, callback); + } + + // Execute startup hook + function executeStartupHook() { + if (!Settings.data.hooks?.enabled) { + return; + } + + const script = Settings.data.hooks?.startup; + if (!script || script === "") { + return; + } + + try { + Quickshell.execDetached(["sh", "-lc", script]); + Logger.d("HooksService", `Executed startup hook: ${script}`); + } catch (e) { + Logger.e("HooksService", `Failed to execute startup hook: ${e}`); + } + } + + // Initialize the service + function init() { + Logger.i("HooksService", "Service started"); + // Initialize lock screen state tracking + Qt.callLater(() => { + if (PanelService.lockScreen) { + wasLocked = PanelService.lockScreen.active; + lockScreenActiveConnection.target = PanelService.lockScreen; + } + // Initialize performance mode state tracking + wasPerformanceModeEnabled = PowerProfileService.noctaliaPerformanceMode; + // Execute startup hook + executeStartupHook(); + }); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Control/IPCService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Control/IPCService.qml new file mode 100644 index 0000000..f07bb7b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Control/IPCService.qml @@ -0,0 +1,940 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets + +import qs.Commons +import qs.Modules.Panels.Settings +import qs.Services.Compositor +import qs.Services.Hardware +import qs.Services.Location +import qs.Services.Media +import qs.Services.Networking +import qs.Services.Noctalia +import qs.Services.Power +import qs.Services.System +import qs.Services.Theming +import qs.Services.UI + +Singleton { + id: root + + // Screen detector, set via init() + property var screenDetector: null + + function init(detector) { + root.screenDetector = detector; + Logger.i("IPCService", "Service started"); + } + + // Helper for index-based notification lookups in IPC + function _getNotificationByIndex(index: string, funcName: string) { + var idx = index === "" ? 0 : parseInt(index); + if (isNaN(idx)) { + Logger.w("IPC", "Argument to ipc call '" + funcName + "' must be a number"); + return null; + } + if (idx < 0 || idx >= NotificationService.popupModel.count) { + Logger.w("IPC", "Notification index out of range: " + idx); + return null; + } + return NotificationService.popupModel.get(idx); + } + + IpcHandler { + target: "bar" + function toggle() { + BarService.toggleVisibility(); + } + function hideBar() { + BarService.hide(); + } + function showBar() { + BarService.show(); + } + function peek() { + BarService.peek(); + } + function setDisplayMode(mode: string, screen: string) { + if (mode === "always_visible" || mode === "non_exclusive" || mode === "auto_hide") { + if (!screen || screen === "all") { + Settings.data.bar.displayMode = mode; + } else { + Settings.setScreenOverride(screen, "displayMode", mode); + } + } + } + function setPosition(position: string, screen: string) { + var valid = position === "top" || position === "bottom" || position === "left" || position === "right"; + if (!valid) { + Logger.w("IPC", "Invalid bar position: " + position + ". Valid: top, bottom, left, right"); + return; + } + if (!screen || screen === "all") { + Settings.data.bar.position = position; + } else { + Settings.setScreenOverride(screen, "position", position); + } + } + } + + // Settings IPC helpers (outside IpcHandler to avoid QVariant IPC warnings) + readonly property var _settingsTabMap: ({ + "about": SettingsPanel.Tab.About, + "audio": SettingsPanel.Tab.Audio, + "bar": SettingsPanel.Tab.Bar, + "colorscheme": SettingsPanel.Tab.ColorScheme, + "lockscreen": SettingsPanel.Tab.LockScreen, + "controlcenter": SettingsPanel.Tab.ControlCenter, + "desktopwidgets": SettingsPanel.Tab.DesktopWidgets, + "osd": SettingsPanel.Tab.OSD, + "display": SettingsPanel.Tab.Display, + "dock": SettingsPanel.Tab.Dock, + "general": SettingsPanel.Tab.General, + "hooks": SettingsPanel.Tab.Hooks, + "launcher": SettingsPanel.Tab.Launcher, + "location": SettingsPanel.Tab.Location, + "connections": SettingsPanel.Tab.Connections, + "notifications": SettingsPanel.Tab.Notifications, + "plugins": SettingsPanel.Tab.Plugins, + "sessionmenu": SettingsPanel.Tab.SessionMenu, + "system": SettingsPanel.Tab.System, + "systemmonitor": SettingsPanel.Tab.System, + "userinterface": SettingsPanel.Tab.UserInterface, + "wallpaper": SettingsPanel.Tab.Wallpaper + }) + + function _parseSettingsTabArg(tabArg) { + var parts = tabArg.split("/"); + var tabId = _resolveSettingsTab(parts[0]); + var subTabId = parts.length > 1 ? parseInt(parts[1]) : -1; + return { + "tab": tabId, + "subTab": isNaN(subTabId) ? -1 : subTabId + }; + } + + function _resolveSettingsTab(tabName) { + if (!tabName) + return SettingsPanel.Tab.General; + var key = tabName.toLowerCase().replace(/[-_]/g, ""); + if (key in _settingsTabMap) + return _settingsTabMap[key]; + Logger.w("IPC", "Unknown settings tab: " + tabName); + return SettingsPanel.Tab.General; + } + + function _settingsToggle(tabId, subTabId) { + root.screenDetector.withCurrentScreen(screen => { + SettingsPanelService.toggle(tabId, subTabId, screen); + }); + } + + function _settingsOpen(tabId, subTabId) { + root.screenDetector.withCurrentScreen(screen => { + SettingsPanelService.openToTab(tabId, subTabId, screen); + }); + } + + IpcHandler { + target: "settings" + + function toggle() { + root._settingsToggle(SettingsPanel.Tab.General, -1); + } + + function toggleTab(tab: string) { + var parsed = root._parseSettingsTabArg(tab); + root._settingsToggle(parsed.tab, parsed.subTab); + } + + function open() { + root._settingsOpen(SettingsPanel.Tab.General, -1); + } + + function openTab(tab: string) { + var parsed = root._parseSettingsTabArg(tab); + root._settingsOpen(parsed.tab, parsed.subTab); + } + } + + IpcHandler { + target: "calendar" + function toggle() { + root.screenDetector.withCurrentScreen(screen => { + var clockPanel = PanelService.getPanel("clockPanel", screen); + clockPanel?.toggle(null, "Clock"); + }); + } + } + + IpcHandler { + target: "notifications" + function toggleHistory() { + // Will attempt to open the panel next to the bar button if any. + root.screenDetector.withCurrentScreen(screen => { + var notificationHistoryPanel = PanelService.getPanel("notificationHistoryPanel", screen); + notificationHistoryPanel.toggle(null, "NotificationHistory"); + }); + } + function toggleDND() { + NotificationService.doNotDisturb = !NotificationService.doNotDisturb; + } + function enableDND() { + NotificationService.doNotDisturb = true; + } + function disableDND() { + NotificationService.doNotDisturb = false; + } + function clear() { + NotificationService.clearHistory(); + } + + function dismissOldest() { + NotificationService.dismissOldestPopup(); + } + + function removeOldestHistory() { + NotificationService.removeOldestHistory(); + } + + function dismissAll() { + NotificationService.dismissAllPopups(); + } + + function getHistory(): string { + return JSON.stringify(NotificationService.getHistorySnapshot(), null, 2); + } + + function removeFromHistory(id: string): bool { + return NotificationService.removeFromHistory(id); + } + + function invokeDefault(index: string): bool { + var notif = root._getNotificationByIndex(index, "notifications invokeDefault"); + if (!notif) + return false; + + var actions = JSON.parse(notif.actionsJson || "[]"); + if (actions.length === 0) + return false; + + var actionId = actions.find(a => a.identifier === "default")?.identifier ?? actions[0].identifier; + return NotificationService.invokeAction(notif.id, actionId); + } + + function invokeDefaultAndDismiss(index: string): bool { + var notif = root._getNotificationByIndex(index, "notifications invokeDefaultAndDismiss"); + if (!notif) + return false; + + var actions = JSON.parse(notif.actionsJson || "[]"); + if (actions.length === 0) { + NotificationService.dismissPopup(notif.id); + return false; + } + + var actionId = actions.find(a => a.identifier === "default")?.identifier ?? actions[0].identifier; + var result = NotificationService.invokeAction(notif.id, actionId); + NotificationService.dismissPopup(notif.id); + return result; + } + + function invokeAction(id: string, actionId: string): bool { + if (!id || !actionId) { + Logger.w("IPC", "Both 'id' and 'actionId' are required for 'notifications invokeAction'"); + return false; + } + return NotificationService.invokeAction(id, actionId); + } + + function getActions(index: string): string { + var notif = root._getNotificationByIndex(index, "notifications getActions"); + if (!notif) + return "[]"; + return notif.actionsJson || "[]"; + } + } + + IpcHandler { + target: "toast" + + function send(json: string) { + try { + var data = JSON.parse(json); + var title = data.title || ""; + var body = data.body || ""; + var icon = data.icon || ""; + var type = data.type || "notice"; + var duration = data.duration; + + switch (type) { + case "warning": + ToastService.showWarning(title, body, duration ?? 4000); + break; + case "error": + ToastService.showError(title, body, duration ?? 6000); + break; + default: + ToastService.showNotice(title, body, icon, duration ?? 3000); + } + } catch (error) { + Logger.e("IPC", "Failed to parse toast JSON: " + error); + } + } + + function dismiss() { + ToastService.dismissToast(); + } + } + + // Idle Inhibitor / Keep Awake + IpcHandler { + target: "idleInhibitor" + function toggle() { + IdleInhibitorService.manualToggle(); + } + function enable() { + IdleInhibitorService.addManualInhibitor(null); + } + function disable() { + IdleInhibitorService.removeManualInhibitor(); + } + function enableFor(seconds: string) { + var secs = parseInt(seconds); + if (isNaN(secs) || secs <= 0) { + Logger.w("IPC", "Argument to 'idleInhibitor enableFor' must be a positive number"); + return; + } + IdleInhibitorService.addManualInhibitor(secs); + } + } + + IpcHandler { + target: "launcher" + function toggle() { + root.screenDetector.withCurrentScreen(screen => { + var searchText = PanelService.getLauncherSearchText(screen); + var isInAppMode = !searchText.startsWith(">"); + if (!PanelService.isLauncherOpen(screen)) { + // Closed -> open in app mode + PanelService.openLauncherWithSearch(screen, ""); + } else if (isInAppMode) { + // Already in app mode -> close + PanelService.closeLauncher(screen); + } else { + // In another mode -> switch to app mode + PanelService.setLauncherSearchText(screen, ""); + } + }, Settings.data.appLauncher.overviewLayer); + } + function clipboard() { + root.screenDetector.withCurrentScreen(screen => { + var searchText = PanelService.getLauncherSearchText(screen); + var isInClipMode = searchText.startsWith(">clip"); + if (!PanelService.isLauncherOpen(screen)) { + // Closed -> open in clipboard mode + PanelService.openLauncherWithSearch(screen, ">clip "); + } else if (isInClipMode) { + // Already in clipboard mode -> close + PanelService.closeLauncher(screen); + } else { + // In another mode -> switch to clipboard mode + PanelService.setLauncherSearchText(screen, ">clip "); + } + }, Settings.data.appLauncher.overviewLayer); + } + function command() { + root.screenDetector.withCurrentScreen(screen => { + var searchText = PanelService.getLauncherSearchText(screen); + var isInCmdMode = searchText.startsWith(">cmd"); + if (!PanelService.isLauncherOpen(screen)) { + PanelService.openLauncherWithSearch(screen, ">cmd "); + } else if (isInCmdMode) { + PanelService.closeLauncher(screen); + } else { + PanelService.setLauncherSearchText(screen, ">cmd "); + } + }, Settings.data.appLauncher.overviewLayer); + } + function emoji() { + root.screenDetector.withCurrentScreen(screen => { + var searchText = PanelService.getLauncherSearchText(screen); + var isInEmojiMode = searchText.startsWith(">emoji"); + if (!PanelService.isLauncherOpen(screen)) { + // Closed -> open in emoji mode + PanelService.openLauncherWithSearch(screen, ">emoji "); + } else if (isInEmojiMode) { + // Already in emoji mode -> close + PanelService.closeLauncher(screen); + } else { + // In another mode -> switch to emoji mode + PanelService.setLauncherSearchText(screen, ">emoji "); + } + }, Settings.data.appLauncher.overviewLayer); + } + function windows() { + root.screenDetector.withCurrentScreen(screen => { + var searchText = PanelService.getLauncherSearchText(screen); + var isInWindowsMode = searchText.startsWith(">win"); + if (!PanelService.isLauncherOpen(screen)) { + PanelService.openLauncherWithSearch(screen, ">win "); + } else if (isInWindowsMode) { + PanelService.closeLauncher(screen); + } else { + PanelService.setLauncherSearchText(screen, ">win "); + } + }, Settings.data.appLauncher.overviewLayer); + } + function settings() { + root.screenDetector.withCurrentScreen(screen => { + var searchText = PanelService.getLauncherSearchText(screen); + var isInSettingsMode = searchText.startsWith(">settings"); + if (!PanelService.isLauncherOpen(screen)) { + PanelService.openLauncherWithSearch(screen, ">settings "); + } else if (isInSettingsMode) { + PanelService.closeLauncher(screen); + } else { + PanelService.setLauncherSearchText(screen, ">settings "); + } + }, Settings.data.appLauncher.overviewLayer); + } + } + + IpcHandler { + target: "lockScreen" + + // New preferred method - lock the screen + function lock() { + // Only lock if not already locked (prevents the red screen issue) + if (!PanelService.lockScreen.active) { + CompositorService.lock(); + } + } + } + + IpcHandler { + target: "brightness" + function increase() { + BrightnessService.increaseBrightness(); + } + function decrease() { + BrightnessService.decreaseBrightness(); + } + function set(value: string) { + var val = parseFloat(value); + if (isNaN(val)) + return; + + // Normalize logic: heuristic handling of 0-100 vs 0-1 + if (val > 1.0) + val = val / 100.0; + + // Clamp + val = Math.max(0.0, Math.min(1.0, val)); + + BrightnessService.setBrightness(val); + } + } + + IpcHandler { + target: "monitors" + function on() { + CompositorService.turnOnMonitors(); + } + function off() { + CompositorService.turnOffMonitors(); + } + } + + IpcHandler { + target: "darkMode" + function toggle() { + Settings.data.colorSchemes.darkMode = !Settings.data.colorSchemes.darkMode; + } + function setDark() { + Settings.data.colorSchemes.darkMode = true; + } + function setLight() { + Settings.data.colorSchemes.darkMode = false; + } + } + + IpcHandler { + target: "nightLight" + function toggle() { + if (!ProgramCheckerService.wlsunsetAvailable) { + Logger.w("IPC", "wlsunset not available, cannot toggle night light"); + return; + } + + if (Settings.data.nightLight.forced) { + Settings.data.nightLight.forced = false; + } else { + if (Settings.data.nightLight.enabled) { + Settings.data.nightLight.enabled = false; + } else { + Settings.data.nightLight.forced = true; + Settings.data.nightLight.enabled = true; + } + } + } + } + + IpcHandler { + target: "colorScheme" + function set(schemeName: string) { + ColorSchemeService.setPredefinedScheme(schemeName); + } + function setGenerationMethod(method: string) { + var valid = false; + for (var i = 0; i < TemplateProcessor.schemeTypes.length; i++) { + if (TemplateProcessor.schemeTypes[i].key === method) { + valid = true; + break; + } + } + + if (valid) { + Settings.data.colorSchemes.generationMethod = method; + } else { + Logger.w("IPC", "Invalid generation method received: " + method); + } + } + } + + IpcHandler { + target: "volume" + function increase() { + AudioService.increaseVolume(); + } + function decrease() { + AudioService.decreaseVolume(); + } + function muteOutput() { + AudioService.setOutputMuted(!AudioService.muted); + } + function increaseInput() { + AudioService.increaseInputVolume(); + } + function decreaseInput() { + AudioService.decreaseInputVolume(); + } + function muteInput() { + AudioService.setInputMuted(!AudioService.inputMuted); + } + function togglePanel() { + root.screenDetector.withCurrentScreen(screen => { + var panel = PanelService.getPanel("audioPanel", screen); + panel?.toggle(null, "Volume"); + }); + } + function openPanel() { + root.screenDetector.withCurrentScreen(screen => { + var panel = PanelService.getPanel("audioPanel", screen); + panel?.open(null, "Volume"); + }); + } + function closePanel() { + root.screenDetector.withCurrentScreen(screen => { + var panel = PanelService.getPanel("audioPanel", screen); + panel?.close(null, "Volume"); + }); + } + } + + IpcHandler { + target: "sessionMenu" + function toggle() { + root.screenDetector.withCurrentScreen(screen => { + var sessionMenuPanel = PanelService.getPanel("sessionMenuPanel", screen); + // Session Menu is never open near the bar + sessionMenuPanel?.toggle(); + }); + } + + function lock() { + if (!PanelService.lockScreen.active) { + CompositorService.lock(); + } + } + + function lockAndSuspend() { + // Only lock and suspend if not already locked + if (!PanelService.lockScreen.active) { + CompositorService.lockAndSuspend(); + } + } + } + + IpcHandler { + target: "controlCenter" + function toggle() { + root.screenDetector.withCurrentScreen(screen => { + var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen); + if (Settings.data.controlCenter.position === "close_to_bar_button") { + // Will attempt to open the panel next to the bar button if any. + controlCenterPanel?.toggle(null, "ControlCenter"); + } else { + controlCenterPanel?.toggle(); + } + }); + } + } + + IpcHandler { + target: "dock" + function toggle() { + Settings.data.dock.enabled = !Settings.data.dock.enabled; + } + } + + // Wallpaper IPC: trigger a new random wallpaper + IpcHandler { + target: "wallpaper" + function toggle() { + if (Settings.data.wallpaper.enabled) { + root.screenDetector.withCurrentScreen(screen => { + var wallpaperPanel = PanelService.getPanel("wallpaperPanel", screen); + wallpaperPanel?.toggle(); + }); + } + } + + function random(screen: string) { + if (Settings.data.wallpaper.enabled) { + if (!screen || screen === "all" || screen.trim().length === 0) { + screen = undefined; + } + WallpaperService.setRandomWallpaper(screen); + } + } + + function get(screen: string): string { + if (screen === "all" || screen === "") { + if (Quickshell.screens.length > 1) { + return JSON.stringify(WallpaperService.getWallpapersEffectiveMap()); + } + return WallpaperService.getWallpaper(Quickshell.screens[0].name) ?? ""; + } else { + var found = Quickshell.screens.find(s => s.name === screen); + if (!found) { + Logger.w("IPC", "wallpaper get: unknown screen: " + screen); + return ""; + } + return WallpaperService.getWallpaper(screen) ?? ""; + } + } + + function set(path: string, screen: string) { + if (screen === "all" || screen === "") { + screen = undefined; + } + WallpaperService.changeWallpaper(path, screen); + } + + function refresh() { + WallpaperService.refreshWallpapersList(); + } + + function toggleAutomation() { + Settings.data.wallpaper.automationEnabled = !Settings.data.wallpaper.automationEnabled; + } + function disableAutomation() { + Settings.data.wallpaper.automationEnabled = false; + } + function enableAutomation() { + Settings.data.wallpaper.automationEnabled = true; + } + } + + IpcHandler { + target: "wifi" + function toggle() { + NetworkService.setWifiEnabled(!NetworkService.wifiEnabled); + } + function enable() { + NetworkService.setWifiEnabled(true); + } + function disable() { + NetworkService.setWifiEnabled(false); + } + } + + IpcHandler { + target: "network" + function togglePanel() { + root.screenDetector.withCurrentScreen(screen => { + var networkPanel = PanelService.getPanel("networkPanel", screen); + networkPanel?.toggle(null, "Network"); + }); + } + } + + IpcHandler { + target: "bluetooth" + function toggle() { + BluetoothService.setBluetoothEnabled(!BluetoothService.enabled); + } + function enable() { + BluetoothService.setBluetoothEnabled(true); + } + function disable() { + BluetoothService.setBluetoothEnabled(false); + } + function togglePanel() { + root.screenDetector.withCurrentScreen(screen => { + var bluetoothPanel = PanelService.getPanel("bluetoothPanel", screen); + bluetoothPanel?.toggle(null, "Bluetooth"); + }); + } + function toggleAutoConnect() { + Settings.data.network.bluetoothAutoConnect = !Settings.data.network.bluetoothAutoConnect; + } + function enableAutoConnect() { + Settings.data.network.bluetoothAutoConnect = true; + } + function disableAutoConnect() { + Settings.data.network.bluetoothAutoConnect = false; + } + } + + IpcHandler { + target: "airplaneMode" + function toggle() { + NetworkService.setAirplaneMode(!NetworkService.airplaneModeEnabled); + } + function enable() { + NetworkService.setAirplaneMode(true); + } + function disable() { + NetworkService.setAirplaneMode(false); + } + } + + IpcHandler { + target: "battery" + function togglePanel() { + root.screenDetector.withCurrentScreen(screen => { + var batteryPanel = PanelService.getPanel("batteryPanel", screen); + batteryPanel?.toggle(null, "Battery"); + }); + } + } + + IpcHandler { + target: "powerProfile" + function cycle() { + PowerProfileService.cycleProfile(); + } + + function cycleReverse() { + PowerProfileService.cycleProfileReverse(); + } + + function set(mode: string) { + switch (mode) { + case "performance": + PowerProfileService.setProfile(2); + break; + case "balanced": + PowerProfileService.setProfile(1); + break; + case "powersaver": + PowerProfileService.setProfile(0); + break; + } + } + + function toggleNoctaliaPerformance() { + PowerProfileService.toggleNoctaliaPerformance(); + } + + function enableNoctaliaPerformance() { + PowerProfileService.setNoctaliaPerformance(true); + } + + function disableNoctaliaPerformance() { + PowerProfileService.setNoctaliaPerformance(false); + } + } + + IpcHandler { + target: "media" + + function toggle() { + root.screenDetector.withCurrentScreen(screen => { + var panel = PanelService.getPanel("mediaPlayerPanel", screen); + panel?.toggle(null, "MediaMini"); + }); + } + + function playPause() { + MediaService.playPause(); + } + + function play() { + MediaService.play(); + } + + function stop() { + MediaService.stop(); + } + + function pause() { + MediaService.pause(); + } + + function next() { + MediaService.next(); + } + + function previous() { + MediaService.previous(); + } + + function seekRelative(offset: string) { + var offsetVal = parseFloat(offset); + if (Number.isNaN(offsetVal)) { + Logger.w("Media", "Argument to ipc call 'media seekRelative' must be a number"); + return; + } + MediaService.seekRelative(offsetVal); + } + + function seekByRatio(position: string) { + var positionVal = parseFloat(position); + if (Number.isNaN(positionVal)) { + Logger.w("Media", "Argument to ipc call 'media seekByRatio' must be a number"); + return; + } + MediaService.seekByRatio(positionVal); + } + } + + IpcHandler { + target: "state" + + // Returns all settings and shell state as JSON + function all(): string { + try { + var snapshot = ShellState.buildStateSnapshot(); + if (!snapshot) { + throw new Error("State snapshot unavailable"); + } + return JSON.stringify(snapshot, null, 2); + } catch (error) { + Logger.e("IPC", "Failed to serialize state:", error); + return JSON.stringify({ + "error": "Failed to serialize state: " + error + }, null, 2); + } + } + } + + IpcHandler { + target: "desktopWidgets" + function toggle() { + Settings.data.desktopWidgets.enabled = !Settings.data.desktopWidgets.enabled; + } + function disable() { + Settings.data.desktopWidgets.enabled = false; + } + function enable() { + Settings.data.desktopWidgets.enabled = true; + } + function edit() { + DesktopWidgetRegistry.editMode = !DesktopWidgetRegistry.editMode; + } + } + + IpcHandler { + target: "location" + function get(): string { + return Settings.data.location.name; + } + function set(name: string) { + Settings.data.location.name = name; + LocationService.update(); + } + } + + IpcHandler { + target: "systemMonitor" + function toggle() { + root.screenDetector.withCurrentScreen(screen => { + var panel = PanelService.getPanel("systemStatsPanel", screen); + panel?.toggle(null, "SystemMonitor"); + }); + } + } + + IpcHandler { + target: "plugin" + function openSettings(key: string) { + var manifest = PluginRegistry.getPluginManifest(key); + if (!manifest) { + Logger.w("IPC", "Plugin not found:", key); + return; + } + if (!manifest.entryPoints?.settings) { + Logger.w("IPC", "Plugin has no settings entry point:", key); + return; + } + root.screenDetector.withCurrentScreen(screen => { + BarService.openPluginSettings(screen, manifest); + }); + } + + function openPanel(key: string) { + var manifest = PluginRegistry.getPluginManifest(key); + if (!manifest) { + Logger.w("IPC", "Plugin not found:", key); + return; + } + if (!manifest.entryPoints?.panel) { + Logger.w("IPC", "Plugin has no panel entry point:", key); + return; + } + root.screenDetector.withCurrentScreen(screen => { + PluginService.openPluginPanel(key, screen, null); + }); + } + + function closePanel(key: string) { + var manifest = PluginRegistry.getPluginManifest(key); + if (!manifest) { + Logger.w("IPC", "Plugin not found:", key); + return; + } + if (!manifest.entryPoints?.panel) { + Logger.w("IPC", "Plugin has no panel entry point:", key); + return; + } + root.screenDetector.withCurrentScreen(screen => { + var api = PluginService.getPluginAPI(key); + if (api) { + api.closePanel(screen); + } + }); + } + + function togglePanel(key: string) { + var manifest = PluginRegistry.getPluginManifest(key); + if (!manifest) { + Logger.w("IPC", "Plugin not found:", key); + return; + } + if (!manifest.entryPoints?.panel) { + Logger.w("IPC", "Plugin has no panel entry point:", key); + return; + } + root.screenDetector.withCurrentScreen(screen => { + PluginService.togglePluginPanel(key, screen, null); + }); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Hardware/BatteryService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Hardware/BatteryService.qml new file mode 100644 index 0000000..f615354 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Hardware/BatteryService.qml @@ -0,0 +1,368 @@ +pragma Singleton +import QtQml +import QtQuick + +import Quickshell +import Quickshell.Io +import Quickshell.Services.UPower +import qs.Commons +import qs.Services.Networking +import qs.Services.UI + +Singleton { + id: root + + readonly property var primaryDevice: _laptopBattery || _bluetoothBattery || null // Primary battery device (prioritizes laptop over Bluetooth) + readonly property real batteryPercentage: getPercentage(primaryDevice) + readonly property bool batteryCharging: isCharging(primaryDevice) + readonly property bool batteryPluggedIn: isPluggedIn(primaryDevice) + readonly property bool batteryReady: isDeviceReady(primaryDevice) + readonly property bool batteryPresent: isDevicePresent(primaryDevice) + readonly property real warningThreshold: Settings.data.systemMonitor.batteryWarningThreshold + readonly property real criticalThreshold: Settings.data.systemMonitor.batteryCriticalThreshold + readonly property string batteryIcon: getIcon(batteryPercentage, batteryCharging, batteryPluggedIn, batteryReady) + + readonly property var laptopBatteries: UPower.devices.values.filter(d => d.isLaptopBattery).sort((x, y) => { + // Force DisplayDevice to the top + if (x.nativePath.includes("DisplayDevice")) + return -1; + if (y.nativePath.includes("DisplayDevice")) + return 1; + + // Standard string comparison works for BAT0 vs BAT1 + return x.nativePath.localeCompare(y.nativePath, undefined, { + numeric: true + }); + }) + + readonly property var bluetoothBatteries: { + var list = []; + var btArray = BluetoothService.devices?.values || []; + for (var i = 0; i < btArray.length; i++) { + var btd = btArray[i]; + if (btd && btd.connected && btd.batteryAvailable) { + list.push(btd); + } + } + return list; + } + + readonly property var _laptopBattery: UPower.displayDevice.isPresent ? UPower.displayDevice : (laptopBatteries.length > 0 ? laptopBatteries[0] : null) + readonly property var _bluetoothBattery: bluetoothBatteries.length > 0 ? bluetoothBatteries[0] : null + + property var deviceModel: { + var model = [ + { + "key": "__default__", + "name": I18n.tr("bar.battery.device-default") + } + ]; + const devices = UPower.devices?.values || []; + for (let d of devices) { + if (!d || d.type === UPowerDeviceType.LinePower) { + continue; + } + model.push({ + key: d.nativePath || "", + name: d.model || d.nativePath || I18n.tr("common.unknown") + }); + } + return model; + } + + property var _hasNotified: ({}) + + function findDevice(nativePath) { + if (!nativePath || nativePath === "__default__" || nativePath === "DisplayDevice") { + return _laptopBattery; + } + + if (!UPower.devices) { + return null; + } + + const devices = UPower.devices?.values || []; + for (let d of devices) { + if (d && d.nativePath === nativePath) { + if (d.type === UPowerDeviceType.LinePower) { + continue; + } + return d; + } + } + return null; + } + + function isDevicePresent(device) { + if (!device) { + return false; + } + + // Handle Bluetooth devices (identified by having batteryAvailable property) + if (device.batteryAvailable !== undefined) { + return device.connected === true; + } + + // Handle UPower devices + if (device.type !== undefined) { + if (device.type === UPowerDeviceType.Battery && device.isPresent !== undefined) { + return device.isPresent === true; + } + // Fallback for non-battery UPower devices or if isPresent is missing + return device.ready && device.percentage !== undefined; + } + return false; + } + + function isDeviceReady(device) { + if (!isDevicePresent(device)) { + return false; + } + if (device.batteryAvailable !== undefined) { + return device.battery !== undefined; + } + return device.ready && device.percentage !== undefined; + } + + function getPercentage(device) { + if (!device) { + return -1; + } + if (device.batteryAvailable !== undefined) { + return Math.round((device.battery || 0) * 100); + } + return Math.round((device.percentage || 0) * 100); + } + + function isCharging(device) { + if (!device || isBluetoothDevice(device)) { + // Tracking bluetooth devices can charge or not is a loop hole, none of my devices has it, even if it possible?! + return false; // Assuming not charging until someone/quickshell brings a way to do pretty unlikely. + } + if (device.state !== undefined) { + return device.state === UPowerDeviceState.Charging; + } + return false; + } + + function isPluggedIn(device) { + if (!device || isBluetoothDevice(device)) { + // Tracking bluetooth devices can charge or not is a loop hole, none of my devices has it, even if it possible?! + return false; // Assuming not charging until someone/quickshell brings a way to do pretty unlikely. + } + if (device.state !== undefined) { + return device.state === UPowerDeviceState.FullyCharged || device.state === UPowerDeviceState.PendingCharge; + } + return false; + } + + function isCriticalBattery(device) { + return (!isCharging(device) && !isPluggedIn(device)) && getPercentage(device) <= criticalThreshold; + } + + function isLowBattery(device) { + return (!isCharging(device) && !isPluggedIn(device)) && getPercentage(device) <= warningThreshold && getPercentage(device) > criticalThreshold; + } + + function isBluetoothDevice(device) { + if (!device) { + return false; + } + // Check for Quickshell Bluetooth device property + if (device.batteryAvailable !== undefined) { + return true; + } + // Check for UPower device path indicating it's a Bluetooth device + if (device.nativePath && (device.nativePath.includes("bluez") || device.nativePath.includes("bluetooth"))) { + return true; + } + return false; + } + + function getDeviceName(device) { + if (!isDeviceReady(device)) { + return ""; + } + + if (!isBluetoothDevice(device) && device.isLaptopBattery) { + // If there is more than one battery explicitly name them + // Logger.e("BatteryDebug", "Available Battery count: " + laptopBatteries.length); // can be useful for debugging + if (laptopBatteries.length > 1 && device.nativePath) { + if (device.nativePath === "DisplayDevice") { + return I18n.tr("battery.all-batteries"); + } + var match = device.nativePath.match(/(\d+)$/); + if (match) { + // In case of 2 batteries: bat0 => bat1 bat1 => bat2 + return I18n.tr("common.battery") + " " + (parseInt(match[1]) + 1); // Append numbers + } + } + // Return Battery if there is only one + return I18n.tr("common.battery"); + } + + if (isBluetoothDevice(device) && device.name) { + return device.name; + } + + if (device.model) { + return device.model; + } + + return ""; + } + + function getIcon(percent, charging, pluggedIn, isReady) { + if (!isReady) { + return "battery-exclamation"; + } + if (charging) { + return "battery-charging"; + } + if (pluggedIn) { + return "battery-charging-2"; + } + + const icons = [ + { + threshold: 86, + icon: "battery-4" + }, + { + threshold: 56, + icon: "battery-3" + }, + { + threshold: 31, + icon: "battery-2" + }, + { + threshold: 11, + icon: "battery-1" + }, + { + threshold: 0, + icon: "battery" + } + ]; + + const match = icons.find(tier => percent >= tier.threshold); + return match ? match.icon : "battery-off"; // New fallback icon clearly represent if nothing is true here. + } + + function getRateText(device) { + if (!device || device.changeRate === undefined) { + return ""; + } + const rate = Math.abs(device.changeRate); + if (device.timeToFull > 0) { + return I18n.tr("battery.charging-rate", { + "rate": rate.toFixed(2) + }); + } else if (device.timeToEmpty > 0) { + return I18n.tr("battery.discharging-rate", { + "rate": rate.toFixed(2) + }); + } + } + + function getTimeRemainingText(device) { + if (!isDeviceReady(device)) { + return I18n.tr("battery.no-battery-detected"); + } + if (isPluggedIn(device)) { + return I18n.tr("battery.plugged-in"); + } else if (device.timeToFull > 0) { + return I18n.tr("battery.time-until-full", { + "time": Time.formatVagueHumanReadableDuration(device.timeToFull) + }); + } else if (device.timeToEmpty > 0) { + return I18n.tr("battery.time-left", { + "time": Time.formatVagueHumanReadableDuration(device.timeToEmpty) + }); + } + return I18n.tr("common.idle"); + } + + function checkDevice(device) { + if (!device || !isDeviceReady(device)) { + return; + } + + const percentage = getPercentage(device); + const charging = isCharging(device); + const pluggedIn = isPluggedIn(device); + const level = isLowBattery(device) ? "low" : (isCriticalBattery(device) ? "critical" : ""); + var deviceKey = device.nativePath; + + if (!_hasNotified[deviceKey]) { + _hasNotified[deviceKey] = { + low: false, + critical: false + }; + } + + if (charging || pluggedIn) { + _hasNotified[deviceKey].low = false; + _hasNotified[deviceKey].critical = false; + } + + if (percentage > warningThreshold) { + _hasNotified[deviceKey].low = false; + _hasNotified[deviceKey].critical = false; + } else if (percentage > criticalThreshold) { + _hasNotified[deviceKey].critical = false; + } + + if (level) { + if (!_hasNotified[deviceKey][level]) { + notify(device, level); + _hasNotified[deviceKey][level] = true; + } + } + } + + function notify(device, level) { + if (!Settings.data.notifications.enableBatteryToast) { + return; + } + var name = getDeviceName(device); + var titleKey = level === "critical" ? "toast.battery.critical" : "toast.battery.low"; + var descKey = level === "critical" ? "toast.battery.critical-desc" : "toast.battery.low-desc"; + + var title = I18n.tr(titleKey); + var desc = I18n.tr(descKey, { + "percent": getPercentage(device) + }); + var icon = level === "critical" ? "battery-exclamation" : "battery-charging-2"; + + if (device == _bluetoothBattery && name) { + title = title + " " + name; + } + + // Only 'showNotice' supports custom icons + ToastService.showNotice(title, desc, icon, 6000); + } + + Instantiator { + model: deviceModel + delegate: Connections { + required property var modelData + property var device: findDevice(modelData.key) + target: device + + function onPercentageChanged() { + if (device.isLaptopBattery && modelData.key !== "__default__") { + return; + } + checkDevice(device); + } + function onStateChanged() { + if (device.isLaptopBattery && modelData.key !== "__default__") { + return; + } + checkDevice(device); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Hardware/BrightnessService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Hardware/BrightnessService.qml new file mode 100644 index 0000000..26847a5 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Hardware/BrightnessService.qml @@ -0,0 +1,560 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +Singleton { + id: root + + property list ddcMonitors: [] + readonly property list monitors: variants.instances + property bool appleDisplayPresent: false + property list availableBacklightDevices: [] + + function getMonitorForScreen(screen: ShellScreen): var { + return monitors.find(m => m.modelData === screen); + } + + // Signal emitted when a specific monitor's brightness changes, includes monitor context + signal monitorBrightnessChanged(var monitor, real newBrightness) + + function getAvailableMethods(): list { + var methods = []; + if (Settings.data.brightness.enableDdcSupport && monitors.some(m => m.isDdc)) + methods.push("ddcutil"); + if (monitors.some(m => !m.isDdc)) + methods.push("internal"); + if (appleDisplayPresent) + methods.push("apple"); + return methods; + } + + // Global helpers for IPC and shortcuts + function increaseBrightness(): void { + monitors.forEach(m => m.increaseBrightness()); + } + + function decreaseBrightness(): void { + monitors.forEach(m => m.decreaseBrightness()); + } + + function setBrightness(value: real): void { + monitors.forEach(m => m.setBrightnessDebounced(value)); + } + + function getDetectedDisplays(): list { + return detectedDisplays; + } + + function normalizeBacklightDevicePath(devicePath): string { + if (devicePath === undefined || devicePath === null) + return ""; + + var normalized = String(devicePath).trim(); + if (normalized === "") + return ""; + + if (normalized.startsWith("/sys/class/backlight/")) + return normalized; + + if (normalized.indexOf("/") === -1) + return "/sys/class/backlight/" + normalized; + + return normalized; + } + + function getBacklightDeviceName(devicePath): string { + var normalized = normalizeBacklightDevicePath(devicePath); + if (normalized === "") + return ""; + + var parts = normalized.split("/"); + while (parts.length > 0 && parts[parts.length - 1] === "") { + parts.pop(); + } + return parts.length > 0 ? parts[parts.length - 1] : ""; + } + + function getMappedBacklightDevice(outputName): string { + var normalizedOutput = String(outputName || "").trim(); + if (normalizedOutput === "") + return ""; + + var mappings = Settings.data.brightness.backlightDeviceMappings || []; + for (var i = 0; i < mappings.length; i++) { + var mapping = mappings[i]; + if (!mapping || typeof mapping !== "object") + continue; + + if (String(mapping.output || "").trim() === normalizedOutput) + return normalizeBacklightDevicePath(mapping.device || ""); + } + + return ""; + } + + function setMappedBacklightDevice(outputName, devicePath): void { + var normalizedOutput = String(outputName || "").trim(); + if (normalizedOutput === "") + return; + + var normalizedDevicePath = normalizeBacklightDevicePath(devicePath); + var mappings = Settings.data.brightness.backlightDeviceMappings || []; + var nextMappings = []; + var replaced = false; + + for (var i = 0; i < mappings.length; i++) { + var mapping = mappings[i]; + if (!mapping || typeof mapping !== "object") + continue; + + var mappingOutput = String(mapping.output || "").trim(); + var mappingDevice = normalizeBacklightDevicePath(mapping.device || ""); + if (mappingOutput === "" || mappingDevice === "") + continue; + + if (mappingOutput === normalizedOutput) { + if (!replaced && normalizedDevicePath !== "") { + nextMappings.push({ + "output": normalizedOutput, + "device": normalizedDevicePath + }); + } + replaced = true; + } else { + nextMappings.push({ + "output": mappingOutput, + "device": mappingDevice + }); + } + } + + if (!replaced && normalizedDevicePath !== "") { + nextMappings.push({ + "output": normalizedOutput, + "device": normalizedDevicePath + }); + } + + Settings.data.brightness.backlightDeviceMappings = nextMappings; + } + + function scanBacklightDevices(): void { + if (!scanBacklightProc.running) + scanBacklightProc.running = true; + } + + reloadableId: "brightness" + + Component.onCompleted: { + Logger.i("Brightness", "Service started"); + scanBacklightDevices(); + if (Settings.data.brightness.enableDdcSupport) { + ddcProc.running = true; + } + } + + onMonitorsChanged: { + ddcMonitors = []; + scanBacklightDevices(); + if (Settings.data.brightness.enableDdcSupport) { + ddcProc.running = true; + } + } + + Connections { + target: Settings.data.brightness + function onEnableDdcSupportChanged() { + if (Settings.data.brightness.enableDdcSupport) { + // Re-detect DDC monitors when enabled + ddcMonitors = []; + ddcProc.running = true; + } else { + // Clear DDC monitors when disabled + ddcMonitors = []; + } + } + function onBacklightDeviceMappingsChanged() { + scanBacklightDevices(); + for (var i = 0; i < monitors.length; i++) { + var m = monitors[i]; + if (m && !m.isDdc && !m.isAppleDisplay) + m.initBrightness(); + } + } + } + + Variants { + id: variants + model: Quickshell.screens + Monitor {} + } + + // Check for Apple Display support + Process { + running: true + command: ["sh", "-c", "which asdbctl >/dev/null 2>&1 && asdbctl get || echo ''"] + stdout: StdioCollector { + onStreamFinished: root.appleDisplayPresent = text.trim().length > 0 + } + } + + // Detect available internal backlight devices + Process { + id: scanBacklightProc + command: ["sh", "-c", "for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then echo \"$dev\"; fi; done"] + stdout: StdioCollector { + onStreamFinished: { + var data = text.trim(); + if (data === "") { + root.availableBacklightDevices = []; + return; + } + + var lines = data.split("\n"); + var found = []; + var seen = ({}); + for (var i = 0; i < lines.length; i++) { + var path = root.normalizeBacklightDevicePath(lines[i]); + if (path === "" || seen[path]) + continue; + seen[path] = true; + found.push(path); + } + + root.availableBacklightDevices = found; + } + } + } + + // Detect DDC monitors + Process { + id: ddcProc + property list ddcMonitors: [] + command: ["ddcutil", "detect", "--enable-dynamic-sleep", "--sleep-multiplier=0.5"] + stdout: StdioCollector { + onStreamFinished: { + var displays = text.trim().split("\n\n"); + ddcProc.ddcMonitors = displays.map(d => { + var ddcModelMatch = d.match(/(This monitor does not support DDC\/CI|Invalid display)/); + var modelMatch = d.match(/Model:\s*(.*)/); + var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/); + var connectorMatch = d.match(/DRM[_ ]connector:\s*card\d+-(.+)/); + var ddcModel = ddcModelMatch ? ddcModelMatch.length > 0 : false; + var model = modelMatch ? modelMatch[1] : "Unknown"; + var bus = busMatch ? busMatch[1] : "Unknown"; + var connector = connectorMatch ? connectorMatch[1].trim() : ""; + Logger.i("Brightness", "Detected DDC Monitor:", model, "connector:", connector, "bus:", bus, "is DDC:", !ddcModel); + return { + "model": model, + "busNum": bus, + "connector": connector, + "isDdc": !ddcModel + }; + }); + root.ddcMonitors = ddcProc.ddcMonitors.filter(m => m.isDdc); + } + } + } + + component Monitor: QtObject { + id: monitor + + required property ShellScreen modelData + readonly property bool isDdc: Settings.data.brightness.enableDdcSupport && root.ddcMonitors.some(m => m.connector === modelData.name) + readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? "" + readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") + readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal") + + // Check if brightness control is available for this monitor + readonly property bool brightnessControlAvailable: { + if (isAppleDisplay) + return true; + if (isDdc) + return true; + // For internal displays, check if we have a brightness path + return brightnessPath !== ""; + } + + property real brightness + property real lastBrightness: 0 + property real queuedBrightness: NaN + property bool commandRunning: false + + // For internal displays - store the backlight device path + property string backlightDevice: "" + property string brightnessPath: "" + property string maxBrightnessPath: "" + property int maxBrightness: 100 + property bool ignoreNextChange: false + property bool initInProgress: false + + // Signal for brightness changes + signal brightnessUpdated(real newBrightness) + + // Execute a system command to get the current brightness value directly + readonly property Process refreshProc: Process { + stdout: StdioCollector { + onStreamFinished: { + var dataText = text.trim(); + if (dataText === "") { + return; + } + + var newBrightness = NaN; + + if (monitor.isAppleDisplay) { + // Apple display format: single integer (0-101) + var val = parseInt(dataText); + if (!isNaN(val)) { + newBrightness = val / 101; + } + } else if (monitor.isDdc) { + // DDC format: "VCP 10 C 100 100" (space-separated) + var parts = dataText.split(" "); + if (parts.length >= 4) { + var current = parseInt(parts[3]); + var max = parseInt(parts[4]); + if (!isNaN(current) && !isNaN(max) && max > 0) { + monitor.maxBrightness = max; + newBrightness = current / max; + } + } + } else { + // Internal display format: two lines (current\nmax) + var lines = dataText.split("\n"); + if (lines.length >= 2) { + var current = parseInt(lines[0].trim()); + var max = parseInt(lines[1].trim()); + if (!isNaN(current) && !isNaN(max) && max > 0) { + newBrightness = current / max; + } + } + } + + // Update if we got a valid brightness value + if (!isNaN(newBrightness) && (Math.abs(newBrightness - monitor.brightness) > 0.001 || monitor.brightness === 0)) { + monitor.brightness = newBrightness; + monitor.brightnessUpdated(monitor.brightness); + root.monitorBrightnessChanged(monitor, monitor.brightness); + Logger.d("Brightness", "Refreshed brightness from system:", monitor.modelData.name, monitor.brightness); + } + } + } + } + + readonly property Process setBrightnessProc: Process { + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + monitor.commandRunning = false; + // If there's a queued brightness change, process it now + if (!isNaN(monitor.queuedBrightness)) { + Qt.callLater(() => { + monitor.setBrightness(monitor.queuedBrightness); + monitor.queuedBrightness = NaN; + }); + } + } + } + + // Function to actively refresh the brightness from system + function refreshBrightnessFromSystem() { + if (!monitor.isDdc && !monitor.isAppleDisplay) { + // For internal displays, query the system directly + refreshProc.command = ["sh", "-c", "cat " + monitor.brightnessPath + " && " + "cat " + monitor.maxBrightnessPath]; + refreshProc.running = true; + } else if (monitor.isDdc && monitor.busNum !== "") { + // For DDC displays, get the current value + refreshProc.command = ["ddcutil", "-b", monitor.busNum, "--enable-dynamic-sleep", "--sleep-multiplier=0.05", "getvcp", "10", "--brief"]; + refreshProc.running = true; + } else if (monitor.isAppleDisplay) { + // For Apple displays, get the current value + refreshProc.command = ["asdbctl", "get"]; + refreshProc.running = true; + } + } + + // FileView to watch for external brightness changes (internal displays only) + readonly property FileView brightnessWatcher: FileView { + id: brightnessWatcher + // Only set path for internal displays with a valid brightness path + path: (!monitor.isDdc && !monitor.isAppleDisplay && monitor.brightnessPath !== "") ? monitor.brightnessPath : "" + watchChanges: path !== "" + onFileChanged: { + // When a file change is detected, actively refresh from system + // to ensure we get the most up-to-date value + Qt.callLater(() => { + monitor.refreshBrightnessFromSystem(); + }); + } + } + + // Initialize brightness + readonly property Process initProc: Process { + stdout: StdioCollector { + onStreamFinished: { + var dataText = text.trim(); + if (dataText === "") { + return; + } + + //Logger.i("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText) + if (monitor.isAppleDisplay) { + var val = parseInt(dataText); + if (!isNaN(val)) { + monitor.brightness = val / 101; + Logger.d("Brightness", "Apple display brightness:", monitor.brightness); + } + } else if (monitor.isDdc) { + var parts = dataText.split(" "); + if (parts.length >= 4) { + var current = parseInt(parts[3]); + var max = parseInt(parts[4]); + if (!isNaN(current) && !isNaN(max) && max > 0) { + monitor.maxBrightness = max; + monitor.brightness = current / max; + Logger.d("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness); + } + } + } else { + // Internal backlight - parse the response which includes device path + var lines = dataText.split("\n"); + if (lines.length >= 3) { + monitor.backlightDevice = lines[0]; + monitor.brightnessPath = monitor.backlightDevice + "/brightness"; + monitor.maxBrightnessPath = monitor.backlightDevice + "/max_brightness"; + + var current = parseInt(lines[1]); + var max = parseInt(lines[2]); + if (!isNaN(current) && !isNaN(max) && max > 0) { + monitor.maxBrightness = max; + monitor.brightness = current / max; + Logger.d("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness); + Logger.d("Brightness", "Using backlight device:", monitor.backlightDevice); + } + } else { + monitor.backlightDevice = ""; + monitor.brightnessPath = ""; + monitor.maxBrightnessPath = ""; + } + } + + monitor.initInProgress = false; + } + } + onExited: (exitCode, exitStatus) => { + monitor.initInProgress = false; + } + } + + readonly property real stepSize: Settings.data.brightness.brightnessStep / 100.0 + readonly property real minBrightnessValue: (Settings.data.brightness.enforceMinimum ? 0.01 : 0.0) + + // Timer for debouncing rapid changes + readonly property Timer timer: Timer { + interval: monitor.isDdc ? 250 : 33 + onTriggered: { + if (!isNaN(monitor.queuedBrightness)) { + monitor.setBrightness(monitor.queuedBrightness); + monitor.queuedBrightness = NaN; + } + } + } + + function setBrightnessDebounced(value: real): void { + monitor.queuedBrightness = value; + timer.start(); + } + + function increaseBrightness(): void { + const value = !isNaN(monitor.queuedBrightness) ? monitor.queuedBrightness : monitor.brightness; + // Enforce minimum brightness if enabled + if (Settings.data.brightness.enforceMinimum && value < minBrightnessValue) { + setBrightnessDebounced(Math.max(stepSize, minBrightnessValue)); + } else { + // Normal brightness increase + setBrightnessDebounced(value + stepSize); + } + } + + function decreaseBrightness(): void { + const value = !isNaN(monitor.queuedBrightness) ? monitor.queuedBrightness : monitor.brightness; + setBrightnessDebounced(value - stepSize); + } + + function setBrightness(value: real): void { + value = Math.max(minBrightnessValue, Math.min(1, value)); + var rounded = Math.round(value * 100); + + // Always update internal value and trigger UI feedback immediately + monitor.brightness = value; + monitor.brightnessUpdated(value); + root.monitorBrightnessChanged(monitor, monitor.brightness); + + if (timer.running) { + monitor.queuedBrightness = value; + return; + } + + // If a command is already running, queue this value + if (monitor.commandRunning) { + monitor.queuedBrightness = value; + return; + } + + // Execute the brightness change command + if (isAppleDisplay) { + monitor.commandRunning = true; + monitor.ignoreNextChange = true; + setBrightnessProc.command = ["asdbctl", "set", rounded]; + setBrightnessProc.running = true; + } else if (isDdc && busNum !== "") { + monitor.commandRunning = true; + monitor.ignoreNextChange = true; + var ddcValue = Math.round(value * monitor.maxBrightness); + var ddcBus = busNum; + Qt.callLater(() => { + setBrightnessProc.command = ["ddcutil", "-b", ddcBus, "--noverify", "--async", "--enable-dynamic-sleep", "--sleep-multiplier=0.05", "setvcp", "10", ddcValue]; + setBrightnessProc.running = true; + }); + } else if (!isDdc) { + monitor.commandRunning = true; + monitor.ignoreNextChange = true; + var backlightDeviceName = root.getBacklightDeviceName(monitor.backlightDevice); + if (backlightDeviceName !== "") { + setBrightnessProc.command = ["brightnessctl", "-d", backlightDeviceName, "s", rounded + "%"]; + } else { + setBrightnessProc.command = ["brightnessctl", "s", rounded + "%"]; + } + setBrightnessProc.running = true; + } + } + + function initBrightness(): void { + monitor.initInProgress = true; + if (isAppleDisplay) { + initProc.command = ["asdbctl", "get"]; + initProc.running = true; + } else if (isDdc && busNum !== "") { + initProc.command = ["ddcutil", "-b", busNum, "--enable-dynamic-sleep", "--sleep-multiplier=0.05", "getvcp", "10", "--brief"]; + initProc.running = true; + } else if (!isDdc) { + // Internal backlight: first try explicit output mapping, then fall back to first available. + var preferredDevicePath = root.getMappedBacklightDevice(modelData.name); + var probeScript = ["preferred=\"$1\"", "if [ -n \"$preferred\" ] && [ ! -d \"$preferred\" ]; then preferred=\"/sys/class/backlight/$preferred\"; fi", "selected=\"\"", + "if [ -n \"$preferred\" ] && [ -f \"$preferred/brightness\" ] && [ -f \"$preferred/max_brightness\" ]; then selected=\"$preferred\"; else for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then selected=\"$dev\"; break; fi; done; fi", + "if [ -n \"$selected\" ]; then echo \"$selected\"; cat \"$selected/brightness\"; cat \"$selected/max_brightness\"; fi"].join("; "); + initProc.command = ["sh", "-c", probeScript, "sh", preferredDevicePath]; + initProc.running = true; + } else { + monitor.initInProgress = false; + } + } + + onBusNumChanged: initBrightness() + onIsDdcChanged: initBrightness() + Component.onCompleted: initBrightness() + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Keyboard/ClipboardService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Keyboard/ClipboardService.qml new file mode 100644 index 0000000..3c04bd2 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Keyboard/ClipboardService.qml @@ -0,0 +1,536 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.UI + +// Clipboard history service using cliphist + local content cache +Singleton { + id: root + + // Public API + property bool active: Settings.data.appLauncher.enableClipboardHistory && cliphistAvailable + property bool loading: false + property var items: [] // [{id, preview, mime, isImage}] + + // Check if cliphist is available on the system + property bool cliphistAvailable: false + property bool dependencyChecked: false + + // Optional automatic watchers to feed cliphist DB + property bool autoWatch: true + property bool watchersStarted: false + + // Expose decoded thumbnails by id and a revision to notify bindings + property var imageDataById: ({}) + property var _imageDataInsertOrder: [] // insertion-order IDs for LRU eviction + readonly property int _imageDataMaxEntries: 20 // max decoded images held in RAM at once + property int revision: 0 + + // Local content cache - stores full text content by ID + // This avoids relying on cliphist decode which can be unreliable + property var contentCache: ({}) + + // Track the most recent clipboard content for instant access + property string _latestTextContent: "" + property string _latestTextId: "" + + // Approximate first-seen timestamps for entries this session (seconds) + property var firstSeenById: ({}) + + // Internal: store callback for decode + property var _decodeCallback: null + property int _decodeRequestId: 0 + + // Queue for base64 decodes + property var _b64Queue: [] + property var _b64CurrentCb: null + property string _b64CurrentMime: "" + property string _b64CurrentId: "" + + signal listCompleted + + // Check if cliphist is available + Component.onCompleted: { + checkCliphistAvailability(); + } + + // Check dependency availability + function checkCliphistAvailability() { + if (dependencyChecked) + return; + dependencyCheckProcess.command = ["sh", "-c", "command -v cliphist"]; + dependencyCheckProcess.running = true; + } + + // Process to check if cliphist is available + Process { + id: dependencyCheckProcess + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + root.dependencyChecked = true; + if (exitCode === 0) { + root.cliphistAvailable = true; + // Start watchers if feature is enabled + if (root.active) { + startWatchers(); + } + } else { + root.cliphistAvailable = false; + // Show toast notification if feature is enabled but cliphist is missing + if (Settings.data.appLauncher.enableClipboardHistory) { + ToastService.showWarning(I18n.tr("toast.clipboard.unavailable"), I18n.tr("toast.clipboard.unavailable-desc"), 6000); + } + } + } + } + + // Start/stop watchers when enabled changes + onActiveChanged: { + if (root.active) { + startWatchers(); + } else { + stopWatchers(); + loading = false; + items = []; + } + } + + // Fallback: periodically refresh list so UI updates even if not in clip mode + Timer { + interval: 5000 + repeat: true + running: root.active + onTriggered: list() + } + + // Internal process objects + Process { + id: listProc + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + const out = String(stdout.text); + const lines = out.split('\n').filter(l => l.length > 0); + // cliphist list default format: " " or "\t" + const parsed = lines.map((l, i) => { + let id = ""; + let preview = ""; + const m = l.match(/^(\d+)\s+(.+)$/); + if (m) { + id = m[1]; + preview = m[2]; + } else { + const tab = l.indexOf('\t'); + id = tab > -1 ? l.slice(0, tab) : l; + preview = tab > -1 ? l.slice(tab + 1) : ""; + } + const lower = preview.toLowerCase(); + const isImage = lower.startsWith("[image]") || lower.includes(" binary data "); + // Best-effort mime guess from preview + var mime = "text/plain"; + if (isImage) { + if (lower.includes(" png")) + mime = "image/png"; + else if (lower.includes(" jpg") || lower.includes(" jpeg")) + mime = "image/jpeg"; + else if (lower.includes(" webp")) + mime = "image/webp"; + else if (lower.includes(" gif")) + mime = "image/gif"; + else + mime = "image/*"; + } + // Record first seen time for new ids (approximate copy time) + if (!root.firstSeenById[id]) { + const assumedAge = i * 15 * 60; + root.firstSeenById[id] = Time.timestamp - assumedAge; + } + // Smart type detection + var contentType = "text"; + if (isImage) { + contentType = "image"; + } else { + const t = preview.trim(); + const tLower = t.toLowerCase(); + if (/^#([a-f0-9]{3}|[a-f0-9]{6}|[a-f0-9]{8})$/.test(tLower)) { + contentType = "color"; + } else if (/^https?:\/\//i.test(t)) { + contentType = "link"; + } else if (/^(\/|~\/|file:\/\/)/i.test(t) && !t.startsWith('//') && !t.includes('\n')) { + contentType = "file"; + } else if ((t.includes('{') && t.includes('}') && (t.includes(';') || t.includes('='))) || t.includes('') || t.includes('=>') || t.includes('===') || t.includes('!==') || t.includes('::') || t.includes('->') || + /^(?:const|let|var|function|class|struct|interface|type|enum|import|export|func|fn|pub|def|using|namespace|property|public|private|protected)\b/i.test(t) || /^(?:#include|#define|#\[|@|\/\/|\/\*|<\?| { + if (item.isImage) + return true; + const p = item.preview; + // Skip UTF-16 encoded text (has null bytes between chars), chromium browser artifact + const nullCount = (p.match(/\x00/g) || []).length; + if (nullCount > p.length * 0.2) + return false; + // Skip browser-generated HTML wrapper, firefox + if (p.toLowerCase().startsWith(" root._imageDataMaxEntries) { + const evicted = root._imageDataInsertOrder.shift(); + delete root.imageDataById[evicted]; + } + root.revision += 1; + } + root._b64CurrentCb = null; + root._b64CurrentMime = ""; + root._b64CurrentId = ""; + Qt.callLater(root._startNextB64); + } + } + + // Text watcher - stores to cliphist and triggers content capture + Process { + id: watchText + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + if (root.autoWatch && root.watchersStarted && Settings.data.appLauncher.clipboardWatchTextCommand.trim() !== "") { + watchTextRestartTimer.restart(); + } + } + } + + Timer { + id: watchTextRestartTimer + interval: 1000 + repeat: false + onTriggered: { + if (root.autoWatch && root.watchersStarted) + watchText.running = true; + } + } + + // Image watcher + Process { + id: watchImage + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + if (root.autoWatch && root.watchersStarted && Settings.data.appLauncher.clipboardWatchImageCommand.trim() !== "") { + watchImageRestartTimer.restart(); + } + } + } + + Timer { + id: watchImageRestartTimer + interval: 1000 + repeat: false + onTriggered: { + if (root.autoWatch && root.watchersStarted) + watchImage.running = true; + } + } + + // Capture current clipboard text when needed + Process { + id: captureTextProc + stdout: StdioCollector {} + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + const content = String(stdout.text); + if (content.length > 0) { + root._latestTextContent = content; + // Associate with newest item if we have one + if (root.items.length > 0 && !root.items[0].isImage) { + const newestId = root.items[0].id; + if (!root.contentCache[newestId]) { + root.contentCache[newestId] = content; + root.revision++; + } + } + } + } + } + } + + function startWatchers() { + if (!root.active || !autoWatch || watchersStarted || !root.cliphistAvailable) + return; + watchersStarted = true; + + // Text watcher + watchText.command = ["sh", "-c", Settings.data.appLauncher.clipboardWatchTextCommand]; + watchText.running = true; + + // Image watcher + watchImage.command = ["sh", "-c", Settings.data.appLauncher.clipboardWatchImageCommand]; + watchImage.running = true; + } + + function stopWatchers() { + if (!watchersStarted) + return; + watchText.running = false; + watchImage.running = false; + watchersStarted = false; + } + + // Capture current clipboard text and cache it + function captureCurrentClipboard() { + if (captureTextProc.running) + return; + captureTextProc.command = ["wl-paste", "--no-newline"]; + captureTextProc.running = true; + } + + function list(maxPreviewWidth) { + if (!root.active || !root.cliphistAvailable) { + return; + } + if (listProc.running) + return; + loading = true; + const width = maxPreviewWidth || 100; + listProc.command = ["cliphist", "list", "-preview-width", String(width)]; + listProc.running = true; + } + + // Get content for an ID - uses cache first, falls back to cliphist decode + function getContent(id) { + if (root.contentCache[id]) { + return root.contentCache[id]; + } + return null; + } + + // Async decode - checks cache first, then falls back to cliphist + function decode(id, cb) { + if (!root.cliphistAvailable) { + if (cb) + cb(""); + return; + } + + // Check cache first + const cached = root.contentCache[id]; + if (cached) { + if (cb) + cb(cached); + return; + } + + // Fall back to cliphist decode + if (decodeProc.running) { + decodeProc.running = false; + } + root._decodeRequestId++; + decodeProc.requestId = root._decodeRequestId; + root._decodeCallback = function (content) { + // Cache the result if successful + if (content && content.trim()) { + root.contentCache[id] = content; + } + if (cb) + cb(content); + }; + const idStr = String(id); + decodeProc.command = ["cliphist", "decode", idStr]; + decodeProc.running = true; + } + + function decodeToDataUrl(id, mime, cb) { + if (!root.cliphistAvailable) { + if (cb) + cb(""); + return; + } + // If cached, return immediately + if (root.imageDataById[id]) { + if (cb) + cb(root.imageDataById[id]); + return; + } + // Queue request; ensures single process handles sequentially + root._b64Queue.push({ + "id": id, + "mime": mime || "image/*", + "cb": cb + }); + if (!decodeB64Proc.running && root._b64CurrentCb === null) { + _startNextB64(); + } + } + + function getImageData(id) { + if (id === undefined) { + return null; + } + return root.imageDataById[id]; + } + + function _startNextB64() { + if (root._b64Queue.length === 0 || !root.cliphistAvailable) + return; + const job = root._b64Queue.shift(); + root._b64CurrentCb = job.cb; + root._b64CurrentMime = job.mime; + root._b64CurrentId = job.id; + decodeB64Proc.command = ["sh", "-c", `cliphist decode ${job.id} | base64 -w 0`]; + decodeB64Proc.running = true; + } + + function copyToClipboard(id) { + if (!root.cliphistAvailable) { + return; + } + copyProc.command = ["sh", "-c", `cliphist decode ${id} | wl-copy`]; + copyProc.running = true; + } + + function pasteFromClipboard(id, mime) { + if (!root.cliphistAvailable) { + return; + } + const isImage = mime && mime.startsWith("image/"); + const typeArg = isImage ? ` --type ${mime}` : ""; + const pasteKeys = isImage ? "wtype -M ctrl -k v" : "wtype -M ctrl -M shift v"; + const cmd = `cliphist decode ${id} | wl-copy${typeArg} && ${pasteKeys}`; + pasteProc.command = ["sh", "-c", cmd]; + pasteProc.running = true; + } + + function pasteText(text) { + if (!text) + return; + const escaped = text.replace(/'/g, "'\\''"); + const cmd = `printf '%s' '${escaped}' | wl-copy && wtype -M ctrl -M shift v`; + pasteProc.command = ["sh", "-c", cmd]; + pasteProc.running = true; + } + + function deleteById(id) { + if (!root.cliphistAvailable) { + return; + } + if (deleteProc.running) { + return; + } + const idStr = String(id).trim(); + // Remove from caches + delete root.contentCache[idStr]; + delete root.imageDataById[idStr]; + const orderIdx = root._imageDataInsertOrder.indexOf(idStr); + if (orderIdx !== -1) + root._imageDataInsertOrder.splice(orderIdx, 1); + deleteProc.command = ["sh", "-c", `echo ${idStr} | cliphist delete`]; + deleteProc.running = true; + } + + function wipeAll() { + if (!root.cliphistAvailable) { + return; + } + // Clear caches + root.contentCache = {}; + root.imageDataById = {}; + root._imageDataInsertOrder = []; + root._latestTextContent = ""; + root._latestTextId = ""; + + Quickshell.execDetached(["cliphist", "wipe"]); + revision++; + Qt.callLater(() => list()); + } + + // Parse image metadata from cliphist preview string + function parseImageMeta(preview) { + const re = /\[\[\s*binary data\s+([\d\.]+\s*(?:KiB|MiB|GiB|B))\s+(\w+)\s+(\d+)x(\d+)\s*\]\]/i; + const match = (preview || "").match(re); + if (!match) + return null; + return { + "size": match[1], + "fmt": (match[2] || "").toUpperCase(), + "w": Number(match[3]), + "h": Number(match[4]) + }; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Keyboard/EmojiService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Keyboard/EmojiService.qml new file mode 100644 index 0000000..ef818d2 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Keyboard/EmojiService.qml @@ -0,0 +1,314 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +// Manages emoji data loading, searching, and clipboard operations +Singleton { + id: root + + property var emojis: [] + property bool loaded: false + + // Usage tracking for popular emojis + // Format: { "emoji": { count: number, lastUsed: timestamp } } + property var usageCounts: ({}) + + // File path for persisting usage data + readonly property string usageFilePath: Settings.cacheDir + "emoji_usage.json" + + // Searches emojis based on query + function search(query) { + if (!loaded) { + return []; + } + + if (!query || query.trim() === "") { + return emojis; + } + + const terms = query.toLowerCase().split(" ").filter(t => t); + const results = emojis.filter(emoji => { + for (let term of terms) { + const emojiMatch = emoji.emoji.toLowerCase().includes(term); + const nameMatch = emoji.name.toLowerCase().includes(term); + const keywordMatch = emoji.keywords.some(kw => kw.toLowerCase().includes(term)); + const categoryMatch = emoji.category.toLowerCase().includes(term); + + if (!emojiMatch && !nameMatch && !keywordMatch && !categoryMatch) { + return false; + } + } + return true; + }); + + return results; + } + + function _getPopularEmojis(limit) { + var emojisWithUsage = emojis.map(function (emoji) { + const usageData = usageCounts[emoji.emoji]; + return { + emoji: emoji, + usageCount: usageData ? (usageData.count || 0) : 0, + lastUsed: usageData ? (usageData.lastUsed || 0) : 0 + }; + }).filter(function (item) { + return item.usageCount > 0; + }); + + // Sort by last used timestamp (descending - most recent first) + // Then by usage count if timestamps are equal + emojisWithUsage.sort(function (a, b) { + if (b.lastUsed !== a.lastUsed) { + return b.lastUsed - a.lastUsed; + } + if (b.usageCount !== a.usageCount) { + return b.usageCount - a.usageCount; + } + return a.emoji.name.localeCompare(b.emoji.name); + }); + + // Return the emoji objects limited by the specified count + return emojisWithUsage.slice(0, limit).map(function (item) { + return item.emoji; + }); + } + + function getCategoriesWithCounts() { + if (!loaded) { + return []; + } + + var categoryCounts = {}; + + for (var i = 0; i < emojis.length; i++) { + var emoji = emojis[i]; + var category = emoji.category || "other"; + if (!categoryCounts[category]) { + categoryCounts[category] = 0; + } + categoryCounts[category]++; + } + + var categories = []; + for (var cat in categoryCounts) { + categories.push({ + name: cat, + count: categoryCounts[cat] + }); + } + + return categories; + } + + function getEmojisByCategory(category) { + if (!loaded) { + return []; + } + + if (category === "recent") { + return _getPopularEmojis(25); + } + + return emojis.filter(function (emoji) { + return emoji.category === category; + }); + } + + // Record emoji usage + function recordUsage(emojiChar) { + if (emojiChar) { + const currentData = usageCounts[emojiChar] || { + count: 0, + lastUsed: 0 + }; + usageCounts[emojiChar] = { + count: currentData.count + 1, + lastUsed: Date.now() + }; + _saveUsageData(); + } + } + + // Ensure usage file exists with default content + function _ensureUsageFileExists() { + Quickshell.execDetached(["sh", "-c", `mkdir -p "$(dirname "${root.usageFilePath}")" && echo '{}' > "${root.usageFilePath}"`]); + } + + // File paths + readonly property string userEmojiPath: Settings.configDir + "emoji.json" + readonly property string builtinEmojiPath: `${Quickshell.shellDir}/Assets/Launcher/emoji.json` + + // Internal data + property var _userEmojiData: [] + property var _builtinEmojiData: [] + property int _pendingLoads: 0 + + // Initialize on component completion + Component.onCompleted: { + _loadUsageData(); + _loadEmojis(); + } + + // File loaders + FileView { + id: userEmojiFile + path: root.userEmojiPath + printErrors: false + watchChanges: false + + onLoaded: { + try { + const content = text(); + if (content) { + const parsed = JSON.parse(content); + _userEmojiData = Array.isArray(parsed) ? parsed : []; + } else { + _userEmojiData = []; + } + } catch (e) { + _userEmojiData = []; + } + _onLoadComplete(); + } + + onLoadFailed: function (error) { + _userEmojiData = []; + _onLoadComplete(); + } + } + + FileView { + id: builtinEmojiFile + path: root.builtinEmojiPath + printErrors: false + watchChanges: false + + onLoaded: { + try { + const content = text(); + if (content) { + const parsed = JSON.parse(content); + _builtinEmojiData = Array.isArray(parsed) ? parsed : []; + } else { + _builtinEmojiData = []; + } + } catch (e) { + _builtinEmojiData = []; + } + _onLoadComplete(); + } + + onLoadFailed: function (error) { + _builtinEmojiData = []; + _onLoadComplete(); + } + } + + // Load emoji files + function _loadEmojis() { + _pendingLoads = 2; + userEmojiFile.reload(); + builtinEmojiFile.reload(); + } + + // Called when one file finishes loading + function _onLoadComplete() { + _pendingLoads--; + if (_pendingLoads <= 0) { + _finalizeEmojis(); + } + } + + // Merge and deduplicate emojis + function _finalizeEmojis() { + const emojiMap = new Map(); + + // Add built-in emojis first + for (const emoji of _builtinEmojiData) { + if (emoji && emoji.emoji) { + emojiMap.set(emoji.emoji, emoji); + } + } + + // Add user emojis (override built-ins if duplicate) + for (const emoji of _userEmojiData) { + if (emoji && emoji.emoji) { + emojiMap.set(emoji.emoji, emoji); + } + } + + emojis = Array.from(emojiMap.values()); + loaded = true; + } + + // FileView for usage data + FileView { + id: usageFile + path: root.usageFilePath + printErrors: false + watchChanges: false + + onLoaded: { + try { + const content = text(); + if (content && content.trim() !== "") { + const parsed = JSON.parse(content); + if (parsed && typeof parsed === 'object') { + root.usageCounts = parsed; + } else { + root.usageCounts = {}; + } + } else { + root.usageCounts = {}; + } + } catch (e) { + root.usageCounts = {}; + } + } + + onLoadFailed: function (error) { + root.usageCounts = {}; + Qt.callLater(_ensureUsageFileExists); + } + } + + // Timer for debouncing usage data saves + Timer { + id: saveTimer + interval: 1000 + repeat: false + onTriggered: _doSaveUsageData() + } + + // Load usage data + function _loadUsageData() { + usageFile.reload(); + } + + // Save usage data with debounce + function _saveUsageData() { + saveTimer.restart(); + } + + // Actually save usage data to file + function _doSaveUsageData() { + try { + const content = JSON.stringify(root.usageCounts); + Quickshell.execDetached(["sh", "-c", `mkdir -p "$(dirname "${root.usageFilePath}")" && echo '${content}' > "${root.usageFilePath}"`]); + } catch (e) { + Logger.e("EmojiService", "Failed to save usage data: " + e.message); + } + } + + // Copies emoji to clipboard + function copy(emojiChar) { + if (emojiChar) { + recordUsage(emojiChar); // Record usage before copying + Quickshell.execDetached(["sh", "-c", `echo -n "${emojiChar}" | wl-copy`]); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Keyboard/KeyboardLayoutService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Keyboard/KeyboardLayoutService.qml new file mode 100644 index 0000000..f6e455f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Keyboard/KeyboardLayoutService.qml @@ -0,0 +1,319 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.Compositor +import qs.Services.UI + +Singleton { + id: root + property string currentLayout: I18n.tr("common.unknown") + property string fullLayoutName: I18n.tr("common.unknown") + property string previousLayout: "" + property bool isInitialized: false + + // Updates current layout from various format strings. Called by compositors + function setCurrentLayout(layoutString) { + root.fullLayoutName = layoutString || I18n.tr("common.unknown"); + root.currentLayout = extractLayoutCode(layoutString); + } + + // Extract layout code from various format strings + // Priority: variant > country code > language lookup > fallback + function extractLayoutCode(layoutString) { + if (!layoutString) + return I18n.tr("common.unknown"); + + const str = layoutString.toLowerCase(); + + // If it's already a short code (2-3 chars), return uppercase + if (/^[a-z]{2,3}(\+.*)?$/.test(str)) { + return str.split('+')[0].toUpperCase(); + } + + // Check for layout variants first - these are more meaningful than country codes + // when distinguishing between multiple layouts of the same language + for (const [pattern, display] of Object.entries(variantMap)) { + if (str.includes(pattern)) { + return display; + } + } + + // Extract short code from parentheses like "English (US)" + const shortCodeMatch = str.match(/\(([a-z]{2,3})\)/i); + if (shortCodeMatch) { + return shortCodeMatch[1].toUpperCase(); + } + + // Check for language/country names in the language map + // First, try to match at the start of the string (the primary language) + for (const [lang, code] of Object.entries(languageMap)) { + if (str.startsWith(lang)) { + return code.toUpperCase(); + } + } + // Then try word boundary matching anywhere in the string + for (const [lang, code] of Object.entries(languageMap)) { + const regex = new RegExp(`\\b${lang}\\b`); + if (regex.test(str)) { + return code.toUpperCase(); + } + } + + // If nothing matches, try first 2-3 characters if they look like a code + const codeMatch = str.match(/^([a-z]{2,3})/); + return codeMatch ? codeMatch[1].toUpperCase() : I18n.tr("common.unknown"); + } + + // Watch for layout changes and show toast + onCurrentLayoutChanged: { + // Update previousLayout after checking for changes + const layoutChanged = isInitialized && currentLayout !== previousLayout && currentLayout !== I18n.tr("common.unknown") && previousLayout !== "" && previousLayout !== I18n.tr("common.unknown"); + + if (layoutChanged) { + if (Settings.data.notifications.enableKeyboardLayoutToast) { + const message = I18n.tr("toast.keyboard-layout.changed", { + "layout": fullLayoutName + }); + ToastService.showNotice(I18n.tr("toast.keyboard-layout.title"), message, "", 2000); + } + Logger.d("KeyboardLayout", "Layout changed from", previousLayout, "to", currentLayout); + } + + // Update previousLayout for next comparison + previousLayout = currentLayout; + } + + Component.onCompleted: { + Logger.i("KeyboardLayout", "Service started"); + // Mark as initialized after a delay to allow first layout update to complete + // This prevents showing a toast on the initial load + initializationTimer.start(); + } + + Timer { + id: initializationTimer + interval: 2000 // Wait 2 seconds for first layout update to complete + onTriggered: { + isInitialized = true; + // Set previousLayout to current value after initialization + previousLayout = currentLayout; + Logger.d("KeyboardLayout", "Service initialized, current layout:", currentLayout); + } + } + + // Layout variants - checked BEFORE country codes + // These display the variant name when it's more meaningful than the country + property var variantMap: { + // Alternative keyboard layouts + "colemak": "Colemak", + "dvorak": "Dvorak", + "workman": "Workman", + "programmer dvorak": "Dvk-P", + "norman": "Norman", + // International variants + "intl": "Intl", + "international": "Intl", + "altgr-intl": "Intl", + "with dead keys": "Dead", + // Common variants + "phonetic": "Phon", + "extended": "Ext", + "ergonomic": "Ergo", + "legacy": "Legacy", + // Input methods + "pinyin": "Pinyin", + "cangjie": "Cangjie", + "romaji": "Romaji", + "kana": "Kana" + } + + // Language/country name to ISO code mapping + property var languageMap: { + // English variants + "english": "us", + "american": "us", + "united states": "us", + "us english": "us", + "british": "gb", + "united kingdom": "gb", + "english (uk)": "gb", + "canadian": "ca", + "canada": "ca", + "canadian english": "ca", + "australian": "au", + "australia": "au", + // Nordic countries + "swedish": "se", + "svenska": "se", + "sweden": "se", + "norwegian": "no", + "norsk": "no", + "norway": "no", + "danish": "dk", + "dansk": "dk", + "denmark": "dk", + "finnish": "fi", + "suomi": "fi", + "finland": "fi", + "icelandic": "is", + "íslenska": "is", + "iceland": "is", + // Western/Central European Germanic + "german": "de", + "deutsch": "de", + "germany": "de", + "austrian": "at", + "austria": "at", + "österreich": "at", + "swiss": "ch", + "switzerland": "ch", + "schweiz": "ch", + "suisse": "ch", + "dutch": "nl", + "nederlands": "nl", + "netherlands": "nl", + "holland": "nl", + "belgian": "be", + "belgium": "be", + "belgië": "be", + "belgique": "be", + // Romance languages (Western/Southern Europe) + "french": "fr", + "français": "fr", + "france": "fr", + "canadian french": "ca", + "spanish": "es", + "español": "es", + "spain": "es", + "castilian": "es", + "italian": "it", + "italiano": "it", + "italy": "it", + "portuguese": "pt", + "português": "pt", + "portugal": "pt", + "catalan": "ad", + "català": "ad", + "andorra": "ad", + // Eastern European Romance + "romanian": "ro", + "română": "ro", + "romania": "ro", + // Slavic languages (Eastern Europe) + "russian": "ru", + "русский": "ru", + "russia": "ru", + "polish": "pl", + "polski": "pl", + "poland": "pl", + "czech": "cz", + "čeština": "cz", + "czech republic": "cz", + "slovak": "sk", + "slovenčina": "sk", + "slovakia": "sk", + // Ukrainian + "ukraine": "ua", + "ukrainian": "ua", + "українська": "ua", + "bulgarian": "bg", + "български": "bg", + "bulgaria": "bg", + "serbian": "rs", + "srpski": "rs", + "serbia": "rs", + "croatian": "hr", + "hrvatski": "hr", + "croatia": "hr", + "slovenian": "si", + "slovenščina": "si", + "slovenia": "si", + "bosnian": "ba", + "bosanski": "ba", + "bosnia": "ba", + "macedonian": "mk", + "македонски": "mk", + "macedonia": "mk", + // Celtic languages (Western Europe) + "irish": "ie", + "gaeilge": "ie", + "ireland": "ie", + "welsh": "gb", + "cymraeg": "gb", + "wales": "gb", + "scottish": "gb", + "gàidhlig": "gb", + "scotland": "gb", + // Baltic languages (Northern Europe) + "estonian": "ee", + "eesti": "ee", + "estonia": "ee", + "latvian": "lv", + "latviešu": "lv", + "latvia": "lv", + "lithuanian": "lt", + "lietuvių": "lt", + "lithuania": "lt", + // Other European languages + "hungarian": "hu", + "magyar": "hu", + "hungary": "hu", + "greek": "gr", + "ελληνικά": "gr", + "greece": "gr", + "albanian": "al", + "shqip": "al", + "albania": "al", + "maltese": "mt", + "malti": "mt", + "malta": "mt", + // West/Southwest Asian languages + "turkish": "tr", + "türkçe": "tr", + "turkey": "tr", + "arabic": "ar", + "العربية": "ar", + "arab": "ar", + "hebrew": "il", + "עברית": "il", + "israel": "il", + // South American languages + "brazilian": "br", + "brazilian portuguese": "br", + "brasil": "br", + "brazil": "br", + // East Asian languages + "japanese": "jp", + "日本語": "jp", + "japan": "jp", + "korean": "kr", + "한국어": "kr", + "korea": "kr", + "south korea": "kr", + "chinese": "cn", + "中文": "cn", + "china": "cn", + "simplified chinese": "cn", + "traditional chinese": "tw", + "taiwan": "tw", + "繁體中文": "tw", + // Southeast Asian languages + "thai": "th", + "ไทย": "th", + "thailand": "th", + "vietnamese": "vn", + "tiếng việt": "vn", + "vietnam": "vn", + // South Asian languages + "hindi": "in", + "हिन्दी": "in", + "india": "in", + // African languages + "afrikaans": "za", + "south africa": "za", + "south african": "za" + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Keyboard/LockKeysService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Keyboard/LockKeysService.qml new file mode 100644 index 0000000..463c2ed --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Keyboard/LockKeysService.qml @@ -0,0 +1,151 @@ +pragma Singleton +import Qt.labs.folderlistmodel 2.10 +import QtQml.Models + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +Singleton { + id: root + + // Component registration - only poll when something needs lock key state + function registerComponent(componentId) { + root._registered[componentId] = true; + root._registered = Object.assign({}, root._registered); + Logger.d("LockKeys", "Component registered:", componentId, "- total:", root._registeredCount); + } + + function unregisterComponent(componentId) { + delete root._registered[componentId]; + root._registered = Object.assign({}, root._registered); + Logger.d("LockKeys", "Component unregistered:", componentId, "- total:", root._registeredCount); + } + + property var _registered: ({}) + readonly property int _registeredCount: Object.keys(_registered).length + readonly property bool shouldRun: _registeredCount > 0 + + property bool capsLockOn: false + property bool numLockOn: false + property bool scrollLockOn: false + + signal capsLockChanged(bool active) + signal numLockChanged(bool active) + signal scrollLockChanged(bool active) + + Instantiator { + model: FolderListModel { + id: folderModel + folder: Qt.resolvedUrl("/sys/class/leds") + showFiles: false + showOnlyReadable: true + } + delegate: Component { + FileView { + id: fileView + path: filePath + "/brightness" + // sysfs brightness can fail transiently (e.g. resume); omit console spam like other sysfs FileViews. + printErrors: false + watchChanges: false + + function parseBrightnessLedOn(raw) { + var t = raw.trim(); + if (t === "" || !/^[0-9]+$/.test(t)) + return null; + return parseInt(t, 10) !== 0; + } + + function applyLockState(kind, state, emitIfChanged) { + switch (kind) { + case "numlock": + if (emitIfChanged && root.numLockOn !== state) { + root.numLockOn = state; + root.numLockChanged(state); + Logger.i("LockKeysService", "Num Lock:", state, fileView.path); + } else if (!emitIfChanged) { + root.numLockOn = state; + } + break; + case "capslock": + if (emitIfChanged && root.capsLockOn !== state) { + root.capsLockOn = state; + root.capsLockChanged(state); + Logger.i("LockKeysService", "Caps Lock:", state, fileView.path); + } else if (!emitIfChanged) { + root.capsLockOn = state; + } + break; + case "scrolllock": + if (emitIfChanged && root.scrollLockOn !== state) { + root.scrollLockOn = state; + root.scrollLockChanged(state); + Logger.i("LockKeysService", "Scroll Lock:", state, fileView.path); + } else if (!emitIfChanged) { + root.scrollLockOn = state; + } + break; + } + } + + // Only apply after a successful read — failed reloads must not update UI (empty text is not "off"). + onLoaded: { + if (!fileView.isWanted) + return; + var state = fileView.parseBrightnessLedOn(fileView.text()); + if (state === null) + return; + + var kind = fileName.split("::")[1]; + + // First read after polling starts: sync bar/UI from sysfs without firing + // *Changed signals (OSD listens to those and would flash on startup). + if (!fileView.initialCheckDone) { + fileView.initialCheckDone = true; + fileView.applyLockState(kind, state, false); + return; + } + + fileView.applyLockState(kind, state, true); + } + + // FolderListModel only provides filters for file names, not folders + property bool isWanted: { + if (fileName.startsWith("input") && fileName.includes("::")) { + switch (fileName.split("::")[1]) { + case "numlock": + case "capslock": + case "scrolllock": + return true; + } + } + return false; + } + + // After shouldRun becomes true, first brightness read updates properties only (no *Changed signals). + property bool initialCheckDone: false + property variant connections: Connections { + target: root + function onShouldRunChanged() { + if (root.shouldRun) { + fileView.initialCheckDone = false; + } + } + } + + // sysfs does not provide change notifications + property variant refreshTimer: Timer { + interval: 200 + running: root.shouldRun && fileView.isWanted + repeat: true + onTriggered: fileView.reload() + } + } + } + } + + Component.onCompleted: { + Logger.i("LockKeysService", "Service started (polling deferred until a consumer registers)."); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Location/Calendar/EvolutionDataServer.qml b/fedora/.config/quickshell/noctalia-shell/Services/Location/Calendar/EvolutionDataServer.qml new file mode 100644 index 0000000..830a243 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Location/Calendar/EvolutionDataServer.qml @@ -0,0 +1,148 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.Location + +Singleton { + id: root + + // Python scripts + readonly property string checkCalendarAvailableScript: Quickshell.shellDir + '/Scripts/python/src/calendar/check-calendar.py' + readonly property string listCalendarsScript: Quickshell.shellDir + '/Scripts/python/src/calendar/list-calendars.py' + readonly property string calendarEventsScript: Quickshell.shellDir + '/Scripts/python/src/calendar/calendar-events.py' + + function init() { + availabilityCheckProcess.running = true; + } + function loadCalendars() { + listCalendarsProcess.running = true; + } + function loadEvents(daysAhead = 31, daysBehind = 14) { + CalendarService.loading = true; + CalendarService.lastError = ""; + + const now = new Date(); + const startDate = new Date(now.getTime() - (daysBehind * 24 * 60 * 60 * 1000)); + const endDate = new Date(now.getTime() + (daysAhead * 24 * 60 * 60 * 1000)); + + loadEventsProcess.startTime = Math.floor(startDate.getTime() / 1000); + loadEventsProcess.endTime = Math.floor(endDate.getTime() / 1000); + loadEventsProcess.running = true; + + Logger.d("Calendar", `Loading events (${daysBehind} days behind, ${daysAhead} days ahead): ${startDate.toLocaleDateString()} to ${endDate.toLocaleDateString()}`); + } + + // Process to check for evolution-data-server libraries + Process { + id: availabilityCheckProcess + running: false + command: ["sh", "-c", "command -v python3 >/dev/null 2>&1 && python3 " + root.checkCalendarAvailableScript + " || echo 'unavailable: python3 not installed'"] + + stdout: StdioCollector { + onStreamFinished: { + const result = text.trim(); + if (result === "available") { + CalendarService.dataProvider = CalendarService.dataProvider || root; + CalendarService.available = true; + } + + if (CalendarService.available) { + Logger.i("Calendar", "EDS libraries available"); + loadCalendars(); + } else { + Logger.w("Calendar", "EDS libraries not available: " + result); + CalendarService.lastError = "Evolution Data Server libraries not installed"; + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.d("Calendar", "Availability check error: " + text); + CalendarService.lastError = "Failed to check library availability"; + } + } + } + } + + // Process to list available calendars + Process { + id: listCalendarsProcess + running: false + command: ["python3", root.listCalendarsScript] + + stdout: StdioCollector { + onStreamFinished: { + try { + const result = JSON.parse(text.trim()); + CalendarService.setCalendars(result); + Logger.d("Calendar", `Found ${result.length} calendar(s)`); + + // Auto-load events after discovering calendars + if (result.length > 0) { + loadEvents(); + } + } catch (e) { + Logger.d("Calendar", "Failed to parse calendars: " + e); + CalendarService.lastError = "Failed to parse calendar list"; + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.d("Calendar", "List calendars error: " + text); + CalendarService.lastError = text.trim(); + } + } + } + } + + // Process to load events + Process { + id: loadEventsProcess + running: false + property int startTime: 0 + property int endTime: 0 + + command: ["python3", root.calendarEventsScript, startTime.toString(), endTime.toString()] + + stdout: StdioCollector { + onStreamFinished: { + CalendarService.loading = false; + + try { + const events = JSON.parse(text.trim()); + CalendarService.setEvents(events); + Logger.d("Calendar", `Loaded ${events.length} events(s)`); + } catch (e) { + Logger.d("Calendar", "Failed to parse events: " + e); + CalendarService.lastError = "Failed to parse events"; + // Fall back to cached events + CalendarService.loadCachedEvents(); + Logger.d("Calendar", "Using cached events"); + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + CalendarService.loading = false; + + if (text.trim()) { + Logger.d("Calendar", "Load events error: " + text); + CalendarService.lastError = text.trim(); + + // Fall back to cached events + CalendarService.loadCachedEvents(); + Logger.d("Calendar", "Using cached events"); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Location/Calendar/Khal.qml b/fedora/.config/quickshell/noctalia-shell/Services/Location/Calendar/Khal.qml new file mode 100644 index 0000000..2e14e23 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Location/Calendar/Khal.qml @@ -0,0 +1,182 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.Location + +Singleton { + id: root + + readonly property string khalEventsScript: Quickshell.shellDir + '/Scripts/python/src/calendar/khal-events.py' + + function init() { + availabilityCheckProcess.running = true; + } + function loadCalendars() { + listCalendarsProcess.running = true; + } + function loadEvents(daysAhead = 31, daysBehind = 14) { + CalendarService.loading = true; + CalendarService.lastError = ""; + + const now = new Date(); + const startDate = new Date(now.getTime() - (daysBehind * 24 * 60 * 60 * 1000)); + + loadEventsProcess.startTime = formatDateYMD(startDate); + loadEventsProcess.duration = `${daysAhead + daysBehind}d`; + loadEventsProcess.running = true; + + Logger.d("Calendar", `Loading events (${daysBehind} days behind, ${daysAhead} days ahead): ${loadEventsProcess.startTime} ${loadEventsProcess.duration}`); + } + + // Process to check for khal installation + Process { + id: availabilityCheckProcess + running: false + command: ["sh", "-c", "command -v khal >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1"] + onExited: function (exitCode) { + if (exitCode === 0) { + CalendarService.available = true; + CalendarService.dataProvider = CalendarService.dataProvider || root; + } + + if (CalendarService.available) { + Logger.i("Calendar", "Khal available"); + loadCalendars(); + } else { + Logger.w("Calendar", "Khal not available"); + CalendarService.lastError = "khal binary not installed"; + } + } + } + + // Process to list available calendars + Process { + id: listCalendarsProcess + running: false + command: ["khal", "printcalendars"] + + stdout: StdioCollector { + onStreamFinished: { + try { + const calendars = text.trim().split("\n"); + CalendarService.setCalendars(calendars); + + Logger.d("Calendar", `Found ${calendars.length} calendar(s)`); + + // Auto-load events after discovering calendars + if (calendars.length > 0) { + loadEvents(); + } + } catch (e) { + Logger.d("Calendar", "Failed to parse calendars: " + e); + CalendarService.lastError = "Failed to parse calendar list"; + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.d("Calendar", "List calendars error: " + text); + CalendarService.lastError = text.trim(); + } + } + } + } + + // Process to load events + Process { + id: loadEventsProcess + running: false + property string startTime: "" + property string duration: "" + + command: ["python3", root.khalEventsScript, startTime, duration] + + stdout: StdioCollector { + onStreamFinished: { + CalendarService.loading = false; + + try { + const events = parseEvents(text.trim()); + CalendarService.setEvents(events); + Logger.d("Calendar", `Loaded ${events.length} events(s)`); + } catch (e) { + Logger.d("Calendar", "Failed to parse events: " + e); + CalendarService.lastError = "Failed to parse events"; + // Fall back to cached events + CalendarService.loadCachedEvents(); + Logger.d("Calendar", "Using cached events"); + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + CalendarService.loading = false; + + if (text.trim()) { + Logger.d("Calendar", "Load events error: " + text); + CalendarService.lastError = text.trim(); + // Fall back to cached events + CalendarService.loadCachedEvents(); + Logger.d("Calendar", "Using cached events"); + } + } + } + } + + function parseEvents(text) { + const result = []; + const duplicates = new Set(); + + for (const line of text.split("\n")) { + if (!line.trim()) + continue; + const dayEvents = JSON.parse(line); + for (const event of dayEvents) { + if (event["repeat-pattern"] !== "") { + // if there is a repeat pattern, the event must be included each time + result.push({ + uid: event.uid, + calendar: event.calendar, + summary: event.title, + start: parseTimestamp(event["start-long-full"]), + end: parseTimestamp(event["end-long-full"]), + location: event.location, + description: event.description + }); + } else if (!duplicates.has(event.uid)) { + // in any other cases, we must remove duplicates using the uid of the event + result.push({ + uid: event.uid, + calendar: event.calendar, + summary: event.title, + start: parseTimestamp(event["start-long-full"]), + end: parseTimestamp(event["end-long-full"]), + location: event.location, + description: event.description + }); + duplicates.add(event.uid); + } + } + } + + return result; + } + + function parseTimestamp(timeStr) { + // expects ISO8601 format + return Math.floor((Date.parse(timeStr)).valueOf() / 1000); + } + + function formatDateYMD(date) { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, "0"); + const d = String(date.getDate()).padStart(2, "0"); + return y + "-" + m + "-" + d; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Location/CalendarService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Location/CalendarService.qml new file mode 100644 index 0000000..7ef3b5f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Location/CalendarService.qml @@ -0,0 +1,136 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.Location.Calendar +import qs.Services.System + +Singleton { + id: root + + // Core state + property var events: ([]) + property bool loading: false + property bool available: false + property string lastError: "" + property var calendars: ([]) + + property var dataProvider: null + + // Persistent cache + property string cacheFile: Settings.cacheDir + "calendar.json" + + // Cache file handling + FileView { + id: cacheFileView + path: root.cacheFile + printErrors: false + + JsonAdapter { + id: cacheAdapter + property var cachedEvents: ([]) + property var cachedCalendars: ([]) + property string lastUpdate: "" + } + + onLoadFailed: { + cacheAdapter.cachedEvents = ([]); + cacheAdapter.cachedCalendars = ([]); + cacheAdapter.lastUpdate = ""; + } + + onLoaded: { + loadFromCache(); + } + } + + Component.onCompleted: { + Logger.i("Calendar", "Service started"); + loadFromCache(); + checkAvailability(); + } + + // Save cache with debounce + Timer { + id: saveDebounce + interval: 1000 + onTriggered: cacheFileView.writeAdapter() + } + + function setEvents(newEvents) { + root.events = newEvents; + cacheAdapter.cachedEvents = newEvents; + cacheAdapter.lastUpdate = new Date().toISOString(); + saveCache(); + } + + function setCalendars(newCalendars) { + root.calendars = newCalendars; + cacheAdapter.cachedCalendars = newCalendars; + saveCache(); + } + + function loadCachedEvents() { + if (cacheAdapter.cachedEvents.length > 0) { + root.events = cacheAdapter.cachedEvents; + } + } + + function saveCache() { + saveDebounce.restart(); + } + + // Load events and calendars from cache + function loadFromCache() { + if (cacheAdapter.cachedEvents && cacheAdapter.cachedEvents.length > 0) { + root.events = cacheAdapter.cachedEvents; + Logger.d("Calendar", `Loaded ${cacheAdapter.cachedEvents.length} cached event(s)`); + } + + if (cacheAdapter.cachedCalendars && cacheAdapter.cachedCalendars.length > 0) { + root.calendars = cacheAdapter.cachedCalendars; + Logger.d("Calendar", `Loaded ${cacheAdapter.cachedCalendars.length} cached calendar(s)`); + } + + if (cacheAdapter.lastUpdate) { + Logger.d("Calendar", `Cache last updated: ${cacheAdapter.lastUpdate}`); + } + } + + // Auto-refresh timer (every 5 minutes) + Timer { + id: refreshTimer + interval: 300000 + running: true + repeat: true + onTriggered: loadEvents() + } + + // Core functions + function checkAvailability() { + if (!Settings.data.location.showCalendarEvents) { + root.available = false; + return; + } + + Khal.init(); + EvolutionDataServer.init(); + } + + function loadCalendars() { + if (!root.available || !dataProvider) { + return; + } + + dataProvider.loadCalendars(); + } + function loadEvents(daysAhead = 31, daysBehind = 14) { + if (!root.available || !dataProvider) { + return; + } + + dataProvider.loadEvents(daysAhead, daysBehind); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Location/DarkModeService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Location/DarkModeService.qml new file mode 100644 index 0000000..5eb9f5d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Location/DarkModeService.qml @@ -0,0 +1,195 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons + +Singleton { + id: root + + property bool initComplete: false + property bool nextDarkModeState: false + + Connections { + target: LocationService.data + enabled: Settings.data.colorSchemes.schedulingMode == "location" + function onWeatherChanged() { + if (LocationService.data.weather !== null) { + const changes = root.collectWeatherChanges(LocationService.data.weather); + if (!root.initComplete) { + root.initComplete = true; + root.applyCurrentMode(changes); + } + root.scheduleNextMode(changes); + } + } + } + + Connections { + target: Settings.data.colorSchemes + enabled: Settings.data.colorSchemes.schedulingMode == "manual" + function onManualSunriseChanged() { + const changes = root.collectManualChanges(); + root.applyCurrentMode(changes); + root.scheduleNextMode(changes); + } + function onManualSunsetChanged() { + const changes = root.collectManualChanges(); + root.applyCurrentMode(changes); + root.scheduleNextMode(changes); + } + } + + Connections { + target: Settings.data.colorSchemes + function onSchedulingModeChanged() { + root.update(); + } + } + + Connections { + target: Time + function onResumed() { + Logger.i("DarkModeService", "System resumed - re-evaluating dark mode"); + root.update(); + resumeRetryTimer.restart(); + } + } + + Timer { + id: timer + onTriggered: { + Settings.data.colorSchemes.darkMode = root.nextDarkModeState; + root.update(); + } + } + + Timer { + id: resumeRetryTimer + interval: 2000 + repeat: false + onTriggered: { + Logger.i("DarkModeService", "Resume retry - re-evaluating dark mode again"); + root.update(); + } + } + + function init() { + Logger.i("DarkModeService", "Service started"); + root.update(); + } + + function update() { + if (Settings.data.colorSchemes.schedulingMode == "manual") { + const changes = collectManualChanges(); + initComplete = true; + applyCurrentMode(changes); + scheduleNextMode(changes); + } else if (Settings.data.colorSchemes.schedulingMode == "location" && LocationService.data.weather) { + const changes = collectWeatherChanges(LocationService.data.weather); + initComplete = true; + applyCurrentMode(changes); + scheduleNextMode(changes); + } + } + + function parseTime(timeString) { + const parts = timeString.split(":").map(Number); + return { + "hour": parts[0], + "minute": parts[1] + }; + } + + function collectManualChanges() { + const sunriseTime = parseTime(Settings.data.colorSchemes.manualSunrise); + const sunsetTime = parseTime(Settings.data.colorSchemes.manualSunset); + + const now = new Date(); + const year = now.getFullYear(); + const month = now.getMonth(); + const day = now.getDate(); + + const yesterdaysSunset = new Date(year, month, day - 1, sunsetTime.hour, sunsetTime.minute); + const todaysSunrise = new Date(year, month, day, sunriseTime.hour, sunriseTime.minute); + const todaysSunset = new Date(year, month, day, sunsetTime.hour, sunsetTime.minute); + const tomorrowsSunrise = new Date(year, month, day + 1, sunriseTime.hour, sunriseTime.minute); + + return [ + { + "time": yesterdaysSunset.getTime(), + "darkMode": true + }, + { + "time": todaysSunrise.getTime(), + "darkMode": false + }, + { + "time": todaysSunset.getTime(), + "darkMode": true + }, + { + "time": tomorrowsSunrise.getTime(), + "darkMode": false + } + ]; + } + + function collectWeatherChanges(weather) { + const changes = []; + + if (Date.now() < Date.parse(weather.daily.sunrise[0])) { + // The sun has not risen yet + changes.push({ + "time": Date.now() - 1, + "darkMode": true + }); + } + + for (var i = 0; i < weather.daily.sunrise.length; i++) { + changes.push({ + "time": Date.parse(weather.daily.sunrise[i]), + "darkMode": false + }); + changes.push({ + "time": Date.parse(weather.daily.sunset[i]), + "darkMode": true + }); + } + + return changes; + } + + function applyCurrentMode(changes) { + const now = Date.now(); + Logger.i("DarkModeService", `Applying mode at ${new Date(now).toLocaleString()} (${now})`); + + // changes.findLast(change => change.time < now) // not available in QML... + let lastChange = null; + for (var i = 0; i < changes.length; i++) { + Logger.d("DarkModeService", `Checking change: time=${changes[i].time} (${new Date(changes[i].time).toLocaleString()}), darkMode=${changes[i].darkMode}`); + if (changes[i].time < now) { + lastChange = changes[i]; + } + } + + if (lastChange) { + Logger.i("DarkModeService", `Selected change: time=${lastChange.time}, darkMode=${lastChange.darkMode}`); + Settings.data.colorSchemes.darkMode = lastChange.darkMode; + Logger.d("DarkModeService", `Reset: darkmode=${lastChange.darkMode}`); + } else { + Logger.w("DarkModeService", "No suitable change found for current time!"); + } + } + + function scheduleNextMode(changes) { + const now = Date.now(); + const nextChange = changes.find(change => change.time > now); + if (nextChange) { + root.nextDarkModeState = nextChange.darkMode; + timer.interval = nextChange.time - now; + timer.restart(); + Logger.d("DarkModeService", `Scheduled: darkmode=${nextChange.darkMode} in ${timer.interval} ms`); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Location/LocationService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Location/LocationService.qml new file mode 100644 index 0000000..b48baf8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Location/LocationService.qml @@ -0,0 +1,397 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +// Location and weather service with decoupled geocoding and weather fetching. +Singleton { + id: root + + property string locationFile: Quickshell.env("NOCTALIA_WEATHER_FILE") || (Settings.cacheDir + "location.json") + property int weatherUpdateFrequency: 30 * 60 + property bool isFetchingWeather: false + + // Talia weather + readonly property int taliaMascotWeatherMonth: 3 + readonly property int taliaMascotWeatherDay: 1 + + readonly property bool taliaWeatherMascotDayActive: { + const d = Time.now; + return d.getMonth() === root.taliaMascotWeatherMonth && d.getDate() === root.taliaMascotWeatherDay; + } + + readonly property bool taliaWeatherMascotActive: taliaWeatherMascotDayActive || Settings.data.location.weatherTaliaMascotAlways + + readonly property alias data: adapter + + // True when the user has set a location name or enabled auto-locate + readonly property bool locationConfigured: Settings.data.location.name !== "" || Settings.data.location.autoLocate + + // Stable UI properties - only updated when location is successfully geocoded + property bool coordinatesReady: false + property string stableLatitude: "" + property string stableLongitude: "" + property string stableName: "" + + FileView { + id: locationFileView + path: locationFile + printErrors: false + onAdapterUpdated: saveTimer.start() + onLoaded: { + Logger.d("Location", "Loaded cached data"); + if (adapter.latitude !== "" && adapter.longitude !== "" && adapter.weatherLastFetch > 0) { + root.stableLatitude = adapter.latitude; + root.stableLongitude = adapter.longitude; + root.stableName = adapter.name; + root.coordinatesReady = true; + Logger.i("Location", "Coordinates ready"); + } + update(); + } + onLoadFailed: function (error) { + update(); + } + + JsonAdapter { + id: adapter + property string latitude: "" + property string longitude: "" + property string name: "" + property int weatherLastFetch: 0 + property var weather: null + } + } + + // Formatted coordinates for UI display + readonly property string displayCoordinates: { + if (!root.coordinatesReady || root.stableLatitude === "" || root.stableLongitude === "") { + return ""; + } + const lat = parseFloat(root.stableLatitude).toFixed(4); + const lon = parseFloat(root.stableLongitude).toFixed(4); + return `${lat}, ${lon}`; + } + + // Auto-geolocate timer - periodically updates location via IP geolocation + Timer { + id: autoLocateTimer + interval: 30 * 60 * 1000 + running: Settings.data.location.autoLocate + repeat: true + triggeredOnStart: true + onTriggered: root.geolocateAndApply() + } + + // Update timer runs when weather is enabled or location-based scheduling is active + Timer { + id: updateTimer + interval: 20 * 1000 + running: Settings.data.location.weatherEnabled || Settings.data.colorSchemes.schedulingMode == "location" + repeat: true + onTriggered: { + update(); + } + } + + Timer { + id: saveTimer + running: false + interval: 1000 + onTriggered: locationFileView.writeAdapter() + } + + function init() { + Logger.i("Location", "Service started"); + } + + function resetWeather() { + Logger.i("Location", "Resetting location and weather data"); + + root.coordinatesReady = false; + root.stableLatitude = ""; + root.stableLongitude = ""; + root.stableName = ""; + + adapter.latitude = ""; + adapter.longitude = ""; + adapter.name = ""; + adapter.weatherLastFetch = 0; + adapter.weather = null; + isFetchingWeather = false; + update(); + } + + // Main update function - geocodes location if needed, then fetches weather if enabled + function update() { + updateLocation(); + + if (Settings.data.location.weatherEnabled) { + updateWeatherData(); + } + } + + // Runs independently of weather toggle + function updateLocation() { + const locationChanged = adapter.name !== Settings.data.location.name; + const needsGeocoding = (adapter.latitude === "") || (adapter.longitude === "") || locationChanged; + + if (!needsGeocoding) { + return; + } + + if (isFetchingWeather) { + return; + } + + isFetchingWeather = true; + + if (locationChanged) { + root.coordinatesReady = false; + Logger.d("Location", "Location changed from", adapter.name, "to", Settings.data.location.name); + } + + geocodeLocation(Settings.data.location.name, function (latitude, longitude, name, country) { + adapter.name = Settings.data.location.name; + adapter.latitude = latitude.toString(); + adapter.longitude = longitude.toString(); + root.stableLatitude = adapter.latitude; + root.stableLongitude = adapter.longitude; + root.stableName = `${name}, ${country}`; + root.coordinatesReady = true; + + isFetchingWeather = false; + Logger.i("Location", `Geocoded ${Settings.data.location.name}: ${root.stableLatitude}, ${root.stableLongitude}`); + + if (locationChanged) { + adapter.weatherLastFetch = 0; + updateWeatherData(); + } + }, errorCallback); + } + + // Fetch weather data if enabled and coordinates are available + function updateWeatherData() { + if (!Settings.data.location.weatherEnabled) { + return; + } + + if (isFetchingWeather) { + return; + } + + if (adapter.latitude === "" || adapter.longitude === "") { + Logger.w("Location", "Cannot fetch weather without coordinates"); + return; + } + const needsWeatherUpdate = (adapter.weatherLastFetch === "") || (adapter.weather === null) || (Time.timestamp >= adapter.weatherLastFetch + weatherUpdateFrequency); + + if (needsWeatherUpdate) { + isFetchingWeather = true; + fetchWeatherData(adapter.latitude, adapter.longitude, errorCallback); + } + } + + // Query geocoding API to convert location name to coordinates + function geocodeLocation(locationName, callback, errorCallback) { + if (locationName === "") { + isFetchingWeather = false; + return; + } + + Logger.d("Location", "Geocoding location name"); + var geoUrl = "https://api.noctalia.dev/geocode?city=" + encodeURIComponent(locationName); + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + try { + var geoData = JSON.parse(xhr.responseText); + if (geoData.lat != null) { + callback(geoData.lat, geoData.lng, geoData.name, geoData.country); + } else { + errorCallback("Location", "could not resolve location name"); + } + } catch (e) { + errorCallback("Location", "Failed to parse geocoding data: " + e); + } + } else { + errorCallback("Location", `Geocoding error: ${xhr.status} ${xhr.responseText}`); + } + } + }; + xhr.open("GET", geoUrl); + xhr.send(); + } + + // Fetch weather data from Open-Meteo API + function fetchWeatherData(latitude, longitude, errorCallback) { + Logger.d("Location", "Fetching weather from api.open-meteo.com"); + var url = "https://api.open-meteo.com/v1/forecast?latitude=" + latitude + "&longitude=" + longitude + "¤t_weather=true¤t=relativehumidity_2m,surface_pressure,is_day&daily=temperature_2m_max,temperature_2m_min,weathercode,sunset,sunrise&timezone=auto"; + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + try { + var weatherData = JSON.parse(xhr.responseText); + //console.log(JSON.stringify(weatherData)) + + // Save core data + data.weather = weatherData; + data.weatherLastFetch = Time.timestamp; + + // Update stable display values only when complete and successful + root.stableLatitude = data.latitude = weatherData.latitude.toString(); + root.stableLongitude = data.longitude = weatherData.longitude.toString(); + root.coordinatesReady = true; + + isFetchingWeather = false; + Logger.d("Location", "Cached weather to disk - stable coordinates updated"); + } catch (e) { + errorCallback("Location", "Failed to parse weather data"); + } + } else { + errorCallback("Location", `Weather error: ${xhr.status} ${xhr.responseText}`); + } + } + }; + xhr.open("GET", url); + xhr.send(); + } + + // Geolocate via IP address using the Noctalia API + function geolocate(callback, errorCallback) { + Logger.d("Location", "Geolocating via IP"); + var url = "https://api.noctalia.dev/geolocate"; + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + try { + var data = JSON.parse(xhr.responseText); + if (data.lat != null) { + callback(data.lat, data.lng, data.city, data.country); + } else { + errorCallback("Location", "Geolocate: no coordinates returned"); + } + } catch (e) { + errorCallback("Location", "Failed to parse geolocate data: " + e); + } + } else { + errorCallback("Location", `Geolocate error: ${xhr.status} ${xhr.responseText}`); + } + } + }; + xhr.open("GET", url); + xhr.send(); + } + + // Geolocate via IP and apply the result as the current location + function geolocateAndApply() { + if (isFetchingWeather) { + Logger.w("Location", "Geolocate skipped, fetch already in progress"); + return; + } + geolocate(function (lat, lng, city, country) { + Logger.i("Location", "Geolocated to", city + ",", country + ":", lat + "," + lng); + + const locationChanged = adapter.name !== city; + Settings.data.location.name = city; + adapter.name = city; + adapter.latitude = lat.toString(); + adapter.longitude = lng.toString(); + root.stableLatitude = adapter.latitude; + root.stableLongitude = adapter.longitude; + root.stableName = `${city}, ${country}`; + root.coordinatesReady = true; + + if (locationChanged) { + adapter.weatherLastFetch = 0; + adapter.weather = null; + } + + if (Settings.data.location.weatherEnabled) { + updateWeatherData(); + } + }, errorCallback); + } + + // -------------------------------- + function errorCallback(module, message) { + Logger.w(module, message); + isFetchingWeather = false; + } + + // -------------------------------- + function weatherSymbolFromCode(code) { + var isDay = data.weather ? data.weather.current_weather.is_day : true; + if (code === 0) + return isDay ? "weather-sun" : "weather-moon"; + if (code === 1 || code === 2) + return isDay ? "weather-cloud-sun" : "weather-moon-stars"; + if (code === 3) + return "weather-cloud"; + if (code >= 45 && code <= 48) + return "weather-cloud-haze"; + if (code >= 51 && code <= 67) + return "weather-cloud-rain"; + if (code >= 80 && code <= 82) + return "weather-cloud-rain"; + if (code >= 71 && code <= 77) + return "weather-cloud-snow"; + if (code >= 71 && code <= 77) + return "weather-cloud-snow"; + if (code >= 85 && code <= 86) + return "weather-cloud-snow"; + if (code >= 95 && code <= 99) + return "weather-cloud-lightning"; + return "weather-cloud"; + } + + // -------------------------------- + function taliaWeatherImageFromCode(code) { + var isDay = data.weather ? data.weather.current_weather.is_day : true; + if (code >= 40 && code <= 49) + return Quickshell.shellDir + "/Assets/Talia/TaliaDazed.png"; + if (code >= 95 && code <= 99) + return Quickshell.shellDir + "/Assets/Talia/TaliaFear.png"; + var wet = (code >= 51 && code <= 67) || (code >= 80 && code <= 82) || (code >= 71 && code <= 77) || (code >= 85 && code <= 86); + if (wet) + return Quickshell.shellDir + "/Assets/Talia/TaliaSob.png"; + if ((code === 0 || code === 1 || code === 2) && isDay === false) + return Quickshell.shellDir + "/Assets/Talia/TaliaVampire.png"; + if ((code === 0 && isDay === true) || code === 1 || code === 2) + return Quickshell.shellDir + "/Assets/Talia/TaliaParty.png"; + return Quickshell.shellDir + "/Assets/Talia/TaliaBlank.png"; + } + + // -------------------------------- + function weatherDescriptionFromCode(code) { + if (code === 0) + return "Clear sky"; + if (code === 1) + return "Mainly clear"; + if (code === 2) + return "Partly cloudy"; + if (code === 3) + return "Overcast"; + if (code === 45 || code === 48) + return "Fog"; + if (code >= 51 && code <= 67) + return "Drizzle"; + if (code >= 71 && code <= 77) + return "Snow"; + if (code >= 80 && code <= 82) + return "Rain showers"; + if (code >= 95 && code <= 99) + return "Thunderstorm"; + return "Unknown"; + } + + // -------------------------------- + function celsiusToFahrenheit(celsius) { + return 32 + celsius * 1.8; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Location/NightLightService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Location/NightLightService.qml new file mode 100644 index 0000000..16f6a0f --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Location/NightLightService.qml @@ -0,0 +1,290 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.UI + +Singleton { + id: root + + // Night Light properties - directly bound to settings + readonly property var params: Settings.data.nightLight + property var lastCommand: [] + + // Crash tracking for auto-restart + property int _crashCount: 0 + property int _maxCrashes: 5 + + // Manual schedule tracking + property bool _manualNightPhase: false + + // Kill any stale wlsunset processes on startup to prevent issues after shell restart + Component.onCompleted: { + killStaleProcess.running = true; + } + + Process { + id: killStaleProcess + running: false + command: ["pkill", "-x", "wlsunset"] + onExited: function (code, status) { + if (code === 0) { + Logger.i("NightLight", "Killed stale wlsunset process from previous session"); + } + // Now apply the settings after cleanup + root.apply(); + } + } + + Timer { + id: restartTimer + interval: 2000 + repeat: false + onTriggered: { + if (root.params.enabled && !runner.running) { + Logger.w("NightLight", "Restarting after crash..."); + if (root.isManualMode()) { + root.applyManualSchedule(); + } else { + runner.running = true; + } + } + } + } + + Timer { + id: manualScheduleTimer + repeat: false + onTriggered: { + Logger.i("NightLight", "Manual schedule: phase boundary reached"); + root.applyManualSchedule(); + } + } + + function timeToMinutes(timeStr) { + var parts = timeStr.split(":").map(Number); + return parts[0] * 60 + parts[1]; + } + + function isManualMode() { + return !params.forced && !params.autoSchedule; + } + + function isCurrentlyNight() { + var now = new Date(); + var nowMin = now.getHours() * 60 + now.getMinutes(); + var sunsetMin = timeToMinutes(params.manualSunset); + var sunriseMin = timeToMinutes(params.manualSunrise); + + if (sunsetMin < sunriseMin) { + // Inverted: e.g. sunset=03:00, sunrise=07:00 → night is [03:00, 07:00) + return nowMin >= sunsetMin && nowMin < sunriseMin; + } else { + // Normal: e.g. sunset=18:00, sunrise=06:00 → night is [18:00, 06:00) + return nowMin >= sunsetMin || nowMin < sunriseMin; + } + } + + function msUntilNextBoundary() { + var now = new Date(); + var nowMin = now.getHours() * 60 + now.getMinutes(); + var sunsetMin = timeToMinutes(params.manualSunset); + var sunriseMin = timeToMinutes(params.manualSunrise); + + var targetMin = isCurrentlyNight() ? sunriseMin : sunsetMin; + var diffMin = targetMin - nowMin; + if (diffMin <= 0) + diffMin += 1440; + + return diffMin * 60 * 1000 - now.getSeconds() * 1000 - now.getMilliseconds(); + } + + function applyManualSchedule() { + if (!params.enabled) { + manualScheduleTimer.stop(); + runner.running = false; + return; + } + + var night = isCurrentlyNight(); + _manualNightPhase = night; + + if (night) { + var cmd = ["wlsunset"]; + cmd.push("-t", `${params.nightTemp}`, "-T", `${params.dayTemp}`); + cmd.push("-S", "23:59"); + cmd.push("-s", "00:00"); + cmd.push("-d", 1); + + if (JSON.stringify(cmd) !== JSON.stringify(lastCommand) || !runner.running) { + lastCommand = cmd; + runner.command = cmd; + runner.running = false; + runner.running = true; + } + Logger.i("NightLight", "Manual schedule: night phase - wlsunset forced on"); + } else { + lastCommand = []; + runner.running = false; + Logger.i("NightLight", "Manual schedule: day phase - wlsunset stopped"); + } + + var ms = msUntilNextBoundary(); + manualScheduleTimer.interval = Math.max(ms, 1000); + manualScheduleTimer.restart(); + Logger.i("NightLight", "Manual schedule: next boundary in " + Math.round(ms / 1000) + "s"); + } + + function apply(force = false) { + // If using LocationService, wait for it to be ready + if (!params.forced && params.autoSchedule && !LocationService.coordinatesReady) { + return; + } + + // Manual mode: handle scheduling ourselves + if (isManualMode() && params.enabled) { + _crashCount = 0; + restartTimer.stop(); + applyManualSchedule(); + return; + } + + // Not in manual mode - clean up manual timer + manualScheduleTimer.stop(); + + var command = buildCommand(); + + // Compare with previous command to avoid unnecessary restart + if (force || JSON.stringify(command) !== JSON.stringify(lastCommand)) { + lastCommand = command; + runner.command = command; + + // Set running to false so it may restart below if still enabled + runner.running = false; + } + runner.running = params.enabled; + } + + function buildCommand() { + var cmd = ["wlsunset"]; + if (params.forced) { + // Force immediate full night temperature regardless of time + // Keep distinct day/night temps but set times so we're effectively always in "night" + cmd.push("-t", `${params.nightTemp}`, "-T", `${params.dayTemp}`); + // Night spans from sunset (00:00) to sunrise (23:59) covering almost the full day + cmd.push("-S", "23:59"); // sunrise very late + cmd.push("-s", "00:00"); // sunset at midnight + // Near-instant transition + cmd.push("-d", 1); + } else if (params.autoSchedule) { + cmd.push("-t", `${params.nightTemp}`, "-T", `${params.dayTemp}`); + cmd.push("-l", `${LocationService.stableLatitude}`, "-L", `${LocationService.stableLongitude}`); + cmd.push("-d", 60 * 15); // 15min progressive fade at sunset/sunrise + } + return cmd; + } + + // Observe setting changes and location readiness + Connections { + target: Settings.data.nightLight + function onEnabledChanged() { + apply(); + // Toast: night light toggled + const enabled = !!Settings.data.nightLight.enabled; + ToastService.showNotice(I18n.tr("common.night-light"), enabled ? I18n.tr("common.enabled") : I18n.tr("common.disabled"), enabled ? "nightlight-on" : "nightlight-off"); + } + function onForcedChanged() { + apply(); + if (Settings.data.nightLight.enabled) { + ToastService.showNotice(I18n.tr("common.night-light"), Settings.data.nightLight.forced ? I18n.tr("toast.night-light.forced") : I18n.tr("toast.night-light.normal"), Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on"); + } + } + function onNightTempChanged() { + apply(); + } + function onDayTempChanged() { + apply(); + } + function onManualSunriseChanged() { + apply(); + } + function onManualSunsetChanged() { + apply(); + } + function onAutoScheduleChanged() { + apply(); + } + } + + Connections { + target: LocationService + function onCoordinatesReadyChanged() { + if (LocationService.coordinatesReady) { + root.apply(); + } + } + } + + Timer { + id: resumeRetryTimer + interval: 2000 + repeat: false + onTriggered: { + Logger.i("NightLight", "Resume retry - re-applying night light again"); + root.apply(true); + } + } + + Connections { + target: Time + function onResumed() { + Logger.i("NightLight", "System resumed - re-applying night light"); + root.apply(true); + resumeRetryTimer.restart(); + } + } + + // Foreground process runner + Process { + id: runner + running: false + onStarted: { + Logger.i("NightLight", "Wlsunset started:", runner.command); + // Reset crash count on successful start + if (root._crashCount > 0) { + root._crashCount = 0; + } + } + onExited: function (code, status) { + if (root.params.enabled && root.isManualMode()) { + // Manual mode: only treat as crash if we're in the night phase + if (root._manualNightPhase) { + root._crashCount++; + if (root._crashCount <= root._maxCrashes) { + Logger.w("NightLight", "Wlsunset exited unexpectedly during manual night phase (code: " + code + "), restarting in 2s... (attempt " + root._crashCount + "/" + root._maxCrashes + ")"); + restartTimer.start(); + } else { + Logger.e("NightLight", "Wlsunset crashed too many times (" + root._maxCrashes + "), giving up"); + } + } else { + Logger.i("NightLight", "Wlsunset exited (manual day phase):", code, status); + root._crashCount = 0; + } + } else if (root.params.enabled) { + // Non-manual mode: any exit while enabled is a crash + root._crashCount++; + if (root._crashCount <= root._maxCrashes) { + Logger.w("NightLight", "Wlsunset exited unexpectedly (code: " + code + "), restarting in 2s... (attempt " + root._crashCount + "/" + root._maxCrashes + ")"); + restartTimer.start(); + } else { + Logger.e("NightLight", "Wlsunset crashed too many times (" + root._maxCrashes + "), giving up"); + } + } else { + Logger.i("NightLight", "Wlsunset exited (disabled):", code, status); + root._crashCount = 0; + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Media/AudioService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Media/AudioService.qml new file mode 100644 index 0000000..fffdb1c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Media/AudioService.qml @@ -0,0 +1,1002 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pipewire +import qs.Commons +import qs.Services.System + +Singleton { + id: root + + // Rate limiting for volume feedback (minimum 100ms between sounds) + property var lastVolumeFeedbackTime: 0 + readonly property int minVolumeFeedbackInterval: 100 + + // Track the last sink that produced volume feedback so we can suppress the + // initial onVolumeChanged that fires on startup and device switches. + property PwNode _lastFeedbackSink: null + + // Devices + readonly property var sink: Pipewire.ready ? Pipewire.defaultAudioSink : null + readonly property var source: validatedSource + readonly property bool hasInput: !!source + readonly property list sinks: deviceNodes.sinks + readonly property list sources: deviceNodes.sources + readonly property real maxVolume: Settings.data.audio.volumeOverdrive ? 1.5 : 1.0 + readonly property real epsilon: 0.005 + + // Fallback state sourced from wpctl when PipeWire node values go stale. + property bool wpctlAvailable: false + property bool wpctlStateValid: false + property real wpctlOutputVolume: 0 + property bool wpctlOutputMuted: true + property bool wpctlInputStateValid: false + property real wpctlInputVolume: 0 + property bool wpctlInputMuted: true + + signal volumeAtMaximum + signal volumeAtMinimum + + function clampOutputVolume(vol: real): real { + if (vol === undefined || isNaN(vol)) { + return 0; + } + return Math.max(0, Math.min(root.maxVolume, vol)); + } + + function refreshWpctlOutputState(): void { + if (!wpctlAvailable || wpctlStateProcess.running) { + return; + } + wpctlStateProcess.command = ["wpctl", "get-volume", "@DEFAULT_AUDIO_SINK@"]; + wpctlStateProcess.running = true; + } + + function refreshWpctlInputState(): void { + if (!wpctlAvailable || wpctlInputStateProcess.running) { + return; + } + wpctlInputStateProcess.command = ["wpctl", "get-volume", "@DEFAULT_AUDIO_SOURCE@"]; + wpctlInputStateProcess.running = true; + } + + function applyWpctlOutputState(raw: string): bool { + const text = String(raw || "").trim(); + const match = text.match(/Volume:\s*([0-9]*\.?[0-9]+)/i); + if (!match || match.length < 2) { + return false; + } + + const parsedVolume = Number(match[1]); + if (isNaN(parsedVolume)) { + return false; + } + + wpctlOutputVolume = clampOutputVolume(parsedVolume); + wpctlOutputMuted = /\[MUTED\]/i.test(text); + wpctlStateValid = true; + return true; + } + + function applyWpctlInputState(raw: string): bool { + const text = String(raw || "").trim(); + const match = text.match(/Volume:\s*([0-9]*\.?[0-9]+)/i); + if (!match || match.length < 2) { + return false; + } + + const parsedVolume = Number(match[1]); + if (isNaN(parsedVolume)) { + return false; + } + + wpctlInputVolume = Math.max(0, Math.min(root.maxVolume, parsedVolume)); + wpctlInputMuted = /\[MUTED\]/i.test(text); + wpctlInputStateValid = true; + return true; + } + + // Output volume (prefer wpctl state when available) + readonly property real volume: { + if (wpctlAvailable && wpctlStateValid) { + return clampOutputVolume(wpctlOutputVolume); + } + + if (!sink?.audio) { + return 0; + } + return clampOutputVolume(sink.audio.volume); + } + readonly property bool muted: { + if (wpctlAvailable && wpctlStateValid) { + return wpctlOutputMuted; + } + return sink?.audio?.muted ?? true; + } + + // Input volume (prefer wpctl state when available — matches set-volume % round-trip) + readonly property real inputVolume: { + if (wpctlAvailable && wpctlInputStateValid) { + return Math.max(0, Math.min(root.maxVolume, wpctlInputVolume)); + } + if (!source?.audio) { + return 0; + } + const vol = source.audio.volume; + if (vol === undefined || isNaN(vol)) { + return 0; + } + return Math.max(0, Math.min(root.maxVolume, vol)); + } + readonly property bool inputMuted: { + if (wpctlAvailable && wpctlInputStateValid) { + return wpctlInputMuted; + } + return source?.audio?.muted ?? true; + } + + // Allow callers to skip the next OSD notification when they are already + // presenting volume state (e.g. the Audio Panel UI). We track this as a short + // time window so suppression applies to every monitor, not just the first one + // that receives the signal. + property double outputOSDSuppressedUntilMs: 0 + property double inputOSDSuppressedUntilMs: 0 + + function suppressOutputOSD(durationMs = 400) { + const target = Date.now() + durationMs; + outputOSDSuppressedUntilMs = Math.max(outputOSDSuppressedUntilMs, target); + } + + function suppressInputOSD(durationMs = 400) { + const target = Date.now() + durationMs; + inputOSDSuppressedUntilMs = Math.max(inputOSDSuppressedUntilMs, target); + } + + function consumeOutputOSDSuppression(): bool { + return Date.now() < outputOSDSuppressedUntilMs; + } + + function consumeInputOSDSuppression(): bool { + return Date.now() < inputOSDSuppressedUntilMs; + } + + readonly property real stepVolume: Settings.data.audio.volumeStep / 100.0 + + // Filtered device nodes (non-stream sinks and sources) + readonly property var deviceNodes: Pipewire.ready ? Pipewire.nodes.values.reduce((acc, node) => { + if (!node.isStream) { + // Filter out quickshell nodes (unlikely to be devices, but for consistency) + const name = node.name || ""; + const mediaName = (node.properties && node.properties["media.name"]) || ""; + if (name === "quickshell" || mediaName === "quickshell") { + return acc; + } + + if (node.isSink) { + acc.sinks.push(node); + } else if (node.audio) { + acc.sources.push(node); + } + } + return acc; + }, { + "sources": [], + "sinks": [] + }) : { + "sources": [], + "sinks": [] + } + + // Validated source (ensures it's a proper audio source, not a sink) + readonly property var validatedSource: { + if (!Pipewire.ready) { + return null; + } + const raw = Pipewire.defaultAudioSource; + if (!raw || raw.isSink || !raw.audio) { + return null; + } + // Optional: check type if available (type reflects media.class per docs) + if (raw.type && typeof raw.type === "string" && !raw.type.startsWith("Audio/Source")) { + return null; + } + return raw; + } + + // Internal state for feedback loop prevention + property bool isSettingOutputVolume: false + property bool isSettingInputVolume: false + + // Bind default sink and source to ensure their properties are available + PwObjectTracker { + id: sinkTracker + objects: root.sink ? [root.sink] : [] + } + + PwObjectTracker { + id: sourceTracker + objects: root.source ? [root.source] : [] + } + + // Track links to the default sink to find active streams + PwNodeLinkTracker { + id: sinkLinkTracker + } + + onSinkChanged: { + if (root.sink) { + sinkLinkTracker.node = root.sink; + } + } + + // Track all streams globally to prevent binding loops for filtered out streams + readonly property var streamNodes: Pipewire.ready ? Pipewire.nodes.values.filter(n => n && n.isStream) : [] + + // Find application streams that are connected to the default sink + readonly property var appStreams: { + if (!Pipewire.ready || !root.sink) { + return []; + } + + var connectedStreamIds = {}; + var connectedStreams = []; + + // Use PwNodeLinkTracker to get properly bound link groups + if (!sinkLinkTracker.linkGroups) { + return []; + } + + var linkGroupsCount = 0; + if (sinkLinkTracker.linkGroups.length !== undefined) { + linkGroupsCount = sinkLinkTracker.linkGroups.length; + } else if (sinkLinkTracker.linkGroups.count !== undefined) { + linkGroupsCount = sinkLinkTracker.linkGroups.count; + } else { + return []; + } + + if (linkGroupsCount === 0) { + return []; + } + + var intermediateNodeIds = {}; + var nodesToCheck = []; + + for (var i = 0; i < linkGroupsCount; i++) { + var linkGroup; + if (sinkLinkTracker.linkGroups.get) { + linkGroup = sinkLinkTracker.linkGroups.get(i); + } else { + linkGroup = sinkLinkTracker.linkGroups[i]; + } + + if (!linkGroup || !linkGroup.source) { + continue; + } + + var sourceNode = linkGroup.source; + + // Filter out quickshell + const name = sourceNode.name || ""; + const mediaName = (sourceNode.properties && sourceNode.properties["media.name"]) || ""; + if (name === "quickshell" || mediaName === "quickshell") { + continue; + } + + // Filter out filter (intermediate) streams + const isVirtual = (sourceNode.properties && sourceNode.properties["node.virtual"]) || ""; + // If it's an application stream node, add it directly + if (sourceNode.isStream && sourceNode.audio && !isVirtual) { + if (!connectedStreamIds[sourceNode.id]) { + connectedStreamIds[sourceNode.id] = true; + connectedStreams.push(sourceNode); + } + } else { + // Not a stream - this is an intermediate node, track it + intermediateNodeIds[sourceNode.id] = true; + nodesToCheck.push(sourceNode); + } + } + + // If we found intermediate nodes, we need to find streams connected to them + if (nodesToCheck.length > 0 || connectedStreams.length === 0) { + try { + var allNodes = Pipewire.nodes.values || []; + + // Find all stream nodes + for (var j = 0; j < allNodes.length; j++) { + var node = allNodes[j]; + if (!node || !node.isStream || !node.audio) { + continue; + } + + // Filter out quickshell + const nodeName = node.name || ""; + const nodeMediaName = (node.properties && node.properties["media.name"]) || ""; + if (nodeName === "quickshell" || nodeMediaName === "quickshell") { + continue; + } + + // Filter out filter streams + const nodeIsVirtual = (node.properties && node.properties["node.virtual"]) || ""; + if (nodeIsVirtual) { + continue; + } + + var streamId = node.id; + if (connectedStreamIds[streamId]) { + continue; + } + + if (Object.keys(intermediateNodeIds).length > 0) { + connectedStreamIds[streamId] = true; + connectedStreams.push(node); + } else if (connectedStreams.length === 0) { + connectedStreamIds[streamId] = true; + connectedStreams.push(node); + } + } + } catch (e) {} + } + + return connectedStreams; + } + + // Bind all devices to ensure their properties are available + PwObjectTracker { + objects: [...root.sinks, ...root.sources] + } + + // Per-stream volume overrides (app + media identity) so concurrent browser streams do not share one entry. + property var appVolumeOverrides: ({}) + property var _knownAppStreamIds: ({}) + property bool _isApplyingAppOverride: false + + PwObjectTracker { + objects: root.streamNodes + } + + // Keep appVolumeOverrides aligned with PipeWire when apps change volume/mute. + Item { + width: 0 + height: 0 + visible: false + + Repeater { + model: root.appStreams + + delegate: Item { + required property var modelData + + Connections { + target: modelData?.audio ?? null + + function onVolumeChanged() { + if (root._isApplyingAppOverride || !modelData?.audio) { + return; + } + var key = root.getAppKey(modelData); + if (key) { + root.setAppStreamVolume(key, modelData.audio.volume); + } + } + + function onMutedChanged() { + if (root._isApplyingAppOverride || !modelData?.audio) { + return; + } + var key = root.getAppKey(modelData); + if (key) { + root.setAppStreamMuted(key, modelData.audio.muted); + } + } + } + } + } + } + + function getAppKey(node): string { + if (!node || !node.properties) { + return ""; + } + var props = node.properties; + var base = ""; + var binary = props["application.process.binary"] || ""; + if (binary) { + var parts = binary.split("/"); + base = parts[parts.length - 1].toLowerCase(); + } + if (!base) { + var appName = props["application.name"] || ""; + if (appName) { + base = appName.toLowerCase(); + } + } + if (!base) { + var appId = props["application.id"] || ""; + if (appId) { + base = appId.toLowerCase(); + } + } + if (!base) { + return ""; + } + + var mediaName = (props["media.name"] || "").trim().toLowerCase(); + var mediaRole = (props["media.role"] || "").trim().toLowerCase(); + var tagParts = []; + if (mediaName) { + tagParts.push(mediaName); + } + if (mediaRole) { + tagParts.push(mediaRole); + } + if (tagParts.length > 0) { + return base + "\u001f" + tagParts.join("\u001e"); + } + return base + "\u001f" + String(node.id); + } + + function setAppStreamVolume(appKey: string, volume: real): void { + if (!appKey) { + return; + } + var o = appVolumeOverrides; + if (!o[appKey]) { + o[appKey] = {}; + } + + o[appKey].volume = volume; + appVolumeOverrides = o; + } + + function setAppStreamMuted(appKey: string, muted: bool): void { + if (!appKey) { + return; + } + var o = appVolumeOverrides; + if (!o[appKey]) { + o[appKey] = {}; + } + o[appKey].muted = muted; + appVolumeOverrides = o; + } + + function getAppVolumeOverride(appKey: string) { + return appKey ? (appVolumeOverrides[appKey] || null) : null; + } + + function _applyAppOverrides(): void { + var streams = root.appStreams; + if (!streams) { + return; + } + + var prevKnown = root._knownAppStreamIds; + var currentIds = {}; + _isApplyingAppOverride = true; + for (var i = 0; i < streams.length; i++) { + var s = streams[i]; + if (!s) { + continue; + } + + currentIds[s.id] = true; + var key = getAppKey(s); + var ov = key ? appVolumeOverrides[key] : null; + + // New stream node (reload, app restart, etc.): adopt PipeWire state into + // overrides so we do not force an outdated Noctalia-only value. + if (key && s.audio && !prevKnown[s.id]) { + var seeded = appVolumeOverrides; + if (!seeded[key]) { + seeded[key] = {}; + } + seeded[key].volume = s.audio.volume; + seeded[key].muted = s.audio.muted; + appVolumeOverrides = seeded; + ov = seeded[key]; + } + + if (!ov || !s.audio) { + continue; + } + if (ov.volume !== undefined && Math.abs(s.audio.volume - ov.volume) > root.epsilon) { + s.audio.volume = ov.volume; + } + if (ov.muted !== undefined && s.audio.muted !== ov.muted) { + s.audio.muted = ov.muted; + } + } + _knownAppStreamIds = currentIds; + _isApplyingAppOverride = false; + } + + Connections { + target: root + function onAppStreamsChanged() { + _appOverrideTimer.restart(); + } + } + + Timer { + id: _appOverrideTimer + interval: 50 + onTriggered: root._applyAppOverrides() + } + + Timer { + id: _appOverrideEnforcer + interval: 1000 + running: Object.keys(root.appVolumeOverrides).length > 0 && root.appStreams.length > 0 + repeat: true + onTriggered: root._applyAppOverrides() + } + + Component.onCompleted: { + wpctlAvailabilityProcess.running = true; + } + + Connections { + target: root + function onSinkChanged() { + if (root.wpctlAvailable) { + root.refreshWpctlOutputState(); + } + } + + function onSourceChanged() { + if (root.wpctlAvailable) { + root.refreshWpctlInputState(); + } + } + } + + Timer { + id: wpctlPollTimer + // Safety net only; regular updates are event-driven from sink audio signals. + interval: 20000 + running: root.wpctlAvailable + repeat: true + onTriggered: { + root.refreshWpctlOutputState(); + root.refreshWpctlInputState(); + } + } + + Process { + id: wpctlAvailabilityProcess + command: ["sh", "-c", "command -v wpctl"] + running: false + + onExited: function (code) { + root.wpctlAvailable = (code === 0); + root.wpctlStateValid = false; + root.wpctlInputStateValid = false; + if (root.wpctlAvailable) { + root.refreshWpctlOutputState(); + root.refreshWpctlInputState(); + } + } + } + + Process { + id: wpctlStateProcess + running: false + + onExited: function (code) { + if (code !== 0 || !root.applyWpctlOutputState(stdout.text)) { + root.wpctlStateValid = false; + } + } + + stdout: StdioCollector {} + } + + Process { + id: wpctlSetVolumeProcess + running: false + + onExited: function (code) { + root.isSettingOutputVolume = false; + if (code !== 0) { + Logger.w("AudioService", "wpctl set-volume failed, falling back to PipeWire node audio"); + if (root.sink?.audio) { + root.sink.audio.muted = false; + root.sink.audio.volume = root.clampOutputVolume(root.wpctlOutputVolume); + } + } + root.refreshWpctlOutputState(); + } + } + + Process { + id: wpctlSetMuteProcess + running: false + + onExited: function (_code) { + root.refreshWpctlOutputState(); + } + } + + Process { + id: wpctlInputStateProcess + running: false + + onExited: function (code) { + if (code !== 0 || !root.applyWpctlInputState(stdout.text)) { + root.wpctlInputStateValid = false; + } + } + + stdout: StdioCollector {} + } + + Process { + id: wpctlSetInputVolumeProcess + running: false + + onExited: function (code) { + root.isSettingInputVolume = false; + if (code !== 0) { + Logger.w("AudioService", "wpctl set-volume failed for default source, falling back to PipeWire node audio"); + if (root.source?.audio) { + root.source.audio.muted = false; + root.source.audio.volume = Math.max(0, Math.min(root.maxVolume, root.wpctlInputVolume)); + } + } + root.refreshWpctlInputState(); + } + } + + Process { + id: wpctlSetInputMuteProcess + running: false + + onExited: function (_code) { + root.refreshWpctlInputState(); + } + } + + // Watch output device changes for clamping + Connections { + target: sink?.audio ?? null + + function onVolumeChanged() { + if (root.wpctlAvailable) { + root.refreshWpctlOutputState(); + } + + // Ignore volume changes if we're the one setting it (to prevent feedback loop) + if (root.isSettingOutputVolume) { + return; + } + + if (!root.sink?.audio) { + return; + } + + const vol = root.sink.audio.volume; + if (vol === undefined || isNaN(vol)) { + return; + } + + if (root.sink !== root._lastFeedbackSink) { + root._lastFeedbackSink = root.sink; + } else { + playVolumeFeedback(clampOutputVolume(vol)); + } + + // If volume exceeds max, clamp it (but only if we didn't just set it) + if (vol > root.maxVolume) { + root.isSettingOutputVolume = true; + Qt.callLater(() => { + if (root.sink?.audio && root.sink.audio.volume > root.maxVolume) { + root.sink.audio.volume = root.maxVolume; + } + root.isSettingOutputVolume = false; + }); + } + } + + function onMutedChanged() { + if (root.wpctlAvailable) { + root.refreshWpctlOutputState(); + } + } + } + + // Watch input device changes for clamping + Connections { + target: source?.audio ?? null + + function onVolumeChanged() { + if (root.wpctlAvailable) { + root.refreshWpctlInputState(); + } + + // Ignore volume changes if we're the one setting it (to prevent feedback loop) + if (root.isSettingInputVolume) { + return; + } + + if (!root.source?.audio) { + return; + } + + const vol = root.source.audio.volume; + if (vol === undefined || isNaN(vol)) { + return; + } + + // If volume exceeds max, clamp it (but only if we didn't just set it) + if (vol > root.maxVolume) { + root.isSettingInputVolume = true; + Qt.callLater(() => { + if (root.source?.audio && root.source.audio.volume > root.maxVolume) { + root.source.audio.volume = root.maxVolume; + } + root.isSettingInputVolume = false; + }); + } + } + + function onMutedChanged() { + if (root.wpctlAvailable) { + root.refreshWpctlInputState(); + } + } + } + + // Output Control + function increaseVolume() { + if (!Pipewire.ready || (!sink?.audio && !wpctlAvailable)) { + return; + } + if (volume >= root.maxVolume) { + volumeAtMaximum(); + return; + } + setVolume(Math.min(root.maxVolume, volume + stepVolume)); + } + + function decreaseVolume() { + if (!Pipewire.ready || (!sink?.audio && !wpctlAvailable)) { + return; + } + if (volume <= 0) { + volumeAtMinimum(); + return; + } + setVolume(Math.max(0, volume - stepVolume)); + } + + function setVolume(newVolume: real) { + if (!Pipewire.ready || (!sink?.audio && !wpctlAvailable)) { + Logger.w("AudioService", "No sink available or not ready"); + return; + } + + const clampedVolume = clampOutputVolume(newVolume); + const delta = Math.abs(clampedVolume - volume); + if (delta < root.epsilon) { + return; + } + + if (wpctlAvailable) { + if (wpctlSetVolumeProcess.running) { + return; + } + + isSettingOutputVolume = true; + wpctlOutputMuted = false; + wpctlOutputVolume = clampedVolume; + wpctlStateValid = true; + + const volumePct = Math.round(clampedVolume * 10000) / 100; + wpctlSetVolumeProcess.command = ["sh", "-c", "wpctl set-mute @DEFAULT_AUDIO_SINK@ 0 && wpctl set-volume @DEFAULT_AUDIO_SINK@ " + volumePct + "%"]; + wpctlSetVolumeProcess.running = true; + + playVolumeFeedback(clampedVolume); + return; + } + + if (!sink?.ready || !sink?.audio) { + Logger.w("AudioService", "No sink available or not ready"); + return; + } + + // Set flag to prevent feedback loop, then set the actual volume + isSettingOutputVolume = true; + sink.audio.muted = false; + sink.audio.volume = clampedVolume; + + playVolumeFeedback(clampedVolume); + + // Clear flag after a short delay to allow external changes to be detected + Qt.callLater(() => { + isSettingOutputVolume = false; + }); + } + + function setOutputMuted(muted: bool) { + if (!Pipewire.ready || (!sink?.audio && !wpctlAvailable)) { + Logger.w("AudioService", "No sink available or Pipewire not ready"); + return; + } + + if (wpctlAvailable) { + if (wpctlSetMuteProcess.running) { + return; + } + + wpctlOutputMuted = muted; + wpctlStateValid = true; + wpctlSetMuteProcess.command = ["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", muted ? "1" : "0"]; + wpctlSetMuteProcess.running = true; + return; + } + + sink.audio.muted = muted; + } + + function getOutputIcon() { + if (muted) { + return "volume-mute"; + } + + const clampedVolume = Math.max(0, Math.min(volume, root.maxVolume)); + + // Show volume-x icon when volume is effectively 0% (within rounding threshold) + if (clampedVolume < root.epsilon) { + return "volume-x"; + } + if (clampedVolume <= 0.5) { + return "volume-low"; + } + return "volume-high"; + } + + // Input Control + function increaseInputVolume() { + if (!Pipewire.ready || (!source?.audio && !wpctlAvailable)) { + return; + } + if (inputVolume >= root.maxVolume) { + return; + } + setInputVolume(Math.min(root.maxVolume, inputVolume + stepVolume)); + } + + function decreaseInputVolume() { + if (!Pipewire.ready || (!source?.audio && !wpctlAvailable)) { + return; + } + setInputVolume(Math.max(0, inputVolume - stepVolume)); + } + + function setInputVolume(newVolume: real) { + if (!Pipewire.ready) { + return; + } + + const clampedVolume = Math.max(0, Math.min(root.maxVolume, newVolume)); + var currentVol = 0; + if (wpctlAvailable && wpctlInputStateValid) { + currentVol = wpctlInputVolume; + } else if (source?.audio && source.audio.volume !== undefined && !isNaN(source.audio.volume)) { + currentVol = source.audio.volume; + } + const delta = Math.abs(clampedVolume - currentVol); + if (delta < root.epsilon) { + return; + } + + if (wpctlAvailable) { + if (wpctlSetInputVolumeProcess.running) { + return; + } + + isSettingInputVolume = true; + wpctlInputMuted = false; + wpctlInputVolume = clampedVolume; + wpctlInputStateValid = true; + + const volumePct = Math.round(clampedVolume * 10000) / 100; + wpctlSetInputVolumeProcess.command = ["sh", "-c", "wpctl set-mute @DEFAULT_AUDIO_SOURCE@ 0 && wpctl set-volume @DEFAULT_AUDIO_SOURCE@ " + volumePct + "%"]; + wpctlSetInputVolumeProcess.running = true; + return; + } + + if (!source?.ready || !source?.audio) { + Logger.w("AudioService", "No source available or not ready"); + return; + } + + isSettingInputVolume = true; + source.audio.muted = false; + source.audio.volume = clampedVolume; + + Qt.callLater(() => { + isSettingInputVolume = false; + }); + } + + function playVolumeFeedback(currentVolume: real) { + if (!SoundService.multimediaAvailable) { + return; + } + + if (!Settings.data.audio.volumeFeedback) { + return; + } + + const now = Date.now(); + if (now - lastVolumeFeedbackTime < minVolumeFeedbackInterval) { + return; + } + lastVolumeFeedbackTime = now; + + const feedbackVolume = currentVolume; + const configuredSoundFile = Settings.data.audio.volumeFeedbackSoundFile; + const soundFile = (configuredSoundFile && configuredSoundFile.trim() !== "") ? configuredSoundFile : "volume-change.wav"; + + SoundService.playSound(soundFile, { + volume: feedbackVolume, + fallback: false, + repeat: false + }); + } + + function setInputMuted(muted: bool) { + if (!Pipewire.ready) { + Logger.w("AudioService", "Pipewire not ready"); + return; + } + + if (wpctlAvailable) { + if (wpctlSetInputMuteProcess.running) { + return; + } + + wpctlInputMuted = muted; + wpctlInputStateValid = true; + wpctlSetInputMuteProcess.command = ["wpctl", "set-mute", "@DEFAULT_AUDIO_SOURCE@", muted ? "1" : "0"]; + wpctlSetInputMuteProcess.running = true; + return; + } + + if (!source?.audio) { + Logger.w("AudioService", "No source available"); + return; + } + + source.audio.muted = muted; + } + + function getInputIcon() { + if (inputMuted || inputVolume <= Number.EPSILON) { + return "microphone-mute"; + } + return "microphone"; + } + + // Device Selection + function setAudioSink(newSink: PwNode): void { + if (!Pipewire.ready) { + Logger.w("AudioService", "Pipewire not ready"); + return; + } + Pipewire.preferredDefaultAudioSink = newSink; + } + + function setAudioSource(newSource: PwNode): void { + if (!Pipewire.ready) { + Logger.w("AudioService", "Pipewire not ready"); + return; + } + Pipewire.preferredDefaultAudioSource = newSource; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Media/MediaService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Media/MediaService.qml new file mode 100644 index 0000000..e30762d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Media/MediaService.qml @@ -0,0 +1,356 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Services.Mpris +import qs.Commons + +Singleton { + id: root + + function formatTime(seconds) { + if (isNaN(seconds) || seconds < 0) + return "0:00"; + var h = Math.floor(seconds / 3600); + var m = Math.floor((seconds % 3600) / 60); + var s = Math.floor(seconds % 60); + var pad = function (n) { + return (n < 10) ? ("0" + n) : n; + }; + + if (h > 0) { + return h + ":" + pad(m) + ":" + pad(s); + } else { + return m + ":" + pad(s); + } + } + + property var currentPlayer: null + property string playerIdentity: currentPlayer ? (currentPlayer.identity || "") : "" + property real currentPosition: 0 + property bool isSeeking: false + property int selectedPlayerIndex: 0 + property bool isPlaying: currentPlayer ? (currentPlayer.playbackState === MprisPlaybackState.Playing || currentPlayer.isPlaying) : false + property string trackTitle: currentPlayer ? (currentPlayer.trackTitle !== undefined ? currentPlayer.trackTitle.replace(/(\r\n|\n|\r)/g, "") : "") : "" + property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "") : "" + property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "") : "" + property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl || "") : "" + property real trackLength: currentPlayer ? ((currentPlayer.length < infiniteTrackLength) ? currentPlayer.length : 0) : 0 + property bool canPlay: currentPlayer ? currentPlayer.canPlay : false + property bool canPause: currentPlayer ? currentPlayer.canPause : false + property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false + property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false + property bool canSeek: currentPlayer ? currentPlayer.canSeek : false + property string positionString: formatTime(currentPosition) + property string lengthString: formatTime(trackLength) + property real infiniteTrackLength: 922337203685 + + Component.onCompleted: { + updateCurrentPlayer(); + } + + function getAvailablePlayers() { + if (!Mpris.players || !Mpris.players.values) { + return []; + } + + let allPlayers = Mpris.players.values; + let finalPlayers = []; + const genericBrowsers = ["firefox", "chromium", "chrome"]; + const blacklist = (Settings.data.audio && Settings.data.audio.mprisBlacklist) ? Settings.data.audio.mprisBlacklist : []; + + // Separate players into specific and generic lists + let specificPlayers = []; + let genericPlayers = []; + for (var i = 0; i < allPlayers.length; i++) { + if (!allPlayers[i]) + continue; + const identity = String(allPlayers[i].identity || "").toLowerCase(); + const dbusName = String(allPlayers[i].dbusName || "").toLowerCase(); + const match = blacklist.find(b => { + const s = String(b || "").toLowerCase(); + return s && (identity.includes(s) || dbusName.includes(s)); + }); + if (match) + continue; + if (genericBrowsers.some(b => identity.includes(b))) { + genericPlayers.push(allPlayers[i]); + } else { + specificPlayers.push(allPlayers[i]); + } + } + + let matchedGenericIndices = {}; + + // For each specific player, try to find and pair it with a generic partner + for (var i = 0; i < specificPlayers.length; i++) { + let specificPlayer = specificPlayers[i]; + let title1 = String(specificPlayer.trackTitle || "").trim(); + let wasMatched = false; + + if (title1) { + for (var j = 0; j < genericPlayers.length; j++) { + if (matchedGenericIndices[j]) + continue; + let genericPlayer = genericPlayers[j]; + let title2 = String(genericPlayer.trackTitle || "").trim(); + + if (title2 && (title1.includes(title2) || title2.includes(title1))) { + let dataPlayer = genericPlayer; + let identityPlayer = specificPlayer; + + let scoreSpecific = (specificPlayer.trackArtUrl ? 1 : 0); + let scoreGeneric = (genericPlayer.trackArtUrl ? 1 : 0); + if (scoreSpecific > scoreGeneric) { + dataPlayer = specificPlayer; + } + + let virtualPlayer = { + "identity": identityPlayer.identity, + "desktopEntry": identityPlayer.desktopEntry, + "trackTitle": dataPlayer.trackTitle, + "trackArtist": dataPlayer.trackArtist, + "trackAlbum": dataPlayer.trackAlbum, + "trackArtUrl": dataPlayer.trackArtUrl, + "length": dataPlayer.length || 0, + "position": dataPlayer.position || 0, + "playbackState": dataPlayer.playbackState, + "isPlaying": dataPlayer.isPlaying || false, + "canPlay": dataPlayer.canPlay || false, + "canPause": dataPlayer.canPause || false, + "canGoNext": dataPlayer.canGoNext || false, + "canGoPrevious": dataPlayer.canGoPrevious || false, + "canSeek": dataPlayer.canSeek || false, + "canControl": dataPlayer.canControl || false, + "_stateSource": dataPlayer, + "_controlTarget": identityPlayer + }; + finalPlayers.push(virtualPlayer); + matchedGenericIndices[j] = true; + wasMatched = true; + break; + } + } + } + if (!wasMatched) { + finalPlayers.push(specificPlayer); + } + } + + // Add any generic players that were not matched + for (var i = 0; i < genericPlayers.length; i++) { + if (!matchedGenericIndices[i]) { + finalPlayers.push(genericPlayers[i]); + } + } + + // Filter for controllable players + let controllablePlayers = []; + for (var i = 0; i < finalPlayers.length; i++) { + let player = finalPlayers[i]; + if (player && player.canPlay) { + controllablePlayers.push(player); + } + } + return controllablePlayers; + } + + function findActivePlayer() { + let availablePlayers = getAvailablePlayers(); + if (availablePlayers.length === 0) { + //Logger.i("Media", "No active player found") + return null; + } + + // Prioritize the actively playing player --- + for (var i = 0; i < availablePlayers.length; i++) { + if (availablePlayers[i] && availablePlayers[i].playbackState === MprisPlaybackState.Playing) { + Logger.d("Media", "Found actively playing player: " + availablePlayers[i].identity); + selectedPlayerIndex = i; + return availablePlayers[i]; + } + } + + // fallback if nothing is playing) + const preferred = (Settings.data.audio.preferredPlayer || ""); + if (preferred !== "") { + for (var i = 0; i < availablePlayers.length; i++) { + const p = availablePlayers[i]; + const identity = String(p.identity || "").toLowerCase(); + const pref = preferred.toLowerCase(); + if (identity.includes(pref)) { + selectedPlayerIndex = i; + return p; + } + } + } + + if (selectedPlayerIndex < availablePlayers.length) { + return availablePlayers[selectedPlayerIndex]; + } else { + selectedPlayerIndex = 0; + return availablePlayers[0]; + } + } + + property bool autoSwitchingPaused: false + + function switchToPlayer(index) { + let availablePlayers = getAvailablePlayers(); + if (index >= 0 && index < availablePlayers.length) { + let newPlayer = availablePlayers[index]; + if (newPlayer !== currentPlayer) { + currentPlayer = newPlayer; + selectedPlayerIndex = index; + currentPosition = currentPlayer ? currentPlayer.position : 0; + Logger.d("Media", "Manually switched to player " + currentPlayer.identity); + } + } + } + + // Switch to the most recently active player + function updateCurrentPlayer() { + let newPlayer = findActivePlayer(); + if (newPlayer !== currentPlayer) { + currentPlayer = newPlayer; + currentPosition = currentPlayer ? currentPlayer.position : 0; + Logger.d("Media", "Switching player"); + } + } + + function playPause() { + if (currentPlayer) { + let stateSource = currentPlayer._stateSource || currentPlayer; + let controlTarget = currentPlayer._controlTarget || currentPlayer; + if (stateSource.playbackState === MprisPlaybackState.Playing) { + controlTarget.pause(); + } else { + controlTarget.play(); + } + } + } + + function play() { + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null; + if (target && target.canPlay) { + target.play(); + } + } + + function stop() { + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null; + if (target) { + target.stop(); + } + } + + function pause() { + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null; + if (target && target.canPause) { + target.pause(); + } + } + + function next() { + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null; + if (target && target.canGoNext) { + target.next(); + } + } + + function previous() { + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null; + if (target && target.canGoPrevious) { + target.previous(); + } + } + + function seek(position) { + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null; + if (target && target.canSeek) { + target.position = position; + currentPosition = position; + } + } + + function seekRelative(offset) { + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null; + if (target && target.canSeek && target.length > 0) { + let seekPosition = target.position + offset; + target.position = seekPosition; + currentPosition = seekPosition; + } + } + + // Seek to position based on ratio (0.0 to 1.0) + function seekByRatio(ratio) { + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null; + if (target && target.canSeek && target.length > 0) { + let seekPosition = ratio * target.length; + target.position = seekPosition; + currentPosition = seekPosition; + } + } + + // Update progress bar every second while playing + Timer { + id: positionTimer + interval: 1000 + running: currentPlayer && !root.isSeeking && currentPlayer.isPlaying && currentPlayer.length > 0 && currentPlayer.playbackState === MprisPlaybackState.Playing + repeat: true + onTriggered: { + if (currentPlayer && !root.isSeeking && currentPlayer.isPlaying && currentPlayer.playbackState === MprisPlaybackState.Playing) { + currentPosition = currentPlayer.position; + } else { + running = false; + } + } + } + + // Avoid overwriting currentPosition while seeking due to backend position changes + Connections { + target: currentPlayer + function onPositionChanged() { + if (!root.isSeeking && currentPlayer) { + currentPosition = currentPlayer.position; + } + } + function onPlaybackStateChanged() { + if (!root.isSeeking && currentPlayer) { + currentPosition = currentPlayer.position; + } + } + } + + // Reset position when switching to inactive player + onCurrentPlayerChanged: { + if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) { + currentPosition = 0; + } + } + + Timer { + id: playerStateMonitor + interval: 2000 // Check every 2 seconds + repeat: true + running: true + onTriggered: { + //Logger.d("MediaService", "playerStateMonitor triggered. autoSwitchingPaused: " + root.autoSwitchingPaused) + if (autoSwitchingPaused) + return; + // Only update if we don't have a playing player or if current player is paused + if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) { + updateCurrentPlayer(); + } + } + } + + // Update current player when available players change + Connections { + target: Mpris.players + function onValuesChanged() { + Logger.d("Media", "Players changed"); + updateCurrentPlayer(); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Media/SpectrumService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Media/SpectrumService.qml new file mode 100644 index 0000000..63c2c3e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Media/SpectrumService.qml @@ -0,0 +1,81 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Services.Pipewire +import qs.Commons +import qs.Services.UI + +Singleton { + id: root + + // TODO Remove in may 2026 + Component.onCompleted: { + _setBandsCount(); + } + + // Register a component that needs audio data, call this when a visualizer becomes active. + // Pass a unique identifier (e.g., "lockscreen", "controlcenter:screen1", "plugin:fancy-audiovisualizer") + function registerComponent(componentId) { + root._registeredComponents[componentId] = true; + root._registeredComponents = Object.assign({}, root._registeredComponents); + Logger.d("Spectrum", "Component registered:", componentId, "- total:", root._registeredCount); + } + + // Unregister a component when it no longer needs audio data. + function unregisterComponent(componentId) { + delete root._registeredComponents[componentId]; + root._registeredComponents = Object.assign({}, root._registeredComponents); + Logger.d("Spectrum", "Component unregistered:", componentId, "- total:", root._registeredCount); + } + + // Check if a component is registered + function isRegistered(componentId) { + return root._registeredComponents[componentId] === true; + } + + // Component registration - any component needing audio data registers here + property var _registeredComponents: ({}) + readonly property int _registeredCount: Object.keys(_registeredComponents).length + property bool _shouldRun: _registeredCount > 0 + + property var values: [] + property bool isIdle: true + + PwAudioSpectrum { + id: spectrum + node: Pipewire.defaultAudioSink + enabled: root._shouldRun + // TODO Uncomment this in may 2026 + // bandCount: Settings.data.audio.spectrumMirrored ? 32 : 64 + frameRate: Settings.data.audio.spectrumFrameRate + lowerCutoff: 50 + upperCutoff: 12000 + noiseReduction: 0.77 + smoothing: true + + onValuesChanged: { + root.values = spectrum.values; + } + + onIdleChanged: { + root.isIdle = spectrum.idle; + } + } + + // TODO Remove in may 2026 - temporary until noctalia-qs is fully propagated + Connections { + target: Settings.data.audio + function onSpectrumMirroredChanged() { + _setBandsCount(); + } + } + function _setBandsCount() { + const bandCount = Settings.data.audio.spectrumMirrored ? 32 : 64; + if (spectrum.bandCount !== undefined) { + spectrum.bandCount = bandCount; + } else if (spectrum.barCount !== undefined) { + spectrum.barCount = bandCount; + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Networking/BluetoothRssi.qml b/fedora/.config/quickshell/noctalia-shell/Services/Networking/BluetoothRssi.qml new file mode 100644 index 0000000..cc22127 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Networking/BluetoothRssi.qml @@ -0,0 +1,72 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import "../../Helpers/BluetoothUtils.js" as BluetoothUtils + +QtObject { + id: root + + // Controls + property bool enabled: false + property int intervalMs: 10000 + property var connectedDevices: [] + + // Output cache and version for bindings + property var cache: ({}) // addr -> percent (0..100) + property int version: 0 + + // Internal rotation state + property int _index: 0 + property string _currentAddr: "" + + // Single process reused for RSSI queries + property Process rssiProcess: Process { + id: proc + running: false + stdout: StdioCollector { + id: out + } + onExited: function (exitCode, exitStatus) { + try { + var text = out.text || ""; + var dbm = BluetoothUtils.parseRssiOutput(text); + if (root._currentAddr !== "" && dbm !== null) { + var pct = BluetoothUtils.dbmToPercent(dbm); + if (pct !== null) { + root.cache[root._currentAddr] = pct; + root.version++; + } + } + } catch (e) {} finally { + root._currentAddr = ""; + } + } + } + + // Periodic RSSI polling timer + property Timer rssiTimer: Timer { + interval: root.intervalMs + repeat: true + running: root.enabled + onTriggered: { + var list = root.connectedDevices || []; + if (!list || list.length === 0) + return; + if (root._index >= list.length) + root._index = 0; + var dev = list[root._index++]; + if (!dev) + return; + var addr = BluetoothUtils.macFromDevice(dev); + if (!addr || addr.length < 7) + return; + if (proc.running) + return; // avoid overlap + root._currentAddr = addr; + proc.command = ["sh", "-c", `bluetoothctl info "${addr}"`]; + try { + proc.running = true; + } catch (e) {} + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Networking/BluetoothService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Networking/BluetoothService.qml new file mode 100644 index 0000000..5635918 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Networking/BluetoothService.qml @@ -0,0 +1,542 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Bluetooth +import Quickshell.Io +import "../../Helpers/BluetoothUtils.js" as BluetoothUtils +import qs.Commons +import qs.Services.System +import qs.Services.UI + +Singleton { + id: root + + readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter + + // Power/availability state + readonly property bool bluetoothAvailable: !!adapter + readonly property bool enabled: adapter?.enabled ?? false + readonly property bool blocked: adapter?.state === BluetoothAdapter.Blocked + + // Exposed scanning flag for UI button state; reflects adapter discovery when available + readonly property bool scanningActive: adapter?.discovering ?? false + + // Adapter discoverability (advertising) flag + readonly property bool discoverable: adapter?.discoverable ?? false + readonly property var devices: adapter ? adapter.devices : null + readonly property var connectedDevices: { + if (!adapter || !adapter.devices) { + return []; + } + return adapter.devices.values.filter(dev => dev && dev.connected); + } + + // Experimental: best‑effort RSSI polling for connected devices (without root) + // Enabled in debug mode or via user setting in Settings > Network + property bool rssiPollingEnabled: Settings?.data?.network?.bluetoothRssiPollingEnabled || Settings?.isDebug || false + // Interval can be configured from Settings; defaults to 60s + property int rssiPollIntervalMs: Settings?.data?.network?.bluetoothRssiPollIntervalMs || 60000 + // RSSI helper sub‑component + property BluetoothRssi rssi: BluetoothRssi { + enabled: root.enabled && root.rssiPollingEnabled + intervalMs: root.rssiPollIntervalMs + connectedDevices: root.connectedDevices + } + + // Tunables for CLI pairing/connect flow + property int pairWaitSeconds: 45 + property int connectAttempts: 5 + property int connectRetryIntervalMs: 2000 + + // Interaction state + property bool pinRequired: false + + // Internal variables + property bool _discoveryWasRunning: false + property bool _ctlInit: false + property var _autoConnectQueue: [] + + // Persistent cache for per-device auto-connect toggle + property string cacheFile: Settings.cacheDir + "bluetooth_devices.json" + + FileView { + id: cacheFileView + path: root.cacheFile + printErrors: false + + JsonAdapter { + id: cacheAdapter + property var autoConnectSettings: ({}) + } + } + + // Handle system wakeup to force-poll and ensure state is up-to-date + Connections { + target: Time + function onResumed() { + Logger.i("Bluetooth", "System resumed - forcing state poll"); + ctlPollTimer.restart(); + } + } + + // Track adapter state changes + Connections { + target: adapter + function onStateChanged() { + if (!adapter || adapter.state === BluetoothAdapter.Enabling || adapter.state === BluetoothAdapter.Disabling) { + return; + } + checkAirplaneMode(); + } + function onEnabledChanged() { + if (adapter && adapter.enabled && Settings.data.network.bluetoothAutoConnect) { + autoConnectTimer.restart(); + } + } + } + + Connections { + target: Settings.data.network + function onBluetoothAutoConnectChanged() { + if (Settings.data.network.bluetoothAutoConnect && adapter && adapter.enabled) { + autoConnectTimer.restart(); + } else { + autoConnectTimer.stop(); + } + } + } + + Component.onCompleted: { + Logger.i("Bluetooth", "Service started"); + autoConnectTimer.restart(); + } + + Timer { + id: autoConnectTimer + interval: 1500 + repeat: false + onTriggered: attemptAutoConnect() + } + + Timer { + id: autoConnectStepTimer + interval: 500 + repeat: false + onTriggered: { + var device = root._autoConnectQueue.shift(); + if (device && device.paired && !device.connected && !device.blocked) { + Logger.i("Bluetooth", "Auto-connecting to:", device.name || device.deviceName); + connectDeviceWithTrust(device); + } + if (root._autoConnectQueue.length > 0) { + autoConnectStepTimer.restart(); + } + } + } + + Timer { + id: ctlPollTimer + interval: 250 + running: false + onTriggered: { + if (!adapter || !ProgramCheckerService.bluetoothctlAvailable) { + return; + } + ctlPollProcess.running = true; + } + } + + // Adapter power (enable/disable) via bluetoothctl + function setBluetoothEnabled(state) { + if (!adapter) { + Logger.d("Bluetooth", "Enable/Disable skipped: no adapter"); + return; + } + try { + adapter.enabled = state; + Logger.i("Bluetooth", "SetBluetoothEnabled", state); + } catch (e) { + Logger.w("Bluetooth", "Enable/Disable failed", e); + ToastService.showWarning(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.state-change-failed")); + } + } + + // Check if airplane mode has been toggled + function checkAirplaneMode() { + var isAirplaneModeActive = !NetworkService.wifiEnabled && adapter.state === BluetoothAdapter.Blocked; + if (isAirplaneModeActive && !NetworkService.airplaneModeEnabled) { + NetworkService.airplaneModeToggled = true; + NetworkService.airplaneModeEnabled = true; + ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.enabled"), "plane"); + Logger.i("AirplaneMode", "Enabled"); + } else if (!isAirplaneModeActive && NetworkService.airplaneModeEnabled) { + NetworkService.airplaneModeToggled = true; + NetworkService.airplaneModeEnabled = false; + ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.disabled"), "plane-off"); + Logger.i("AirplaneMode", "Disabled"); + } else if (adapter.enabled) { + ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.enabled"), "bluetooth"); + Logger.d("Bluetooth", "Adapter enabled"); + } else { + ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.disabled"), "bluetooth-off"); + Logger.d("Bluetooth", "Adapter disabled"); + } + } + + // Unify discovery controls + function setScanActive(active) { + if (!adapter) { + Logger.d("Bluetooth", "Scan request ignored: adapter unavailable"); + return; + } + try { + if (active || adapter.discovering) { // Only attempt to set if activating, or if deactivating and currently currently discovering + adapter.discovering = active; + } + } catch (e) { + Logger.e("Bluetooth", "setScanActive failed", e); + } + } + + // Toggle adapter discoverability (advertising visibility) via bluetoothctl + function setDiscoverable(state) { + if (!adapter) { + Logger.d("Bluetooth", "Discoverable change skipped: no adapter"); + return; + } + try { + adapter.discoverable = state; + Logger.i("Bluetooth", "Discoverable state set to:", state); + } catch (e) { + Logger.w("Bluetooth", "Failed to change discoverable state", e); + ToastService.showWarning(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.discoverable-change-failed")); + } + } + + function sortDevices(devices) { + return devices.sort(function (a, b) { + var aName = a.name || a.deviceName || ""; + var bName = b.name || b.deviceName || ""; + + var aHasRealName = aName.indexOf(" ") !== -1 && aName.length > 3; + var bHasRealName = bName.indexOf(" ") !== -1 && bName.length > 3; + + if (aHasRealName && !bHasRealName) { + return -1; + } + if (!aHasRealName && bHasRealName) { + return 1; + } + + var aSignal = (a.signalStrength !== undefined && a.signalStrength > 0) ? a.signalStrength : 0; + var bSignal = (b.signalStrength !== undefined && b.signalStrength > 0) ? b.signalStrength : 0; + return bSignal - aSignal; + }); + } + + function getDeviceIcon(device) { + if (!device) { + return "bt-device-generic"; + } + return BluetoothUtils.deviceIcon(device.name || device.deviceName, device.icon); + } + + function canConnect(device) { + if (!device) { + return false; + } + return !device.connected && (device.paired || device.trusted) && !device.pairing && !device.blocked; + } + + function canDisconnect(device) { + if (!device) { + return false; + } + return device.connected && !device.pairing && !device.blocked; + } + + // Textual signal quality (translated) + function getSignalStrength(device) { + var p = getSignalPercent(device); + if (p === null) { + return I18n.tr("bluetooth.panel.signal-text-unknown"); + } + if (p >= 80) { + return I18n.tr("bluetooth.panel.signal-text-excellent"); + } + if (p >= 60) { + return I18n.tr("bluetooth.panel.signal-text-good"); + } + if (p >= 40) { + return I18n.tr("bluetooth.panel.signal-text-fair"); + } + if (p >= 20) { + return I18n.tr("bluetooth.panel.signal-text-poor"); + } + return I18n.tr("bluetooth.panel.signal-text-very-poor"); + } + + // Numeric helpers for UI rendering + function getSignalPercent(device) { + // Establish binding dependency so UI updates when RSSI cache changes + var _v = rssi.version; + return BluetoothUtils.signalPercent(device, rssi.cache, _v); + } + + function getBatteryPercent(device) { + return BluetoothUtils.batteryPercent(device); + } + + function getSignalIcon(device) { + var p = getSignalPercent(device); + return BluetoothUtils.signalIcon(p); + } + + function isDeviceBusy(device) { + if (!device) { + return false; + } + return device.pairing || device.state === BluetoothDevice.Disconnecting || device.state === BluetoothDevice.Connecting; + } + + // Return a stable unique key for a device (prefer MAC address) + function deviceKey(device) { + return BluetoothUtils.deviceKey(device); + } + + // Deduplicate a list of devices using the stable key + function dedupeDevices(devList) { + return BluetoothUtils.dedupeDevices(devList); + } + + // Separate capability helpers + function canPair(device) { + if (!device) { + return false; + } + return !device.connected && !device.paired && !device.trusted && !device.pairing && !device.blocked; + } + + // Pairing and unpairing helpers + function pairDevice(device) { + if (!device) { + return; + } + ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.pairing"), "bluetooth"); + try { + pairWithBluetoothctl(device); + } catch (e) { + Logger.w("Bluetooth", "pairDevice failed", e); + ToastService.showWarning(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.pair-failed")); + } + } + + function submitPin(pin) { + if (pairingProcess.running) { + pairingProcess.write(pin + "\n"); + root.pinRequired = false; + } + } + + function cancelPairing() { + if (pairingProcess.running) { + pairingProcess.running = false; + } + root.pinRequired = false; + } + + // Pair using bluetoothctl which registers its own BlueZ agent internally. + function pairWithBluetoothctl(device) { + if (!device) { + return; + } + var addr = BluetoothUtils.macFromDevice(device); + if (!addr || addr.length < 7) { + Logger.w("Bluetooth", "pairWithBluetoothctl: no valid address for device"); + return; + } + + Logger.i("Bluetooth", "pairWithBluetoothctl", addr); + + if (pairingProcess.running) { + pairingProcess.running = false; + } + root.pinRequired = false; + + const pairWait = Math.max(5, Number(root.pairWaitSeconds) | 0); + const attempts = Math.max(1, Number(root.connectAttempts) | 0); + const intervalMs = Math.max(500, Number(root.connectRetryIntervalMs) | 0); + const intervalSec = Math.max(1, Math.round(intervalMs / 1000)); + + // Temporarily pause discovery during pair/connect to reduce HCI churn + root._discoveryWasRunning = root.scanningActive; + if (root.scanningActive) { + root.setScanActive(false); + } + + const scriptPath = Quickshell.shellDir + "/Scripts/python/src/network/bluetooth-pair.py"; + pairingProcess.command = ["python3", scriptPath, String(addr), String(pairWait), String(attempts), String(intervalSec)]; + pairingProcess.running = true; + } + + // Helper to run bluetoothctl and scripts with consistent error logging + function btExec(args) { + try { + Quickshell.execDetached(args); + } catch (e) { + Logger.w("Bluetooth", "btExec failed", e); + } + } + + // Status key for a device (untranslated) + function getStatusKey(device) { + if (!device) { + return ""; + } + try { + if (device.pairing) + return "pairing"; + if (device.blocked) + return "blocked"; + if (device.state === BluetoothDevice.Connecting) + return "connecting"; + if (device.state === BluetoothDevice.Disconnecting) + return "disconnecting"; + } catch (_) {} + return ""; + } + + function unpairDevice(device) { + forgetDevice(device); + } + + function getDeviceAutoConnect(device) { + if (!device || !device.address || !cacheAdapter.autoConnectSettings) { + return false; + } + const mac = device.address; + const settings = cacheAdapter.autoConnectSettings[mac]; + return settings ? !!settings.autoConnect : false; + } + + function setDeviceAutoConnect(device, enabled) { + if (!device || !device.address) { + return; + } + const mac = device.address; + let settings = cacheAdapter.autoConnectSettings || ({}); + if (enabled) { + settings[mac] = { + autoConnect: true, + deviceName: device.name || device.deviceName || "" + }; + } else { + delete settings[mac]; + } + cacheAdapter.autoConnectSettings = settings; + cacheFileView.writeAdapter(); + } + + function attemptAutoConnect() { + if (NetworkService.airplaneModeEnabled || !adapter || !adapter.enabled || !Settings.data.network.bluetoothAutoConnect) { + return; + } + + _autoConnectQueue = adapter.devices.values.filter(dev => dev && dev.paired && !dev.connected && !dev.blocked && getDeviceAutoConnect(dev) === true); + + if (root._autoConnectQueue.length > 0) { + autoConnectStepTimer.restart(); + } + } + + function connectDeviceWithTrust(device) { + if (!device) { + return; + } + try { + device.trusted = true; + device.connect(); + } catch (e) { + Logger.w("Bluetooth", "connectDeviceWithTrust failed", e); + ToastService.showWarning(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.connect-failed")); + } + } + + function disconnectDevice(device) { + if (!device) { + return; + } + try { + device.disconnect(); + } catch (e) { + Logger.w("Bluetooth", "disconnectDevice failed", e); + ToastService.showWarning(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.disconnect-failed")); + } + } + + function forgetDevice(device) { + if (!device) { + return; + } + try { + device.trusted = false; + device.forget(); + } catch (e) { + Logger.w("Bluetooth", "forgetDevice failed", e); + ToastService.showWarning(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.forget-failed")); + } + } + + // Poll Bluetooth power state with bluetoothctl to handle a Quickshell bug on resume after suspend + Process { + id: ctlPollProcess + command: ["bluetoothctl", "show"] + running: false + stdout: StdioCollector { + onStreamFinished: { + var powered = false; + var mp = text.match(/\bPowered:\s*(yes|no)\b/i); + if (mp) { + powered = mp[1].toLowerCase() === 'yes'; + } + if (adapter.enabled !== powered) { + adapter.enabled = powered; + } + } + } + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.d("Bluetooth", "Failed to parse bluetoothctl show output" + text); + } + } + } + } + + // Interactive pairing process + Process { + id: pairingProcess + stdout: SplitParser { + onRead: data => { + Logger.d("Bluetooth", data); + if (data.indexOf("PIN_REQUIRED") !== -1) { + root.pinRequired = true; + Logger.i("Bluetooth", "PIN required for pairing"); + } + } + } + onExited: { + root.pinRequired = false; + Logger.i("Bluetooth", "Pairing process exited."); + // Restore discovery if we paused it + if (root._discoveryWasRunning) { + root.setScanActive(true); + } + root._discoveryWasRunning = false; + } + environment: ({ + "LC_ALL": "C" + }) + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Networking/NetworkService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Networking/NetworkService.qml new file mode 100644 index 0000000..8f1f106 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Networking/NetworkService.qml @@ -0,0 +1,1161 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Networking +import qs.Commons +import qs.Services.System +import qs.Services.UI + +Singleton { + id: root + // Shared core (read-only) properties + readonly property bool wifiAvailable: _wifiAvailable + readonly property bool ethernetAvailable: _ethernetAvailable + readonly property bool internetConnectivity: _internetConnectivity + readonly property string networkConnectivity: _networkConnectivity + + // Supported Wi-Fi security types + readonly property var supportedSecurityTypes: [ + { + key: "open", + name: I18n.tr("wifi.panel.security-open") + }, + { + key: "wep", + name: I18n.tr("wifi.panel.security-wep") + }, + { + key: "wpa-psk", + name: I18n.tr("wifi.panel.security-wpa") + }, + { + key: "wpa2-psk", + name: I18n.tr("wifi.panel.security-wpa23") + }, + { + key: "sae", + name: I18n.tr("wifi.panel.security-wpa3") + }, + { + key: "wpa-eap", + name: I18n.tr("wifi.panel.security-wpa-ent") + }, + { + key: "wpa2-eap", + name: I18n.tr("wifi.panel.security-wpa2-ent") + }, + { + key: "wpa3-eap", + name: I18n.tr("wifi.panel.security-wpa3-ent") + } + ] + + // Core properties + property bool _wifiAvailable: false + property bool _ethernetAvailable: false + property string _networkConnectivity: "unknown" + property bool _internetConnectivity: false + property string lastError: "" + property int activeDetailsTtlMs: 10000 + + // Ethernet properties + property var ethernetInterfaces: ([]) + property var activeEthernetDetails: ({}) + property bool ethernetConnected: false + property string activeEthernetIf: "" + property bool ethernetDetailsLoading: false + property double activeEthernetDetailsTimestamp: 0 + + // Wi-Fi properties + readonly property bool wifiEnabled: Networking.wifiEnabled + property var networks: ({}) + property var activeWifiDetails: ({}) + property bool wifiConnected: false + property string activeWifiIf: "" + property bool wifiDetailsLoading: false + property double activeWifiDetailsTimestamp: 0 + property bool wifiInit: false + + // Wi-Fi adapter/connection properties + property bool connecting: false + property string connectingTo: "" + property string disconnectingFrom: "" + property string forgettingNetwork: "" + property bool scanPending: false + property bool scanningActive: false + property var existingProfiles: ({}) + + // Airplane mode status + property bool airplaneModeEnabled: false + property bool airplaneModeToggled: false + + Connections { + target: root + function onWifiEnabledChanged() { + if (!root.wifiInit) { + return; + } + wifiDebounce.restart(); + } + } + + // Start initial checks when nmcli becomes available + Connections { + target: ProgramCheckerService + function onNmcliAvailableChanged() { + if (ProgramCheckerService.nmcliAvailable) { + deviceStatusProcess.running = true; + connectivityCheckProcess.running = true; + } + } + } + + Component.onCompleted: { + Logger.i("Network", "Service started"); + wifiInitTimer.running = true; + + // Ensure initial detection if nmcli is already available at startup + if (ProgramCheckerService.nmcliAvailable) { + deviceStatusProcess.running = true; + connectivityCheckProcess.running = true; + } + } + + // Prevent an initial "Wi-Fi enabled" toast and trigger initial scan + Timer { + id: wifiInitTimer + interval: 500 + onTriggered: { + root.wifiInit = true; + if (root.wifiEnabled) { + scan(); + } + if (!root.wifiEnabled && BluetoothService.blocked) { + root.airplaneModeEnabled = true; + } + } + } + + // Debounce to prevent multiple toast notifications from transient states + Timer { + id: wifiDebounce + interval: 300 + onTriggered: { + if (!ProgramCheckerService.nmcliAvailable) { + return; + } + if (root.airplaneModeToggled) { + root.airplaneModeToggled = false; + if (root.wifiEnabled) { + scan(); + } else { + root.networks = ({}); + } + return; + } + var isAirplaneModeActive = !root.wifiEnabled && BluetoothService.blocked; + // Extra check for Airplane Mode if Bluetooth has been blocked before Wi-Fi + if (isAirplaneModeActive && !root.airplaneModeEnabled) { + root.airplaneModeEnabled = true; + ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.enabled"), "plane"); + Logger.i("AirplaneMode", "Enabled"); + root.networks = ({}); + return; + } + // Extra check for Airplane Mode if Wi-Fi has been unblocked before Bluetooth + if (!isAirplaneModeActive && root.airplaneModeEnabled) { + root.airplaneModeEnabled = false; + ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.disabled"), "plane-off"); + Logger.i("AirplaneMode", "Disabled"); + scan(); + return; + } + if (root.wifiEnabled) { + ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("common.enabled"), "wifi"); + scan(); + } else { + ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("common.disabled"), "wifi-off"); + root.networks = ({}); + } + } + } + + // Internet connectivity check timer + Timer { + id: connectivityCheckTimer + interval: 15000 + running: ProgramCheckerService.nmcliAvailable && (root.ethernetConnected || root.wifiConnected) + repeat: true + onTriggered: connectivityCheckProcess.running = true + } + + // Delayed scan timer + Timer { + id: delayedScanTimer + interval: 7000 + onTriggered: scan() + } + + // Core functions + function setWifiEnabled(enabled) { + if (!ProgramCheckerService.nmcliAvailable) { + return; + } + Logger.i("Wi-Fi", "SetWifiEnabled", enabled); + Networking.wifiEnabled = enabled; + } + + function setAirplaneMode(state) { + if (state) { + Quickshell.execDetached(["rfkill", "block", "all"]); + } else { + Quickshell.execDetached(["rfkill", "unblock", "all"]); + } + } + + function scan() { + if (!ProgramCheckerService.nmcliAvailable || !root.wifiEnabled) { + return; + } + lastError = ""; + + // If scanning in progress, mark as pending to trigger another scan when current when finished. + if (profileCheckProcess.running || scanProcess.running) { + root.scanPending = true; + return; + } + + // Get existing profiles first, then scan + profileCheckProcess.running = true; + root.scanningActive = true; + Logger.d("Network", "Scanning Wi-Fi networks..."); + } + + function connect(ssid, password = "", isHidden = false, securityKey = "", identity = "", enterpriseConfig = {}) { + if (!ProgramCheckerService.nmcliAvailable || connecting) { + return; + } + + const isSaved = (networks[ssid] && networks[ssid].existing); + const isEnt = securityKey ? isEnterprise(securityKey) : isEnterprise(networks[ssid] ? networks[ssid].security : ""); + + connecting = true; + connectingTo = ssid; + lastError = ""; + + connectProcess.ssid = ssid; + connectProcess.password = password; + connectProcess.isHidden = isHidden; + + if (isSaved) { + connectProcess.mode = "saved"; + } else if (isEnt || securityKey === "wep" || (securityKey && securityKey !== "open" && securityKey !== "wpa-psk" && securityKey !== "wpa2-psk")) { + connectProcess.mode = "manual"; + connectProcess.securityKey = securityKey || (networks[ssid] ? networks[ssid].security : "wpa-psk"); + connectProcess.identity = identity; + connectProcess.eap = enterpriseConfig.eap || "peap"; + connectProcess.phase2 = enterpriseConfig.phase2 || "mschapv2"; + connectProcess.anonIdentity = enterpriseConfig.anonIdentity || ""; + connectProcess.caCert = enterpriseConfig.caCert || ""; + } else { + connectProcess.mode = "new"; + } + + connectProcess.running = true; + } + + function disconnect(ssid) { + if (!ProgramCheckerService.nmcliAvailable) { + return; + } + disconnectingFrom = ssid; + disconnectProcess.ssid = ssid; + disconnectProcess.running = true; + } + + function forget(ssid) { + if (!ProgramCheckerService.nmcliAvailable) { + return; + } + forgettingNetwork = ssid; + + // Remove from system + forgetProcess.ssid = ssid; + forgetProcess.running = true; + } + + // Refresh details for the currently active Wi‑Fi link + function refreshActiveWifiDetails() { + const now = Date.now(); + if (wifiDetailsLoading || (activeWifiIf && wifiConnected && activeWifiDetails && (now - activeWifiDetailsTimestamp) < activeDetailsTtlMs)) { + return; + } + if (wifiConnected && activeWifiIf) { + wifiDetailsLoading = true; + deviceStatusProcess.running = true; + } + } + + // Refresh details for the currently active Ethernet link + function refreshActiveEthernetDetails() { + const now = Date.now(); + if (ethernetDetailsLoading || activeEthernetIf && activeEthernetDetails && (now - activeEthernetDetailsTimestamp) < activeDetailsTtlMs) { + return; + } + if (ethernetConnected && activeEthernetIf) { + ethernetDetailsLoading = true; + deviceStatusProcess.running = true; + } + } + + // Helper function to immediately update network status + function updateNetworkStatus(ssid, connected) { + let nets = networks; + + // Update all networks connected status + for (let key in nets) { + if (nets[key].connected && key !== ssid) { + nets[key].connected = false; + } + } + // Update the target network if it exists + if (nets[ssid]) { + nets[ssid].connected = connected; + nets[ssid].existing = true; + } else if (connected) { + // Create a temporary entry if network doesn't exist yet + nets[ssid] = { + "ssid": ssid, + "security": "--", + "signal": 100, + "connected": true, + "existing": true + }; + } + // Trigger property change notification + networks = ({}); + networks = nets; + } + + // Helper functions + function getSignalInfo(signal, isConnected) { + let icon = ""; + if (isConnected) { + if (root._networkConnectivity === "limited") { + icon = "wifi-exclamation"; + } else if (root._networkConnectivity === "portal" || root._networkConnectivity === "unknown") { + icon = "wifi-question"; + } + } + const label = signal >= 80 ? I18n.tr("wifi.signal.excellent") : signal >= 60 ? I18n.tr("wifi.signal.good") : signal >= 35 ? I18n.tr("wifi.signal.fair") : signal >= 15 ? I18n.tr("wifi.signal.poor") : I18n.tr("wifi.signal.weak"); + if (!icon) { + icon = signal >= 80 ? "wifi" : signal >= 60 ? "wifi-3" : signal >= 35 ? "wifi-2" : signal >= 15 ? "wifi-1" : "wifi-0"; + } + return { + icon, + label + }; + } + + function isSecured(security) { + return security && security !== "--" && security.trim() !== ""; + } + + function isEnterprise(security) { + if (!security) { + return false; + } + const s = security.toUpperCase(); + return s.indexOf("802.1X") !== -1 || s.indexOf("EAP") !== -1 || s.indexOf("ENTERPRISE") !== -1; + } + + function parseIpDetails(text) { + const details = { + connectionName: "", + ipv4: "", + gateway4: "", + dns4: [], + ipv6: [], + gateway6: [], + dns6: [], + hwAddr: "", + speed: "" + }; + const addUnique = (arr, val) => { + if (val && arr.indexOf(val) === -1) { + arr.push(val); + } + }; + const handlers = { + "GENERAL.CONNECTION": v => { + details.connectionName = v; + }, + "GENERAL.HWADDR": v => { + details.hwAddr = v; + }, + "CAPABILITIES.SPEED": v => { + if (v && v !== "unknown") { + details.speed = v; + } + }, + "IP4.ADDRESS": v => { + details.ipv4 = v.split("/")[0]; + }, + "IP4.GATEWAY": v => { + details.gateway4 = v; + }, + "IP6.ADDRESS": v => { + addUnique(details.ipv6, v.split("/")[0]); + }, + "IP6.GATEWAY": v => { + addUnique(details.gateway6, v); + }, + "IP4.DNS": v => { + addUnique(details.dns4, v); + }, + "IP6.DNS": v => { + addUnique(details.dns6, v); + } + }; + const lines = text.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (!line) { + continue; + } + const idx = line.indexOf(":"); + if (idx === -1) { + continue; + } + const key = line.substring(0, idx).replace(/\[\d+\]$/, ""); + const val = line.substring(idx + 1).trim(); + if (handlers[key]) { + handlers[key](val); + } + } + return details; + } + + // Functions used in /Modules/Panels/ControlCenter/Widgets/Network.qml & /Modules/Bar/Widgets/Network.qml + function getStatusText(showSpeed = false) { + // This variable can be tied to a toggle + if (root.connecting) { + return root.connectingTo ? I18n.tr("common.connecting") + " " + root.connectingTo : I18n.tr("common.connecting"); + } + + if (NetworkService.airplaneModeEnabled) { + return I18n.tr("toast.airplane-mode.title"); + } + if (!root.wifiEnabled) { + return ""; + } + + // Ethernet + if (root.ethernetConnected) { + const eth = root.activeEthernetDetails; + const name = eth.connectionName || (root.ethernetInterfaces.length > 0 ? root.ethernetInterfaces[0].connectionName : "") || ""; + const speed = eth.speed || ""; + return (name + (showSpeed && speed ? " - " + speed : "")); + } + + // Wi-Fi + if (root.wifiConnected) { + const wl = root.activeWifiDetails; + const speed = wl.rateShort || wl.rate || ""; + const connectedNet = Object.values(root.networks).find(net => net.connected); + const name = connectedNet ? connectedNet.ssid : (wl.connectionName || ""); + return (name + (showSpeed && speed ? " - " + speed : "")); + } + return ""; + } + + function getIcon(forceEthernet = false) { + if (NetworkService.airplaneModeEnabled && !forceEthernet) { + return "plane"; + } + + // 1. Ethernet Priority: Show Ethernet icon if connected OR if specifically requested (Panel) + if (root.ethernetConnected || forceEthernet) { + switch (root._networkConnectivity) { + case "limited": + return "ethernet-exclamation"; + case "portal": + case "unknown": + return "ethernet-question"; + case "full": + return "ethernet"; + default: + return "ethernet-off"; + } + } + + // 2. Wi-Fi Fallback + if (root.wifiAvailable || !forceEthernet) { + const networkCount = Object.values(root.networks).length; + if (!root.wifiEnabled) { + return "wifi-off"; + } + if (root.wifiConnected) { + let s = (root.activeWifiDetails && root.activeWifiDetails.signal !== undefined && root.activeWifiDetails.signal !== "") ? root.activeWifiDetails.signal : 0; + return root.getSignalInfo(s, true).icon; + } + if (root.connecting || networkCount > 0) { + return "wifi-question"; + } + } + return (root.ethernetAvailable || root.ethernetConnected) ? "ethernet-off" : root.wifiAvailable ? "wifi-0" : "wifi-off"; + } + + // Processes + // Discover connected interface[s] and fetch details [1] + Process { + id: deviceStatusProcess + running: false + command: ["sh", "-c", "nmcli -t -f GENERAL.DEVICE,GENERAL.TYPE,GENERAL.STATE,GENERAL.CONNECTION,GENERAL.HWADDR,IP4.ADDRESS,IP4.GATEWAY,IP4.DNS,IP6.ADDRESS,IP6.GATEWAY,IP6.DNS,CAPABILITIES.SPEED device show; echo \"------\"; nmcli -t -f IN-USE,SIGNAL,RATE,CHAN,FREQ,BANDWIDTH device wifi list"] + environment: ({ + "LC_ALL": "C" + }) + + stdout: StdioCollector { + onStreamFinished: { + const outputParts = text.split("------"); + const deviceText = outputParts[0]; + const wifiText = outputParts[1] || ""; + + let lines = deviceText.split("\n"); + let deviceBlocks = []; + let currentBlock = []; + + for (let i = 0; i < lines.length; i++) { + let line = lines[i].trim(); + if (!line) { + continue; + } + if (line.startsWith("GENERAL.DEVICE:")) { + if (currentBlock.length > 0) { + deviceBlocks.push(currentBlock); + } + currentBlock = [line]; + } else if (currentBlock.length > 0) { + currentBlock.push(line); + } + } + if (currentBlock.length > 0) { + deviceBlocks.push(currentBlock); + } + + let activeEthIf = ""; + let activeWifiIf = ""; + let wifiAvailable = false; + let ethernetAvailable = false; + let ethList = []; + + let newActiveWifiDetails = ({}); + let newActiveEthernetDetails = ({}); + + for (let b = 0; b < deviceBlocks.length; b++) { + let block = deviceBlocks[b]; + let blockText = block.join("\n"); + let details = root.parseIpDetails(blockText); + + let name = ""; + let type = ""; + let stateStr = ""; + + for (let l = 0; l < block.length; l++) { + let line = block[l]; + if (line.startsWith("GENERAL.DEVICE:")) { + name = line.substring(15).trim(); + } else if (line.startsWith("GENERAL.TYPE:")) { + type = line.substring(13).trim(); + } else if (line.startsWith("GENERAL.STATE:")) { + stateStr = line.substring(14).trim(); + } + } + + if (stateStr.indexOf("(unmanaged)") !== -1) { + continue; + } + let isConnected = stateStr.indexOf("(connected)") !== -1; + + if (type === "ethernet") { + ethernetAvailable = true; + let stateName = stateStr.split(" ")[1] ? stateStr.split(" ")[1].replace(/[()]/g, "") : stateStr; + ethList.push({ + ifname: name, + state: stateName, + connected: isConnected, + connectionName: details.connectionName + }); + if (isConnected && !activeEthIf) { + activeEthIf = name; + newActiveEthernetDetails = details; + newActiveEthernetDetails.ifname = name; + } + } else if (type === "wifi") { + wifiAvailable = true; + if (isConnected && !activeWifiIf) { + activeWifiIf = name; + newActiveWifiDetails = details; + newActiveWifiDetails.ifname = name; + } + } + } + + // Parse Wi-Fi details if active + if (activeWifiIf && wifiText) { + let rate = ""; + let freq = ""; + let channel = ""; + let width = ""; + let signal = ""; + + const wifiLines = wifiText.split("\n"); + for (let i = 0; i < wifiLines.length; i++) { + const line = wifiLines[i].trim(); + if (line.startsWith("*")) { + const parts = line.split(":"); + if (parts.length >= 6) { + signal = parts[1]; + rate = parts[2]; + channel = parts[3]; + freq = parts[4].replace(" MHz", ""); + width = parts[5]; + } + break; + } + } + + let band = ""; + if (freq) { + const f = +freq; + if (f) { + switch (true) { + case (f >= 5925 && f < 7125): + band = "6 GHz"; + break; + case (f >= 5150 && f < 5925): + band = "5 GHz"; + break; + case (f >= 2400 && f < 2500): + band = "2.4 GHz"; + break; + default: + band = `${f} MHz`; + } + } + } + + let rateShort = ""; + if (rate) { + var rparts = rate.trim().split(" "); + var compact = []; + for (var i = 0; i < rparts.length; i++) { + if (rparts[i]) { + compact.push(rparts[i]); + } + } + var unitIdx = -1; + for (var j = 0; j < compact.length; j++) { + var token = compact[j].toLowerCase(); + if (token === "mbit/s" || token === "mb/s" || token === "mbits/s") { + unitIdx = j; + break; + } + } + if (unitIdx > 0) { + var num = compact[unitIdx - 1]; + var parsed = parseFloat(num); + if (!isNaN(parsed)) { + rateShort = parsed + " Mbit/s"; + } + } + if (!rateShort) { + rateShort = compact.slice(0, 2).join(" "); + } + } + + let enhancedBand = band; + if (channel && width && width !== "0 MHz") { + enhancedBand = `${band} / ${channel} (${width})`; + } else if (channel) { + enhancedBand = `${band} / ${channel}`; + } + + if (newActiveWifiDetails.speed) { + newActiveWifiDetails.rate = newActiveWifiDetails.speed.replace(/Mb\/s/i, "Mbit/s"); + newActiveWifiDetails.rateShort = newActiveWifiDetails.rate; + } else { + newActiveWifiDetails.rate = rate; + newActiveWifiDetails.rateShort = rateShort; + } + newActiveWifiDetails.band = enhancedBand; + newActiveWifiDetails.channel = channel; + newActiveWifiDetails.width = width; + newActiveWifiDetails.signal = signal; + } + + root._wifiAvailable = wifiAvailable; + root._ethernetAvailable = ethernetAvailable; + root.ethernetConnected = (activeEthIf !== ""); + root.wifiConnected = (activeWifiIf !== ""); + + Logger.d("Network", "Device sync: wifiAvailable: " + wifiAvailable + ", ethAvailable: " + ethernetAvailable + ", wifiConnected: " + root.wifiConnected + " (" + activeWifiIf + "), ethConnected: " + root.ethernetConnected + " (" + activeEthIf + ")"); + + ethList.sort((a, b) => (a.connected !== b.connected) ? (a.connected ? -1 : 1) : a.ifname.localeCompare(b.ifname)); + root.ethernetInterfaces = ethList; + + root.activeEthernetIf = activeEthIf; + root.activeEthernetDetails = newActiveEthernetDetails; + root.activeEthernetDetailsTimestamp = Date.now(); + root.ethernetDetailsLoading = false; + + root.activeWifiIf = activeWifiIf; + root.activeWifiDetails = newActiveWifiDetails; + root.activeWifiDetailsTimestamp = Date.now(); + root.wifiDetailsLoading = false; + } + } + stderr: StdioCollector { + onStreamFinished: { + if (text && text.trim()) { + Logger.w("Network", "nmcli device show stderr:", text.trim()); + } + root.ethernetDetailsLoading = false; + root.wifiDetailsLoading = false; + } + } + } + + // Process to check the internet connectivity of the connected network + Process { + id: connectivityCheckProcess + running: false + command: ["nmcli", "networking", "connectivity", "check"] + stdout: StdioCollector { + onStreamFinished: { + const r = text.trim(); + if (!r) { + return; + } + root._networkConnectivity = (r === "none") ? "unknown" : r; + root._internetConnectivity = (r === "full"); + } + } + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.w("Network", "Connectivity check error: " + text); + } + } + } + } + + // Helper process to get existing profiles + Process { + id: profileCheckProcess + running: false + command: ["nmcli", "-t", "-f", "NAME", "connection", "show"] + + stdout: StdioCollector { + onStreamFinished: { + var profiles = {}; + var lines = text.split("\n"); + for (var i = 0; i < lines.length; i++) { + var l = lines[i]; + if (l && l.trim()) { + profiles[l.trim()] = true; + } + } + root.existingProfiles = profiles; + scanProcess.running = true; + } + } + stderr: StdioCollector { + onStreamFinished: { + if (text && text.trim()) { + Logger.w("Network", "Profile check stderr:", text.trim()); + if (root.scanningActive) { + if (root.scanPending) { + root.scanPending = false; + delayedScanTimer.interval = 3000; + } else { + delayedScanTimer.interval = 5000; + } + delayedScanTimer.restart(); + } + } + } + } + } + + // Scan for Wi-Fi networks + Process { + id: scanProcess + running: false + command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list", "--rescan", "yes"] + + stdout: StdioCollector { + onStreamFinished: { + const lines = text.trim().split("\n"); + const networksMap = {}; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (!line) { + continue; + } + + // Parse SSID:SECURITY:SIGNAL:IN-USE + const parts = line.split(":"); + if (parts.length < 4) { + continue; + } + + const inUse = parts[parts.length - 1]; + const signal = parseInt(parts[parts.length - 2]) || 0; + let security = parts[parts.length - 3]; + if (security) { + security = security.replace("WPA2 WPA3", "WPA2/WPA3").replace("WPA1 WPA2", "WPA1/WPA2"); + } + const ssid = parts.slice(0, parts.length - 3).join(":"); + + if (ssid) { + const isConnected = (inUse === "*"); + if (!networksMap[ssid]) { + networksMap[ssid] = { + "ssid": ssid, + "security": security || "--", + "signal": signal, + "connected": isConnected, + "existing": !!root.existingProfiles[ssid] + }; + } else { + if (isConnected) { + networksMap[ssid].connected = true; + networksMap[ssid].signal = signal; + connectivityCheckProcess.running = true; + } else if (!networksMap[ssid].connected && signal > networksMap[ssid].signal) { + networksMap[ssid].signal = signal; + } + } + } + } + + // Logging & Diffing + const oldSSIDs = Object.keys(root.networks); + const newSSIDs = Object.keys(networksMap); + const newNetworks = newSSIDs.filter(s => oldSSIDs.indexOf(s) === -1); + const lostNetworks = oldSSIDs.filter(s => newSSIDs.indexOf(s) === -1); + + // Always update networks, this makes more reflective of state/signal. + root.networks = networksMap; + + if (newNetworks.length > 0 || lostNetworks.length > 0) { + if (newNetworks.length > 0) { + Logger.d("Network", "New Wi-Fi network appeared:", newNetworks.join(", ")); + } + if (lostNetworks.length > 0) { + Logger.d("Network", "Wi-Fi network disappeared:", lostNetworks.join(", ")); + } + Logger.d("Network", "Total Wi-Fi networks:", Object.keys(networksMap).length); + } + + if (Object.values(networksMap).some(n => n.connected)) { + root.refreshActiveWifiDetails(); + } + + if (root.scanPending) { + root.scanPending = false; + delayedScanTimer.interval = 100; + delayedScanTimer.restart(); + } + root.scanningActive = false; + } + } + + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + Logger.w("Network", "Scan error: " + text); + + // Even on error, if a scan was pending, try again + if (root.scanPending) { + root.scanPending = false; + delayedScanTimer.interval = 3000; + } else if (root.scanningActive) { + delayedScanTimer.interval = 10000; + } + delayedScanTimer.restart(); + } + root.scanningActive = false; + } + } + } + + // Connect to Wi-Fi network + Process { + id: connectProcess + property string mode: "new" // "saved", "new", or "manual" + property string ssid: "" + property string password: "" + property bool isHidden: false + // Manual properties + property string securityKey: "" + property string identity: "" + property string eap: "peap" + property string phase2: "mschapv2" + property string anonIdentity: "" + property string caCert: "" + running: false + + command: { + if (mode === "saved") { + return ["nmcli", "-t", "connection", "up", "id", ssid]; + } else if (mode === "manual") { + const nmArgs = ["connection", "add", "type", "wifi", "con-name", ssid, "ssid", ssid, "--", "802-11-wireless.hidden", isHidden ? "yes" : "no"]; + + if (securityKey === "wpa-psk" || securityKey === "wpa2-psk") { + nmArgs.push("wifi-sec.key-mgmt", "wpa-psk", "wifi-sec.psk", password); + } else if (securityKey === "sae") { + nmArgs.push("wifi-sec.key-mgmt", "sae", "wifi-sec.psk", password); + } else if (securityKey === "wep") { + nmArgs.push("wifi-sec.key-mgmt", "none", "wifi-sec.wep-key0", password); + } else if (securityKey && securityKey.indexOf("-eap") !== -1) { + nmArgs.push("wifi-sec.key-mgmt", "wpa-eap", "802-1x.eap", eap, "802-1x.phase2-auth", phase2, "802-1x.identity", identity, "802-1x.password", password); + if (anonIdentity) { + nmArgs.push("802-1x.anonymous-identity", anonIdentity); + } + if (caCert) { + nmArgs.push("802-1x.ca-cert", caCert); + } + } + + const script = ` + SSID="$1" + shift + # Find existing profile by Name and Type + UUID=$(nmcli -t -f NAME,UUID,TYPE connection show | awk -F: -v target="$SSID" '$1 == target && $3 == "802-11-wireless" { print $2; exit }') + + if [ -n "$UUID" ]; then + echo "Using existing profile: $UUID" + nmcli connection delete uuid "$UUID" 2>/dev/null || true + else + echo "Creating new profile for $SSID" + fi + nmcli "$@" + nmcli connection up id "$SSID" + `; + + return ["sh", "-c", script, "--", ssid].concat(nmArgs); + } else { + var cmd = ["nmcli", "-t", "device", "wifi", "connect", ssid]; + if (isHidden) { + cmd.push("hidden", "yes"); + } + if (password) { + cmd.push("password", password); + } + if (root.activeWifiIf) { + cmd.push("ifname", root.activeWifiIf); + } + return cmd; + } + } + + environment: ({ + "LC_ALL": "C" + }) + + stdout: StdioCollector { + onStreamFinished: { + const output = text.trim(); + if (!output || (output.indexOf("successfully activated") === -1 && output.indexOf("Connection successfully") === -1)) { + return; + } + + root.wifiConnected = true; + root.updateNetworkStatus(connectProcess.ssid, true); + root.refreshActiveWifiDetails(); // This needs wifiConnected true. + + root.connecting = false; + root.connectingTo = ""; + Logger.i("Network", "Connected to network: '" + connectProcess.ssid + "' (" + connectProcess.mode + ")"); + ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("toast.wifi.connected", { + "ssid": connectProcess.ssid + }), root.getIcon(false)); + + delayedScanTimer.interval = 5000; + delayedScanTimer.restart(); + } + } + + stderr: StdioCollector { + onStreamFinished: { + if (text.trim()) { + root.connecting = false; + root.connectingTo = ""; + + if (text.indexOf("Secrets were required") !== -1 || text.indexOf("no secrets provided") !== -1) { + root.lastError = I18n.tr("toast.wifi.incorrect-password"); + forget(connectProcess.ssid); + } else if (text.indexOf("No network with SSID") !== -1) { + root.lastError = I18n.tr("toast.wifi.network-not-found"); + } else if (text.indexOf("Timeout") !== -1) { + root.lastError = I18n.tr("toast.wifi.connection-timeout"); + } else { + root.lastError = I18n.tr("toast.wifi.connection-failed"); + } + + Logger.w("Network", "Connect error (" + connectProcess.mode + "): " + text); + ToastService.showWarning(I18n.tr("common.wifi"), root.lastError || I18n.tr("toast.wifi.connection-failed"), "wifi-exclamation"); + wifiConnected = false; + } + } + } + } + + // Disconnect from Wi-Fi network + Process { + id: disconnectProcess + property string ssid: "" + running: false + command: ["nmcli", "connection", "down", "id", ssid] + + stdout: StdioCollector { + onStreamFinished: { + Logger.i("Network", "Disconnected from network: '" + disconnectProcess.ssid + "'"); + root.wifiConnected = false; + ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("toast.wifi.disconnected", { + "ssid": disconnectProcess.ssid + }), "wifi-off"); + + // Immediately update UI on successful disconnect + root.updateNetworkStatus(disconnectProcess.ssid, false); + root.disconnectingFrom = ""; + + // Do a scan to refresh the list + delayedScanTimer.interval = 3000; + delayedScanTimer.restart(); + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.disconnectingFrom = ""; + if (text.trim()) { + Logger.w("Network", "Disconnect error: " + text); + } + // Still trigger a scan even on error + delayedScanTimer.interval = 5000; + delayedScanTimer.restart(); + } + } + } + + // Forget given Wi-Fi network + Process { + id: forgetProcess + property string ssid: "" + running: false + environment: ({ + "LC_ALL": "C" + }) + + // Try multiple common profile name patterns + command: { + var script = ` + ssid="$1" + deleted=false + + # Find existing profile by Name and Type + UUID=$(nmcli -t -f NAME,UUID,TYPE connection show | awk -F: -v target="$ssid" '$1 == target && $3 == "802-11-wireless" { print $2; exit }') + + if [ -n "$UUID" ]; then + if nmcli connection delete uuid "$UUID" 2>/dev/null; then + echo "Deleted profile: $ssid ($UUID)" + deleted=true + fi + fi + + # Fallback: try common patterns if UUID lookup failed + if [ "$deleted" = "false" ]; then + # Try "Auto $ssid" pattern + if nmcli connection delete id "Auto $ssid" 2>/dev/null; then + echo "Deleted profile: Auto $ssid" + deleted=true + fi + + # Try "$ssid 1", "$ssid 2", etc. patterns + for i in 1 2 3; do + if nmcli connection delete id "$ssid $i" 2>/dev/null; then + echo "Deleted profile: $ssid $i" + deleted=true + fi + done + fi + + if [ "$deleted" = "false" ]; then + echo "No profiles found for SSID: $ssid" + fi + `; + + return ["sh", "-c", script, "--", ssid]; + } + + stdout: StdioCollector { + onStreamFinished: { + Logger.i("Network", "Forget network: \"" + forgetProcess.ssid + "\""); + Logger.d("Network", text.trim().replace(/[\r\n]/g, " ")); + + // Update existing status immediately + let nets = root.networks; + if (nets[forgetProcess.ssid]) { + nets[forgetProcess.ssid].existing = false; + // Trigger property change + root.networks = ({}); + root.networks = nets; + } + + root.forgettingNetwork = ""; + + // Scan to verify the profile is gone + delayedScanTimer.interval = 5000; + delayedScanTimer.restart(); + } + } + + stderr: StdioCollector { + onStreamFinished: { + root.forgettingNetwork = ""; + if (text.trim() && text.indexOf("No profiles found") === -1) { + Logger.w("Network", "Forget error: " + text); + } + // Still Trigger a scan even on error + delayedScanTimer.interval = 5000; + delayedScanTimer.restart(); + } + } + } + + // Listen to NetworkManager events in real-time (roaming, auto-connect) -- ~9mb Memory usage. + Process { + id: networkMonitorProcess + running: ProgramCheckerService.nmcliAvailable + command: ["nmcli", "-t", "monitor"] + environment: ({ + "LC_ALL": "C" + }) + stdout: SplitParser { + onRead: data => { + if (data.endsWith(": connected") || data.endsWith(": disconnected")) { + Logger.d("Network", "State changed: " + data); + deviceStatusProcess.running = true; + connectivityCheckProcess.running = true; + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Networking/VPNService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Networking/VPNService.qml new file mode 100644 index 0000000..d867307 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Networking/VPNService.qml @@ -0,0 +1,287 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.UI + +Singleton { + id: root + + property var connections: ({}) + property bool refreshing: false + property bool connecting: false + property bool disconnecting: false + property string connectingUuid: "" + property string disconnectingUuid: "" + property string lastError: "" + property bool refreshPending: false + + readonly property var activeConnections: { + const result = []; + const map = connections; + for (const key in map) { + const conn = map[key]; + if (conn && conn.active) { + result.push(conn); + } + } + return result; + } + + readonly property var inactiveConnections: { + const result = []; + const map = connections; + for (const key in map) { + const conn = map[key]; + if (conn && !conn.active) { + result.push(conn); + } + } + return result; + } + + readonly property bool hasActiveConnection: activeConnections.length > 0 + + Timer { + id: refreshTimer + interval: 5000 + running: true + repeat: true + onTriggered: refresh() + } + + Timer { + id: delayedRefreshTimer + interval: 1000 + repeat: false + onTriggered: refresh() + } + + Component.onCompleted: { + Logger.i("VPN", "Service started"); + refresh(); + } + + function refresh() { + if (refreshing) { + refreshPending = true; + return; + } + refreshing = true; + lastError = ""; + refreshProcess.running = true; + } + + function connect(uuid) { + if (connecting || !uuid) { + return; + } + const conn = connections[uuid]; + if (!conn) { + return; + } + connecting = true; + connectingUuid = uuid; + lastError = ""; + connectProcess.uuid = uuid; + connectProcess.name = conn.name; + connectProcess.running = true; + } + + function disconnect(uuid) { + if (disconnecting || !uuid) { + return; + } + const conn = connections[uuid]; + if (!conn) { + return; + } + disconnecting = true; + disconnectingUuid = uuid; + lastError = ""; + disconnectProcess.uuid = uuid; + disconnectProcess.name = conn.name; + disconnectProcess.running = true; + } + + function toggle(uuid) { + const conn = connections[uuid]; + if (!conn) { + return; + } + if (conn.active) { + disconnect(uuid); + } else { + connect(uuid); + } + } + + function setConnection(uuid, data) { + if (!uuid) { + return; + } + const map = Object.assign({}, connections); + if (map[uuid]) { + map[uuid] = Object.assign({}, map[uuid], data); + connections = map; + } + } + + function scheduleRefresh(interval) { + delayedRefreshTimer.interval = interval; + delayedRefreshTimer.restart(); + } + + Process { + id: refreshProcess + running: false + command: ["nmcli", "-t", "-f", "NAME,UUID,TYPE,DEVICE", "connection", "show"] + + stdout: StdioCollector { + onStreamFinished: { + const lines = text.split("\n"); + const map = {}; + for (let i = 0; i < lines.length; ++i) { + const line = lines[i].trim(); + if (!line) { + continue; + } + const lastColonIdx = line.lastIndexOf(":"); + if (lastColonIdx === -1) { + continue; + } + const device = line.substring(lastColonIdx + 1); + const remaining = line.substring(0, lastColonIdx); + const secondLastColonIdx = remaining.lastIndexOf(":"); + if (secondLastColonIdx === -1) { + continue; + } + const type = remaining.substring(secondLastColonIdx + 1); + if (type !== "vpn" && type !== "wireguard") { + continue; + } + const remaining2 = remaining.substring(0, secondLastColonIdx); + const thirdLastColonIdx = remaining2.lastIndexOf(":"); + if (thirdLastColonIdx === -1) { + continue; + } + const uuid = remaining2.substring(thirdLastColonIdx + 1); + const name = remaining2.substring(0, thirdLastColonIdx); + if (!uuid || !name) { + continue; + } + const active = device && device !== "--"; + map[uuid] = { + "uuid": uuid, + "name": name, + "device": device, + "active": active + }; + } + connections = map; + const pending = refreshPending; + refreshing = false; + refreshPending = false; + if (pending) { + scheduleRefresh(200); + } + } + } + + stderr: StdioCollector { + onStreamFinished: { + const pending = refreshPending; + refreshing = false; + refreshPending = false; + if (text.trim()) { + lastError = text.split("\n")[0].trim(); + Logger.w("VPN", "Refresh error: " + text); + } + if (pending) { + scheduleRefresh(2000); + } + } + } + } + + Process { + id: connectProcess + property string uuid: "" + property string name: "" + running: false + command: ["nmcli", "connection", "up", "uuid", uuid] + + stdout: StdioCollector { + onStreamFinished: { + const output = text.trim(); + if (!output || (!output.includes("successfully activated") && !output.includes("Connection successfully"))) { + return; + } + setConnection(connectProcess.uuid, { + "active": true + }); + connecting = false; + connectingUuid = ""; + lastError = ""; + Logger.i("VPN", "Connected to " + connectProcess.name); + ToastService.showNotice(connectProcess.name, I18n.tr("toast.vpn.connected", { + "name": connectProcess.name + }), "shield-lock"); + scheduleRefresh(1000); + } + } + + stderr: StdioCollector { + onStreamFinished: { + const trimmed = text.trim(); + if (trimmed) { + lastError = trimmed.split("\n")[0].trim(); + Logger.w("VPN", "Connect error: " + trimmed); + ToastService.showWarning(connectProcess.name, lastError); + } + connecting = false; + connectingUuid = ""; + } + } + } + + Process { + id: disconnectProcess + property string uuid: "" + property string name: "" + running: false + command: ["nmcli", "connection", "down", "uuid", uuid] + + stdout: StdioCollector { + onStreamFinished: { + Logger.i("VPN", "Disconnected from " + disconnectProcess.name); + setConnection(disconnectProcess.uuid, { + "active": false, + "device": "" + }); + disconnecting = false; + disconnectingUuid = ""; + lastError = ""; + ToastService.showNotice(disconnectProcess.name, I18n.tr("toast.vpn.disconnected", { + "name": disconnectProcess.name + }), "shield-off"); + scheduleRefresh(1000); + } + } + + stderr: StdioCollector { + onStreamFinished: { + const trimmed = text.trim(); + if (trimmed) { + lastError = trimmed.split("\n")[0].trim(); + Logger.w("VPN", "Disconnect error: " + trimmed); + ToastService.showWarning(disconnectProcess.name, lastError); + } + disconnecting = false; + disconnectingUuid = ""; + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/GitHubService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/GitHubService.qml new file mode 100644 index 0000000..d23a4d2 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/GitHubService.qml @@ -0,0 +1,336 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.UI + +// GitHub API logic for contributors +Singleton { + id: root + + property string githubDataFile: Quickshell.env("NOCTALIA_GITHUB_FILE") || (Settings.cacheDir + "github.json") + property int githubUpdateFrequency: 60 * 60 // 1 hour expressed in seconds + property bool isFetchingData: false + readonly property alias data: adapter // Used to access via GitHubService.data.xxx.yyy + + // Public properties for easy access + property string latestVersion: I18n.tr("common.unknown") + property string latestQSVersion: I18n.tr("common.unknown") + property var contributors: [] + + // Avatar caching properties (simplified - uses ImageCacheService) + property var cachedAvatars: ({}) // username → file:// path + property bool avatarsCached: false // Track if we've already processed avatars + + property bool isInitialized: false + + FileView { + id: githubDataFileView + path: githubDataFile + printErrors: false + watchChanges: false // Disable to prevent reload on our own writes + Component.onCompleted: { + // Data loading handled by FileView onLoaded + } + onLoaded: { + if (!root.isInitialized) { + root.isInitialized = true; + loadFromCache(); + } + } + onLoadFailed: function (error) { + if (error.toString().includes("No such file") || error === 2) { + // No cache file exists, fetch fresh data + root.isInitialized = true; + fetchFromGitHub(); + } + } + + JsonAdapter { + id: adapter + + property string version: I18n.tr("common.unknown") + property string qsVersion: I18n.tr("common.unknown") + property var contributors: [] + property real timestamp: 0 + } + } + + // -------------------------------- + function init() { + Logger.i("GitHub", "Service started"); + // FileView will handle loading automatically via onLoaded + } + + // -------------------------------- + function loadFromCache() { + const now = Time.timestamp; + var needsRefetch = false; + + Logger.i("GitHub", "Checking cache - timestamp:", data.timestamp, "now:", now, "age:", data.timestamp ? Math.round((now - data.timestamp) / 60) : "N/A", "minutes"); + + if (!data.timestamp || (now >= data.timestamp + githubUpdateFrequency)) { + needsRefetch = true; + Logger.i("GitHub", "Cache expired or missing, scheduling fetch (update frequency:", Math.round(githubUpdateFrequency / 60), "minutes)"); + } else { + Logger.i("GitHub", "Cache is fresh, using cached data (age:", Math.round((now - data.timestamp) / 60) + " minutes)"); + } + + if (data.version) { + root.latestVersion = data.version; + } + if (data.qsVersion) { + root.latestQSVersion = data.qsVersion; + } + if (data.contributors && data.contributors.length > 0) { + root.contributors = data.contributors; + Logger.d("GitHub", "Loaded", data.contributors.length, "contributors from cache"); + } + + if (needsRefetch) { + fetchFromGitHub(); + } + } + + // -------------------------------- + function fetchFromGitHub() { + if (isFetchingData) { + Logger.d("GitHub", "GitHub data is still fetching"); + return; + } + + isFetchingData = true; + versionProcess.running = true; + qsVersionProcess.running = true; + contributorsProcess.running = true; + } + + // -------------------------------- + function saveData() { + data.timestamp = Time.timestamp; + Logger.d("GitHub", "Saving data to cache file:", githubDataFile, "with timestamp:", data.timestamp); + Logger.d("GitHub", "Data to save - version:", data.version, "qsVersion:", data.qsVersion, "contributors:", data.contributors.length); + + // Ensure cache directory exists + Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]); + + try { + // Write immediately instead of Qt.callLater to ensure it completes + githubDataFileView.writeAdapter(); + Logger.d("GitHub", "Cache file written successfully"); + } catch (error) { + Logger.e("GitHub", "Failed to write cache file:", error); + } + } + + // -------------------------------- + function checkAndSaveData() { + // Only save when all processes are finished + if (!versionProcess.running && !qsVersionProcess.running && !contributorsProcess.running) { + root.isFetchingData = false; + + // Check results + var anySucceeded = versionProcess.fetchSucceeded || qsVersionProcess.fetchSucceeded || contributorsProcess.fetchSucceeded; + var wasRateLimited = versionProcess.wasRateLimited || qsVersionProcess.wasRateLimited || contributorsProcess.wasRateLimited; + + if (anySucceeded) { + root.saveData(); + Logger.d("GitHub", "Successfully fetched data from GitHub"); + } else if (wasRateLimited) { + root.saveData(); + Logger.w("GitHub", "API rate limited - using cached data (retry in", Math.round(githubUpdateFrequency / 60), "minutes)"); + } else { + Logger.w("GitHub", "API request failed - using cached data without updating timestamp"); + } + + // Reset fetch flags for next time + versionProcess.fetchSucceeded = false; + versionProcess.wasRateLimited = false; + qsVersionProcess.fetchSucceeded = false; + qsVersionProcess.wasRateLimited = false; + contributorsProcess.fetchSucceeded = false; + contributorsProcess.wasRateLimited = false; + } + } + + // -------------------------------- + function resetCache() { + data.version = I18n.tr("common.unknown"); + data.qsVersion = I18n.tr("common.unknown"); + data.contributors = []; + data.timestamp = 0; + + // Try to fetch immediately + fetchFromGitHub(); + } + + // -------------------------------- + // Avatar Caching Functions (simplified - uses ImageCacheService) + // -------------------------------- + + function getAvatarPath(username) { + return cachedAvatars[username] || ""; + } + + function cacheTopContributorAvatars() { + if (contributors.length === 0) + return; + + avatarsCached = true; + + for (var i = 0; i < Math.min(contributors.length, 20); i++) { + var contributor = contributors[i]; + var username = contributor.login; + var avatarUrl = contributor.avatar_url; + + // Use closure to capture username + (function (uname, url) { + ImageCacheService.getCircularAvatar(url, uname, function (cachedPath, success) { + if (success) { + cachedAvatars[uname] = "file://" + cachedPath; + cachedAvatarsChanged(); + } + }); + })(username, avatarUrl); + } + } + + // -------------------------------- + // Hook into contributors change - only process once + onContributorsChanged: { + if (contributors.length > 0 && !avatarsCached && ImageCacheService.initialized) { + Qt.callLater(cacheTopContributorAvatars); + } + } + + // Also watch for ImageCacheService to become initialized + Connections { + target: ImageCacheService + function onInitializedChanged() { + if (ImageCacheService.initialized && contributors.length > 0 && !avatarsCached) { + Qt.callLater(cacheTopContributorAvatars); + } + } + } + + Process { + id: versionProcess + + property bool fetchSucceeded: false + property bool wasRateLimited: false + + command: ["curl", "-s", "https://api.github.com/repos/noctalia-dev/noctalia-shell/releases/latest"] + + stdout: StdioCollector { + onStreamFinished: { + try { + const response = text; + if (response && response.trim()) { + const data = JSON.parse(response); + if (data.tag_name) { + const version = data.tag_name; + root.data.version = version; + root.latestVersion = version; + versionProcess.fetchSucceeded = true; + Logger.d("GitHub", "Latest version fetched:", version); + } else if (data.message) { + // Check if it's a rate limit error + if (data.message.includes("rate limit")) { + versionProcess.wasRateLimited = true; + } else { + Logger.w("GitHub", "Version API error:", data.message); + } + } + } + } catch (e) { + Logger.e("GitHub", "Failed to parse version response:", e); + } + + // Check if all processes are done + checkAndSaveData(); + } + } + } + + Process { + id: qsVersionProcess + + property bool fetchSucceeded: false + property bool wasRateLimited: false + + command: ["curl", "-s", "https://api.github.com/repos/noctalia-dev/noctalia-qs/releases/latest"] + + stdout: StdioCollector { + onStreamFinished: { + try { + const response = text; + if (response && response.trim()) { + const data = JSON.parse(response); + if (data.tag_name) { + const version = data.tag_name; + root.data.qsVersion = version; + root.latestQSVersion = version; + qsVersionProcess.fetchSucceeded = true; + Logger.d("GitHub", "Latest QS version fetched:", version); + } else if (data.message) { + // Check if it's a rate limit error + if (data.message.includes("rate limit")) { + qsVersionProcess.wasRateLimited = true; + } else { + Logger.w("GitHub", "QS Version API error:", data.message); + } + } + } + } catch (e) { + Logger.e("GitHub", "Failed to parse QS version response:", e); + } + + // Check if all processes are done + checkAndSaveData(); + } + } + } + + Process { + id: contributorsProcess + + property bool fetchSucceeded: false + property bool wasRateLimited: false + + command: ["curl", "-s", "https://api.github.com/repos/noctalia-dev/noctalia-shell/contributors?per_page=100"] + + stdout: StdioCollector { + onStreamFinished: { + try { + const response = text; + Logger.d("GitHub", "Raw contributors response length:", response ? response.length : 0); + if (response && response.trim()) { + const data = JSON.parse(response); + Logger.d("GitHub", "Parsed contributors data type:", typeof data, "length:", Array.isArray(data) ? data.length : "not array"); + // Only update if we got a valid array + if (Array.isArray(data)) { + root.data.contributors = data; + root.contributors = root.data.contributors; + contributorsProcess.fetchSucceeded = true; + Logger.d("GitHub", "Contributors fetched:", root.contributors.length); + } else if (data.message) { + // Check if it's a rate limit error + if (data.message.includes("rate limit")) { + contributorsProcess.wasRateLimited = true; + } else { + Logger.w("GitHub", "Contributors API error:", data.message); + } + } + } + } catch (e) { + Logger.e("GitHub", "Failed to parse contributors response:", e); + } + + // Check if all processes are done + checkAndSaveData(); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/PluginRegistry.qml b/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/PluginRegistry.qml new file mode 100644 index 0000000..4364ea0 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/PluginRegistry.qml @@ -0,0 +1,620 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import "../../Helpers/sha256.js" as Crypto +import qs.Commons + +Singleton { + id: root + + readonly property string pluginsDir: Settings.configDir + "plugins" + readonly property string pluginsFile: Settings.configDir + "plugins.json" + + readonly property int currentVersion: 2 + // Main source URL - plugins from this source keep plain IDs + readonly property string mainSourceUrl: "https://github.com/noctalia-dev/noctalia-plugins" + + Component.onCompleted: { + ensurePluginsDirectory(); + ensurePluginsFile(); + } + + // Generate a short hash (6 characters) from a source URL + function generateSourceHash(sourceUrl) { + var hash = Crypto.sha256(sourceUrl); + return hash.substring(0, 6); + } + + // Check if a source is the main Noctalia plugins repository + function isMainSource(sourceUrl) { + return sourceUrl === root.mainSourceUrl; + } + + // Generate composite key: plain ID for official, "hash:id" for custom + function generateCompositeKey(pluginId, sourceUrl) { + if (!sourceUrl || isMainSource(sourceUrl)) { + return pluginId; + } + var hash = generateSourceHash(sourceUrl); + return hash + ":" + pluginId; + } + + // Parse composite key back to components + function parseCompositeKey(compositeKey) { + var colonIndex = compositeKey.indexOf(":"); + // If no colon or colon is after position 6 (hash length), it's a plain ID + if (colonIndex === -1 || colonIndex > 6) { + return { + sourceHash: null, + pluginId: compositeKey, + isOfficial: true + }; + } + // Has hash prefix (custom source plugin) + return { + sourceHash: compositeKey.substring(0, colonIndex), + pluginId: compositeKey.substring(colonIndex + 1), + isOfficial: false + }; + } + + // Get source name by URL + function getSourceNameByUrl(sourceUrl) { + for (var i = 0; i < root.pluginSources.length; i++) { + if (root.pluginSources[i].url === sourceUrl) { + return root.pluginSources[i].name; + } + } + return null; + } + + // Get source name by hash + function getSourceNameByHash(hash) { + for (var i = 0; i < root.pluginSources.length; i++) { + if (generateSourceHash(root.pluginSources[i].url) === hash) { + return root.pluginSources[i].name; + } + } + return null; + } + + // Get source URL from plugin state + function getPluginSourceUrl(compositeKey) { + var state = root.pluginStates[compositeKey]; + return state?.sourceUrl || root.mainSourceUrl; + } + + // Signals + signal pluginsChanged + + // In-memory plugin cache (populated by scanning disk) + property var installedPlugins: ({}) // { pluginId: manifest } + property var pluginStates: ({}) // { pluginId: { enabled: bool } } + property var pluginSources: [] // Array of { name, url } + property var pluginLoadVersions: ({}) // { pluginId: versionNumber } - for cache busting + + // Track async loading + property int pendingManifests: 0 + + // File storage (minimal - only states and sources) + property FileView pluginsFileView: FileView { + id: pluginsFileView + path: root.pluginsFile + + adapter: JsonAdapter { + id: adapter + property int version: root.currentVersion + property var states: ({}) + property list sources: [] + } + + onLoaded: { + Logger.i("PluginRegistry", "Loaded plugin states from:", path); + root.pluginStates = adapter.states || {}; + root.pluginSources = adapter.sources || []; + + // Ensure default repo is in sources + if (root.pluginSources.length === 0) { + root.pluginSources = [ + { + "name": "Noctalia Plugins", + "url": "https://github.com/noctalia-dev/noctalia-plugins", + "enabled": true + } + ]; + root.save(); + } + + // Migrate from v1 to v2 (add sourceUrl to states) + root.migratePluginData(); + + // Scan plugin folder to discover installed plugins + scanPluginFolder(); + } + + onLoadFailed: function (error) { + Logger.w("PluginRegistry", "Failed to load plugins.json, will create it:", error); + // Initialize defaults and continue + root.pluginStates = {}; + root.pluginSources = [ + { + "name": "Noctalia Plugins", + "url": "https://github.com/noctalia-dev/noctalia-plugins", + "enabled": true + } + ]; + // Scan for installed plugins + root.scanPluginFolder(); + } + } + + function init() { + Logger.d("PluginRegistry", "Initialized"); + // Force instantiation of PluginService to set up signal listener + PluginService.initialized; + } + + // Migrate plugin data from older versions + function migratePluginData() { + var needsSave = false; + + // Migration v1 -> v2: add sourceUrl to states + for (var pluginId in root.pluginStates) { + if (root.pluginStates[pluginId].sourceUrl === undefined) { + Logger.i("PluginRegistry", "Migrating plugin data to v2 (adding sourceUrl)"); + + var newStates = {}; + for (var id in root.pluginStates) { + // For v1 -> v2 migration, we assume plugins are from main source + // Custom plugins installed before this feature need to be reinstalled + newStates[id] = { + enabled: root.pluginStates[id].enabled, + sourceUrl: root.mainSourceUrl + }; + } + root.pluginStates = newStates; + needsSave = true; + break; + } + } + + // Migration: rename "Official Noctalia Plugins" -> "Noctalia Plugins" + var newSources = []; + var sourcesChanged = false; + for (var i = 0; i < root.pluginSources.length; i++) { + var source = root.pluginSources[i]; + if (source.name === "Official Noctalia Plugins") { + newSources.push({ + name: "Noctalia Plugins", + url: source.url, + enabled: source.enabled + }); + sourcesChanged = true; + Logger.i("PluginRegistry", "Migrating source name: 'Official Noctalia Plugins' -> 'Noctalia Plugins'"); + } else { + newSources.push(source); + } + } + if (sourcesChanged) { + root.pluginSources = newSources; + needsSave = true; + } + + if (needsSave) { + root.save(); + Logger.i("PluginRegistry", "Migration complete"); + } + } + + // Ensure plugins directory exists + function ensurePluginsDirectory() { + var mkdirProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + command: ["mkdir", "-p", "${root.pluginsDir}"] + } + `, root, "MkdirPlugins"); + + mkdirProcess.exited.connect(function (exitCode) { + if (exitCode === 0) { + Logger.d("PluginRegistry", "Plugins directory ensured:", root.pluginsDir); + } else { + Logger.e("PluginRegistry", "Failed to create plugins directory"); + } + mkdirProcess.destroy(); + }); + + mkdirProcess.running = true; + } + + // Ensure plugins.json exists (create minimal one if it doesn't) + function ensurePluginsFile() { + var checkProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + command: ["sh", "-c", "test -f '${root.pluginsFile}' || echo '{\\"version\\":${root.currentVersion},\\"states\\":{},\\"sources\\":[]}' > '${root.pluginsFile}'"] + } + `, root, "EnsurePluginsFile"); + + checkProcess.exited.connect(function (exitCode) { + if (exitCode === 0) { + Logger.d("PluginRegistry", "Plugins file ensured:", root.pluginsFile); + } + checkProcess.destroy(); + }); + + checkProcess.running = true; + } + + // Scan plugin folder to discover installed plugins (single process reads all manifests) + function scanPluginFolder() { + Logger.i("PluginRegistry", "Scanning plugin folder:", root.pluginsDir); + + var scanProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + command: ["sh", "-c", "for d in '${root.pluginsDir}'/*/; do [ -d \\"$d\\" ] || continue; [ -f \\"$d/manifest.json\\" ] || continue; echo \\"@@PLUGIN@@$(basename \\"$d\\")\\" ; cat \\"$d/manifest.json\\" ; done"] + stdout: StdioCollector {} + running: true + } + `, root, "ScanAllPlugins"); + + scanProcess.exited.connect(function (exitCode) { + var output = String(scanProcess.stdout.text || ""); + var sections = output.split("@@PLUGIN@@"); + var loadedCount = 0; + + for (var i = 1; i < sections.length; i++) { + var section = sections[i]; + var newlineIdx = section.indexOf('\n'); + if (newlineIdx === -1) + continue; + + var pluginId = section.substring(0, newlineIdx).trim(); + var manifestJson = section.substring(newlineIdx + 1).trim(); + + if (!pluginId || !manifestJson) + continue; + + try { + var manifest = JSON.parse(manifestJson); + var validation = validateManifest(manifest); + + if (validation.valid) { + manifest.compositeKey = pluginId; + root.installedPlugins[pluginId] = manifest; + Logger.i("PluginRegistry", "Loaded plugin:", pluginId, "-", manifest.name); + + if (!root.pluginStates[pluginId]) { + root.pluginStates[pluginId] = { + enabled: false + }; + } + loadedCount++; + } else { + Logger.e("PluginRegistry", "Invalid manifest for", pluginId + ":", validation.error); + } + } catch (e) { + Logger.e("PluginRegistry", "Failed to parse manifest for", pluginId + ":", e.toString()); + } + } + + Logger.i("PluginRegistry", "All plugin manifests loaded. Total plugins:", loadedCount); + root.pluginsChanged(); + scanProcess.destroy(); + }); + } + + // Load a single plugin's manifest from disk + function loadPluginManifest(pluginId) { + var manifestPath = root.pluginsDir + "/" + pluginId + "/manifest.json"; + + var catProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + command: ["cat", "${manifestPath}"] + stdout: StdioCollector {} + running: true + } + `, root, "LoadManifest_" + pluginId); + + catProcess.exited.connect(function (exitCode) { + var output = String(catProcess.stdout.text || ""); + if (exitCode === 0 && output) { + try { + var manifest = JSON.parse(output); + var validation = validateManifest(manifest); + + if (validation.valid) { + manifest.compositeKey = pluginId; + root.installedPlugins[pluginId] = manifest; + Logger.i("PluginRegistry", "Loaded plugin:", pluginId, "-", manifest.name); + + // Ensure state exists (default to disabled) + if (!root.pluginStates[pluginId]) { + root.pluginStates[pluginId] = { + enabled: false + }; + } + } else { + Logger.e("PluginRegistry", "Invalid manifest for", pluginId + ":", validation.error); + } + } catch (e) { + Logger.e("PluginRegistry", "Failed to parse manifest for", pluginId + ":", e.toString()); + } + } else { + Logger.d("PluginRegistry", "No manifest found for:", pluginId); + } + + // Decrement pending count and emit signal when all are done + root.pendingManifests--; + Logger.d("PluginRegistry", "Pending manifests remaining:", root.pendingManifests); + if (root.pendingManifests === 0) { + var installedIds = Object.keys(root.installedPlugins); + Logger.i("PluginRegistry", "All plugin manifests loaded. Total plugins:", installedIds.length); + Logger.d("PluginRegistry", "Installed plugin IDs:", JSON.stringify(installedIds)); + root.pluginsChanged(); + } + + catProcess.destroy(); + }); + } + + // Save registry to disk (only states and sources) + function save() { + adapter.version = root.currentVersion; + adapter.states = root.pluginStates; + adapter.sources = root.pluginSources; + + Qt.callLater(() => { + pluginsFileView.writeAdapter(); + Logger.d("PluginRegistry", "Plugin states saved"); + }); + } + + // Enable/disable a plugin + function setPluginEnabled(pluginId, enabled) { + if (!root.installedPlugins[pluginId]) { + Logger.w("PluginRegistry", "Cannot set state for non-existent plugin:", pluginId); + return; + } + + if (!root.pluginStates[pluginId]) { + root.pluginStates[pluginId] = { + enabled: enabled + }; + } else { + root.pluginStates[pluginId].enabled = enabled; + } + + save(); + root.pluginsChanged(); + Logger.i("PluginRegistry", "Plugin", pluginId, enabled ? "enabled" : "disabled"); + } + + // Check if plugin is enabled + function isPluginEnabled(pluginId) { + return root.pluginStates[pluginId]?.enabled || false; + } + + // Check if plugin is downloaded/installed + function isPluginDownloaded(pluginId) { + return pluginId in root.installedPlugins; + } + + // Get plugin manifest from cache + function getPluginManifest(pluginId) { + return root.installedPlugins[pluginId] || null; + } + + // Get ALL installed plugin IDs (discovered from disk) + function getAllInstalledPluginIds() { + return Object.keys(root.installedPlugins); + } + + // Get enabled plugin IDs only + function getEnabledPluginIds() { + return Object.keys(root.pluginStates).filter(function (id) { + return root.pluginStates[id].enabled === true; + }); + } + + // Register a plugin (add to installed plugins after download) + // sourceUrl is required for new plugins to generate composite key + function registerPlugin(manifest, sourceUrl) { + var compositeKey = generateCompositeKey(manifest.id, sourceUrl); + manifest.compositeKey = compositeKey; + root.installedPlugins[compositeKey] = manifest; + + // Ensure state exists (default to disabled, store sourceUrl) + if (!root.pluginStates[compositeKey]) { + root.pluginStates[compositeKey] = { + enabled: false, + sourceUrl: sourceUrl || root.mainSourceUrl + }; + } else { + // Preserve enabled state but update sourceUrl + root.pluginStates[compositeKey].sourceUrl = sourceUrl || root.mainSourceUrl; + } + + save(); + root.pluginsChanged(); + Logger.i("PluginRegistry", "Registered plugin:", compositeKey); + return compositeKey; + } + + // Unregister a plugin (remove from registry) + function unregisterPlugin(pluginId) { + delete root.pluginStates[pluginId]; + delete root.installedPlugins[pluginId]; + save(); + root.pluginsChanged(); + Logger.i("PluginRegistry", "Unregistered plugin:", pluginId); + } + + // Increment plugin load version (for cache busting when plugin is updated) + function incrementPluginLoadVersion(pluginId) { + var versions = Object.assign({}, root.pluginLoadVersions); + versions[pluginId] = (versions[pluginId] || 0) + 1; + root.pluginLoadVersions = versions; + Logger.d("PluginRegistry", "Incremented load version for", pluginId, "to", versions[pluginId]); + return versions[pluginId]; + } + + // Remove plugin state (call after deleting plugin folder) + function removePluginState(pluginId) { + delete root.pluginStates[pluginId]; + delete root.installedPlugins[pluginId]; + save(); + root.pluginsChanged(); + Logger.i("PluginRegistry", "Removed plugin state:", pluginId); + } + + // Add a plugin source + function addPluginSource(name, url) { + for (var i = 0; i < root.pluginSources.length; i++) { + if (root.pluginSources[i].url === url) { + Logger.w("PluginRegistry", "Source already exists:", url); + return false; + } + } + + // Create a new array to trigger property change notification + var newSources = root.pluginSources.slice(); + newSources.push({ + name: name, + url: url, + enabled: true + }); + root.pluginSources = newSources; + save(); + Logger.i("PluginRegistry", "Added plugin source:", name); + return true; + } + + // Remove a plugin source + function removePluginSource(url) { + var newSources = []; + for (var i = 0; i < root.pluginSources.length; i++) { + if (root.pluginSources[i].url !== url) { + newSources.push(root.pluginSources[i]); + } + } + + if (newSources.length === root.pluginSources.length) { + Logger.w("PluginRegistry", "Source not found:", url); + return false; + } + + root.pluginSources = newSources; + save(); + Logger.i("PluginRegistry", "Removed plugin source:", url); + return true; + } + + // Set source enabled/disabled state + function setSourceEnabled(url, enabled) { + var newSources = []; + var found = false; + for (var i = 0; i < root.pluginSources.length; i++) { + if (root.pluginSources[i].url === url) { + newSources.push({ + name: root.pluginSources[i].name, + url: root.pluginSources[i].url, + enabled: enabled + }); + found = true; + } else { + newSources.push(root.pluginSources[i]); + } + } + + if (!found) { + Logger.w("PluginRegistry", "Source not found:", url); + return false; + } + + root.pluginSources = newSources; + save(); + Logger.i("PluginRegistry", "Source", url, enabled ? "enabled" : "disabled"); + return true; + } + + // Check if source is enabled + function isSourceEnabled(url) { + for (var i = 0; i < root.pluginSources.length; i++) { + if (root.pluginSources[i].url === url) { + return root.pluginSources[i].enabled !== false; // Default to true if not set + } + } + return false; + } + + // Get enabled sources only + function getEnabledSources() { + var enabledSources = []; + for (var i = 0; i < root.pluginSources.length; i++) { + if (root.pluginSources[i].enabled !== false) { + enabledSources.push(root.pluginSources[i]); + } + } + return enabledSources; + } + + // Get plugin directory path + function getPluginDir(pluginId) { + return root.pluginsDir + "/" + pluginId; + } + + // Get plugin settings file path + function getPluginSettingsFile(pluginId) { + return getPluginDir(pluginId) + "/settings.json"; + } + + // Validate manifest + function validateManifest(manifest) { + if (!manifest) { + return { + valid: false, + error: "Manifest is null or undefined" + }; + } + + var required = ["id", "name", "version", "author", "description"]; + for (var i = 0; i < required.length; i++) { + if (!manifest[required[i]]) { + return { + valid: false, + error: "Missing required field: " + required[i] + }; + } + } + + if (!manifest.entryPoints) { + return { + valid: false, + error: "Missing 'entryPoints' field" + }; + } + + // Check version format (simple x.y.z check) + var versionRegex = /^\d+\.\d+\.\d+$/; + if (!versionRegex.test(manifest.version)) { + return { + valid: false, + error: "Invalid version format (must be x.y.z)" + }; + } + + return { + valid: true, + error: null + }; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/PluginService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/PluginService.qml new file mode 100644 index 0000000..7b01744 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/PluginService.qml @@ -0,0 +1,2087 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Modules.Panels.Settings +import qs.Services.Noctalia +import qs.Services.UI + +Singleton { + id: root + + signal pluginLoaded(string pluginId) + signal pluginUnloaded(string pluginId) + signal pluginEnabled(string pluginId) + signal pluginDisabled(string pluginId) + signal pluginReloaded(string pluginId) + signal availablePluginsUpdated + signal allPluginsLoaded + + // When available plugins are updated, check if we should perform update check + onAvailablePluginsUpdated: { + if (shouldCheckUpdatesAfterFetch && Object.keys(activeFetches).length === 0) { + Logger.d("PluginService", "All registry fetches complete, performing update check"); + performUpdateCheck(); + } + } + + // Loaded plugin instances + property var loadedPlugins: ({}) // { pluginId: { component, instance, api } } + + // Available plugins from all sources (fetched from registries) + property var availablePlugins: ([]) // Array of plugin metadata from all sources + + // Plugin updates available: { pluginId: { currentVersion, availableVersion } } + property var pluginUpdates: ({}) + + // Plugin updates that require a newer Noctalia version: { pluginId: { currentVersion, availableVersion, minNoctaliaVersion } } + property var pluginUpdatesPending: ({}) + + // Plugin load errors: { pluginId: { error: string, entryPoint: string, timestamp: date } } + property var pluginErrors: ({}) + signal pluginLoadError(string pluginId, string entryPoint, string error) + + // Track currently installing plugins: { pluginId: true } + property var installingPlugins: ({}) + + // Hot reload: file watchers for plugin directories + property var pluginFileWatchers: ({}) // { pluginId: FileView } + property list pluginHotReloadEnabled: [] // List of pluginIds that have hot reload enabled + + // Track active fetches + property var activeFetches: ({}) + + property bool initialized: false + property bool pluginsFullyLoaded: false + + // Plugin container from shell.qml (for placing Main instances in graphics scene) + property var pluginContainer: null + + // Screen detector from shell.qml (for withCurrentScreen in plugin API) + property var screenDetector: null + + // Track if we need to initialize once container is ready + property bool needsInit: false + + // Watch for pluginContainer to be set + onPluginContainerChanged: { + if (root.pluginContainer && root.needsInit) { + Logger.d("PluginService", "Plugin container now available, initializing plugins"); + root.needsInit = false; + root.init(); + } + } + + // Listen for PluginRegistry to finish loading + Connections { + target: PluginRegistry + + function onPluginsChanged() { + if (!root.initialized) { + if (root.pluginContainer) { + // Container already available, init now + root.init(); + } else { + // Container not ready, wait for it + Logger.d("PluginService", "Deferring plugin init until container is ready"); + root.needsInit = true; + } + } + } + } + + // When debug mode is disabled, tear down all hot reload watchers + Connections { + target: Settings + + function onIsDebugChanged() { + if (!Settings.isDebug && root.pluginHotReloadEnabled.length > 0) { + Logger.i("PluginService", "Debug mode disabled, removing all hot reload watchers"); + // Remove watchers for all hot-reload-enabled plugins + var plugins = root.pluginHotReloadEnabled.slice(); // copy since we mutate + for (var i = 0; i < plugins.length; i++) { + removePluginFileWatcher(plugins[i]); + } + root.pluginHotReloadEnabled = []; + } + } + } + + // Listen for language changes to reload plugin translations + Connections { + target: I18n + + function onLanguageChanged() { + Logger.d("PluginService", "Language changed to:", I18n.langCode, "- reloading plugin translations"); + + // Reload translations for all loaded plugins + for (var pluginId in root.loadedPlugins) { + // Use IIFE to capture current loop values (avoid closure bug) + (function (id, plugin) { + if (plugin && plugin.api && plugin.manifest) { + // Update current language + plugin.api.currentLanguage = I18n.langCode; + + // Reload translations + loadPluginTranslationsAsync(id, plugin.manifest, I18n.langCode, function (translations) { + plugin.api.pluginTranslations = translations; + + // Reload English fallback for non-English languages + if (I18n.langCode !== "en") { + loadPluginTranslationsAsync(id, plugin.manifest, "en", function (fallbackTranslations) { + plugin.api.pluginFallbackTranslations = fallbackTranslations; + plugin.api.translationVersion++; + Logger.d("PluginService", "Reloaded translations for plugin:", id); + }); + } else { + plugin.api.pluginFallbackTranslations = {}; + plugin.api.translationVersion++; + Logger.d("PluginService", "Reloaded translations for plugin:", id); + } + }); + } + })(pluginId, root.loadedPlugins[pluginId]); + } + + // Update translation file watchers to watch the new language's files + if (root.pluginHotReloadEnabled.length > 0) { + updateTranslationWatchers(); + } + } + } + + // Track pending plugin loads for init completion + property int _pendingPluginLoads: 0 + + function init() { + if (root.initialized) { + Logger.d("PluginService", "Already initialized, skipping"); + return; + } + + Logger.i("PluginService", "Initializing plugin system"); + root.initialized = true; + + // Debug: Check what's in PluginRegistry + var allInstalled = PluginRegistry.getAllInstalledPluginIds(); + Logger.d("PluginService", "All installed plugins:", JSON.stringify(allInstalled)); + Logger.d("PluginService", "Plugin states:", JSON.stringify(PluginRegistry.pluginStates)); + + // Load all enabled plugins + var enabledIds = PluginRegistry.getEnabledPluginIds(); + Logger.i("PluginService", "Found", enabledIds.length, "enabled plugins:", JSON.stringify(enabledIds)); + + // Count plugins that need to be loaded + var pluginsToLoad = []; + for (var i = 0; i < enabledIds.length; i++) { + var manifest = PluginRegistry.getPluginManifest(enabledIds[i]); + if (manifest) { + pluginsToLoad.push(enabledIds[i]); + } else { + Logger.w("PluginService", "Plugin", enabledIds[i], "is enabled but not found on disk - install"); + var sourceUrl = PluginRegistry.getPluginSourceUrl(enabledIds[i]); + root.installPlugin({ + id: enabledIds[i], + source: { + url: sourceUrl + } + }, false, function (success, error, registeredKey) { + if (success) { + ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.install-success", { + "plugin": registeredKey + })); + // Load the plugin since it was already enabled (state persisted but files were missing) + loadPlugin(registeredKey); + + // Add plugin widget to bar if it provides one + var manifest = PluginRegistry.getPluginManifest(registeredKey); + if (manifest && manifest.entryPoints && manifest.entryPoints.barWidget) { + var widgetId = "plugin:" + registeredKey; + addWidgetToBar(widgetId, "right"); + } + } else { + ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.install-error", { + "error": error || "Unknown error" + })); + } + }); + } + } + + // If no plugins to load, mark as complete immediately + if (pluginsToLoad.length === 0) { + root.pluginsFullyLoaded = true; + Logger.i("PluginService", "No plugins to load"); + root.allPluginsLoaded(); + root._isStartupCheck = true; + refreshAvailablePlugins(); + return; + } + + // Track pending loads + root._pendingPluginLoads = pluginsToLoad.length; + + // Load all plugins (async - they will call _onPluginLoadComplete when done) + for (var j = 0; j < pluginsToLoad.length; j++) { + Logger.d("PluginService", "Attempting to load plugin:", pluginsToLoad[j]); + loadPlugin(pluginsToLoad[j]); + } + } + + // Called when a plugin finishes loading (success or failure) + function _onPluginLoadComplete() { + root._pendingPluginLoads--; + + if (root._pendingPluginLoads <= 0) { + // All plugins finished loading + root.pluginsFullyLoaded = true; + Logger.i("PluginService", "All plugins loaded"); + root.allPluginsLoaded(); + + // Fetch available plugins from all sources + root._isStartupCheck = true; + refreshAvailablePlugins(); + } + } + + // Refresh available plugins from all sources + function refreshAvailablePlugins() { + // If fetches are already in progress, don't start new ones + if (Object.keys(activeFetches).length > 0) { + Logger.d("PluginService", "Refresh already in progress, skipping duplicate refresh"); + return; + } + + Logger.i("PluginService", "Refreshing available plugins"); + root.availablePlugins = []; + + // Signal that we want to check for updates after refresh completes + shouldCheckUpdatesAfterFetch = true; + + var enabledSources = PluginRegistry.getEnabledSources(); + Logger.d("PluginService", "Fetching from", enabledSources.length, "enabled sources"); + for (var i = 0; i < enabledSources.length; i++) { + fetchPluginRegistry(enabledSources[i]); + } + } + + // Fetch plugin registry from a source using git sparse-checkout + function fetchPluginRegistry(source) { + var repoUrl = source.url; + + Logger.d("PluginService", "Fetching registry from:", repoUrl); + + // Use git sparse-checkout to fetch only registry.json (--no-cone for single file) + // GIT_TERMINAL_PROMPT=0 prevents hanging on private repos that need auth + var fetchCmd = "temp_dir=$(mktemp -d) && GIT_TERMINAL_PROMPT=0 git clone --filter=blob:none --sparse --depth=1 --quiet '" + repoUrl + "' \"$temp_dir\" 2>/dev/null && cd \"$temp_dir\" && git sparse-checkout set --no-cone /registry.json 2>/dev/null && cat \"$temp_dir/registry.json\"; rm -rf \"$temp_dir\""; + + var fetchProcess = Qt.createQmlObject('import QtQuick; import Quickshell.Io; Process { command: ["sh", "-c", "' + fetchCmd.replace(/"/g, '\\"') + '"]; stdout: StdioCollector {} }', root, "FetchRegistry_" + Date.now()); + + activeFetches[source.url] = fetchProcess; + + fetchProcess.stdout.onStreamFinished.connect(function () { + var response = fetchProcess.stdout.text; + + // Debug: log the raw response + Logger.d("PluginService", "Registry response length:", response ? response.length : 0); + + if (!response || response.trim() === "") { + Logger.e("PluginService", "Empty response from", source.name); + delete activeFetches[source.url]; + fetchProcess.destroy(); + return; + } + + try { + var registry = JSON.parse(response); + + if (registry && registry.plugins && Array.isArray(registry.plugins)) { + // Add source info to each plugin + for (var i = 0; i < registry.plugins.length; i++) { + var plugin = registry.plugins[i]; + plugin.source = source; + + // Check if already downloaded - use composite key + var compositeKey = PluginRegistry.generateCompositeKey(plugin.id, source.url); + plugin.downloaded = PluginRegistry.isPluginDownloaded(compositeKey); + plugin.enabled = PluginRegistry.isPluginEnabled(compositeKey); + + root.availablePlugins.push(plugin); + } + + Logger.i("PluginService", `Parsed ${registry.plugins.length} plugins manifest from '${source.name}'`); + + // Remove from active fetches BEFORE emitting signal so handler sees correct count + delete activeFetches[source.url]; + fetchProcess.destroy(); + + root.availablePluginsUpdated(); + return; + } + } catch (e) { + Logger.e("PluginService", "Failed to parse registry from", source.name, ":", e); + Logger.e("PluginService", "Response was:", response ? response.substring(0, 200) : "null"); + } + + // Clean up on error or empty response + delete activeFetches[source.url]; + fetchProcess.destroy(); + }); + + fetchProcess.exited.connect(function (exitCode) { + if (exitCode !== 0) { + Logger.e("PluginService", "Failed to fetch registry from", source.name, "- exit code:", exitCode); + delete activeFetches[source.url]; + fetchProcess.destroy(); + } + }); + + fetchProcess.running = true; + } + + // Check for plugin ID collision before installation + function checkPluginCollision(pluginMetadata) { + var sourceUrl = pluginMetadata.source.url; + var compositeKey = PluginRegistry.generateCompositeKey(pluginMetadata.id, sourceUrl); + + // Check if this exact composite key already exists + if (PluginRegistry.isPluginDownloaded(compositeKey)) { + return { + collision: true, + reason: "already_installed", + existingKey: compositeKey, + message: I18n.tr("panels.plugins.collision-already-installed") + }; + } + + // For official plugins, also check if any custom version with same base ID exists + if (PluginRegistry.isMainSource(sourceUrl)) { + var allInstalled = PluginRegistry.getAllInstalledPluginIds(); + for (var i = 0; i < allInstalled.length; i++) { + var parsed = PluginRegistry.parseCompositeKey(allInstalled[i]); + if (parsed.pluginId === pluginMetadata.id && !parsed.isOfficial) { + var sourceName = PluginRegistry.getSourceNameByHash(parsed.sourceHash) || I18n.tr("panels.plugins.source-custom"); + return { + collision: true, + reason: "custom_version_exists", + existingKey: allInstalled[i], + message: I18n.tr("panels.plugins.collision-custom-version-exists", { + source: sourceName + }) + }; + } + } + } + + // For custom plugins, check if official version exists + if (!PluginRegistry.isMainSource(sourceUrl)) { + if (PluginRegistry.isPluginDownloaded(pluginMetadata.id)) { + return { + collision: true, + reason: "official_version_exists", + existingKey: pluginMetadata.id, + message: I18n.tr("panels.plugins.collision-official-version-exists") + }; + } + } + + return { + collision: false + }; + } + + // Download and install a plugin using git sparse-checkout + // skipCollisionCheck: set to true when updating an existing plugin + function installPlugin(pluginMetadata, skipCollisionCheck, callback) { + var pluginId = pluginMetadata.id; + // Do not include hash for 3rd party plugins + var pluginIdRegex = /^[a-f0-9]{6}:/; + if (pluginIdRegex.test(pluginId)) { + pluginId = pluginId.substring(7); + } + + var source = pluginMetadata.source; + + // Check for collision first (skip when updating) + if (!skipCollisionCheck) { + var collision = checkPluginCollision(pluginMetadata); + if (collision.collision) { + Logger.w("PluginService", "Plugin collision detected:", collision.message); + ToastService.showError(I18n.tr("panels.plugins.title"), collision.message); + if (callback) + callback(false, collision.message); + return; + } + + // Check Noctalia version compatibility (skip when updating - that's handled in performUpdateCheck) + if (pluginMetadata.minNoctaliaVersion) { + var noctaliaVersion = UpdateService.baseVersion; + if (compareVersions(pluginMetadata.minNoctaliaVersion, noctaliaVersion) > 0) { + var incompatibleMsg = I18n.tr("panels.plugins.install-incompatible", { + "plugin": pluginMetadata.name, + "version": pluginMetadata.minNoctaliaVersion + }); + Logger.w("PluginService", "Plugin incompatible:", incompatibleMsg); + if (callback) + callback(false, incompatibleMsg); + return; + } + } + } + + // Generate composite key for the plugin folder + var compositeKey = PluginRegistry.generateCompositeKey(pluginId, source.url); + Logger.i("PluginService", "Installing plugin:", compositeKey, "from", source.name); + + var pluginDir = PluginRegistry.getPluginDir(compositeKey); + var repoUrl = source.url; + + // Use git sparse-checkout to clone only the plugin subfolder + // GIT_TERMINAL_PROMPT=0 prevents hanging on private repos that need auth + // Note: We download from the original pluginId folder in the repo, but save to compositeKey folder + var downloadCmd = "temp_dir=$(mktemp -d) && GIT_TERMINAL_PROMPT=0 git clone --filter=blob:none --sparse --depth=1 --quiet '" + repoUrl + "' \"$temp_dir\" 2>/dev/null && cd \"$temp_dir\" && git sparse-checkout set '" + pluginId + "' 2>/dev/null && mkdir -p '" + pluginDir + "' && rm -f \"$temp_dir/" + pluginId + "/settings.json\" && cp -r \"$temp_dir/" + pluginId + + "/.\" '" + pluginDir + "/'; exit_code=$?; rm -rf \"$temp_dir\"; exit $exit_code"; + + // Mark as installing + var newInstalling = Object.assign({}, root.installingPlugins); + newInstalling[pluginId] = true; + root.installingPlugins = newInstalling; + + var downloadProcess = Qt.createQmlObject('import QtQuick; import Quickshell.Io; Process { command: ["sh", "-c", "' + downloadCmd.replace(/"/g, '\\"') + '"] }', root, "DownloadPlugin_" + pluginId); + + downloadProcess.exited.connect(function (exitCode) { + // Mark as finished (remove from installing) + var currentInstalling = Object.assign({}, root.installingPlugins); + delete currentInstalling[pluginId]; + root.installingPlugins = currentInstalling; + + if (exitCode === 0) { + Logger.i("PluginService", "Downloaded plugin:", compositeKey); + + // Load and validate manifest + var manifestPath = pluginDir + "/manifest.json"; + loadManifest(manifestPath, function (success, manifest) { + if (success) { + var validation = PluginRegistry.validateManifest(manifest); + if (validation.valid) { + // Register plugin with source URL + var registeredKey = PluginRegistry.registerPlugin(manifest, source.url); + Logger.i("PluginService", "Installed plugin:", registeredKey); + + // Update available plugins list + updatePluginInAvailable(pluginId, { + downloaded: true + }); + + if (callback) + callback(true, null, registeredKey); + } else { + Logger.e("PluginService", "Invalid manifest:", validation.error); + if (callback) + callback(false, "Invalid manifest: " + validation.error); + } + } else { + Logger.e("PluginService", "Failed to load manifest for:", compositeKey); + if (callback) + callback(false, "Failed to load manifest"); + } + }); + } else { + Logger.e("PluginService", "Failed to download plugin:", compositeKey); + if (callback) + callback(false, "Download failed"); + } + + downloadProcess.destroy(); + }); + + downloadProcess.running = true; + } + + // Uninstall a plugin (compositeKey is the full key like "abc123:my-plugin" or plain "my-plugin") + function uninstallPlugin(compositeKey, callback) { + Logger.i("PluginService", "Uninstalling plugin:", compositeKey); + + // Disable and unload first + if (PluginRegistry.isPluginEnabled(compositeKey)) { + disablePlugin(compositeKey); + } + + var pluginDir = PluginRegistry.getPluginDir(compositeKey); + + var removeProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + command: ["rm", "-rf", "${pluginDir}"] + } + `, root, "RemovePlugin_" + compositeKey); + + removeProcess.exited.connect(function (exitCode) { + if (exitCode === 0) { + PluginRegistry.unregisterPlugin(compositeKey); + Logger.i("PluginService", "Uninstalled plugin:", compositeKey); + + // Update available plugins list (use plain ID to match against availablePlugins) + var parsed = PluginRegistry.parseCompositeKey(compositeKey); + updatePluginInAvailable(parsed.pluginId, { + downloaded: false, + enabled: false + }); + + if (callback) + callback(true, null); + } else { + Logger.e("PluginService", "Failed to uninstall plugin:", pluginId); + if (callback) + callback(false, "Failed to remove plugin files"); + } + + removeProcess.destroy(); + }); + + removeProcess.running = true; + } + + // Enable a plugin (compositeKey is the full key like "abc123:my-plugin" or plain "my-plugin") + function enablePlugin(compositeKey, skipAddToBar) { + if (PluginRegistry.isPluginEnabled(compositeKey)) { + Logger.w("PluginService", "Plugin already enabled:", compositeKey); + return true; + } + + if (!PluginRegistry.isPluginDownloaded(compositeKey)) { + Logger.e("PluginService", "Cannot enable: plugin not downloaded:", compositeKey); + return false; + } + + PluginRegistry.setPluginEnabled(compositeKey, true); + loadPlugin(compositeKey); + + // Add plugin widget to bar if it provides one (unless we're restoring from backup) + if (!skipAddToBar) { + var manifest = PluginRegistry.getPluginManifest(compositeKey); + if (manifest && manifest.entryPoints && manifest.entryPoints.barWidget) { + var widgetId = "plugin:" + compositeKey; + addWidgetToBar(widgetId, "right"); + } + } + + // Update available plugins list (use plain ID to match against availablePlugins) + var parsed = PluginRegistry.parseCompositeKey(compositeKey); + updatePluginInAvailable(parsed.pluginId, { + enabled: true + }); + root.pluginEnabled(compositeKey); + + return true; + } + + // Helper function to add a widget to the bar (global + all screen overrides) + function addWidgetToBar(widgetId, section) { + section = section || "right"; // Default to right section + + // Check if widget already exists in any section (global) + var sections = ["left", "center", "right"]; + for (var s = 0; s < sections.length; s++) { + var widgets = Settings.data.bar.widgets[sections[s]] || []; + for (var i = 0; i < widgets.length; i++) { + if (widgets[i].id === widgetId) { + Logger.d("PluginService", "Widget already in bar:", widgetId); + return false; + } + } + } + + // Add to global + var globalWidgets = Settings.data.bar.widgets[section] || []; + globalWidgets.push({ + id: widgetId + }); + Settings.data.bar.widgets[section] = globalWidgets; + + // Also add to any screen overrides that have widget configurations + var overrides = Settings.data.bar.screenOverrides || []; + for (var o = 0; o < overrides.length; o++) { + if (overrides[o] && overrides[o].widgets) { + var overrideWidgets = overrides[o].widgets; + var sectionWidgets = overrideWidgets[section] || []; + // Check if widget already exists in this override + var alreadyExists = false; + for (var j = 0; j < sections.length; j++) { + var owSec = overrideWidgets[sections[j]] || []; + for (var k = 0; k < owSec.length; k++) { + if (owSec[k].id === widgetId) { + alreadyExists = true; + break; + } + } + if (alreadyExists) + break; + } + if (!alreadyExists) { + sectionWidgets.push({ + id: widgetId + }); + overrideWidgets[section] = sectionWidgets; + Settings.setScreenOverride(overrides[o].name, "widgets", overrideWidgets); + } + } + } + + Logger.i("PluginService", "Added widget", widgetId, "to bar section:", section); + return true; + } + + // Disable a plugin (compositeKey is the full key like "abc123:my-plugin" or plain "my-plugin") + function disablePlugin(compositeKey) { + if (!PluginRegistry.isPluginEnabled(compositeKey)) { + Logger.w("PluginService", "Plugin already disabled:", compositeKey); + return true; + } + + // Remove plugin widget from bar before unloading + var widgetId = "plugin:" + compositeKey; + removeWidgetFromBar(widgetId); + + PluginRegistry.setPluginEnabled(compositeKey, false); + unloadPlugin(compositeKey); + + // Update available plugins list (use plain ID to match against availablePlugins) + var parsed = PluginRegistry.parseCompositeKey(compositeKey); + updatePluginInAvailable(parsed.pluginId, { + enabled: false + }); + root.pluginDisabled(compositeKey); + return true; + } + + // Helper function to remove a widget from all bar sections (global + screen overrides) + function removeWidgetFromBar(widgetId) { + var sections = ["left", "center", "right"]; + var changed = false; + + // Remove from global + for (var s = 0; s < sections.length; s++) { + var section = sections[s]; + var widgets = Settings.data.bar.widgets[section] || []; + var newWidgets = []; + + for (var i = 0; i < widgets.length; i++) { + if (widgets[i].id !== widgetId) { + newWidgets.push(widgets[i]); + } else { + changed = true; + Logger.i("PluginService", "Removed widget", widgetId, "from bar section:", section); + } + } + + if (changed) { + Settings.data.bar.widgets[section] = newWidgets; + } + } + + // Also remove from any screen overrides that have widget configurations + var overrides = Settings.data.bar.screenOverrides || []; + for (var o = 0; o < overrides.length; o++) { + if (overrides[o] && overrides[o].widgets) { + var overrideWidgets = overrides[o].widgets; + var overrideChanged = false; + for (var s2 = 0; s2 < sections.length; s2++) { + var sec = sections[s2]; + var owWidgets = overrideWidgets[sec] || []; + var owNew = []; + for (var j = 0; j < owWidgets.length; j++) { + if (owWidgets[j].id !== widgetId) { + owNew.push(owWidgets[j]); + } else { + overrideChanged = true; + changed = true; + Logger.i("PluginService", "Removed widget", widgetId, "from screen override:", overrides[o].name, "section:", sec); + } + } + if (overrideChanged) { + overrideWidgets[sec] = owNew; + } + } + if (overrideChanged) { + Settings.setScreenOverride(overrides[o].name, "widgets", overrideWidgets); + } + } + } + + // Signal the bar to refresh if widgets were removed + if (changed) { + BarService.widgetsRevision++; + } + + return changed; + } + + // Remove plugin desktop widgets from all monitors' saved settings + function removePluginDesktopWidgetsFromSettings(pluginId) { + var widgetId = "plugin:" + pluginId; + var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || []; + var changed = false; + + for (var m = 0; m < monitorWidgets.length; m++) { + var monitor = monitorWidgets[m]; + var widgets = monitor.widgets || []; + var newWidgets = []; + + for (var i = 0; i < widgets.length; i++) { + if (widgets[i].id !== widgetId) { + newWidgets.push(widgets[i]); + } else { + changed = true; + Logger.i("PluginService", "Removed desktop widget", widgetId, "from monitor:", monitor.name); + } + } + + if (newWidgets.length !== widgets.length) { + monitorWidgets[m].widgets = newWidgets; + } + } + + if (changed) { + Settings.data.desktopWidgets.monitorWidgets = monitorWidgets; + } + + return changed; + } + + // Load plugin settings and translations before instantiating components + // This ensures pluginApi is fully populated before being passed to createObject() + function loadPluginData(pluginId, manifest, callback) { + // Load settings first + loadPluginSettings(pluginId, function (settings) { + // Then load translations + loadPluginTranslationsAsync(pluginId, manifest, I18n.langCode, function (translations) { + // Load English fallback for non-English languages + if (I18n.langCode !== "en") { + loadPluginTranslationsAsync(pluginId, manifest, "en", function (fallbackTranslations) { + callback(settings, translations, fallbackTranslations); + }); + } else { + callback(settings, translations, {}); + } + }); + }); + } + + // Load a plugin + function loadPlugin(pluginId) { + if (root.loadedPlugins[pluginId]) { + Logger.w("PluginService", "Plugin already loaded:", pluginId); + return; + } + + var manifest = PluginRegistry.getPluginManifest(pluginId); + if (!manifest) { + Logger.e("PluginService", "Cannot load: manifest not found for:", pluginId); + return; + } + + var pluginDir = PluginRegistry.getPluginDir(pluginId); + + Logger.i("PluginService", "Loading plugin:", pluginId); + + // Load settings and translations FIRST, then create API and instantiate components + loadPluginData(pluginId, manifest, function (settings, translations, fallbackTranslations) { + // Create plugin API object with pre-loaded data + var pluginApi = createPluginAPI(pluginId, manifest, settings, translations, fallbackTranslations); + + // Initialize plugin entry with API and manifest + root.loadedPlugins[pluginId] = { + barWidget: null, + desktopWidget: null, + launcherProvider: null, + mainInstance: null, + api: pluginApi, + manifest: manifest + }; + + // Clear any previous errors for this plugin + root.clearPluginError(pluginId); + + // Load Main.qml entry point if it exists + if (manifest.entryPoints && manifest.entryPoints.main) { + var mainPath = pluginDir + "/" + manifest.entryPoints.main; + var loadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0; + var mainComponent = Qt.createComponent("file://" + mainPath + "?v=" + loadVersion); + + if (mainComponent.status === Component.Ready) { + // Get the plugin container from shell.qml (must be in graphics scene) + if (!root.pluginContainer) { + Logger.e("PluginService", "Plugin container not set. Shell must set PluginService.pluginContainer."); + return; + } + + // Instantiate Main.qml with pluginApi passed directly in createObject + var mainInstance = mainComponent.createObject(root.pluginContainer, { + pluginApi: pluginApi + }); + + if (mainInstance) { + root.loadedPlugins[pluginId].mainInstance = mainInstance; + pluginApi.mainInstance = mainInstance; + Logger.i("PluginService", "Loaded Main.qml for plugin:", pluginId); + } else { + root.recordPluginError(pluginId, "main", "Failed to instantiate Main.qml"); + } + } else if (mainComponent.status === Component.Error) { + root.recordPluginError(pluginId, "main", mainComponent.errorString()); + } + } + + // Load bar widget component if provided (don't instantiate - BarWidgetRegistry will do that) + if (manifest.entryPoints && manifest.entryPoints.barWidget) { + var widgetPath = pluginDir + "/" + manifest.entryPoints.barWidget; + var widgetLoadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0; + var widgetComponent = Qt.createComponent("file://" + widgetPath + "?v=" + widgetLoadVersion); + + if (widgetComponent.status === Component.Ready) { + root.loadedPlugins[pluginId].barWidget = widgetComponent; + pluginApi.barWidget = widgetComponent; + + // Register with BarWidgetRegistry + BarWidgetRegistry.registerPluginWidget(pluginId, widgetComponent, manifest.metadata); + Logger.i("PluginService", "Loaded bar widget for plugin:", pluginId); + + // Now that the widget is registered, bump widgetsRevision so the bar can render it + BarService.widgetsRevision++; + } else if (widgetComponent.status === Component.Error) { + root.recordPluginError(pluginId, "barWidget", widgetComponent.errorString()); + } + } + + // Load desktop widget component if provided (don't instantiate - DesktopWidgetRegistry will do that) + if (manifest.entryPoints && manifest.entryPoints.desktopWidget) { + var desktopWidgetPath = pluginDir + "/" + manifest.entryPoints.desktopWidget; + var desktopWidgetLoadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0; + var desktopWidgetComponent = Qt.createComponent("file://" + desktopWidgetPath + "?v=" + desktopWidgetLoadVersion); + + if (desktopWidgetComponent.status === Component.Ready) { + root.loadedPlugins[pluginId].desktopWidget = desktopWidgetComponent; + pluginApi.desktopWidget = desktopWidgetComponent; + + // Register with DesktopWidgetRegistry + DesktopWidgetRegistry.registerPluginWidget(pluginId, desktopWidgetComponent, manifest.metadata); + Logger.i("PluginService", "Loaded desktop widget for plugin:", pluginId); + } else if (desktopWidgetComponent.status === Component.Error) { + root.recordPluginError(pluginId, "desktopWidget", desktopWidgetComponent.errorString()); + } + } + + // Load launcher provider component if provided (don't instantiate - Launcher will do that) + if (manifest.entryPoints && manifest.entryPoints.launcherProvider) { + var launcherProviderPath = pluginDir + "/" + manifest.entryPoints.launcherProvider; + var launcherProviderLoadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0; + var launcherProviderComponent = Qt.createComponent("file://" + launcherProviderPath + "?v=" + launcherProviderLoadVersion); + + if (launcherProviderComponent.status === Component.Ready) { + root.loadedPlugins[pluginId].launcherProvider = launcherProviderComponent; + pluginApi.launcherProvider = launcherProviderComponent; + + // Register with LauncherProviderRegistry + LauncherProviderRegistry.registerPluginProvider(pluginId, launcherProviderComponent, manifest.metadata); + Logger.i("PluginService", "Loaded launcher provider for plugin:", pluginId); + } else if (launcherProviderComponent.status === Component.Error) { + root.recordPluginError(pluginId, "launcherProvider", launcherProviderComponent.errorString()); + } + } + + // Load control center widget component if provided + if (manifest.entryPoints && manifest.entryPoints.controlCenterWidget) { + var ccWidgetPath = pluginDir + "/" + manifest.entryPoints.controlCenterWidget; + var ccWidgetLoadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0; + var ccWidgetComponent = Qt.createComponent("file://" + ccWidgetPath + "?v=" + ccWidgetLoadVersion); + + if (ccWidgetComponent.status === Component.Ready) { + root.loadedPlugins[pluginId].controlCenterWidget = ccWidgetComponent; + pluginApi.controlCenterWidget = ccWidgetComponent; + + // Register with ControlCenterWidgetRegistry + ControlCenterWidgetRegistry.registerPluginWidget(pluginId, ccWidgetComponent, manifest.metadata); + Logger.i("PluginService", "Loaded control center widget for plugin:", pluginId); + } else if (ccWidgetComponent.status === Component.Error) { + root.recordPluginError(pluginId, "controlCenterWidget", ccWidgetComponent.errorString()); + } + } + + Logger.i("PluginService", "Plugin loaded:", pluginId); + root.pluginLoaded(pluginId); + + // Set up hot reload watcher if enabled + setupPluginFileWatcher(pluginId); + + // Notify that this plugin finished loading (for init tracking) + root._onPluginLoadComplete(); + }); + } + + // Unload a plugin + // preserveSettings: if true, don't remove desktop widget settings (used for hot reload) + function unloadPlugin(pluginId, preserveSettings) { + var plugin = root.loadedPlugins[pluginId]; + if (!plugin) { + Logger.w("PluginService", "Plugin not loaded:", pluginId); + return; + } + + Logger.i("PluginService", "Unloading plugin:", pluginId); + + // Remove hot reload watcher + removePluginFileWatcher(pluginId); + + // Unregister from BarWidgetRegistry + if (plugin.manifest.entryPoints && plugin.manifest.entryPoints.barWidget) { + BarWidgetRegistry.unregisterPluginWidget(pluginId); + } + + // Unregister from DesktopWidgetRegistry + if (plugin.manifest.entryPoints && plugin.manifest.entryPoints.desktopWidget) { + // Only remove settings when uninstalling, not during hot reload + if (!preserveSettings) { + removePluginDesktopWidgetsFromSettings(pluginId); + } + DesktopWidgetRegistry.unregisterPluginWidget(pluginId); + } + + // Unregister from LauncherProviderRegistry + if (plugin.manifest.entryPoints && plugin.manifest.entryPoints.launcherProvider) { + LauncherProviderRegistry.unregisterPluginProvider(pluginId); + } + + // Unregister from ControlCenterWidgetRegistry + if (plugin.manifest.entryPoints && plugin.manifest.entryPoints.controlCenterWidget) { + ControlCenterWidgetRegistry.unregisterPluginWidget(pluginId); + } + + // Destroy Main instance if any + if (plugin.mainInstance) { + plugin.mainInstance.destroy(); + } + + delete root.loadedPlugins[pluginId]; + root.pluginUnloaded(pluginId); + Logger.i("PluginService", "Unloaded plugin:", pluginId); + } + + // Create plugin API object with pre-loaded settings and translations + function createPluginAPI(pluginId, manifest, settings, translations, fallbackTranslations) { + var pluginDir = PluginRegistry.getPluginDir(pluginId); + + var api = Qt.createQmlObject(` + import QtQuick + + QtObject { + // Plugin-specific + readonly property string pluginId: "${pluginId}" + readonly property string pluginDir: "${pluginDir}" + property var pluginSettings: ({}) + property var manifest: ({}) + + // Instance references (set after loading) + property var mainInstance: null + property var barWidget: null + property var desktopWidget: null + property var launcherProvider: null + property var controlCenterWidget: null + + // Panel state: which screen the plugin's panel is currently open on (null if closed) + property var panelOpenScreen: null + + // IPC handlers storage + property var ipcHandlers: ({}) + + // Translation storage + property var pluginTranslations: ({}) + property var pluginFallbackTranslations: ({}) // English fallback for missing keys + property string currentLanguage: "" + property int translationVersion: 0 // Increments when translations change - plugins should depend on this + + // Functions will be bound below + property var saveSettings: null + property var openPanel: null + property var closePanel: null + property var togglePanel: null + property var openLauncher: null + property var closeLauncher: null + property var toggleLauncher: null + property var withCurrentScreen: null + property var tr: null + property var trp: null + property var hasTranslation: null + } + `, root, "PluginAPI_" + pluginId); + + // Set manifest + api.manifest = manifest; + + // Set current language (can't use binding in Qt.createQmlObject string) + api.currentLanguage = I18n.langCode; + + // Merge manifest defaults with loaded settings (user settings take priority) + var defaults = (manifest.metadata && manifest.metadata.defaultSettings) || {}; + api.pluginSettings = Object.assign({}, defaults, settings || {}); + api.pluginTranslations = translations || {}; + api.pluginFallbackTranslations = fallbackTranslations || {}; + + // ---------------------------------------- + // Helper function to get nested property by dot notation + var getNestedProperty = function (obj, path) { + var keys = path.split('.'); + var current = obj; + for (var i = 0; i < keys.length; i++) { + if (current === undefined || current === null) { + return undefined; + } + current = current[keys[i]]; + } + return current; + }; + + // ---------------------------------------- + // Bind functions + // ---------------------------------------- + api.saveSettings = function () { + savePluginSettings(pluginId, api.pluginSettings); + + // Replace the entire pluginSettings object to trigger QML property bindings + // Make a shallow copy so bindings detect the change + api.pluginSettings = Object.assign({}, api.pluginSettings); + }; + + // ---------------------------------------- + api.togglePanel = function (screen, buttonItem) { + // Toggle this plugin's panel on the specified screen + // buttonItem: optional, if provided the panel will position near this button + if (!screen) { + Logger.w("PluginAPI", "No screen available for toggling panel"); + return false; + } + return togglePluginPanel(pluginId, screen, buttonItem); + }; + + // ---------------------------------------- + api.openPanel = function (screen, buttonItem) { + // Open this plugin's panel on the specified screen + // buttonItem: optional, if provided the panel will position near this button + if (!screen) { + Logger.w("PluginAPI", "No screen available for opening panel"); + return false; + } + return openPluginPanel(pluginId, screen, buttonItem); + }; + + // ---------------------------------------- + api.closePanel = function (screen) { + // Close this plugin's panel (find which slot it's in and close it) + for (var slotNum = 1; slotNum <= 2; slotNum++) { + var panelName = "pluginPanel" + slotNum; + var panel = PanelService.getPanel(panelName, screen); + if (panel && panel.currentPluginId === pluginId) { + panel.close(); + return true; + } + } + return false; + }; + + // ---------------------------------------- + // Launcher provider methods + // ---------------------------------------- + + // Get the search prefix for this plugin's launcher provider + var getSearchPrefix = function () { + var metadata = LauncherProviderRegistry.getProviderMetadata("plugin:" + pluginId); + var prefix = (metadata && metadata.commandPrefix) ? metadata.commandPrefix : pluginId; + return ">" + prefix + " "; + }; + + api.openLauncher = function (screen) { + // Open the launcher with this plugin's provider active + if (!screen) { + Logger.w("PluginAPI", "No screen available for opening launcher"); + return; + } + PanelService.openLauncherWithSearch(screen, getSearchPrefix()); + }; + + api.closeLauncher = function (screen) { + // Close the launcher + if (!screen) { + Logger.w("PluginAPI", "No screen available for closing launcher"); + return; + } + PanelService.closeLauncher(screen); + }; + + api.toggleLauncher = function (screen) { + // Toggle the launcher with this plugin's provider active + if (!screen) { + Logger.w("PluginAPI", "No screen available for toggling launcher"); + return; + } + var searchPrefix = getSearchPrefix(); + var searchText = PanelService.getLauncherSearchText(screen); + var isInThisMode = searchText.startsWith(searchPrefix); + if (!PanelService.isLauncherOpen(screen)) { + PanelService.openLauncherWithSearch(screen, searchPrefix); + } else if (isInThisMode) { + PanelService.closeLauncher(screen); + } else { + PanelService.setLauncherSearchText(screen, searchPrefix); + } + }; + + // ---------------------------------------- + api.withCurrentScreen = function (callback) { + // Detect which screen the cursor is on and call callback with that screen + if (!root.screenDetector) { + Logger.w("PluginAPI", "Screen detector not available, using primary screen"); + callback(Quickshell.screens[0]); + return; + } + root.screenDetector.withCurrentScreen(callback); + }; + + // ---------------------------------------- + // Translation function + api.tr = function (key, interpolations) { + if (typeof interpolations === 'undefined') { + interpolations = {}; + } + + var translation = getNestedProperty(api.pluginTranslations, key); + + // Fallback to English if not found in current language + if (translation === undefined || translation === null || typeof translation !== 'string') { + translation = getNestedProperty(api.pluginFallbackTranslations, key); + } + + // Return formatted key if translation not found in any language + if (translation === undefined || translation === null) { + return `!!${key}!!`; + } + + // Ensure translation is a string + if (typeof translation !== 'string') { + return `!!${key}!!`; + } + + // Handle interpolations (e.g., "Hello {name}!") + var result = translation; + for (var placeholder in interpolations) { + var regex = new RegExp('\\{' + placeholder + '\\}', 'g'); + result = result.replace(regex, interpolations[placeholder]); + } + + return result; + }; + + // ---------------------------------------- + // Plural translation function + api.trp = function (key, count, interpolations) { + if (typeof interpolations === 'undefined') { + interpolations = {}; + } + + // Use key for singular, key-plural for plural + const realKey = count === 1 ? key : `${key}-plural`; + + // Merge interpolations with count + var finalInterpolations = { + 'count': count + }; + for (var prop in interpolations) { + finalInterpolations[prop] = interpolations[prop]; + } + + // Use tr() to look up the translation + return api.tr(realKey, finalInterpolations); + }; + + // ---------------------------------------- + // Check if translation exists + api.hasTranslation = function (key) { + return getNestedProperty(api.pluginTranslations, key) !== undefined || getNestedProperty(api.pluginFallbackTranslations, key) !== undefined; + }; + + return api; + } + + // Load plugin translations asynchronously + function loadPluginTranslationsAsync(pluginId, manifest, language, callback) { + var pluginDir = PluginRegistry.getPluginDir(pluginId); + var translationFile = pluginDir + "/i18n/" + language + ".json"; + + var readProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + command: ["cat", "${translationFile}"] + stdout: StdioCollector {} + } + `, root, "ReadTranslation_" + pluginId + "_" + language); + + readProcess.exited.connect(function (exitCode) { + var translations = {}; + + if (exitCode === 0) { + try { + translations = JSON.parse(readProcess.stdout.text); + Logger.d("PluginService", "Loaded translations for", pluginId, "language:", language); + } catch (e) { + Logger.w("PluginService", "Failed to parse translations for", pluginId, "language:", language); + } + } else { + Logger.d("PluginService", "No translation file for", pluginId, "language:", language); + } + + if (callback) { + callback(translations); + } + + readProcess.destroy(); + }); + + readProcess.running = true; + } + + // Load plugin settings + function loadPluginSettings(pluginId, callback) { + var settingsFile = PluginRegistry.getPluginSettingsFile(pluginId); + + var readProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + command: ["cat", "${settingsFile}"] + stdout: StdioCollector {} + } + `, root, "ReadSettings_" + pluginId); + + readProcess.exited.connect(function (exitCode) { + if (exitCode === 0) { + try { + var settings = JSON.parse(readProcess.stdout.text); + callback(settings); + } catch (e) { + Logger.w("PluginService", "Failed to parse settings for", pluginId, "- using defaults"); + callback({}); + } + } else { + // File doesn't exist - use defaults + callback({}); + } + + readProcess.destroy(); + }); + + readProcess.running = true; + } + + // Save plugin settings + function savePluginSettings(pluginId, settings) { + var settingsFile = PluginRegistry.getPluginSettingsFile(pluginId); + var settingsJson = JSON.stringify(settings, null, 2); + + // Use heredoc delimiter pattern to avoid all escaping issues + var delimiter = "PLUGIN_SETTINGS_EOF_" + Math.random().toString(36).substr(2, 9); + var fileEsc = settingsFile.replace(/'/g, "'\\''"); + + // Get parent directory and ensure it exists + var settingsDir = settingsFile.substring(0, settingsFile.lastIndexOf('/')); + var dirEsc = settingsDir.replace(/'/g, "'\\''"); + + // Build the shell command with heredoc (create dir first) + var writeCmd = "mkdir -p '" + dirEsc + "' && cat > '" + fileEsc + "' << '" + delimiter + "'\n" + settingsJson + "\n" + delimiter + "\n"; + + Logger.d("PluginService", "Saving settings to:", settingsFile); + + // Use Quickshell.execDetached to execute the command (use array syntax) + var pid = Quickshell.execDetached(["sh", "-c", writeCmd]); + Logger.d("PluginService", "Write process started, PID:", pid); + } + + // Load manifest from file + function loadManifest(manifestPath, callback) { + var readProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + command: ["cat", "${manifestPath}"] + stdout: StdioCollector {} + } + `, root, "ReadManifest_" + Date.now()); + + readProcess.exited.connect(function (exitCode) { + if (exitCode === 0) { + try { + var manifest = JSON.parse(readProcess.stdout.text); + callback(true, manifest); + } catch (e) { + Logger.e("PluginService", "Failed to parse manifest:", e); + callback(false, null); + } + } else { + Logger.e("PluginService", "Failed to read manifest at:", manifestPath); + callback(false, null); + } + + readProcess.destroy(); + }); + + readProcess.running = true; + } + + // Update plugin metadata in available plugins list + function updatePluginInAvailable(pluginId, updates) { + for (var i = 0; i < root.availablePlugins.length; i++) { + if (root.availablePlugins[i].id === pluginId) { + for (var key in updates) { + root.availablePlugins[i][key] = updates[key]; + } + root.availablePluginsUpdated(); + break; + } + } + } + + // Find available plugin by ID + function findAvailablePlugin(compositeKeyOrId) { + var parsed = PluginRegistry.parseCompositeKey(compositeKeyOrId); + var pluginId = parsed.pluginId; + var sourceUrl = PluginRegistry.getPluginSourceUrl(compositeKeyOrId); + + for (var i = 0; i < root.availablePlugins.length; i++) { + if (root.availablePlugins[i].id === pluginId && root.availablePlugins[i].source.url === sourceUrl) { + return root.availablePlugins[i]; + } + } + return null; + } + + // Internal flag to track if we should check for updates after registry fetch + property bool shouldCheckUpdatesAfterFetch: false + + // Flag to track if this is the initial startup update check (for auto-update) + property bool _isStartupCheck: false + + // Check for plugin updates (call this after availablePlugins are loaded) + function checkForUpdates() { + Logger.i("PluginService", "Checking for plugin updates"); + + // If we have available plugins, check immediately regardless of active fetches + if (root.availablePlugins.length > 0) { + Logger.d("PluginService", "Available plugins already loaded, checking now"); + performUpdateCheck(); + return; + } + + // No plugins yet - check if fetch is in progress + if (Object.keys(activeFetches).length > 0) { + Logger.d("PluginService", "Registry fetch in progress, will check after fetch completes"); + shouldCheckUpdatesAfterFetch = true; + return; + } + + // No plugins and no fetches - trigger refresh + Logger.d("PluginService", "No available plugins yet, triggering refresh"); + shouldCheckUpdatesAfterFetch = true; + refreshAvailablePlugins(); + } + + // Perform the actual update check + function performUpdateCheck() { + var updates = {}; + var pendingUpdates = {}; + var installedIds = PluginRegistry.getAllInstalledPluginIds(); + + Logger.d("PluginService", "Checking", installedIds.length, "installed plugins against", root.availablePlugins.length, "available plugins"); + + for (var i = 0; i < installedIds.length; i++) { + var pluginId = installedIds[i]; + var installedManifest = PluginRegistry.getPluginManifest(pluginId); + var availablePlugin = findAvailablePlugin(pluginId); + + if (installedManifest && availablePlugin) { + var currentVersion = installedManifest.version; + var availableVersion = availablePlugin.version; + + Logger.d("PluginService", "Comparing", pluginId + ":", currentVersion, "vs", availableVersion); + + // Compare versions + if (compareVersions(availableVersion, currentVersion) > 0) { + // Check if the available version requires a higher Noctalia version + if (availablePlugin.minNoctaliaVersion) { + var noctaliaVersion = UpdateService.baseVersion; + if (compareVersions(availablePlugin.minNoctaliaVersion, noctaliaVersion) > 0) { + Logger.d("PluginService", "Pending update for", pluginId + ": requires Noctalia v" + availablePlugin.minNoctaliaVersion + " (current: v" + noctaliaVersion + ")"); + pendingUpdates[pluginId] = { + currentVersion: currentVersion, + availableVersion: availableVersion, + minNoctaliaVersion: availablePlugin.minNoctaliaVersion + }; + continue; + } + } + + updates[pluginId] = { + currentVersion: currentVersion, + availableVersion: availableVersion + }; + Logger.i("PluginService", "Update available for", pluginId + ":", currentVersion, "→", availableVersion); + } + } else if (installedManifest && !availablePlugin) { + Logger.d("PluginService", "Plugin", pluginId, "not found in available plugins (might be from disabled source)"); + } + } + + root.pluginUpdates = updates; + root.pluginUpdatesPending = pendingUpdates; + var updateCount = Object.keys(updates).length; + var pendingCount = Object.keys(pendingUpdates).length; + var updatesDescription = Object.keys(updates).map(function (pluginId) { + return pluginId + ": " + updates[pluginId].currentVersion + " → " + updates[pluginId].availableVersion; + }).join("\n"); + + if (updateCount > 0) { + Logger.i("PluginService", updateCount, "plugin update(s) available"); + + if (Settings.data.plugins.notifyUpdates) { + ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.trp("panels.plugins.update-available", updateCount) + "\n\n" + updatesDescription, "plugin", 5000, I18n.tr("panels.plugins.open-plugins-tab"), function () { + // Open settings panel to Plugins tab on the screen where the cursor is + if (root.screenDetector) { + root.screenDetector.withCurrentScreen(function (screen) { + var panel = PanelService.getPanel("settingsPanel", screen); + if (panel) { + panel.requestedTab = SettingsPanel.Tab.Plugins; + panel.open(); + } + }); + } else { + // Fallback to primary screen if screen detector is not available + var panel = PanelService.getPanel("settingsPanel", Quickshell.screens[0]); + if (panel) { + panel.requestedTab = SettingsPanel.Tab.Plugins; + panel.open(); + } + } + }); + } + } else if (pendingCount > 0) { + Logger.i("PluginService", pendingCount, "plugin update(s) pending (require newer Noctalia)"); + } else { + Logger.i("PluginService", "All installed plugins are up to date"); + } + + // Auto-update on startup if enabled + if (root._isStartupCheck && Settings.data.plugins.autoUpdate && updateCount > 0) { + Logger.i("PluginService", "Auto-updating", updateCount, "plugin(s)"); + updateAllPlugins(); + } + + root._isStartupCheck = false; + shouldCheckUpdatesAfterFetch = false; + } + + // Update all plugins sequentially + function updateAllPlugins(callback) { + var pluginIds = Object.keys(root.pluginUpdates); + var currentIndex = 0; + + function updateNext() { + if (currentIndex >= pluginIds.length) { + ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.update-all-success")); + if (callback) + callback(); + return; + } + + var pluginId = pluginIds[currentIndex]; + currentIndex++; + + root.updatePlugin(pluginId, function (success, error) { + if (!success) { + Logger.w("PluginService", "Failed to auto-update", pluginId + ":", error); + } + Qt.callLater(updateNext); + }); + } + + updateNext(); + } + + // Simple version comparison (semantic versioning x.y.z) + function compareVersions(a, b) { + var aParts = a.split('.').map(function (x) { + return parseInt(x) || 0; + }); + var bParts = b.split('.').map(function (x) { + return parseInt(x) || 0; + }); + + for (var i = 0; i < 3; i++) { + var aNum = aParts[i] || 0; + var bNum = bParts[i] || 0; + if (aNum > bNum) + return 1; + if (aNum < bNum) + return -1; + } + return 0; + } + + // Update a plugin to the latest version + function updatePlugin(pluginId, callback) { + Logger.i("PluginService", "Updating plugin:", pluginId); + + // Find available plugin metadata + var availablePlugin = findAvailablePlugin(pluginId); + if (!availablePlugin) { + Logger.e("PluginService", "Plugin not found in available plugins:", pluginId); + if (callback) + callback(false, "Plugin not found"); + return; + } + + // Check Noctalia compatibility + if (availablePlugin.minNoctaliaVersion) { + // Simple check: just warn, don't block (UpdateService would have more sophisticated logic) + Logger.d("PluginService", "Plugin requires Noctalia v" + availablePlugin.minNoctaliaVersion); + } + + // Backup entire bar layout (global + screen overrides) + var barBackup = { + left: JSON.parse(JSON.stringify(Settings.data.bar.widgets.left || [])), + center: JSON.parse(JSON.stringify(Settings.data.bar.widgets.center || [])), + right: JSON.parse(JSON.stringify(Settings.data.bar.widgets.right || [])) + }; + var screenOverridesBackup = JSON.parse(JSON.stringify(Settings.data.bar.screenOverrides || [])); + Logger.d("PluginService", "Backed up bar layout (global + screen overrides)"); + + // Backup desktop widget settings (includes this plugin's widgets) + var desktopWidgetsBackup = JSON.parse(JSON.stringify(Settings.data.desktopWidgets.monitorWidgets || [])); + Logger.d("PluginService", "Backed up desktop widget settings"); + + // Close any open panels for this plugin before update + for (var slotNum = 1; slotNum <= 2; slotNum++) { + var panelName = "pluginPanel" + slotNum; + for (var s = 0; s < Quickshell.screens.length; s++) { + var panel = PanelService.getPanel(panelName, Quickshell.screens[s]); + if (panel && panel.currentPluginId === pluginId) { + Logger.d("PluginService", "Closing plugin panel before update"); + panel.close(); + panel.unloadPluginPanel(); + } + } + } + + // Disable plugin (this removes widgets and unloads code) + if (PluginRegistry.isPluginEnabled(pluginId)) { + disablePlugin(pluginId); + } + + // Now install the new version (reuse installPlugin logic, skip collision check since we're updating) + installPlugin(availablePlugin, true, function (success, error) { + if (success) { + Logger.i("PluginService", "Plugin updated successfully:", pluginId); + + // Increment load version to invalidate Qt component cache + PluginRegistry.incrementPluginLoadVersion(pluginId); + + // Re-enable the plugin first, so the new component is registered + // Skip adding to bar since we'll restore the layout from backup + enablePlugin(pluginId, true); + + // Then restore bar layout (so BarWidgetLoaders can find the new component) + Settings.data.bar.widgets.left = barBackup.left; + Settings.data.bar.widgets.center = barBackup.center; + Settings.data.bar.widgets.right = barBackup.right; + Settings.data.bar.screenOverrides = screenOverridesBackup; + Logger.d("PluginService", "Restored bar layout (global + screen overrides)"); + + // Restore desktop widget settings + Settings.data.desktopWidgets.monitorWidgets = desktopWidgetsBackup; + Logger.d("PluginService", "Restored desktop widget settings"); + + // Persist restored layout immediately to prevent race with file watcher reload + // (the earlier disablePlugin write triggers a reload that can overwrite the restore) + Settings.saveImmediate(); + + // Remove from updates list + var updates = Object.assign({}, root.pluginUpdates); + delete updates[pluginId]; + root.pluginUpdates = updates; + + if (callback) + callback(true, null); + } else { + Logger.e("PluginService", "Failed to update plugin:", pluginId, error); + + // Restore bar layout even on failure (global + screen overrides) + Settings.data.bar.widgets.left = barBackup.left; + Settings.data.bar.widgets.center = barBackup.center; + Settings.data.bar.widgets.right = barBackup.right; + Settings.data.bar.screenOverrides = screenOverridesBackup; + + // Restore desktop widget settings even on failure + Settings.data.desktopWidgets.monitorWidgets = desktopWidgetsBackup; + Settings.saveImmediate(); + + if (callback) + callback(false, error); + } + }); + } + + // Get plugin API for a loaded plugin + function getPluginAPI(pluginId) { + return root.loadedPlugins[pluginId]?.api || null; + } + + // Check if plugin is loaded + function isPluginLoaded(pluginId) { + return !!root.loadedPlugins[pluginId]; + } + + // Open a plugin's panel (finds a free slot and loads the panel) + function openPluginPanel(pluginId, screen, buttonItem) { + if (!isPluginLoaded(pluginId)) { + Logger.w("PluginService", "Cannot open panel: plugin not loaded:", pluginId); + return false; + } + + var plugin = root.loadedPlugins[pluginId]; + if (!plugin || !plugin.manifest || !plugin.manifest.entryPoints || !plugin.manifest.entryPoints.panel) { + Logger.w("PluginService", "Plugin does not provide a panel:", pluginId); + return false; + } + + // Try to find the plugin panel slot (pluginPanel1 or pluginPanel2) + // Priority: 1) toggle same plugin, 2) empty slot, 3) closed slot, 4) replace open slot + var closedSlot = null; + + for (var slotNum = 1; slotNum <= 2; slotNum++) { + var panelName = "pluginPanel" + slotNum; + var panel = PanelService.getPanel(panelName, screen); + + if (panel) { + // If this slot is already showing this plugin's panel, toggle it + if (panel.currentPluginId === pluginId) { + panel.toggle(buttonItem); + return true; + } + + // If this slot is empty, use it + if (panel.currentPluginId === "") { + panel.currentPluginId = pluginId; + panel.open(buttonItem); + return true; + } + + // Track first closed slot (panel assigned but not showing) + if (!closedSlot && !panel.isPanelOpen) { + closedSlot = panel; + } + } + } + + // Prefer reusing a closed slot over replacing an open one + if (closedSlot) { + closedSlot.currentPluginId = pluginId; + closedSlot.open(buttonItem); + return true; + } + + // If both slots are occupied and open, use slot 1 (replace existing) + var panel1 = PanelService.getPanel("pluginPanel1", screen); + if (panel1) { + var wasAlreadyOpen = panel1.isPanelOpen; + panel1.unloadPluginPanel(); + panel1.currentPluginId = pluginId; + + // If panel was already open, Component.onCompleted won't fire again + // since panelContent is already loaded. We need to load the plugin manually. + if (wasAlreadyOpen && panel1.contentLoader) { + panel1.loadPluginPanel(pluginId); + } + + panel1.open(buttonItem); + return true; + } + + Logger.e("PluginService", "Failed to find plugin panel slot"); + return false; + } + + // Toggle a plugin's panel - close if open, open if closed + // buttonItem: optional, if provided the panel will position near this button + function togglePluginPanel(pluginId, screen, buttonItem) { + if (!isPluginLoaded(pluginId)) { + Logger.w("PluginService", "Cannot toggle panel: plugin not loaded:", pluginId); + return false; + } + + var plugin = root.loadedPlugins[pluginId]; + if (!plugin || !plugin.manifest || !plugin.manifest.entryPoints || !plugin.manifest.entryPoints.panel) { + Logger.w("PluginService", "Plugin does not provide a panel:", pluginId); + return false; + } + + // Check if this plugin's panel is already open in any slot + for (var slotNum = 1; slotNum <= 2; slotNum++) { + var panelName = "pluginPanel" + slotNum; + var panel = PanelService.getPanel(panelName, screen); + + if (panel && panel.currentPluginId === pluginId) { + // Panel is open for this plugin - toggle it (close) + panel.toggle(buttonItem); + return true; + } + } + + // Panel is not open - open it using the existing logic + return openPluginPanel(pluginId, screen, buttonItem); + } + + // ----- Error tracking functions ----- + + function recordPluginError(pluginId, entryPoint, errorMessage) { + var errors = Object.assign({}, root.pluginErrors); + errors[pluginId] = { + error: errorMessage, + entryPoint: entryPoint, + timestamp: new Date() + }; + root.pluginErrors = errors; + root.pluginLoadError(pluginId, entryPoint, errorMessage); + Logger.e("PluginService", "Plugin load error [" + pluginId + "/" + entryPoint + "]:", errorMessage); + } + + function clearPluginError(pluginId) { + if (pluginId in root.pluginErrors) { + var errors = Object.assign({}, root.pluginErrors); + delete errors[pluginId]; + root.pluginErrors = errors; + } + } + + function getPluginError(pluginId) { + return root.pluginErrors[pluginId] || null; + } + + function hasPluginError(pluginId) { + return pluginId in root.pluginErrors; + } + + // ----- Hot reload functions ----- + + // Set up file watcher for a plugin directory + function setupPluginFileWatcher(pluginId) { + if (!isPluginHotReloadEnabled(pluginId)) { + return; + } + + // Don't create duplicate watchers + if (root.pluginFileWatchers[pluginId]) { + return; + } + + var manifest = PluginRegistry.getPluginManifest(pluginId); + if (!manifest) { + return; + } + + var pluginDir = PluginRegistry.getPluginDir(pluginId); + + // Create a debounce timer for this plugin + var debounceTimer = Qt.createQmlObject(` + import QtQuick + Timer { + property string targetPluginId: "" + property var reloadCallback: null + interval: 500 + repeat: false + onTriggered: { + if (reloadCallback) reloadCallback(targetPluginId); + } + } + `, root, "HotReloadDebounce_" + pluginId); + + // Set properties after creation to pass the callback + debounceTimer.targetPluginId = pluginId; + debounceTimer.reloadCallback = root.reloadPlugin; + + // Watch the manifest file - changes here indicate plugin updates + var manifestWatcher = Qt.createQmlObject(` + import Quickshell.Io + FileView { + path: "${pluginDir}/manifest.json" + watchChanges: true + } + `, root, "ManifestWatcher_" + pluginId); + + var watchers = [manifestWatcher]; + + // Only watch .qml and .js files, also follow symlinks since some of the plugins might have been symlinked in. + var qmlWatcher = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + + import qs.Commons + + Item { + id: root + signal fileChanged(); + + Process { + command: [ "sh", "-c", "find -L ${pluginDir} -name '*.qml' -o -name '*.js'" ] + running: true + stdout: SplitParser { + splitMarker: "\n" + onRead: line => { + fileWatcher.createObject(root, { path: Qt.resolvedUrl(line) }); + } + } + } + + Component { + id: fileWatcher + FileView { + watchChanges: true + + onFileChanged: { + root.fileChanged(); + } + } + } + + } + `, root, "QmlWatcher_" + pluginId); + watchers.push(qmlWatcher); + + // Connect all watchers to the debounce timer + for (var j = 0; j < watchers.length; j++) { + watchers[j].fileChanged.connect(function () { + debounceTimer.restart(); + }); + } + + // Create a separate debounce timer for translation reloads (lighter weight) + var translationDebounceTimer = Qt.createQmlObject(` + import QtQuick + Timer { + property string targetPluginId: "" + property var reloadCallback: null + interval: 300 + repeat: false + onTriggered: { + if (reloadCallback) reloadCallback(targetPluginId); + } + } + `, root, "TranslationReloadDebounce_" + pluginId); + + translationDebounceTimer.targetPluginId = pluginId; + translationDebounceTimer.reloadCallback = root.reloadPluginTranslations; + + // Watch the current language's translation file + var translationWatcher = createTranslationWatcher(pluginId, pluginDir, I18n.langCode, translationDebounceTimer); + + root.pluginFileWatchers[pluginId] = { + watchers: watchers, + debounceTimer: debounceTimer, + translationWatcher: translationWatcher, + translationDebounceTimer: translationDebounceTimer, + pluginDir: pluginDir + }; + + Logger.d("PluginService", "Set up hot reload watcher for plugin:", pluginId, "(including translations)"); + } + + // Create a translation file watcher for a specific language + function createTranslationWatcher(pluginId, pluginDir, language, debounceTimer) { + var translationFile = pluginDir + "/i18n/" + language + ".json"; + + var watcher = Qt.createQmlObject(` + import Quickshell.Io + FileView { + path: "${translationFile}" + watchChanges: true + } + `, root, "TranslationWatcher_" + pluginId + "_" + language); + + watcher.fileChanged.connect(function () { + debounceTimer.restart(); + }); + + Logger.d("PluginService", "Watching translation file:", translationFile); + return watcher; + } + + // Update translation watchers when language changes + function updateTranslationWatchers() { + for (var pluginId in root.pluginFileWatchers) { + var watcherData = root.pluginFileWatchers[pluginId]; + if (!watcherData || !watcherData.translationDebounceTimer) + continue; + + // Destroy old translation watcher + if (watcherData.translationWatcher) { + watcherData.translationWatcher.destroy(); + } + + // Create new watcher for current language + watcherData.translationWatcher = createTranslationWatcher(pluginId, watcherData.pluginDir, I18n.langCode, watcherData.translationDebounceTimer); + } + Logger.d("PluginService", "Updated translation watchers for language:", I18n.langCode); + } + + // Remove file watcher for a plugin + function removePluginFileWatcher(pluginId) { + var watcherData = root.pluginFileWatchers[pluginId]; + if (!watcherData) { + return; + } + + // Destroy all watchers + if (watcherData.watchers) { + for (var i = 0; i < watcherData.watchers.length; i++) { + if (watcherData.watchers[i]) { + watcherData.watchers[i].destroy(); + } + } + } + + // Destroy debounce timer + if (watcherData.debounceTimer) { + watcherData.debounceTimer.destroy(); + } + + // Destroy translation watcher + if (watcherData.translationWatcher) { + watcherData.translationWatcher.destroy(); + } + + // Destroy translation debounce timer + if (watcherData.translationDebounceTimer) { + watcherData.translationDebounceTimer.destroy(); + } + + delete root.pluginFileWatchers[pluginId]; + Logger.d("PluginService", "Removed hot reload watcher for plugin:", pluginId); + } + + // Reload a plugin (hot reload) + function reloadPlugin(pluginId) { + if (!root.loadedPlugins[pluginId]) { + Logger.w("PluginService", "Cannot reload: plugin not loaded:", pluginId); + return false; + } + + Logger.i("PluginService", "Hot reloading plugin:", pluginId); + + var manifest = PluginRegistry.getPluginManifest(pluginId); + if (!manifest) { + Logger.e("PluginService", "Cannot reload: manifest not found for:", pluginId); + return false; + } + + // Unregister widget instances from the bar + BarService.destroyPluginWidgetInstances(pluginId); + + // Unload the plugin (destroys components and instances) + // Pass true to preserve desktop widget settings during hot reload + unloadPlugin(pluginId, true); + + // Increment load version to invalidate Qt's component cache + PluginRegistry.incrementPluginLoadVersion(pluginId); + + // Use Qt.callLater to ensure destruction is complete before reloading + // This prevents IPC handler conflicts and other timing issues + Qt.callLater(function () { + // Reload the plugin + loadPlugin(pluginId); + + // Re-setup file watcher (it was destroyed during unload) + setupPluginFileWatcher(pluginId); + + // Emit signal + root.pluginReloaded(pluginId); + + // Show toast notification + var pluginName = manifest.name || pluginId; + ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.hot-reloaded", { + "name": pluginName + })); + + Logger.i("PluginService", "Hot reload complete for plugin:", pluginId); + }); + + return true; + } + + // Hot reload only translations for a plugin (lightweight, no component reload) + function reloadPluginTranslations(pluginId) { + var plugin = root.loadedPlugins[pluginId]; + if (!plugin || !plugin.api || !plugin.manifest) { + Logger.w("PluginService", "Cannot reload translations: plugin not loaded:", pluginId); + return false; + } + + Logger.i("PluginService", "Hot reloading translations for plugin:", pluginId); + + loadPluginTranslationsAsync(pluginId, plugin.manifest, I18n.langCode, function (translations) { + plugin.api.pluginTranslations = translations; + + // Also reload English fallback for non-English languages + if (I18n.langCode !== "en") { + loadPluginTranslationsAsync(pluginId, plugin.manifest, "en", function (fallbackTranslations) { + plugin.api.pluginFallbackTranslations = fallbackTranslations; + plugin.api.translationVersion++; + + var pluginName = plugin.manifest.name || pluginId; + ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.translations-reloaded", { + "name": pluginName + })); + Logger.i("PluginService", "Translation hot reload complete for plugin:", pluginId); + }); + } else { + plugin.api.pluginFallbackTranslations = {}; + plugin.api.translationVersion++; + + var pluginName = plugin.manifest.name || pluginId; + ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.translations-reloaded", { + "name": pluginName + })); + Logger.i("PluginService", "Translation hot reload complete for plugin:", pluginId); + } + }); + + return true; + } + + // Check if a certain plugin has hot reload enabled + function isPluginHotReloadEnabled(pluginId) { + return root.pluginHotReloadEnabled.indexOf(pluginId) !== -1; + } + + // Toggle the hot reload state of a certain plugin + function togglePluginHotReload(pluginId) { + const index = root.pluginHotReloadEnabled.indexOf(pluginId); + if (index === -1) { + root.pluginHotReloadEnabled.push(pluginId); + setupPluginFileWatcher(pluginId); + Logger.i("PluginService", "Hot reload enabled for plugin:", pluginId); + } else { + root.pluginHotReloadEnabled.splice(index, 1); + removePluginFileWatcher(pluginId); + Logger.i("PluginService", "Hot reload disabled for plugin:", pluginId); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/SupporterService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/SupporterService.qml new file mode 100644 index 0000000..15605f0 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/SupporterService.qml @@ -0,0 +1,169 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.UI + +Singleton { + id: root + + property string supporterDataFile: Settings.cacheDir + "supporters.json" + property int updateFrequency: 60 * 60 // 1 hour in seconds + property bool isFetching: false + property bool isInitialized: false + + readonly property alias data: adapter + + property var supporters: [] + property var cachedAvatars: ({}) // username -> file:// path + property bool avatarsCached: false + + FileView { + id: supporterDataFileView + path: supporterDataFile + printErrors: false + watchChanges: false + + onLoaded: { + if (!root.isInitialized) { + root.isInitialized = true; + loadFromCache(); + } + } + onLoadFailed: function (error) { + if (error.toString().includes("No such file") || error === 2) { + root.isInitialized = true; + fetchFromApi(); + } + } + + JsonAdapter { + id: adapter + property var supporters: [] + property real timestamp: 0 + } + } + + function init() { + Logger.i("Supporter", "Service started"); + } + + function loadFromCache() { + const now = Time.timestamp; + var needsRefetch = false; + + if (!data.timestamp || (now >= data.timestamp + updateFrequency)) { + needsRefetch = true; + Logger.i("Supporter", "Cache expired or missing, scheduling fetch"); + } else { + Logger.i("Supporter", "Cache is fresh, using cached data"); + } + + if (data.supporters && data.supporters.length > 0) { + root.supporters = data.supporters; + Logger.d("Supporter", "Loaded", data.supporters.length, "supporters from cache"); + } + + if (needsRefetch) { + fetchFromApi(); + } + } + + function fetchFromApi() { + if (isFetching) { + Logger.d("Supporter", "Already fetching"); + return; + } + + isFetching = true; + supporterProcess.running = true; + } + + function saveData() { + data.timestamp = Time.timestamp; + Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]); + + try { + supporterDataFileView.writeAdapter(); + Logger.d("Supporter", "Cache file written successfully"); + } catch (error) { + Logger.e("Supporter", "Failed to write cache file:", error); + } + } + + function getAvatarPath(username) { + return cachedAvatars[username] || ""; + } + + function cacheAvatars() { + if (supporters.length === 0) + return; + + avatarsCached = true; + + for (var i = 0; i < supporters.length; i++) { + var supporter = supporters[i]; + var username = supporter.github_username; + + // Only cache avatars for supporters with GitHub accounts + if (!username) + continue; + + var avatarUrl = "https://github.com/" + username + ".png?size=256"; + + (function (uname, url) { + ImageCacheService.getCircularAvatar(url, "supporter_" + uname, function (cachedPath, success) { + if (success) { + cachedAvatars[uname] = "file://" + cachedPath; + cachedAvatarsChanged(); + } + }); + })(username, avatarUrl); + } + } + + onSupportersChanged: { + if (supporters.length > 0 && !avatarsCached && ImageCacheService.initialized) { + Qt.callLater(cacheAvatars); + } + } + + Connections { + target: ImageCacheService + function onInitializedChanged() { + if (ImageCacheService.initialized && supporters.length > 0 && !avatarsCached) { + Qt.callLater(cacheAvatars); + } + } + } + + Process { + id: supporterProcess + + command: ["curl", "-s", "https://api.noctalia.dev/supporters"] + + stdout: StdioCollector { + onStreamFinished: { + try { + const response = text; + if (response && response.trim()) { + const parsed = JSON.parse(response); + if (Array.isArray(parsed)) { + root.data.supporters = parsed; + root.supporters = parsed; + root.saveData(); + Logger.d("Supporter", "Fetched", parsed.length, "supporters"); + } else if (parsed.message) { + Logger.w("Supporter", "API error:", parsed.message); + } + } + } catch (e) { + Logger.e("Supporter", "Failed to parse response:", e); + } + root.isFetching = false; + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/TelemetryService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/TelemetryService.qml new file mode 100644 index 0000000..ed303bf --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/TelemetryService.qml @@ -0,0 +1,156 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.Compositor +import qs.Services.Noctalia +import qs.Services.System + +Singleton { + id: root + + property bool initialized: false + property bool isSending: false + property int totalRamGb: 0 + property string instanceId: "" + + readonly property string telemetryEndpoint: Quickshell.env("NOCTALIA_TELEMETRY_ENDPOINT") || "https://api.noctalia.dev/ping" + + function init() { + if (initialized) + return; + + initialized = true; + + if (!Settings.data.general.telemetryEnabled) { + Logger.d("Telemetry", "Telemetry disabled by user"); + return; + } + + // Get or generate instance ID from ShellState + instanceId = ShellState.getTelemetryInstanceId(); + if (!instanceId) { + instanceId = generateRandomId(); + ShellState.setTelemetryInstanceId(instanceId); + Logger.d("Telemetry", "Generated new random instance ID"); + } else { + Logger.d("Telemetry", "Using stored instance ID"); + } + + // Read RAM info, then send ping + memInfoProcess.running = true; + } + + function generateRandomId() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + + function getInstanceId() { + return instanceId; + } + + Process { + id: memInfoProcess + command: ["sh", "-c", "grep MemTotal /proc/meminfo | awk '{print int($2/1048576)}'"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const ramGb = parseInt(text.trim()) || 0; + root.totalRamGb = ramGb; + root.sendPing(); + } + } + + onExited: function (exitCode) { + if (exitCode !== 0) { + // Still send ping even if RAM detection fails + root.sendPing(); + } + } + } + + function sendPing() { + if (isSending) + return; + + isSending = true; + + const payload = { + instanceId: instanceId, + version: UpdateService.currentVersion, + compositor: getCompositorType(), + os: HostService.osPretty || "Unknown", + ramGb: totalRamGb, + monitors: getMonitorInfo(), + ui: { + scaleRatio: Settings.data.general.scaleRatio, + fontDefaultScale: Settings.data.ui.fontDefaultScale, + fontFixedScale: Settings.data.ui.fontFixedScale + } + }; + + Logger.d("Telemetry", "Sending anonymous ping:", JSON.stringify(payload)); + + const request = new XMLHttpRequest(); + request.onreadystatechange = function () { + if (request.readyState === XMLHttpRequest.DONE) { + if (request.status >= 200 && request.status < 300) { + Logger.d("Telemetry", "Ping sent successfully"); + } else { + Logger.d("Telemetry", "Ping failed with status:", request.status); + } + isSending = false; + } + }; + + request.open("POST", telemetryEndpoint); + request.setRequestHeader("Content-Type", "application/json"); + request.send(JSON.stringify(payload)); + } + + function getMonitorInfo() { + const monitors = []; + const screens = Quickshell.screens || []; + const scales = CompositorService.displayScales || {}; + + for (let i = 0; i < screens.length; i++) { + const screen = screens[i]; + const name = screen.name || "Unknown"; + const scaleData = scales[name]; + // Extract just the numeric scale value + const scaleValue = (typeof scaleData === "object" && scaleData !== null) ? (scaleData.scale || 1.0) : (scaleData || 1.0); + monitors.push({ + width: screen.width || 0, + height: screen.height || 0, + scale: scaleValue + }); + } + + return monitors; + } + + function getCompositorType() { + if (CompositorService.isHyprland) + return "Hyprland"; + if (CompositorService.isNiri) + return "Niri"; + if (CompositorService.isScroll) + return "Scroll"; + if (CompositorService.isSway) + return "Sway"; + if (CompositorService.isMango) + return "MangoWC"; + if (CompositorService.isLabwc) + return "LabWC"; + if (CompositorService.isExtWorkspace) + return "ExtWorkspace"; + return "Unknown"; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/UpdateService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/UpdateService.qml new file mode 100644 index 0000000..c0c194e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Noctalia/UpdateService.qml @@ -0,0 +1,435 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.Noctalia +import qs.Services.UI + +Singleton { + id: root + + // Version properties + readonly property string baseVersion: "4.7.5" + readonly property bool isDevelopment: false + readonly property string developmentSuffix: "-git" + readonly property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + developmentSuffix}` + + // Telemetry was introduced in this version - users upgrading from earlier need to see the wizard + readonly property string telemetryIntroVersion: "4.0.2" + + // URLs + readonly property string discordUrl: "https://discord.noctalia.dev" + readonly property string feedbackUrl: Quickshell.env("NOCTALIA_CHANGELOG_FEEDBACK_URL") || "" + readonly property string upgradeLogBaseUrl: Quickshell.env("NOCTALIA_UPGRADELOG_URL") || "https://api.noctalia.dev/upgradelog" + + // Changelog properties + property bool initialized: false + property bool changelogPending: false + property string changelogFromVersion: "" + property string changelogToVersion: "" + property string previousVersion: "" + property string changelogCurrentVersion: "" + property string releaseContent: "" + property string lastShownVersion: "" + property bool popupScheduled: false + property string fetchError: "" + property string changelogLastSeenVersion: "" + property bool changelogStateLoaded: false + property bool pendingShowRequest: false + property bool pendingTelemetryWizardCheck: false + + // Fix for FileView race condition + property bool saveInProgress: false + property bool pendingSave: false + property int saveDebounceTimer: 0 + + Connections { + target: PanelService + function onPopupMenuWindowRegistered(screen) { + if (popupScheduled) { + if (!viewChangelogTargetScreen || viewChangelogTargetScreen.name === screen.name) { + openWhenReady(); + } + } + } + } + + signal popupQueued(string fromVersion, string toVersion) + signal telemetryWizardNeeded + + function init() { + if (initialized) + return; + + initialized = true; + Logger.i("UpdateService", "Version:", root.currentVersion); + + // Load changelog state from ShellState + Qt.callLater(() => { + if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { + loadChangelogState(); + } + }); + } + + Connections { + target: typeof ShellState !== 'undefined' ? ShellState : null + function onIsLoadedChanged() { + if (ShellState.isLoaded) { + loadChangelogState(); + } + } + } + + // Debounce timer to prevent rapid successive saves + Timer { + id: saveDebouncer + interval: 300 + repeat: false + onTriggered: executeSave() + } + + function handleChangelogRequest() { + const fromVersion = changelogFromVersion || ""; + const toVersion = changelogToVersion || ""; + + if (Settings.shouldOpenSetupWizard) { + // If you'll see the setup wizard then you don't need to see the changelog + markChangelogSeen(toVersion); + return; + } + + if (!toVersion) + return; + + if (popupScheduled && changelogCurrentVersion === toVersion) + return; + + if (!popupScheduled && lastShownVersion === toVersion) + return; + + previousVersion = fromVersion; + changelogCurrentVersion = toVersion; + + // Fetch the upgrade log from the server + fetchUpgradeLog(fromVersion, toVersion); + + popupScheduled = true; + root.popupQueued(previousVersion, changelogCurrentVersion); + + clearChangelogRequest(); + } + + function fetchUpgradeLog(fromVersion, toVersion) { + // Normalize and ensure "v" prefix for consistent URL format + let from = ensureVersionPrefix(fromVersion || changelogLastSeenVersion || "3.0.0"); + let to = ensureVersionPrefix(toVersion); + + // Strip -git suffix + from = from.replace(root.developmentSuffix, ""); + to = to.replace(root.developmentSuffix, ""); + + // 'from' always needs to be before 'to' (use semantic comparison) + if (compareVersions(from, to) >= 0) { + from = "v3.0.0"; + } + + const url = `${upgradeLogBaseUrl}/${from}/${to}`; + Logger.i("UpdateService", "Fetching upgrade log:", url); + const request = new XMLHttpRequest(); + request.onreadystatechange = function () { + if (request.readyState === XMLHttpRequest.DONE) { + Logger.d("UpdateService", "Request completed with status:", request.status); + Logger.d("UpdateService", "Response text length:", request.responseText ? request.responseText.length : 0); + + if (request.status >= 200 && request.status < 300) { + releaseContent = request.responseText || ""; + Logger.d("UpdateService", "Successfully fetched upgrade log"); + fetchError = ""; + openWhenReady(); + } else { + Logger.w("UpdateService", "Failed to fetch upgrade log, status:", request.status); + releaseContent = ""; + + if (request.status === 404) { + // Changelog not available for this version range - skip silently + Logger.w("UpdateService", "Changelog not found, skipping display"); + fetchError = ""; + popupScheduled = false; + markChangelogSeen(toVersion); + } else { + // Network error or server issue - show error to user + fetchError = I18n.tr("changelog.error.fetch-failed"); + openWhenReady(); + } + } + } + }; + request.open("GET", url); + request.send(); + } + + function normalizeVersion(version) { + if (!version) + return ""; + return version.startsWith("v") ? version.substring(1) : version; + } + + function ensureVersionPrefix(version) { + if (!version) + return ""; + return version.startsWith("v") ? version : "v" + version; + } + + function parseVersionParts(version) { + const clean = normalizeVersion(version); + if (!clean) + return []; + return clean.split(/[^0-9]+/).filter(part => part.length > 0).map(part => parseInt(part)); + } + + function compareVersions(a, b) { + if (a === b) + return 0; + const partsA = parseVersionParts(a); + const partsB = parseVersionParts(b); + const length = Math.max(partsA.length, partsB.length); + for (var i = 0; i < length; i++) { + const valA = partsA[i] || 0; + const valB = partsB[i] || 0; + if (valA > valB) + return 1; + if (valA < valB) + return -1; + } + return 0; + } + + // Check if user is upgrading from a version before telemetry was introduced + function shouldShowTelemetryWizard() { + if (!changelogStateLoaded) + return false; + if (Settings.isFreshInstall) + return false; + if (Settings.shouldOpenSetupWizard) + return false; + + // No previous version recorded but settings exist - assume upgrading from old version + // (e.g., user deleted shell-state.json but has existing settings) + if (!changelogLastSeenVersion || changelogLastSeenVersion === "") + return true; + + // Check if last seen version is before telemetry introduction + return compareVersions(changelogLastSeenVersion, telemetryIntroVersion) < 0; + } + + // Called by shell.qml to check for telemetry wizard after init + // If state isn't loaded yet, sets a pending flag and emits telemetryWizardNeeded later + function checkTelemetryWizardOrChangelog() { + Logger.d("UpdateService", "checkTelemetryWizardOrChangelog called, stateLoaded:", changelogStateLoaded); + if (!changelogStateLoaded) { + // State not loaded yet, set pending flags + Logger.d("UpdateService", "State not loaded yet, setting pending flags"); + pendingTelemetryWizardCheck = true; + pendingShowRequest = true; + return; + } + + // State is already loaded, check immediately + const needsTelemetryWizard = shouldShowTelemetryWizard(); + Logger.d("UpdateService", "shouldShowTelemetryWizard:", needsTelemetryWizard, "lastSeenVersion:", changelogLastSeenVersion); + if (needsTelemetryWizard) { + Logger.i("UpdateService", "Emitting telemetryWizardNeeded signal"); + root.telemetryWizardNeeded(); + } else { + showLatestChangelog(); + } + } + + function openWhenReady() { + if (!popupScheduled) + return; + + if (!Quickshell.screens || Quickshell.screens.length === 0) { + return; + } + + let targetScreen = viewChangelogTargetScreen; + + if (targetScreen) { + // Explicit screen requested - validate it + if (!PanelService.canShowPanelsOnScreen(targetScreen)) { + Logger.w("UpdateService", "Changelog cannot be shown on screen without bar:", targetScreen.name); + popupScheduled = false; + viewChangelogTargetScreen = null; + return; + } + } else { + // No explicit screen - find one that can show panels + targetScreen = PanelService.findScreenForPanels(); + if (!targetScreen) { + Logger.w("UpdateService", "No screen available to show changelog"); + popupScheduled = false; + return; + } + } + + const panel = PanelService.getPanel("changelogPanel", targetScreen); + if (!panel) { + // Panel not found yet. Wait for popupMenuWindowRegistered signal. + // This avoids the memory leak (#1306). + Logger.d("UpdateService", "Waiting for changelogPanel on screen:", targetScreen.name); + return; + } + + panel.open(); + popupScheduled = false; + lastShownVersion = changelogCurrentVersion; + viewChangelogTargetScreen = null; + } + + function openDiscord() { + if (!discordUrl) + return; + Quickshell.execDetached(["xdg-open", discordUrl]); + } + + function openFeedbackForm() { + if (!feedbackUrl) + return; + Quickshell.execDetached(["xdg-open", feedbackUrl]); + } + + function showLatestChangelog() { + if (!currentVersion) + return; + + if (!changelogStateLoaded) { + pendingShowRequest = true; + return; + } + + // Normalize versions for comparison (strip -git, ensure v prefix) + const lastSeen = ensureVersionPrefix(changelogLastSeenVersion.replace(developmentSuffix, "")); + const target = ensureVersionPrefix(currentVersion.replace(developmentSuffix, "")); + + if (lastSeen === target) + return; + + if (!Settings.data.general.showChangelogOnStartup) { + // user has opted out of seeing changelogs, mark as seen + markChangelogSeen(target); + return; + } + + changelogFromVersion = lastSeen; + changelogToVersion = target; + changelogPending = true; + handleChangelogRequest(); + } + + // Manual changelog viewing (e.g., from Settings > About > View Changelog) + // Shows all changes since v3.0.0, unlike showLatestChangelog() which uses lastSeenVersion + property var viewChangelogTargetScreen: null + + function viewChangelog(screen) { + if (!currentVersion) + return; + + const target = ensureVersionPrefix(currentVersion.replace(developmentSuffix, "")); + const fromVersion = "v3.8.2"; + + previousVersion = fromVersion; + changelogCurrentVersion = target; + viewChangelogTargetScreen = screen || null; + popupScheduled = true; + fetchUpgradeLog(fromVersion, target); + } + + function clearChangelogRequest() { + changelogPending = false; + changelogFromVersion = ""; + changelogToVersion = ""; + } + + function markChangelogSeen(version) { + if (!version) + return; + changelogLastSeenVersion = version; + debouncedSaveChangelogState(); + } + + function loadChangelogState() { + try { + const changelog = ShellState.getChangelogState(); + changelogLastSeenVersion = changelog.lastSeenVersion || ""; + + // Migration is now handled in Settings.qml + Logger.d("UpdateService", "Loaded changelog state from ShellState"); + } catch (error) { + Logger.e("UpdateService", "Failed to load changelog state:", error); + } + changelogStateLoaded = true; + + // Handle pending telemetry wizard check first + if (pendingTelemetryWizardCheck) { + pendingTelemetryWizardCheck = false; + if (shouldShowTelemetryWizard()) { + root.telemetryWizardNeeded(); + } else if (pendingShowRequest) { + pendingShowRequest = false; + Qt.callLater(root.showLatestChangelog); + } + return; + } + + if (pendingShowRequest) { + pendingShowRequest = false; + Qt.callLater(root.showLatestChangelog); + } + } + + function debouncedSaveChangelogState() { + // Queue a save and restart the debounce timer + pendingSave = true; + saveDebouncer.restart(); + } + + function executeSave() { + if (!pendingSave) + return; + + // Prevent concurrent saves + if (saveInProgress) { + // Retry after a short delay + saveDebouncer.start(); + return; + } + + pendingSave = false; + saveInProgress = true; + + try { + ShellState.setChangelogState({ + lastSeenVersion: changelogLastSeenVersion || "" + }); + Logger.d("UpdateService", "Saved changelog state to ShellState"); + saveInProgress = false; + + // Check if another save was queued while we were saving + if (pendingSave) { + Qt.callLater(executeSave); + } + } catch (error) { + Logger.e("UpdateService", "Failed to save changelog state:", error); + saveInProgress = false; + } + } + + function saveChangelogState() { + // Immediate save (backward compatibility) + debouncedSaveChangelogState(); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Power/IdleInhibitorService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Power/IdleInhibitorService.qml new file mode 100644 index 0000000..c7904b1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Power/IdleInhibitorService.qml @@ -0,0 +1,220 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.UI + +Singleton { + id: root + + property bool isInhibited: false + property string reason: I18n.tr("system.user-requested") + property var activeInhibitors: [] + property var timeout: null // in seconds + + // True when the native Wayland IdleInhibitor is handling inhibition + // (set by the IdleInhibitor element in MainScreen via the nativeInhibitor property) + property bool nativeInhibitorAvailable: false + + function init() { + Logger.i("IdleInhibitor", "Service started"); + } + + // Add an inhibitor + function addInhibitor(id, reason = "Application request") { + if (activeInhibitors.includes(id)) { + Logger.w("IdleInhibitor", "Inhibitor already active:", id); + return false; + } + + activeInhibitors.push(id); + updateInhibition(reason); + Logger.d("IdleInhibitor", "Added inhibitor:", id); + return true; + } + + // Remove an inhibitor + function removeInhibitor(id) { + const index = activeInhibitors.indexOf(id); + if (index === -1) { + Logger.w("IdleInhibitor", "Inhibitor not found:", id); + return false; + } + + activeInhibitors.splice(index, 1); + updateInhibition(); + Logger.d("IdleInhibitor", "Removed inhibitor:", id); + return true; + } + + // Update the actual system inhibition + function updateInhibition(newReason = reason) { + const shouldInhibit = activeInhibitors.length > 0; + + if (shouldInhibit === isInhibited) { + return; + // No change needed + } + + if (shouldInhibit) { + startInhibition(newReason); + } else { + stopInhibition(); + } + } + + // Start system inhibition + function startInhibition(newReason) { + reason = newReason; + + if (nativeInhibitorAvailable) { + // Native IdleInhibitor in MainScreen handles it via isInhibited binding + Logger.d("IdleInhibitor", "Native inhibitor active"); + } else { + startSubprocessInhibition(); + } + + isInhibited = true; + Logger.i("IdleInhibitor", "Started inhibition:", reason); + } + + // Stop system inhibition + function stopInhibition() { + if (!isInhibited) + return; + + if (!nativeInhibitorAvailable && inhibitorProcess.running) { + inhibitorProcess.signal(15); // SIGTERM + } + + isInhibited = false; + Logger.i("IdleInhibitor", "Stopped inhibition"); + } + + // Subprocess fallback using systemd-inhibit + function startSubprocessInhibition() { + inhibitorProcess.command = ["systemd-inhibit", "--what=idle", "--why=" + reason, "--mode=block", "sleep", "infinity"]; + inhibitorProcess.running = true; + } + + // Process for maintaining the inhibition (subprocess fallback only) + Process { + id: inhibitorProcess + running: false + + onExited: function (exitCode, exitStatus) { + if (isInhibited) { + Logger.w("IdleInhibitor", "Inhibitor process exited unexpectedly:", exitCode); + isInhibited = false; + } + } + + onStarted: function () { + Logger.d("IdleInhibitor", "Inhibitor process started successfully"); + } + } + + Timer { + id: inhibitorTimeout + repeat: true + interval: 1000 // 1 second + onTriggered: function () { + if (timeout == null) { + inhibitorTimeout.stop(); + return; + } + + timeout -= 1; + if (timeout <= 0) { + removeManualInhibitor(); + return; + } + } + } + + // Manual toggle for user control + function manualToggle() { + // clear any existing timeout + timeout = null; + if (activeInhibitors.includes("manual")) { + removeManualInhibitor(); + return false; + } else { + addManualInhibitor(null); + return true; + } + } + + function changeTimeout(delta) { + if (timeout == null && delta < 0) { + // no inhibitor, ignored + return; + } + + if (timeout == null && delta > 0) { + // enable manual inhibitor and set timeout + addManualInhibitor(timeout + delta); + return; + } + + if (timeout + delta <= 0) { + // disable manual inhibitor + removeManualInhibitor(); + return; + } + + if (timeout + delta > 0) { + // change timeout + addManualInhibitor(timeout + delta); + return; + } + } + + function removeManualInhibitor() { + if (timeout !== null) { + timeout = null; + if (inhibitorTimeout.running) { + inhibitorTimeout.stop(); + } + } + + if (activeInhibitors.includes("manual")) { + removeInhibitor("manual"); + ToastService.showNotice(I18n.tr("tooltips.keep-awake"), I18n.tr("common.disabled"), "keep-awake-off"); + Logger.i("IdleInhibitor", "Manual inhibition disabled"); + } + } + + function addManualInhibitor(timeoutSec) { + if (!activeInhibitors.includes("manual")) { + addInhibitor("manual", "Manually activated by user"); + ToastService.showNotice(I18n.tr("tooltips.keep-awake"), I18n.tr("common.enabled"), "keep-awake-on"); + } + + if (timeoutSec === null && timeout === null) { + Logger.i("IdleInhibitor", "Manual inhibition enabled"); + return; + } else if (timeoutSec !== null && timeout === null) { + timeout = timeoutSec; + inhibitorTimeout.start(); + Logger.i("IdleInhibitor", "Manual inhibition enabled with timeout:", timeoutSec); + return; + } else if (timeoutSec !== null && timeout !== null) { + timeout = timeoutSec; + Logger.i("IdleInhibitor", "Manual inhibition timeout changed to:", timeoutSec); + return; + } else if (timeoutSec === null && timeout !== null) { + timeout = null; + inhibitorTimeout.stop(); + Logger.i("IdleInhibitor", "Manual inhibition timeout cleared"); + return; + } + } + + // Clean up on shutdown + Component.onDestruction: { + stopInhibition(); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Power/IdleService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Power/IdleService.qml new file mode 100644 index 0000000..2978157 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Power/IdleService.qml @@ -0,0 +1,397 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.Compositor +import qs.Services.UI + +/** +* IdleService — native idle detection via ext-idle-notify-v1 Wayland protocol. +* +* Three configurable stages: +* 1. Screen-off (DPMS) — dims / turns off monitors +* 2. Lock screen — activates the session lock +* 3. Suspend — systemctl suspend +* +* Each stage shows a fade-to-black overlay for a configurable grace period +* before executing the action. Any mouse movement cancels the fade. +* +* IdleMonitor instances are created with Qt.createQmlObject() so the shell +* does not crash on compositors that lack the protocol. +* +* Timeouts come from Settings.data.idle (in seconds). 0 = disabled. +*/ +Singleton { + id: root + + // True if ext-idle-notify-v1 is supported by the compositor + readonly property bool nativeIdleMonitorAvailable: _monitorsCreated + + // Live idle time in seconds (updated by the 1s heartbeat monitor) + property int idleSeconds: 0 + + // Fade overlay state — "" means no fade in progress + property string fadePending: "" + readonly property int fadeDuration: Settings.data.idle.fadeDuration + + property bool _monitorsCreated: false + property var _screenOffMonitor: null + property var _lockMonitor: null + property var _suspendMonitor: null + property var _heartbeatMonitor: null + property var _customMonitors: ({}) + property var _queuedStages: [] + property bool _screenOffActive: false + + // Signals for external listeners (plugins, modules) + signal screenOffRequested + signal lockRequested + signal suspendRequested + + // ------------------------------------------------------- + function init() { + Logger.i("IdleService", "Service started"); + _applyTimeouts(); + } + + // Grace period timer — fires when fade completes without cancellation + Timer { + id: graceTimer + interval: root.fadeDuration * 1000 + repeat: false + onTriggered: { + const action = root.fadePending; + root._executeAction(action); + overlayCleanupTimer.start(); + } + } + + Timer { + id: overlayCleanupTimer + interval: 500 + repeat: false + onTriggered: { + root.fadePending = ""; + root._runNextQueuedStage(); + } + } + + // Counts up idleSeconds while the heartbeat monitor reports idle + Timer { + id: idleCounter + interval: 1000 + repeat: true + onTriggered: root.idleSeconds++ + } + + // ------------------------------------------------------- + function cancelFade() { + if (fadePending === "") { + _queuedStages = []; + _restoreMonitors(); + return; + } + Logger.i("IdleService", "Fade cancelled for:", fadePending); + fadePending = ""; + _queuedStages = []; + graceTimer.stop(); + overlayCleanupTimer.stop(); + _restoreMonitors(); + } + + function _restoreMonitors() { + if (!_screenOffActive) + return; + _screenOffActive = false; + Logger.i("IdleService", "Restoring monitors (DPMS on)"); + CompositorService.turnOnMonitors(); + + if (Settings.data.idle.resumeScreenOffCommand) { + Logger.i("IdleService", "Executing screen-off resume command"); + Quickshell.execDetached(["sh", "-c", Settings.data.idle.resumeScreenOffCommand]); + } + } + + function _queueStage(stage) { + if (!_isValidStage(stage)) { + Logger.w("IdleService", "Ignoring unknown queued stage:", stage); + return; + } + if (stage === fadePending) + return; + if (_queuedStages.indexOf(stage) !== -1) + return; + _queuedStages.push(stage); + Logger.d("IdleService", "Queued idle stage while fade is active:", stage); + } + + function _isValidStage(stage) { + return stage === "screenOff" || stage === "lock" || stage === "suspend"; + } + + function _isStageEnabled(stage) { + const idle = Settings.data.idle; + if (stage === "screenOff") + return idle.screenOffTimeout > 0; + if (stage === "lock") + return idle.lockTimeout > 0; + if (stage === "suspend") + return idle.suspendTimeout > 0; + return false; + } + + function _runNextQueuedStage() { + if (fadePending !== "") + return; + if (idleSeconds <= 0) { + _queuedStages = []; + return; + } + + while (_queuedStages.length > 0) { + const nextStage = _queuedStages.shift(); + if (!_isValidStage(nextStage)) { + Logger.w("IdleService", "Dropping queued unknown stage:", nextStage); + continue; + } + if (!_isStageEnabled(nextStage)) { + Logger.d("IdleService", "Dropping queued disabled stage:", nextStage); + continue; + } + + Logger.i("IdleService", "Running queued idle stage:", nextStage); + _onIdle(nextStage); + return; + } + } + + function _onIdle(stage) { + if (!_isValidStage(stage)) { + Logger.w("IdleService", "Idle fired with unknown stage:", stage); + return; + } + if (!_isStageEnabled(stage)) { + Logger.d("IdleService", "Ignoring idle stage because it is disabled:", stage); + return; + } + + if (fadePending !== "") { + _queueStage(stage); + return; + } + Logger.i("IdleService", "Idle fired:", stage); + fadePending = stage; + graceTimer.restart(); + } + + function _executeAction(stage) { + Logger.i("IdleService", "Executing action:", stage); + if (stage === "screenOff") { + if (Settings.data.idle.screenOffCommand) + Quickshell.execDetached(["sh", "-c", Settings.data.idle.screenOffCommand]); + CompositorService.turnOffMonitors(); + root._screenOffActive = true; + root.screenOffRequested(); + } else if (stage === "lock") { + if (Settings.data.idle.lockCommand) + Quickshell.execDetached(["sh", "-c", Settings.data.idle.lockCommand]); + if (PanelService.lockScreen && !PanelService.lockScreen.active) { + PanelService.lockScreen.active = true; + } + root.lockRequested(); + } else if (stage === "suspend") { + if (Settings.data.idle.suspendCommand) + Quickshell.execDetached(["sh", "-c", Settings.data.idle.suspendCommand]); + if (Settings.data.general.lockOnSuspend) { + CompositorService.lockAndSuspend(); + } else { + CompositorService.suspend(); + } + root.suspendRequested(); + } else { + Logger.w("IdleService", "Unknown idle stage action:", stage); + } + } + + // ------------------------------------------------------- + // Re-apply when settings change + Connections { + target: Settings + function onSettingsLoaded() { + root._applyTimeouts(); + } + } + + Connections { + target: Settings.data.idle + function onScreenOffTimeoutChanged() { + root._applyTimeouts(); + } + function onLockTimeoutChanged() { + root._applyTimeouts(); + } + function onSuspendTimeoutChanged() { + root._applyTimeouts(); + } + function onEnabledChanged() { + root._applyTimeouts(); + } + function onCustomCommandsChanged() { + root._applyCustomMonitors(); + } + } + + function _applyTimeouts() { + const idle = Settings.data.idle; + const globalEnabled = idle.enabled; + + _setMonitor("screenOff", globalEnabled ? idle.screenOffTimeout : 0); + _setMonitor("lock", globalEnabled ? idle.lockTimeout : 0); + _setMonitor("suspend", globalEnabled ? idle.suspendTimeout : 0); + _ensureHeartbeat(); + _applyCustomMonitors(); + } + + function _applyCustomMonitors() { + // Destroy all existing custom monitors + for (var key in _customMonitors) { + if (_customMonitors[key]) { + _customMonitors[key].destroy(); + } + } + root._customMonitors = {}; + + const idle = Settings.data.idle; + if (!idle.enabled) + return; + + var entries = []; + try { + entries = JSON.parse(idle.customCommands); + } catch (e) { + Logger.w("IdleService", "Failed to parse customCommands:", e); + return; + } + + var newMonitors = {}; + for (var i = 0; i < entries.length; i++) { + const entry = entries[i]; + const timeoutSec = parseInt(entry.timeout); + const cmd = entry.command; + const resumeCmd = entry.resumeCommand || ""; + if (!cmd && !resumeCmd || timeoutSec <= 0) + continue; + try { + const qml = ` + import Quickshell.Wayland + IdleMonitor { timeout: ${timeoutSec} } + `; + + const monitor = Qt.createQmlObject(qml, root, "IdleMonitor_custom_" + i); + const capturedCmd = cmd; + const capturedResumeCmd = resumeCmd; + monitor.isIdleChanged.connect(function () { + if (monitor.isIdle) { + if (capturedCmd) + root._executeCustomCommand(capturedCmd); + } else { + if (capturedResumeCmd) + root._executeCustomCommand(capturedResumeCmd); + } + }); + newMonitors[i] = monitor; + root._monitorsCreated = true; + Logger.i("IdleService", "Custom monitor " + i + " created, timeout", timeoutSec, "s"); + } catch (e) { + Logger.w("IdleService", "Failed to create custom monitor " + i + ":", e); + } + } + root._customMonitors = newMonitors; + } + + function _executeCustomCommand(cmd) { + Logger.i("IdleService", "Executing custom command:", cmd); + Quickshell.execDetached(["sh", "-c", cmd]); + } + + function _setMonitor(stage, timeoutSec) { + const propName = "_" + stage + "Monitor"; + const existing = root[propName]; + + if (timeoutSec <= 0) { + if (existing) { + existing.destroy(); + root[propName] = null; + Logger.d("IdleService", stage + " monitor disabled"); + } + return; + } + + if (existing) { + if (existing.timeout === timeoutSec) + return; + // ext-idle-notify-v1 has no update-timeout request — must recreate + existing.destroy(); + root[propName] = null; + Logger.d("IdleService", stage + " monitor timeout changed to", timeoutSec, "s, recreating"); + } + + try { + const qml = ` + import Quickshell.Wayland + IdleMonitor { timeout: ${timeoutSec} } + `; + + const monitor = Qt.createQmlObject(qml, root, "IdleMonitor_" + stage); + monitor.isIdleChanged.connect(function () { + if (monitor.isIdle) + root._onIdle(stage); + else + root.cancelFade(); + }); + root[propName] = monitor; + root._monitorsCreated = true; + Logger.i("IdleService", stage + " monitor created, timeout", timeoutSec, "s"); + } catch (e) { + Logger.w("IdleService", "IdleMonitor not available (compositor lacks ext-idle-notify-v1):", e); + root._monitorsCreated = false; + } + } + + function _ensureHeartbeat() { + if (_heartbeatMonitor) + return; + try { + const qml = ` + import Quickshell.Wayland + IdleMonitor { timeout: 1 } + `; + + const monitor = Qt.createQmlObject(qml, root, "IdleMonitor_heartbeat"); + monitor.isIdleChanged.connect(function () { + if (monitor.isIdle) { + root.idleSeconds = 1; + idleCounter.start(); + } else { + idleCounter.stop(); + root.idleSeconds = 0; + if (root.fadePending === "lock" && Settings.data.idle.resumeLockCommand) { + Logger.i("IdleService", "Executing lock resume command"); + Quickshell.execDetached(["sh", "-c", Settings.data.idle.resumeLockCommand]); + } else if (root.fadePending === "suspend" && Settings.data.idle.resumeSuspendCommand) { + Logger.i("IdleService", "Executing suspend resume command"); + Quickshell.execDetached(["sh", "-c", Settings.data.idle.resumeSuspendCommand]); + } + root.cancelFade(); + overlayCleanupTimer.stop(); + } + }); + _heartbeatMonitor = monitor; + root._monitorsCreated = true; + Logger.d("IdleService", "Heartbeat monitor created"); + } catch (e) { + Logger.w("IdleService", "Heartbeat monitor failed:", e); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Power/PowerProfileService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Power/PowerProfileService.qml new file mode 100644 index 0000000..5fed8f2 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Power/PowerProfileService.qml @@ -0,0 +1,132 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Services.UPower +import qs.Commons +import qs.Services.UI + +Singleton { + id: root + + readonly property var powerProfiles: PowerProfiles + readonly property bool available: powerProfiles && powerProfiles.hasPerformanceProfile + property int profile: powerProfiles ? powerProfiles.profile : PowerProfile.Balanced + + // Not a power profile but a volatile property to quickly disable shadows, animations, etc.. + property bool noctaliaPerformanceMode: false + + function getName(p) { + if (!available) + return "Unknown"; + + const prof = (p !== undefined) ? p : profile; + + switch (prof) { + case PowerProfile.Performance: + return "Performance"; + case PowerProfile.Balanced: + return "Balanced"; + case PowerProfile.PowerSaver: + return "Power saver"; + default: + return "Unknown"; + } + } + + function getIcon(p) { + if (!available) + return "balanced"; + + const prof = (p !== undefined) ? p : profile; + + switch (prof) { + case PowerProfile.Performance: + return "performance"; + case PowerProfile.Balanced: + return "balanced"; + case PowerProfile.PowerSaver: + return "powersaver"; + default: + return "balanced"; + } + } + + function init() { + Logger.d("PowerProfileService", "Service started"); + } + + function setProfile(p) { + if (!available) + return; + try { + powerProfiles.profile = p; + } catch (e) { + Logger.e("PowerProfileService", "Failed to set profile:", e); + } + } + + function cycleProfile() { + if (!available) + return; + const current = powerProfiles.profile; + if (current === PowerProfile.Performance) + setProfile(PowerProfile.PowerSaver); + else if (current === PowerProfile.Balanced) + setProfile(PowerProfile.Performance); + else if (current === PowerProfile.PowerSaver) + setProfile(PowerProfile.Balanced); + } + + function cycleProfileReverse() { + if (!available) + return; + const current = powerProfiles.profile; + if (current === PowerProfile.Performance) + setProfile(PowerProfile.Balanced); + else if (current === PowerProfile.Balanced) + setProfile(PowerProfile.PowerSaver); + else if (current === PowerProfile.PowerSaver) + setProfile(PowerProfile.Performance); + } + + function isDefault() { + if (!available) + return true; + return (profile === PowerProfile.Balanced); + } + + Connections { + target: powerProfiles + function onProfileChanged() { + root.profile = powerProfiles.profile; + // Only show toast if we have a valid profile name (not "Unknown") + const profileName = root.getName(); + if (profileName !== "Unknown") { + ToastService.showNotice(I18n.tr("toast.power-profile.profile-name", { + "profile": profileName + }), I18n.tr("toast.power-profile.changed"), profileName.toLowerCase().replace(" ", "")); + } + } + } + + // Noctalia Performance Mode + // - Turning shadow off + // - Turning animation off + // - Do Not Disturb + function toggleNoctaliaPerformance() { + noctaliaPerformanceMode = !noctaliaPerformanceMode; + } + + function setNoctaliaPerformance(value) { + noctaliaPerformanceMode = value; + } + + onNoctaliaPerformanceModeChanged: { + if (noctaliaPerformanceMode) { + ToastService.showNotice(I18n.tr("toast.noctalia-performance.label"), I18n.tr("toast.noctalia-performance.enabled"), "rocket"); + } else { + ToastService.showNotice(I18n.tr("toast.noctalia-performance.label"), I18n.tr("toast.noctalia-performance.disabled"), "rocket-off"); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/System/FontService.qml b/fedora/.config/quickshell/noctalia-shell/Services/System/FontService.qml new file mode 100644 index 0000000..8d3a25b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/System/FontService.qml @@ -0,0 +1,182 @@ +pragma Singleton + +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Io +import qs.Commons + +Singleton { + id: root + + property ListModel availableFonts: ListModel {} + property ListModel monospaceFonts: ListModel {} + property bool fontsLoaded: false + property bool isLoading: false + + function init() { + if (fontsLoaded || isLoading) + return; + Logger.i("Font", "Service started"); + loadFontsViaFcList(); + } + + // Load all fonts using fc-list in background - no main thread blocking + function loadFontsViaFcList() { + if (isLoading) + return; + isLoading = true; + allFontsProcess.running = true; + } + + function populateModels(allFontsText, monoFontsText) { + // Parse monospace fonts into a lookup set + // fc-list returns comma-separated family names for fonts with multiple families + var monoLookup = {}; + var monoLines = monoFontsText.split('\n'); + for (var i = 0; i < monoLines.length; i++) { + var line = monoLines[i].trim(); + if (line) { + var monoFamilies = line.split(','); + for (var mi = 0; mi < monoFamilies.length; mi++) { + var monoName = monoFamilies[mi].trim(); + if (monoName) + monoLookup[monoName] = true; + } + } + } + + // Parse all fonts - split comma-separated family names + var allLines = allFontsText.split('\n'); + var fontSet = {}; // Deduplicate font families + + for (var j = 0; j < allLines.length; j++) { + var line = allLines[j].trim(); + if (line) { + var families = line.split(','); + for (var fi = 0; fi < families.length; fi++) { + var fontName = families[fi].trim(); + if (fontName && !fontSet[fontName]) { + fontSet[fontName] = true; + } + } + } + } + + // Sort font names + var sortedFonts = Object.keys(fontSet).sort(function (a, b) { + return a.localeCompare(b); + }); + + // Build arrays for batch insert + var allBatch = []; + var monoBatch = []; + + for (var k = 0; k < sortedFonts.length; k++) { + var name = sortedFonts[k]; + var fontObj = { + "key": name, + "name": name + }; + allBatch.push(fontObj); + + // Check if monospace + if (monoLookup[name] || name.toLowerCase().includes("mono")) { + monoBatch.push(fontObj); + } + } + + // Clear and populate models (single batch operation) + availableFonts.clear(); + monospaceFonts.clear(); + + availableFonts.append({ + "key": Qt.application.font.family, + "name": I18n.tr("panels.indicator.system-default") + }); + monospaceFonts.append({ + "key": "monospace", + "name": I18n.tr("panels.indicator.system-default") + }); + + for (var m = 0; m < allBatch.length; m++) + availableFonts.append(allBatch[m]); + for (var n = 0; n < monoBatch.length; n++) + monospaceFonts.append(monoBatch[n]); + + fontsLoaded = true; + isLoading = false; + Logger.i("Font", "Loaded", availableFonts.count, "fonts,", monospaceFonts.count, "monospace"); + } + + // Temporary storage for process outputs + property string _allFontsOutput: "" + property string _monoFontsOutput: "" + property bool _allFontsDone: false + property bool _monoFontsDone: false + + function checkBothProcessesDone() { + if (_allFontsDone && _monoFontsDone) { + populateModels(_allFontsOutput, _monoFontsOutput); + // Clear temp storage + _allFontsOutput = ""; + _monoFontsOutput = ""; + _allFontsDone = false; + _monoFontsDone = false; + } + } + + // Process to get all font families (runs in background) + Process { + id: allFontsProcess + command: ["fc-list", "--format", "%{family}\\n"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + root._allFontsOutput = this.text; + root._allFontsDone = true; + root.checkBothProcessesDone(); + } + } + + onRunningChanged: { + if (running) { + // Start mono fonts process in parallel + monoFontsProcess.running = true; + } + } + + onExited: function (exitCode, exitStatus) { + if (exitCode !== 0) { + Logger.w("Font", "fc-list failed with exit code", exitCode); + root._allFontsOutput = ""; + root._allFontsDone = true; + root.checkBothProcessesDone(); + } + } + } + + // Process to get monospace font families (runs in background, parallel) + Process { + id: monoFontsProcess + command: ["fc-list", ":mono", "--format", "%{family}\\n"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + root._monoFontsOutput = this.text; + root._monoFontsDone = true; + root.checkBothProcessesDone(); + } + } + + onExited: function (exitCode, exitStatus) { + if (exitCode !== 0) { + root._monoFontsOutput = ""; + root._monoFontsDone = true; + root.checkBothProcessesDone(); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/System/HostService.qml b/fedora/.config/quickshell/noctalia-shell/Services/System/HostService.qml new file mode 100644 index 0000000..7758bc6 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/System/HostService.qml @@ -0,0 +1,214 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +Singleton { + id: root + + // Public properties + property string osPretty: "" + property string osLogo: "" + property bool isNixOS: false + property bool isReady: false + + // User info + readonly property string username: (Quickshell.env("USER") || "") + readonly property string envRealName: (Quickshell.env("NOCTALIA_REALNAME") || "") + property string realName: "" + + // Machine info + property string hostName: "" + + // Internal: pending logo name for fallback after probe fails + property string pendingLogoName: "" + + readonly property string displayName: { + // Explicit override + if (envRealName && envRealName.length > 0) { + return envRealName; + } + + // Name from getent + if (realName && realName.length > 0) { + return realName; + } + + // Fallback: $USER as-is (login names are case-sensitive on some systems) + if (username && username.length > 0) { + return username; + } + + // Last resort: placeholder + return "User"; + } + + function init() { + Logger.i("HostService", "Service started"); + } + + // Internal helpers + function buildCandidates(name) { + const n = (name || "").trim(); + if (!n) + return []; + + const sizes = ["512x512", "256x256", "128x128", "64x64", "48x48", "32x32", "24x24", "22x22", "16x16"]; + const exts = ["svg", "png"]; + const candidates = []; + + // pixmaps + for (const ext of exts) { + candidates.push(`/usr/share/pixmaps/${n}.${ext}`); + } + + // hicolor scalable and raster sizes + candidates.push(`/usr/share/icons/hicolor/scalable/apps/${n}.svg`); + for (const s of sizes) { + for (const ext of exts) { + candidates.push(`/usr/share/icons/hicolor/${s}/apps/${n}.${ext}`); + } + } + + // NixOS hicolor paths + candidates.push(`/run/current-system/sw/share/icons/hicolor/scalable/apps/${n}.svg`); + for (const s of sizes) { + for (const ext of exts) { + candidates.push(`/run/current-system/sw/share/icons/hicolor/${s}/apps/${n}.${ext}`); + } + } + + // Generic icon themes under /usr/share/icons (common cases) + for (const ext of exts) { + candidates.push(`/usr/share/icons/${n}.${ext}`); + candidates.push(`/usr/share/icons/${n}/${n}.${ext}`); + candidates.push(`/usr/share/icons/${n}/apps/${n}.${ext}`); + } + + return candidates; + } + + function resolveLogo(name) { + const n = (name || "").trim(); + if (!n) + return; + + // First try Quickshell's icon lookup for direct file paths + try { + const path = Quickshell.iconPath(n, ""); + if (path && path !== "" && !path.startsWith("image://")) { + // Got a direct file path - use it + const finalPath = path.startsWith("file://") ? path : "file://" + path; + root.osLogo = finalPath; + Logger.d("HostService", "Found logo via icon theme:", root.osLogo); + return; + } + } catch (e) { + // Ignore and continue to manual probe + } + + // Try manual probing for hicolor/pixmaps paths + // Store name for fallback to image:// URI if probe fails + root.pendingLogoName = n; + const all = buildCandidates(n); + if (all.length === 0) { + // No candidates, try image:// URI directly + root.osLogo = `image://icon/${n}`; + Logger.d("HostService", "Using theme icon URI:", root.osLogo); + return; + } + const script = all.map(p => `if [ -f "${p}" ]; then echo "${p}"; exit 0; fi`).join("; ") + "; exit 1"; + probe.command = ["sh", "-c", script]; + probe.running = true; + } + + // Read /etc/os-release and trigger resolution + FileView { + id: osInfo + path: "/etc/os-release" + onLoaded: { + try { + const lines = text().split("\n"); + const val = k => { + const l = lines.find(x => x.startsWith(k + "=")); + return l ? l.split("=")[1].replace(/"/g, "") : ""; + }; + root.osPretty = val("PRETTY_NAME") || val("NAME"); + Logger.i("HostService", "Detected", root.osPretty); + + const osId = (val("ID") || "").toLowerCase(); + root.isNixOS = osId === "nixos" || (root.osPretty || "").toLowerCase().includes("nixos"); + const logoName = val("LOGO"); + Logger.i("HostService", "Looking for logo icon:", logoName); + if (logoName) { + resolveLogo(logoName); + } + root.isReady = true; + } catch (e) { + Logger.w("HostService", "failed to read os-release", e); + } + } + } + + Process { + id: probe + onExited: code => { + const p = String(stdout.text || "").trim(); + if (code === 0 && p) { + root.osLogo = `file://${p}`; + root.pendingLogoName = ""; + Logger.d("HostService", "Found", root.osLogo); + } else if (root.pendingLogoName) { + // Manual probe failed, fallback to image:// URI (theme icon) + root.osLogo = `image://icon/${root.pendingLogoName}`; + root.pendingLogoName = ""; + Logger.d("HostService", "Using theme icon URI:", root.osLogo); + } else { + root.osLogo = ""; + Logger.w("HostService", "No distro logo found"); + } + } + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + // Resolve GECOS real name once on startup + Process { + id: realNameProcess + command: ["sh", "-c", "getent passwd \"$USER\" | cut -d: -f5 | cut -d, -f1"] + running: true + + stdout: StdioCollector { + onStreamFinished: { + const name = String(text || "").trim(); + if (name.length > 0) { + root.realName = name; + Logger.i("HostService", "resolved real name", name); + } + } + } + stderr: StdioCollector {} + } + + // Resolve hostname from distro-specific locations. + // Prefer /etc/hostname, fallback to Gentoo's /etc/conf.d/hostname. + Process { + id: hostNameProcess + command: ["sh", "-c", + "if [ -r /etc/hostname ]; then sed -n '1p' /etc/hostname; exit 0; fi; if [ -r /etc/conf.d/hostname ]; then v=$(sed -n -E 's/^[[:space:]]*[Hh][Oo][Ss][Tt][Nn][Aa][Mm][Ee][[:space:]]*=[[:space:]]*//p' /etc/conf.d/hostname | sed -n '1p'); v=$(printf '%s' \"$v\" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//; s/^\"//; s/\"$//; s/^\x27//; s/\x27$//'); printf '%s\n' \"$v\"; exit 0; fi; exit 0"] + running: true + + stdout: StdioCollector { + onStreamFinished: { + const name = String(text || "").trim(); + if (name.length > 0) { + root.hostName = name; + Logger.i("HostService", "resolved hostname", name); + } + } + } + stderr: StdioCollector {} + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/System/NotificationRulesService.qml b/fedora/.config/quickshell/noctalia-shell/Services/System/NotificationRulesService.qml new file mode 100644 index 0000000..792846e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/System/NotificationRulesService.qml @@ -0,0 +1,86 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +Singleton { + id: root + + readonly property string rulesFilePath: Settings.configDir + "notification-rules.json" + + property var rules: [] + + property FileView rulesFileView: FileView { + id: rulesFileView + path: root.rulesFilePath + watchChanges: true + printErrors: false + + adapter: JsonAdapter { + id: rulesAdapter + property var rules: [] + } + + onLoaded: { + try { + const parsed = JSON.parse(rulesFileView.text()); + const raw = Array.isArray(parsed.rules) ? parsed.rules : []; + root.rules = raw.filter(r => (r.pattern || "").trim() !== ""); + } catch (e) { + root.rules = []; + } + } + + onLoadFailed: function (error) { + root.rules = []; + } + } + + function init() { + } + + function save() { + Quickshell.execDetached(["mkdir", "-p", Settings.configDir]); + root.rules = root.rules.filter(r => (r.pattern || "").trim() !== ""); + rulesAdapter.rules = root.rules; + rulesFileView.writeAdapter(); + } + + function evaluate(appName, summary, body) { + const haystack = [appName || "", summary || "", body || ""].join(" "); + for (let i = 0; i < root.rules.length; i++) { + const r = root.rules[i]; + const pattern = (r.pattern || "").trim(); + if (pattern === "") + continue; + let matched = false; + if (pattern.length >= 3 && pattern.startsWith("/") && pattern.endsWith("/")) { + try { + matched = new RegExp(pattern.slice(1, -1)).test(haystack); + } catch (e) { + Logger.w("NotificationRulesService", "Invalid regex:", pattern, e); + } + } else if (pattern.includes("*")) { + try { + const reStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*"); + matched = new RegExp(reStr, "i").test(haystack); + } catch (e) { + matched = haystack.toLowerCase().includes(pattern.toLowerCase()); + } + } else { + matched = haystack.toLowerCase().includes(pattern.toLowerCase()); + } + if (matched) { + const a = (r.action || "block").toLowerCase(); + if (a === "mute" || a === "hide") + return a; + if (a === "silence") + return "hide"; + return "block"; + } + } + return null; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/System/NotificationService.qml b/fedora/.config/quickshell/noctalia-shell/Services/System/NotificationService.qml new file mode 100644 index 0000000..fff2419 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/System/NotificationService.qml @@ -0,0 +1,1239 @@ +pragma Singleton + +import QtQuick +import QtQuick.Window +import Quickshell +import Quickshell.Io +import Quickshell.Services.Notifications +import Quickshell.Wayland +import "../../Helpers/sha256.js" as Checksum +import qs.Commons +import qs.Services.Compositor +import qs.Services.Media +import qs.Services.Power +import qs.Services.System +import qs.Services.UI + +Singleton { + id: root + + // Configuration + property int maxPopups: 5 + property int maxHistory: 100 + property string historyFile: Quickshell.env("NOCTALIA_NOTIF_HISTORY_FILE") || (Settings.cacheDir + "notifications.json") + + // State + property real lastSeenTs: 0 + // Volatile property that doesn't persist to settings (similar to noctaliaPerformanceMode) + property bool doNotDisturb: false + + // Models + property ListModel popupModel: ListModel {} + property ListModel historyModel: ListModel {} + + // Internal state + property var popupState: ({}) // Maps internal ID to {notification, watcher, cachedActions, metadata} + property var quickshellIdToInternalId: ({}) + + // Rate limiting for notification sounds (minimum 100ms between sounds) + property var lastSoundTime: 0 + readonly property int minSoundInterval: 100 + + // Notification server + property var notificationServerLoader: null + + Component { + id: notificationServerComponent + NotificationServer { + keepOnReload: false + imageSupported: true + actionsSupported: true + onNotification: notification => handleNotification(notification) + } + } + + Component { + id: notificationWatcherComponent + Connections { + property var targetNotification + property var targetDataId + target: targetNotification + + function onSummaryChanged() { + updateNotificationFromObject(targetDataId); + } + function onBodyChanged() { + updateNotificationFromObject(targetDataId); + } + function onAppNameChanged() { + updateNotificationFromObject(targetDataId); + } + function onUrgencyChanged() { + updateNotificationFromObject(targetDataId); + } + function onAppIconChanged() { + updateNotificationFromObject(targetDataId); + } + function onImageChanged() { + updateNotificationFromObject(targetDataId); + } + function onActionsChanged() { + updateNotificationFromObject(targetDataId); + } + } + } + + function updateNotificationServer() { + if (notificationServerLoader) { + notificationServerLoader.destroy(); + notificationServerLoader = null; + } + + if (Settings.isLoaded && Settings.data.notifications.enabled !== false) { + notificationServerLoader = notificationServerComponent.createObject(root); + } + } + + Component.onCompleted: { + if (Settings.isLoaded) { + updateNotificationServer(); + } + + // Load state from ShellState + Qt.callLater(() => { + if (typeof ShellState !== 'undefined' && ShellState.isLoaded) { + loadState(); + } + }); + } + + Connections { + target: typeof ShellState !== 'undefined' ? ShellState : null + function onIsLoadedChanged() { + if (ShellState.isLoaded) { + loadState(); + } + } + } + + Connections { + target: Settings + function onSettingsLoaded() { + updateNotificationServer(); + } + function onSettingsSaved() { + updateNotificationServer(); + } + } + + // Helper function to generate content-based ID for deduplication + function getContentId(summary, body, appName) { + return Checksum.sha256(JSON.stringify({ + "summary": summary || "", + "body": body || "", + "app": appName || "" + })); + } + + // Main handler + function handleNotification(notification) { + const quickshellId = notification.id; + const data = createData(notification); + + const ruleAction = NotificationRulesService.evaluate(data.appName, data.summary, data.body); + if (ruleAction === "block") + return; + if (ruleAction === "hide") { + trySaveToHistory(data, notification); + return; + } + + trySaveToHistory(data, notification); + + if (root.doNotDisturb || PowerProfileService.noctaliaPerformanceMode) + return; + + // Check if this is a replacement notification + const existingInternalId = quickshellIdToInternalId[quickshellId]; + if (existingInternalId && popupState[existingInternalId]) { + updatePopup(existingInternalId, notification, data); + return; + } + + // Check for duplicate content + const duplicateId = findDuplicateNotification(data); + if (duplicateId) { + removePopup(duplicateId); + } + + // Add new notification + addPopup(quickshellId, notification, data); + if (ruleAction !== "mute") + playNotificationSound(data.urgency, data.appName); + } + + function playNotificationSound(urgency, appName) { + if (!SoundService.multimediaAvailable) { + return; + } + if (!Settings.data.notifications?.sounds?.enabled) { + return; + } + if (AudioService.muted) { + return; + } + if (appName) { + const excludedApps = Settings.data.notifications.sounds.excludedApps || ""; + if (excludedApps.trim() !== "") { + const excludedList = excludedApps.toLowerCase().split(',').map(app => app.trim()); + const normalizedName = appName.toLowerCase(); + + if (excludedList.includes(normalizedName)) { + Logger.i("NotificationService", `Skipping sound for excluded app: ${appName}`); + return; + } + } + } + + // Get the sound file for this urgency level + const soundFile = getNotificationSoundFile(urgency); + if (!soundFile || soundFile.trim() === "") { + // No sound file configured for this urgency level + Logger.i("NotificationService", `No sound file configured for urgency ${urgency}`); + return; + } + + // Rate limiting - prevent sound spam + const now = Date.now(); + if (now - lastSoundTime < minSoundInterval) { + return; + } + lastSoundTime = now; + + // Play sound using existing SoundService + const volume = Settings.data.notifications?.sounds?.volume ?? 0.5; + SoundService.playSound(soundFile, { + volume: volume, + fallback: false, + repeat: false + }); + } + + // Get the appropriate sound file path for a given urgency level + function getNotificationSoundFile(urgency) { + const settings = Settings.data.notifications?.sounds; + if (!settings) { + return ""; + } + + // Default sound file path + const defaultSoundFile = Quickshell.shellDir + "/Assets/Sounds/notification-generic.wav"; + + // If separate sounds is disabled, always use normal sound for all urgencies + if (!settings.separateSounds) { + const soundFile = settings.normalSoundFile; + if (soundFile && soundFile.trim() !== "") { + return soundFile; + } + // Return default if no sound file configured + return defaultSoundFile; + } + + // Map urgency levels to sound file keys (when separate sounds is enabled) + let soundKey; + switch (urgency) { + case 0: + soundKey = "lowSoundFile"; + break; + case 1: + soundKey = "normalSoundFile"; + break; + case 2: + soundKey = "criticalSoundFile"; + break; + default: + // Default to normal urgency for invalid values + soundKey = "normalSoundFile"; + break; + } + + const soundFile = settings[soundKey]; + if (soundFile && soundFile.trim() !== "") { + return soundFile; + } + + // Return default sound file if none configured for this urgency level + return defaultSoundFile; + } + + function updatePopup(internalId, notification, data) { + const index = findPopupIndex(internalId); + if (index < 0) + return; + const existing = popupModel.get(index); + const oldTimestamp = existing.timestamp; + const oldProgress = existing.progress; + + // Update properties (keeping original timestamp and progress) + popupModel.setProperty(index, "summary", data.summary); + popupModel.setProperty(index, "summaryMarkdown", data.summaryMarkdown); + popupModel.setProperty(index, "body", data.body); + popupModel.setProperty(index, "bodyMarkdown", data.bodyMarkdown); + popupModel.setProperty(index, "appName", data.appName); + popupModel.setProperty(index, "urgency", data.urgency); + popupModel.setProperty(index, "expireTimeout", data.expireTimeout); + popupModel.setProperty(index, "originalImage", data.originalImage); + popupModel.setProperty(index, "cachedImage", data.cachedImage); + popupModel.setProperty(index, "actionsJson", data.actionsJson); + popupModel.setProperty(index, "timestamp", oldTimestamp); + popupModel.setProperty(index, "progress", oldProgress); + + // Update stored notification object + const notifData = popupState[internalId]; + notifData.notification = notification; + + // Deep copy actions to preserve them even if QML object clears list + var safeActions = []; + if (notification.actions) { + for (var i = 0; i < notification.actions.length; i++) { + safeActions.push({ + "identifier": notification.actions[i].identifier, + "actionObject": notification.actions[i] + }); + } + } + notifData.cachedActions = safeActions; + notifData.metadata.originalId = data.originalId; + + notification.tracked = true; + + function onClosed() { + dismissPopup(internalId); + } + notification.closed.connect(onClosed); + notifData.onClosed = onClosed; + + // Update metadata + notifData.metadata.urgency = data.urgency; + notifData.metadata.duration = calculateDuration(data); + } + + function addPopup(quickshellId, notification, data) { + // Map IDs + quickshellIdToInternalId[quickshellId] = data.id; + + // Create watcher + const watcher = notificationWatcherComponent.createObject(root, { + "targetNotification": notification, + "targetDataId": data.id + }); + + // Deep copy actions + var safeActions = []; + if (notification.actions) { + for (var i = 0; i < notification.actions.length; i++) { + safeActions.push({ + "identifier": notification.actions[i].identifier, + "actionObject": notification.actions[i] + }); + } + } + + // Store notification data + popupState[data.id] = { + "notification": notification, + "watcher": watcher, + "cachedActions": safeActions // Cache actions + , + "metadata": { + "originalId": data.originalId // Store original ID + , + "timestamp": data.timestamp.getTime(), + "duration": calculateDuration(data), + "urgency": data.urgency, + "paused": false, + "pauseTime": 0 + } + }; + + notification.tracked = true; + + function onClosed() { + dismissPopup(data.id); + } + notification.closed.connect(onClosed); + popupState[data.id].onClosed = onClosed; + + // Defer list insertion to prevent re-entrant QML incubation crash. + // Direct insert triggers Repeater.modelUpdated synchronously, which + // incubates delegates whose signal handlers can re-enter the V4 engine + // and crash in QV4::Object::insertMember. + Qt.callLater(() => { + popupModel.insert(0, data); + + // Remove overflow + while (popupModel.count > maxPopups) { + const last = popupModel.get(popupModel.count - 1); + // Overflow only removes from ACTIVE view, but keeps it for history + popupState[last.id]?.notification?.dismiss(); // Visually dismiss + popupModel.remove(popupModel.count - 1); + // DO NOT call cleanupNotification here, we want to keep it for history actions + } + }); + } + + function findDuplicateNotification(data) { + const contentId = getContentId(data.summary, data.body, data.appName); + + for (var i = 0; i < popupModel.count; i++) { + const existing = popupModel.get(i); + const existingContentId = getContentId(existing.summary, existing.body, existing.appName); + if (existingContentId === contentId) { + return existing.id; + } + } + return null; + } + + function calculateDuration(data) { + const durations = [Settings.data.notifications?.lowUrgencyDuration * 1000 || 3000, Settings.data.notifications?.normalUrgencyDuration * 1000 || 8000, Settings.data.notifications?.criticalUrgencyDuration * 1000 || 15000]; + + if (Settings.data.notifications?.respectExpireTimeout) { + if (data.expireTimeout === 0) + return -1; // Never expire + if (data.expireTimeout > 0) + return data.expireTimeout; + } + + return durations[data.urgency]; + } + + function createData(n) { + const time = new Date(); + const id = Checksum.sha256(JSON.stringify({ + "summary": n.summary, + "body": n.body, + "app": n.appName, + "time": time.getTime() + })); + + const image = n.image || getIcon(n.appIcon); + const imageId = generateImageId(n, image); + queueImage(image, n.appName || "", n.summary || "", id); + + return { + "id": id, + "summary": processNotificationText(n.summary || ""), + "summaryMarkdown": processNotificationMarkdown(n.summary || ""), + "body": processNotificationText(n.body || ""), + "bodyMarkdown": processNotificationMarkdown(n.body || ""), + "appName": getAppName(n.appName || n.desktopEntry || ""), + "urgency": n.urgency < 0 || n.urgency > 2 ? 1 : n.urgency, + "expireTimeout": n.expireTimeout, + "timestamp": time, + "progress": 1.0, + "originalImage": image, + "cachedImage": image // Start with original, update when cached + , + "originalId": n.originalId || n.id || 0 // Ensure originalId is passed through + , + "actionsJson": JSON.stringify((n.actions || []).map(a => ({ + "text": (a.text || "").trim() || "Action", + "identifier": a.identifier || "" + }))) + }; + } + + function findPopupIndex(internalId) { + for (var i = 0; i < popupModel.count; i++) { + if (popupModel.get(i).id === internalId) { + return i; + } + } + return -1; + } + + function updateNotificationFromObject(internalId) { + const notifData = popupState[internalId]; + if (!notifData) + return; + const index = findPopupIndex(internalId); + if (index < 0) + return; + const data = createData(notifData.notification); + const existing = popupModel.get(index); + + // Update properties (keeping timestamp and progress) + popupModel.setProperty(index, "summary", data.summary); + popupModel.setProperty(index, "summaryMarkdown", data.summaryMarkdown); + popupModel.setProperty(index, "body", data.body); + popupModel.setProperty(index, "bodyMarkdown", data.bodyMarkdown); + popupModel.setProperty(index, "appName", data.appName); + popupModel.setProperty(index, "urgency", data.urgency); + popupModel.setProperty(index, "expireTimeout", data.expireTimeout); + popupModel.setProperty(index, "originalImage", data.originalImage); + popupModel.setProperty(index, "cachedImage", data.cachedImage); + popupModel.setProperty(index, "actionsJson", data.actionsJson); + + // Update metadata + notifData.metadata.urgency = data.urgency; + notifData.metadata.duration = calculateDuration(data); + } + + function removePopup(id) { + const index = findPopupIndex(id); + if (index >= 0) { + popupModel.remove(index); + } + cleanupNotification(id); + } + + function cleanupNotification(id) { + const notifData = popupState[id]; + if (notifData) { + notifData.watcher?.destroy(); + delete popupState[id]; + } + + // Clean up quickshell ID mapping + for (const qsId in quickshellIdToInternalId) { + if (quickshellIdToInternalId[qsId] === id) { + delete quickshellIdToInternalId[qsId]; + break; + } + } + } + + // Progress updates + Timer { + interval: 50 + repeat: true + running: popupModel.count > 0 + onTriggered: updateAllProgress() + } + + function updateAllProgress() { + const now = Date.now(); + const toRemove = []; + + for (var i = 0; i < popupModel.count; i++) { + const notif = popupModel.get(i); + const notifData = popupState[notif.id]; + if (!notifData) + continue; + const meta = notifData.metadata; + if (meta.duration === -1 || meta.paused) + continue; + const elapsed = now - meta.timestamp; + const progress = Math.max(1.0 - (elapsed / meta.duration), 0.0); + + if (progress <= 0) { + toRemove.push(notif.id); + } else if (Math.abs(notif.progress - progress) > 0.005) { + popupModel.setProperty(i, "progress", progress); + } + } + + if (toRemove.length > 0) { + animateAndRemove(toRemove[0]); + } + } + + // Image handling + function queueImage(path, appName, summary, notificationId) { + if (!path || !notificationId) + return; + + // Cache image:// URIs and temporary file paths (e.g. /tmp/ from Chromium) + const filePath = path.startsWith("file://") ? path.substring(7) : path; + const isImageUri = path.startsWith("image://"); + const isTempFile = (path.startsWith("/") || path.startsWith("file://")) && filePath.startsWith("/tmp/"); + + if (!isImageUri && !isTempFile) + return; + + ImageCacheService.getNotificationIcon(path, appName, summary, function (cachedPath, success) { + if (success && cachedPath) { + updateImagePath(notificationId, "file://" + cachedPath); + } + }); + } + + function updateImagePath(notificationId, path) { + updateModel(popupModel, notificationId, "cachedImage", path); + updateModel(historyModel, notificationId, "cachedImage", path); + saveHistory(); + } + + function updateModel(model, notificationId, prop, value) { + for (var i = 0; i < model.count; i++) { + if (model.get(i).id === notificationId) { + model.setProperty(i, prop, value); + break; + } + } + } + + // History management + function trySaveToHistory(data, notification) { + if (notification.transient) + return; + const s = Settings.data.notifications?.saveToHistory; + if (s) { + let ok = true; + if (data.urgency === 0) + ok = s.low !== false; + else if (data.urgency === 1) + ok = s.normal !== false; + else if (data.urgency === 2) + ok = s.critical !== false; + if (ok) + addToHistory(data); + } else { + addToHistory(data); + } + } + + function addToHistory(data) { + // Defer list insertion to prevent re-entrant QML incubation crash. + // See addPopup for full explanation. + Qt.callLater(() => { + historyModel.insert(0, data); + + while (historyModel.count > maxHistory) { + const old = historyModel.get(historyModel.count - 1); + // Only delete cached images that are in our cache directory + const cachedPath = old.cachedImage ? old.cachedImage.replace(/^file:\/\//, "") : ""; + if (cachedPath && cachedPath.startsWith(ImageCacheService.notificationsDir)) { + Quickshell.execDetached(["rm", "-f", cachedPath]); + } + historyModel.remove(historyModel.count - 1); + } + saveHistory(); + }); + } + + // Persistence - History + FileView { + id: historyFileView + path: historyFile + printErrors: false + onLoaded: loadHistory() + onLoadFailed: error => { + if (error === 2) + writeAdapter(); + } + + JsonAdapter { + id: adapter + property var notifications: [] + } + } + + Timer { + id: saveTimer + interval: 200 + onTriggered: performSaveHistory() + } + + function saveHistory() { + saveTimer.restart(); + } + + function performSaveHistory() { + try { + const items = []; + for (var i = 0; i < historyModel.count; i++) { + const n = historyModel.get(i); + const copy = Object.assign({}, n); + copy.timestamp = n.timestamp.getTime(); + items.push(copy); + } + adapter.notifications = items; + historyFileView.writeAdapter(); + } catch (e) { + Logger.e("Notifications", "Save history failed:", e); + } + } + + function loadHistory() { + try { + historyModel.clear(); + for (const item of adapter.notifications || []) { + const time = new Date(item.timestamp); + + // Use the cached image if it exists and starts with file://, otherwise use originalImage + let cachedImage = item.cachedImage || ""; + if (!cachedImage || (!cachedImage.startsWith("file://") && !cachedImage.startsWith("/"))) { + cachedImage = item.originalImage || ""; + } + + historyModel.append({ + "id": item.id || "", + "summary": item.summary || "", + "summaryMarkdown": processNotificationMarkdown(item.summary || ""), + "body": item.body || "", + "bodyMarkdown": processNotificationMarkdown(item.body || ""), + "appName": item.appName || "", + "urgency": item.urgency < 0 || item.urgency > 2 ? 1 : item.urgency, + "timestamp": time, + "originalImage": item.originalImage || "", + "cachedImage": cachedImage, + "actionsJson": item.actionsJson || "[]", + "originalId": item.originalId || 0 + }); + } + } catch (e) { + Logger.e("Notifications", "Load failed:", e); + } + } + + function loadState() { + try { + const notifState = ShellState.getNotificationsState(); + root.lastSeenTs = notifState.lastSeenTs || 0; + + // Migration is now handled in Settings.qml + Logger.d("Notifications", "Loaded state from ShellState"); + } catch (e) { + Logger.e("Notifications", "Load state failed:", e); + } + } + + function saveState() { + try { + ShellState.setNotificationsState({ + lastSeenTs: root.lastSeenTs + }); + Logger.d("Notifications", "Saved state to ShellState"); + } catch (e) { + Logger.e("Notifications", "Save state failed:", e); + } + } + + function updateLastSeenTs() { + root.lastSeenTs = Time.timestamp * 1000; + saveState(); + } + + // Utility functions + function getAppName(name) { + if (!name || name.trim() === "") + return "Unknown"; + name = name.trim(); + + if (name.includes(".") && (name.startsWith("com.") || name.startsWith("org.") || name.startsWith("io.") || name.startsWith("net."))) { + const parts = name.split("."); + let appPart = parts[parts.length - 1]; + + if (!appPart || appPart === "app" || appPart === "desktop") { + appPart = parts[parts.length - 2] || parts[0]; + } + + if (appPart) + name = appPart; + } + + if (name.includes(".")) { + const parts = name.split("."); + let displayName = parts[parts.length - 1]; + + if (!displayName || /^\d+$/.test(displayName)) { + displayName = parts[parts.length - 2] || parts[0]; + } + + if (displayName) { + displayName = displayName.charAt(0).toUpperCase() + displayName.slice(1); + displayName = displayName.replace(/([a-z])([A-Z])/g, '$1 $2'); + displayName = displayName.replace(/app$/i, '').trim(); + displayName = displayName.replace(/desktop$/i, '').trim(); + displayName = displayName.replace(/flatpak$/i, '').trim(); + + if (!displayName) { + displayName = parts[parts.length - 1].charAt(0).toUpperCase() + parts[parts.length - 1].slice(1); + } + } + + return displayName || name; + } + + let displayName = name.charAt(0).toUpperCase() + name.slice(1); + displayName = displayName.replace(/([a-z])([A-Z])/g, '$1 $2'); + displayName = displayName.replace(/app$/i, '').trim(); + displayName = displayName.replace(/desktop$/i, '').trim(); + + return displayName || name; + } + + function getIcon(icon) { + if (!icon) + return ""; + if (icon.startsWith("/") || icon.startsWith("file://")) + return icon; + return ThemeIcons.iconFromName(icon); + } + + function escapeHtml(text) { + if (!text) + return ""; + return text.replace(/&/g, "&").replace(//g, ">"); + } + + function sanitizeMarkdownUrl(url) { + if (!url) + return ""; + const trimmed = url.trim(); + if (trimmed === "") + return ""; + const lower = trimmed.toLowerCase(); + if (lower.startsWith("http://") || lower.startsWith("https://") || lower.startsWith("mailto:")) { + return encodeURI(trimmed); + } + return ""; + } + + function sanitizeMarkdown(text) { + if (!text) + return ""; + + let input = String(text); + + // Strip images entirely + input = input.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, function (match, alt) { + return alt ? alt : ""; + }); + + // Extract links into placeholders + const links = []; + input = input.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function (match, label, urlAndTitle) { + const urlPart = (urlAndTitle || "").trim().split(/\s+/)[0] || ""; + const safeUrl = sanitizeMarkdownUrl(urlPart); + const safeLabel = escapeHtml(label); + if (!safeUrl) + return safeLabel; + const token = "__MDLINK_" + links.length + "__"; + links.push({ + "label": safeLabel, + "url": safeUrl + }); + return token; + }); + + // Escape any remaining HTML + input = escapeHtml(input); + + // Restore sanitized links + for (let i = 0; i < links.length; i++) { + const token = "__MDLINK_" + i + "__"; + const link = links[i]; + input = input.split(token).join("[" + link.label + "](" + link.url + ")"); + } + + return input; + } + + function processNotificationText(text) { + if (!text) + return ""; + + // Split by tags to process segments separately + const parts = text.split(/(<[^>]+>)/); + let result = ""; + const allowedTags = ["b", "i", "u", "a", "br"]; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part.startsWith("<") && part.endsWith(">")) { + const content = part.substring(1, part.length - 1); + const firstWord = content.split(/[\s/]/).filter(s => s.length > 0)[0]?.toLowerCase(); + + if (allowedTags.includes(firstWord)) { + // Preserve valid HTML tag + result += part; + } else { + // Unknown tag: drop tag without leaking attributes + result += ""; + } + } else { + // Normal text: escape everything + result += escapeHtml(part); + } + } + return result; + } + + function processNotificationMarkdown(text) { + return sanitizeMarkdown(text); + } + + function generateImageId(notification, image) { + if (image && image.startsWith("image://")) { + if (image.startsWith("image://qsimage/")) { + const key = (notification.appName || "") + "|" + (notification.summary || ""); + return Checksum.sha256(key); + } + return Checksum.sha256(image); + } + return ""; + } + + function pauseTimeout(id) { + const notifData = popupState[id]; + if (notifData && !notifData.metadata.paused) { + notifData.metadata.paused = true; + notifData.metadata.pauseTime = Date.now(); + } + } + + function resumeTimeout(id) { + const notifData = popupState[id]; + if (notifData && notifData.metadata.paused) { + notifData.metadata.timestamp += Date.now() - notifData.metadata.pauseTime; + notifData.metadata.paused = false; + } + } + + // Dismiss a popup notification (e.g. clicked close, swipe, or overflow). + // Removes from popup list but KEEPS data for history. + function dismissPopup(id) { + const index = findPopupIndex(id); + if (index >= 0) { + popupModel.remove(index); + } + } + + function dismissOldestPopup() { + if (popupModel.count > 0) { + const lastNotif = popupModel.get(popupModel.count - 1); + dismissPopup(lastNotif.id); + } + } + + function dismissAllPopups() { + for (const id in popupState) { + popupState[id].notification?.dismiss(); + popupState[id].watcher?.destroy(); + } + popupModel.clear(); + popupState = {}; + quickshellIdToInternalId = {}; + } + + function invokeActionAndSuppressClose(id, actionId) { + const notifData = popupState[id]; + const notification = notifData?.notification; + const onClosed = notifData?.onClosed; + let restoreClosedHandler = false; + + if (notification && onClosed) { + try { + // A successful action may synchronously close the notification. Disconnect + // our close handler first so the popup is only dismissed by the action path. + notification.closed.disconnect(onClosed); + restoreClosedHandler = true; + } catch (e) {} + } + + const invoked = invokeAction(id, actionId); + + if (!invoked && restoreClosedHandler && notification && onClosed) { + try { + // If invoking the action failed, restore normal close handling for this popup. + notification.closed.connect(onClosed); + } catch (e) {} + } + + return invoked; + } + + function invokeAction(id, actionId) { + let invoked = false; + const notifData = popupState[id]; + + if (notifData && notifData.notification) { + const actionsToUse = (notifData.notification.actions && notifData.notification.actions.length > 0) ? notifData.notification.actions : (notifData.cachedActions || []); + + if (actionsToUse && actionsToUse.length > 0) { + for (const item of actionsToUse) { + const itemId = item.identifier; + const actionObj = item.actionObject ? item.actionObject : item; + + if (itemId === actionId) { + if (actionObj.invoke) { + try { + actionObj.invoke(); + invoked = true; + } catch (e) { + Logger.w("NotificationService", "invoke() failed, trying manual fallback: " + e); + if (manualInvoke(notifData.metadata.originalId, itemId)) { + invoked = true; + } + } + } else { + if (manualInvoke(notifData.metadata.originalId, itemId)) { + invoked = true; + } + } + break; + } + } + } + + if (!invoked && notifData.metadata.originalId) { + Logger.w("NotificationService", "Action objects exhausted, trying manual invoke for id=" + id + " action=" + actionId); + invoked = manualInvoke(notifData.metadata.originalId, actionId); + } + } else if (!notifData) { + Logger.w("NotificationService", "No active notification data for id=" + id + ", searching history for manual invoke"); + for (var i = 0; i < historyModel.count; i++) { + if (historyModel.get(i).id === id) { + const histEntry = historyModel.get(i); + if (histEntry.originalId) { + invoked = manualInvoke(histEntry.originalId, actionId); + } + break; + } + } + } + + if (!invoked) { + Logger.w("NotificationService", "Failed to invoke action '" + actionId + "' for notification " + id); + return false; + } + + // Clear actions after use + updateModel(popupModel, id, "actionsJson", "[]"); + updateModel(historyModel, id, "actionsJson", "[]"); + saveHistory(); + + return true; + } + + function manualInvoke(originalId, actionId) { + if (!originalId) { + return false; + } + + try { + // Construct the signal emission using dbus-send + // dbus-send --session --type=signal /org/freedesktop/Notifications org.freedesktop.Notifications.ActionInvoked uint32:ID string:"KEY" + const args = ["dbus-send", "--session", "--type=signal", "/org/freedesktop/Notifications", "org.freedesktop.Notifications.ActionInvoked", "uint32:" + originalId, "string:" + actionId]; + + Quickshell.execDetached(args); + return true; + } catch (e) { + Logger.e("NotificationService", "Manual invoke failed: " + e); + return false; + } + } + + function focusSenderWindow(appName) { + if (!appName || appName === "" || appName === "Unknown") + return false; + + const normalizedName = appName.toLowerCase().replace(/\s+/g, ""); + + for (var i = 0; i < CompositorService.windows.count; i++) { + const win = CompositorService.windows.get(i); + const winAppId = (win.appId || "").toLowerCase(); + + const segments = winAppId.split("."); + const lastSegment = segments[segments.length - 1] || ""; + + if (winAppId === normalizedName || lastSegment === normalizedName || winAppId.includes(normalizedName) || normalizedName.includes(lastSegment)) { + CompositorService.focusWindow(win); + return true; + } + } + + Logger.d("NotificationService", "No window found for app: " + appName); + return false; + } + + function removeFromHistory(notificationId) { + for (var i = 0; i < historyModel.count; i++) { + const notif = historyModel.get(i); + if (notif.id === notificationId) { + // Only delete cached images that are in our cache directory + const cachedPath = notif.cachedImage ? notif.cachedImage.replace(/^file:\/\//, "") : ""; + if (cachedPath && cachedPath.startsWith(ImageCacheService.notificationsDir)) { + Quickshell.execDetached(["rm", "-f", cachedPath]); + } + historyModel.remove(i); + saveHistory(); + return true; + } + } + return false; + } + + function removeOldestHistory() { + if (historyModel.count > 0) { + const oldest = historyModel.get(historyModel.count - 1); + // Only delete cached images that are in our cache directory + const cachedPath = oldest.cachedImage ? oldest.cachedImage.replace(/^file:\/\//, "") : ""; + if (cachedPath && cachedPath.startsWith(ImageCacheService.notificationsDir)) { + Quickshell.execDetached(["rm", "-f", cachedPath]); + } + historyModel.remove(historyModel.count - 1); + saveHistory(); + return true; + } + return false; + } + + function clearHistory() { + try { + Quickshell.execDetached(["sh", "-c", `rm -rf "${ImageCacheService.notificationsDir}"*`]); + } catch (e) { + Logger.e("Notifications", "Failed to clear cache directory:", e); + } + + historyModel.clear(); + saveHistory(); + } + + function getHistorySnapshot() { + const items = []; + for (var i = 0; i < historyModel.count; i++) { + const entry = historyModel.get(i); + items.push({ + "id": entry.id, + "summary": entry.summary, + "body": entry.body, + "appName": entry.appName, + "urgency": entry.urgency, + "timestamp": entry.timestamp instanceof Date ? entry.timestamp.getTime() : entry.timestamp, + "originalImage": entry.originalImage, + "cachedImage": entry.cachedImage + }); + } + return items; + } + + // Signals + signal animateAndRemove(string notificationId) + + onDoNotDisturbChanged: { + ToastService.showNotice(doNotDisturb ? I18n.tr("toast.do-not-disturb.enabled") : I18n.tr("toast.do-not-disturb.disabled"), doNotDisturb ? I18n.tr("toast.do-not-disturb.enabled-desc") : I18n.tr("toast.do-not-disturb.disabled-desc"), doNotDisturb ? "bell-off" : "bell"); + } + + // Media toast functionality + property string previousMediaTitle: "" + property string previousMediaArtist: "" + property bool previousMediaIsPlaying: false + property bool mediaToastInitialized: false + + Timer { + id: mediaToastInitTimer + interval: 3000 // Wait 3 seconds after startup to avoid initial toast + running: true + onTriggered: { + root.mediaToastInitialized = true; + root.previousMediaTitle = MediaService.trackTitle; + root.previousMediaArtist = MediaService.trackArtist; + root.previousMediaIsPlaying = MediaService.isPlaying; + } + } + + Timer { + id: mediaToastDebounce + interval: 250 // Dynamic interval based on player + onTriggered: { + checkMediaToast(); + } + } + + function checkMediaToast() { + if (!Settings.data.notifications.enableMediaToast || !mediaToastInitialized) + return; + + if (doNotDisturb || PowerProfileService.noctaliaPerformanceMode) + return; + + // Re-evaluate player identity here to handle race conditions where + // the identity wasn't updated yet when the timer started. + const player = (MediaService.playerIdentity || "").toLowerCase(); + const browsers = ["firefox", "chromium", "chrome", "brave", "edge", "opera", "vivaldi", "zen"]; + const isBrowser = browsers.some(b => player.includes(b)); + + // Safety check: If it's a browser, ensure we waited long enough. + // If we started with a short interval (e.g. 250ms because we thought it was Spotify), + // correct it now and wait the full duration. + if (isBrowser && mediaToastDebounce.interval < 1500) { + mediaToastDebounce.interval = 1500; + mediaToastDebounce.restart(); + return; + } + + const title = MediaService.trackTitle || ""; + const artist = MediaService.trackArtist || ""; + const isPlaying = MediaService.isPlaying; + + // Only show toast if something meaningful changed + const titleChanged = title !== previousMediaTitle && title !== ""; + const playStateChanged = isPlaying !== previousMediaIsPlaying; + const hasMedia = title !== "" || artist !== ""; + + // Browser Specific Logic: + // If a browser reports a new title but is PAUSED, ignore it. + if (isBrowser && !isPlaying && titleChanged) { + previousMediaTitle = title; + previousMediaArtist = artist; + previousMediaIsPlaying = isPlaying; + return; + } + + if (hasMedia && (titleChanged || playStateChanged)) { + const icon = isPlaying ? "media-play" : "media-pause"; + let message = ""; + + if (artist && title) { + message = artist + " — " + title; + } else if (title) { + message = title; + } else if (artist) { + message = artist; + } + + if (message !== "") { + const toastTitle = isPlaying ? I18n.tr("common.play") : I18n.tr("common.pause"); + ToastService.showNotice(toastTitle, message, icon, 3000); + } + } + + previousMediaTitle = title; + previousMediaArtist = artist; + previousMediaIsPlaying = isPlaying; + } + + Connections { + target: MediaService + + function onTrackTitleChanged() { + restartDebounce(); + } + + function onTrackArtistChanged() { + restartDebounce(); + } + + function onIsPlayingChanged() { + restartDebounce(); + } + + function onPlayerIdentityChanged() { + restartDebounce(); + } + } + + function restartDebounce() { + const player = (MediaService.playerIdentity || "").toLowerCase(); + const browsers = ["firefox", "chromium", "chrome", "brave", "edge", "opera", "vivaldi"]; + const isBrowser = browsers.some(b => player.includes(b)); + + // Use long delay for browsers to filter hover previews, short for music apps + mediaToastDebounce.interval = isBrowser ? 1500 : 250; + mediaToastDebounce.restart(); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/System/ProgramCheckerService.qml b/fedora/.config/quickshell/noctalia-shell/Services/System/ProgramCheckerService.qml new file mode 100644 index 0000000..7f50e88 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/System/ProgramCheckerService.qml @@ -0,0 +1,337 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.Theming + +// Service to check if various programs are available on the system +Singleton { + id: root + + // Program availability properties + property bool nmcliAvailable: false + property bool bluetoothctlAvailable: false + property bool wlsunsetAvailable: false + property bool gnomeCalendarAvailable: false + property bool pythonAvailable: false + property bool wtypeAvailable: false + + // Programs to check - maps property names to commands + readonly property var programsToCheck: ({ + "bluetoothctlAvailable": ["sh", "-c", "command -v bluetoothctl"], + "nmcliAvailable": ["sh", "-c", "command -v nmcli"], + "wlsunsetAvailable": ["sh", "-c", "command -v wlsunset"], + "gnomeCalendarAvailable": ["sh", "-c", "command -v gnome-calendar"], + "wtypeAvailable": ["sh", "-c", "command -v wtype"], + "pythonAvailable": ["sh", "-c", "command -v python3"] + }) + + // Discord client auto-detection + property var availableDiscordClients: [] + + // Code client auto-detection + property var availableCodeClients: [] + + // Emacs client auto-detection + property var availableEmacsClients: [] + + // Signal emitted when all checks are complete + signal checksCompleted + + // disable Night Light in settings if wlsunset is not available + onChecksCompleted: { + if (!wlsunsetAvailable && Settings.data.nightLight.enabled) { + Settings.data.nightLight.enabled = false; + } + } + + onWlsunsetAvailableChanged: { + if (!wlsunsetAvailable && Settings.data.nightLight.enabled) { + Settings.data.nightLight.enabled = false; + } + } + + // Function to detect Discord client by checking config directories + function detectDiscordClient() { + // Build shell script to check each client + var scriptParts = ["available_clients=\"\";"]; + + for (var i = 0; i < TemplateRegistry.discordClients.length; i++) { + var client = TemplateRegistry.discordClients[i]; + var clientName = client.name; + var configPath = client.configPath; + + // Use the actual config path from the client, removing ~ prefix + var checkPath = configPath.startsWith("~") ? configPath.substring(2) : configPath.substring(1); + + scriptParts.push("if [ -d \"$HOME/" + checkPath + "\" ]; then available_clients=\"$available_clients " + clientName + "\"; fi;"); + } + + scriptParts.push("echo \"$available_clients\""); + + // Use a Process to check directory existence for all clients + discordDetector.command = ["sh", "-c", scriptParts.join(" ")]; + discordDetector.running = true; + } + + // Process to detect Discord client directories + Process { + id: discordDetector + running: false + + onExited: function (exitCode) { + availableDiscordClients = []; + + if (exitCode === 0) { + var detectedClients = stdout.text.trim().split(/\s+/).filter(function (client) { + return client.length > 0; + }); + + if (detectedClients.length > 0) { + // Build list of available clients + for (var i = 0; i < detectedClients.length; i++) { + var clientName = detectedClients[i]; + for (var j = 0; j < TemplateRegistry.discordClients.length; j++) { + var client = TemplateRegistry.discordClients[j]; + if (client.name === clientName) { + availableDiscordClients.push(client); + break; + } + } + } + + Logger.d("ProgramChecker", "Detected Discord clients:", detectedClients.join(", ")); + } + } + + if (availableDiscordClients.length === 0) { + Logger.d("ProgramChecker", "No Discord clients detected"); + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + // Function to detect Code client by checking config directories + function detectCodeClient() { + // Build shell script to check each client + var scriptParts = ["available_clients=\"\";"]; + + for (var i = 0; i < TemplateRegistry.codeClients.length; i++) { + var client = TemplateRegistry.codeClients[i]; + var clientName = client.name; + var configPath = client.configPath; + + // Check if the config directory exists + scriptParts.push("if [ -d \"$HOME" + configPath.substring(1) + "\" ]; then available_clients=\"$available_clients " + clientName + "\"; fi;"); + } + + scriptParts.push("echo \"$available_clients\""); + + // Use a Process to check directory existence for all clients + codeDetector.command = ["sh", "-c", scriptParts.join(" ")]; + codeDetector.running = true; + } + + // Process to detect Code client directories + Process { + id: codeDetector + running: false + + onExited: function (exitCode) { + availableCodeClients = []; + + if (exitCode === 0) { + var detectedClients = stdout.text.trim().split(/\s+/).filter(function (client) { + return client.length > 0; + }); + + if (detectedClients.length > 0) { + // Build list of available clients + for (var i = 0; i < detectedClients.length; i++) { + var clientName = detectedClients[i]; + for (var j = 0; j < TemplateRegistry.codeClients.length; j++) { + var client = TemplateRegistry.codeClients[j]; + if (client.name === clientName) { + availableCodeClients.push(client); + break; + } + } + } + + Logger.d("ProgramChecker", "Detected Code clients:", detectedClients.join(", ")); + } + } + + if (availableCodeClients.length === 0) { + Logger.d("ProgramChecker", "No Code clients detected"); + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + // Function to detect Emacs client by checking config directories + function detectEmacsClient() { + // Build shell script to check each client + var scriptParts = ["available_clients=\"\";"]; + + for (var i = 0; i < TemplateRegistry.emacsClients.length; i++) { + var client = TemplateRegistry.emacsClients[i]; + var clientName = client.name; + var configPath = client.path; + + // Check if the config directory exists + scriptParts.push("if [ -d \"$HOME" + configPath.substring(1) + "\" ]; then available_clients=\"$available_clients " + clientName + "\"; fi;"); + } + + scriptParts.push("echo \"$available_clients\""); + + // Use a Process to check directory existence for all clients + emacsDetector.command = ["sh", "-c", scriptParts.join(" ")]; + emacsDetector.running = true; + } + + // Process to detect Emacs client directories + Process { + id: emacsDetector + running: false + + onExited: function (exitCode) { + availableEmacsClients = []; + + if (exitCode === 0) { + var detectedClients = stdout.text.trim().split(/\s+/).filter(function (client) { + return client.length > 0; + }); + + if (detectedClients.length > 0) { + // Build list of available clients + for (var i = 0; i < detectedClients.length; i++) { + var clientName = detectedClients[i]; + for (var j = 0; j < TemplateRegistry.emacsClients.length; j++) { + var client = TemplateRegistry.emacsClients[j]; + if (client.name === clientName) { + availableEmacsClients.push(client); + break; + } + } + } + + Logger.d("ProgramChecker", "Detected Emacs clients:", detectedClients.join(", ")); + } + } + + if (availableEmacsClients.length === 0) { + Logger.d("ProgramChecker", "No Emacs clients detected"); + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + // Internal tracking + property int completedChecks: 0 + property int totalChecks: Object.keys(programsToCheck).length + + // Single reusable Process object + Process { + id: checker + running: false + + property string currentProperty: "" + + onExited: function (exitCode) { + // Set the availability property + root[currentProperty] = (exitCode === 0); + + // Stop the process to free resources + running = false; + + // Track completion + root.completedChecks++; + + // Check next program or emit completion signal + if (root.completedChecks >= root.totalChecks) { + // Run Discord, Code and Emacs client detection after all checks are complete + root.detectDiscordClient(); + root.detectCodeClient(); + root.detectEmacsClient(); + root.checksCompleted(); + } else { + root.checkNextProgram(); + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + // Queue of programs to check + property var checkQueue: [] + property int currentCheckIndex: 0 + + // Function to check the next program in the queue + function checkNextProgram() { + if (currentCheckIndex >= checkQueue.length) + return; + var propertyName = checkQueue[currentCheckIndex]; + var command = programsToCheck[propertyName]; + + checker.currentProperty = propertyName; + checker.command = command; + checker.running = true; + + currentCheckIndex++; + } + + // Function to run all program checks + function checkAllPrograms() { + // Reset state + completedChecks = 0; + currentCheckIndex = 0; + checkQueue = Object.keys(programsToCheck); + + // Start first check + if (checkQueue.length > 0) { + checkNextProgram(); + } + } + + // Function to check a specific program + function checkProgram(programProperty) { + if (!programsToCheck.hasOwnProperty(programProperty)) { + Logger.w("ProgramChecker", "Unknown program property:", programProperty); + return; + } + + checker.currentProperty = programProperty; + checker.command = programsToCheck[programProperty]; + checker.running = true; + } + + // Manual function to test Discord detection (for debugging) + function testDiscordDetection() { + Logger.d("ProgramChecker", "Testing Discord detection..."); + Logger.d("ProgramChecker", "HOME:", Quickshell.env("HOME")); + + // Test each client directory + for (var i = 0; i < TemplateRegistry.discordClients.length; i++) { + var client = TemplateRegistry.discordClients[i]; + var configDir = client.configPath.replace("~", Quickshell.env("HOME")); + Logger.d("ProgramChecker", "Checking:", configDir); + } + + detectDiscordClient(); + } + + // Initialize checks when service is created + Component.onCompleted: { + checkAllPrograms(); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/System/SoundService.qml b/fedora/.config/quickshell/noctalia-shell/Services/System/SoundService.qml new file mode 100644 index 0000000..53f9df0 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/System/SoundService.qml @@ -0,0 +1,205 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons + +Singleton { + id: root + + // Map to track active sound players: resolvedPath -> MediaPlayer instance + property var activePlayers: ({}) + + // Check if QtMultimedia is available + property bool multimediaAvailable: false + + // Container for dynamically created players + Item { + id: playersContainer + } + + Component.onCompleted: { + // Test if QtMultimedia is available by trying to create a simple component + try { + var testComponent = Qt.createQmlObject(` + import QtQuick + import QtMultimedia + Item {} + `, root, "MultimediaTest"); + if (testComponent) { + multimediaAvailable = true; + testComponent.destroy(); + Logger.i("SoundService", "QtMultimedia found - sound playback enabled"); + } + } catch (e) { + multimediaAvailable = false; + Logger.w("SoundService", "QtMultimedia not available - no audio will be played from noctalia-shell"); + } + } + + function resolvePath(soundPath) { + if (!soundPath || soundPath === "") { + return ""; + } + + let resolvedPath = soundPath; + + // If it's just a filename (no path separators), assume it's in Assets/Sounds/ + if (!soundPath.includes("/") && !soundPath.startsWith("file://")) { + resolvedPath = Quickshell.shellDir + "/Assets/Sounds/" + soundPath; + } else if (!soundPath.startsWith("/") && !soundPath.startsWith("file://")) { + // Relative path - assume it's relative to shellDir + resolvedPath = Quickshell.shellDir + "/" + soundPath; + } else if (soundPath.startsWith("file://")) { + resolvedPath = soundPath.substring(7); // Remove "file://" prefix + } + // Absolute paths are used as-is + + return resolvedPath; + } + + function playSound(soundPath, options) { + if (!soundPath || soundPath === "") { + Logger.w("SoundService", "No sound path provided"); + return; + } + + if (!multimediaAvailable) { + Logger.d("SoundService", "QtMultimedia not available, cannot play sound:", soundPath); + return; + } + + const opts = options || {}; + const volume = opts.volume !== undefined ? opts.volume : 1.0; + const fallback = opts.fallback !== undefined ? opts.fallback : false; + const repeat = opts.repeat !== undefined ? opts.repeat : false; + + // Resolve path + const resolvedPath = resolvePath(soundPath); + + // Stop any existing player for this path if it's looping + if (repeat && activePlayers[resolvedPath]) { + stopSound(soundPath); + } + + // Create MediaPlayer instance dynamically with QtMultimedia import + const loopsValue = repeat ? "MediaPlayer.Infinite" : "1"; + const escapedPath = resolvedPath.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + const playerQml = ` + import QtQuick + import QtMultimedia + import Quickshell + import qs.Commons + import qs.Services.System + MediaPlayer { + id: mediaPlayer + property string resolvedPath: "${escapedPath}" + property bool shouldFallback: ${fallback && !repeat} + property real soundVolume: ${Math.max(0, Math.min(1, volume))} + source: "file://${escapedPath}" + loops: ${loopsValue} + audioOutput: AudioOutput { + volume: soundVolume + } + onErrorOccurred: { + Logger.w("SoundService", "Error playing sound:", source, error, errorString); + if (shouldFallback) { + const fallbackPath = Quickshell.shellDir + "/Assets/Sounds/notification.mp3"; + if (fallbackPath !== resolvedPath) { + SoundService.playSound(fallbackPath, { + volume: soundVolume, + fallback: false, + repeat: false + }); + } + } + if (SoundService.activePlayers[resolvedPath]) { + delete SoundService.activePlayers[resolvedPath]; + } + destroy(); + } + onPlaybackStateChanged: function (state) { + if (state === MediaPlayer.StoppedState && loops === 1) { + if (SoundService.activePlayers[resolvedPath]) { + delete SoundService.activePlayers[resolvedPath]; + } + destroy(); + } + } + Component.onCompleted: { + play(); + } + } + `; + + try { + const player = Qt.createQmlObject(playerQml, playersContainer, "MediaPlayer_" + resolvedPath.replace(/[^a-zA-Z0-9]/g, "_")); + + if (!player) { + Logger.w("SoundService", "Failed to create MediaPlayer for:", resolvedPath); + // Try fallback if requested + if (fallback && !repeat) { + const defaultSound = Quickshell.shellDir + "/Assets/Sounds/notification.mp3"; + if (defaultSound !== resolvedPath) { + playSound(defaultSound, { + volume: volume, + fallback: false, + repeat: false + }); + } + } + return; + } + + // Store player in activePlayers map + activePlayers[resolvedPath] = player; + + Logger.d("SoundService", "Playing sound:", resolvedPath, `(volume: ${Math.round(volume * 100)}%)`, repeat ? "(repeat)" : ""); + } catch (e) { + Logger.w("SoundService", "Failed to create MediaPlayer:", e); + // Try fallback if requested + if (fallback && !repeat) { + const defaultSound = Quickshell.shellDir + "/Assets/Sounds/notification.mp3"; + if (defaultSound !== resolvedPath) { + playSound(defaultSound, { + volume: volume, + fallback: false, + repeat: false + }); + } + } + } + } + + function stopSound(soundPath) { + if (!multimediaAvailable) { + // If multimedia isn't available, there are no active players to stop + return; + } + + if (soundPath) { + // Resolve path the same way as playSound + const resolvedPath = resolvePath(soundPath); + + // Stop and remove the player for this specific sound + if (activePlayers[resolvedPath]) { + const player = activePlayers[resolvedPath]; + player.stop(); + delete activePlayers[resolvedPath]; + player.destroy(); + Logger.d("SoundService", "Stopped sound:", resolvedPath); + } + } else { + // Stop all active players (typically used for repeating sounds) + const paths = Object.keys(activePlayers); + for (let i = 0; i < paths.length; i++) { + const path = paths[i]; + const player = activePlayers[path]; + player.stop(); + player.destroy(); + } + activePlayers = {}; + Logger.d("SoundService", "Stopped all sounds"); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/System/SystemStatService.qml b/fedora/.config/quickshell/noctalia-shell/Services/System/SystemStatService.qml new file mode 100644 index 0000000..ae7ebf4 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/System/SystemStatService.qml @@ -0,0 +1,1506 @@ +pragma Singleton +import Qt.labs.folderlistmodel + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.UI + +Singleton { + id: root + + // Component registration - only poll when something needs system stat data + function registerComponent(componentId) { + root._registered[componentId] = true; + root._registered = Object.assign({}, root._registered); + Logger.d("SystemStat", "Component registered:", componentId, "- total:", root._registeredCount); + } + + function unregisterComponent(componentId) { + delete root._registered[componentId]; + root._registered = Object.assign({}, root._registered); + Logger.d("SystemStat", "Component unregistered:", componentId, "- total:", root._registeredCount); + } + + property var _registered: ({}) + readonly property int _registeredCount: Object.keys(_registered).length + readonly property bool _lockScreenActive: PanelService.lockScreen?.active ?? false + readonly property bool shouldRun: _registeredCount > 0 && !_lockScreenActive + + // Polling intervals (hardcoded to sensible values per stat type) + readonly property int cpuUsageIntervalMs: 1000 + readonly property int cpuFreqIntervalMs: 3000 + readonly property int memIntervalMs: 5000 + readonly property int networkIntervalMs: 3000 + readonly property int loadAvgIntervalMs: 10000 + readonly property int diskIntervalMs: 30000 + readonly property int gpuIntervalMs: 5000 + + // Public values + property real cpuUsage: 0 + property list coresUsage: [] + property real cpuTemp: 0 + property string cpuFreq: "0.0GHz" + property real cpuFreqRatio: 0 + property real cpuGlobalMaxFreq: 3.5 + property real gpuTemp: 0 + property bool gpuAvailable: false + property string gpuType: "" // "amd", "intel", "nvidia" + property real memGb: 0 + property real memPercent: 0 + property real memTotalGb: 0 + property real swapGb: 0 + property real swapPercent: 0 + property real swapTotalGb: 0 + property var diskPercents: ({}) + property var diskAvailPercents: ({}) // available disk space in percent + property var diskUsedGb: ({}) // Used space in GB per mount point + property var diskAvailableGb: ({}) // available space in GB per mount point + property var diskSizeGb: ({}) // Total size in GB per mount point + property real rxSpeed: 0 + property real txSpeed: 0 + property real zfsArcSizeKb: 0 // ZFS ARC cache size in KB + property real zfsArcCminKb: 0 // ZFS ARC minimum (non-reclaimable) size in KB + property real loadAvg1: 0 + property real loadAvg5: 0 + property real loadAvg15: 0 + property int nproc: 0 // Number of cpu cores + + // History arrays (2 minutes of data, length computed from polling interval) + // Pre-filled with zeros so the graph scrolls smoothly from the start + readonly property int historyDurationMs: (1 * 60 * 1000) // 1 minute + + // Computed history lengths based on polling intervals + readonly property int cpuHistoryLength: Math.ceil(historyDurationMs / cpuUsageIntervalMs) + readonly property int gpuHistoryLength: Math.ceil(historyDurationMs / gpuIntervalMs) + readonly property int memHistoryLength: Math.ceil(historyDurationMs / memIntervalMs) + readonly property int diskHistoryLength: Math.max(10, Math.ceil(historyDurationMs / diskIntervalMs)) + readonly property int networkHistoryLength: Math.ceil(historyDurationMs / networkIntervalMs) + + property var cpuHistory: new Array(cpuHistoryLength).fill(0) + property var cpuTempHistory: new Array(cpuHistoryLength).fill(40) // Reasonable default temp + property var gpuTempHistory: new Array(gpuHistoryLength).fill(40) // Reasonable default temp + property var memHistory: new Array(memHistoryLength).fill(0) + property var diskHistories: ({}) // Keyed by mount path, initialized on first update + property var rxSpeedHistory: new Array(networkHistoryLength).fill(0) + property var txSpeedHistory: new Array(networkHistoryLength).fill(0) + + // Historical min/max tracking (since shell started) for consistent graph scaling + // Temperature defaults create a valid 30-80°C range that expands as real data comes in + property real cpuTempHistoryMin: 30 + property real cpuTempHistoryMax: 80 + property real gpuTempHistoryMin: 30 + property real gpuTempHistoryMax: 80 + // Network uses autoscaling from current history window + // Disk is always 0-100% + + // History management - called from update functions, not change handlers + // (change handlers don't fire when value stays the same) + function pushCpuHistory() { + let h = cpuHistory.slice(); + h.push(cpuUsage); + if (h.length > cpuHistoryLength) + h.shift(); + cpuHistory = h; + } + + function pushCpuTempHistory() { + if (cpuTemp > 0) { + if (cpuTemp < cpuTempHistoryMin) + cpuTempHistoryMin = cpuTemp; + if (cpuTemp > cpuTempHistoryMax) + cpuTempHistoryMax = cpuTemp; + } + let h = cpuTempHistory.slice(); + h.push(cpuTemp); + if (h.length > cpuHistoryLength) + h.shift(); + cpuTempHistory = h; + } + + function pushGpuHistory() { + if (gpuTemp > 0) { + if (gpuTemp < gpuTempHistoryMin) + gpuTempHistoryMin = gpuTemp; + if (gpuTemp > gpuTempHistoryMax) + gpuTempHistoryMax = gpuTemp; + } + let h = gpuTempHistory.slice(); + h.push(gpuTemp); + if (h.length > gpuHistoryLength) + h.shift(); + gpuTempHistory = h; + } + + function pushMemHistory() { + let h = memHistory.slice(); + h.push(memPercent); + if (h.length > memHistoryLength) + h.shift(); + memHistory = h; + } + + function pushDiskHistory() { + let newHistories = {}; + for (let path in diskPercents) { + // Pre-fill with zeros if this is a new path + let h = diskHistories[path] ? diskHistories[path].slice() : new Array(diskHistoryLength).fill(0); + h.push(diskPercents[path]); + if (h.length > diskHistoryLength) + h.shift(); + newHistories[path] = h; + } + diskHistories = newHistories; + } + + function pushNetworkHistory() { + let rxH = rxSpeedHistory.slice(); + rxH.push(rxSpeed); + if (rxH.length > networkHistoryLength) + rxH.shift(); + rxSpeedHistory = rxH; + + let txH = txSpeedHistory.slice(); + txH.push(txSpeed); + if (txH.length > networkHistoryLength) + txH.shift(); + txSpeedHistory = txH; + } + + // Network max speed tracking (autoscales from current history window) + // Minimum floor of 1 MB/s so graph doesn't fluctuate at low speeds + readonly property real rxMaxSpeed: { + const max = Math.max(...rxSpeedHistory); + return Math.max(max, 1000000); // 1 MB/s floor + } + readonly property real txMaxSpeed: { + const max = Math.max(...txSpeedHistory); + return Math.max(max, 512000); // 512 KB/s floor + } + + // Ready-to-use ratios based on current maximums (0..1 range) + readonly property real rxRatio: rxMaxSpeed > 0 ? Math.min(1, rxSpeed / rxMaxSpeed) : 0 + readonly property real txRatio: txMaxSpeed > 0 ? Math.min(1, txSpeed / txMaxSpeed) : 0 + + // Color resolution (respects useCustomColors setting) + readonly property color warningColor: Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.warningColor || Color.mTertiary) : Color.mTertiary + readonly property color criticalColor: Settings.data.systemMonitor.useCustomColors ? (Settings.data.systemMonitor.criticalColor || Color.mError) : Color.mError + + // Threshold values from settings + readonly property int cpuWarningThreshold: Settings.data.systemMonitor.cpuWarningThreshold + readonly property int cpuCriticalThreshold: Settings.data.systemMonitor.cpuCriticalThreshold + readonly property int tempWarningThreshold: Settings.data.systemMonitor.tempWarningThreshold + readonly property int tempCriticalThreshold: Settings.data.systemMonitor.tempCriticalThreshold + readonly property int gpuWarningThreshold: Settings.data.systemMonitor.gpuWarningThreshold + readonly property int gpuCriticalThreshold: Settings.data.systemMonitor.gpuCriticalThreshold + readonly property int memWarningThreshold: Settings.data.systemMonitor.memWarningThreshold + readonly property int memCriticalThreshold: Settings.data.systemMonitor.memCriticalThreshold + readonly property int swapWarningThreshold: Settings.data.systemMonitor.swapWarningThreshold + readonly property int swapCriticalThreshold: Settings.data.systemMonitor.swapCriticalThreshold + readonly property int diskWarningThreshold: Settings.data.systemMonitor.diskWarningThreshold + readonly property int diskCriticalThreshold: Settings.data.systemMonitor.diskCriticalThreshold + readonly property int diskAvailWarningThreshold: Settings.data.systemMonitor.diskAvailWarningThreshold + readonly property int diskAvailCriticalThreshold: Settings.data.systemMonitor.diskAvailCriticalThreshold + + // Computed warning/critical states (uses >= inclusive comparison) + readonly property bool cpuWarning: cpuUsage >= cpuWarningThreshold + readonly property bool cpuCritical: cpuUsage >= cpuCriticalThreshold + readonly property bool tempWarning: cpuTemp >= tempWarningThreshold + readonly property bool tempCritical: cpuTemp >= tempCriticalThreshold + readonly property bool gpuWarning: gpuAvailable && gpuTemp >= gpuWarningThreshold + readonly property bool gpuCritical: gpuAvailable && gpuTemp >= gpuCriticalThreshold + readonly property bool memWarning: memPercent >= memWarningThreshold + readonly property bool memCritical: memPercent >= memCriticalThreshold + readonly property bool swapWarning: swapPercent >= swapWarningThreshold + readonly property bool swapCritical: swapPercent >= swapCriticalThreshold + + // Helper functions for disk (disk path is dynamic) + function isDiskWarning(diskPath, available = false) { + return available ? (diskAvailPercents[diskPath] || 0) <= diskAvailWarningThreshold : (diskPercents[diskPath] || 0) >= diskWarningThreshold; + } + + function isDiskCritical(diskPath, available = false) { + return available ? (diskAvailPercents[diskPath] || 0) <= diskAvailCriticalThreshold : (diskPercents[diskPath] || 0) >= diskCriticalThreshold; + } + + // Ready-to-use stat colors (for gauges, panels, icons) + readonly property color cpuColor: cpuCritical ? criticalColor : (cpuWarning ? warningColor : Color.mPrimary) + readonly property color tempColor: tempCritical ? criticalColor : (tempWarning ? warningColor : Color.mPrimary) + readonly property color gpuColor: gpuCritical ? criticalColor : (gpuWarning ? warningColor : Color.mPrimary) + readonly property color memColor: memCritical ? criticalColor : (memWarning ? warningColor : Color.mPrimary) + readonly property color swapColor: swapCritical ? criticalColor : (swapWarning ? warningColor : Color.mPrimary) + + function getCoreUsageColor(usage) { + if (usage >= cpuCriticalThreshold) + return criticalColor; + if (usage >= cpuWarningThreshold) + return warningColor; + return Color.mPrimary; + } + + function getDiskColor(diskPath, available = false) { + return isDiskCritical(diskPath, available) ? criticalColor : (isDiskWarning(diskPath, available) ? warningColor : Color.mPrimary); + } + + // Internal state for CPU calculation + property var prevCpuStats: null + property var prevCpuCoresStats: null + + // Internal state for network speed calculation + // Previous Bytes need to be stored as 'real' as they represent the total of bytes transfered + // since the computer started, so their value will easily overlfow a 32bit int. + property real prevRxBytes: 0 + property real prevTxBytes: 0 + property real prevTime: 0 + + // Cpu temperature is the most complex + readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"] + property string cpuTempSensorName: "" + property string cpuTempHwmonPath: "" + // For Intel coretemp averaging of all cores/sensors + property var intelTempValues: [] + property int intelTempFilesChecked: 0 + property int intelTempMaxFiles: 20 // Will test up to temp20_input + + // Thermal zone fallback (for ARM SoCs with SCMI sensors, etc.) + property var cpuThermalZonePaths: [] // All matching CPU zones for averaging + property string gpuThermalZonePath: "" + + // GPU temperature detection + // On dual-GPU systems, we prioritize discrete GPUs over integrated GPUs + // Priority: NVIDIA (opt-in) > AMD dGPU > Intel Arc > AMD iGPU + // Note: NVIDIA requires opt-in because nvidia-smi wakes the dGPU on laptops, draining battery + readonly property var supportedTempGpuSensorNames: ["amdgpu", "xe"] + property string gpuTempHwmonPath: "" + property var foundGpuSensors: [] // [{hwmonPath, type, hasDedicatedVram}] + property int gpuVramCheckIndex: 0 + + // -------------------------------------------- + Component.onCompleted: { + Logger.i("SystemStat", "Service started (polling deferred until a consumer registers)."); + + // Kickoff the cpu name detection for temperature (one-time probes, not polling) + cpuTempNameReader.checkNext(); + + // Kickoff the gpu sensor detection for temperature (one-time probes, not polling) + gpuTempNameReader.checkNext(); + + // Get nproc on startup (one-time) + nprocProcess.running = true; + } + + onShouldRunChanged: { + if (shouldRun) { + // Reset differential state so first readings after resume are clean + root.prevCpuStats = null; + root.prevCpuCoresStats = null; + root.prevTime = 0; + + // Trigger initial reads + zfsArcStatsFile.reload(); + loadAvgFile.reload(); + + // Start persistent disk shell + if (!dfShell.running) { + dfShell.running = true; + } + } else { + // Stop persistent disk shell + if (dfShell.running) { + dfShell.running = false; + } + } + } + + // Re-run GPU detection when dGPU opt-in setting changes + Connections { + target: Settings.data.systemMonitor + function onEnableDgpuMonitoringChanged() { + Logger.i("SystemStat", "dGPU monitoring opt-in setting changed, re-detecting GPUs"); + restartGpuDetection(); + } + } + + // Reset differential state after suspend so the first reading is treated as fresh + Connections { + target: Time + function onResumed() { + Logger.i("SystemStat", "System resumed - resetting differential state"); + root.prevCpuStats = null; + root.prevCpuCoresStats = null; + root.prevTime = 0; + } + } + + function restartGpuDetection() { + // Reset GPU state + root.gpuAvailable = false; + root.gpuType = ""; + root.gpuTempHwmonPath = ""; + root.gpuTemp = 0; + root.foundGpuSensors = []; + root.gpuVramCheckIndex = 0; + + // Restart GPU detection + gpuTempNameReader.currentIndex = 0; + gpuTempNameReader.checkNext(); + } + + // -------------------------------------------- + // Timer for CPU usage and temperature + Timer { + id: cpuTimer + interval: root.cpuUsageIntervalMs + repeat: true + running: root.shouldRun + triggeredOnStart: true + onTriggered: { + cpuStatFile.reload(); + updateCpuTemperature(); + } + } + + // Timer for CPU frequency (slower — /proc/cpuinfo is large and freq changes infrequently) + Timer { + id: cpuFreqTimer + interval: root.cpuFreqIntervalMs + repeat: true + running: root.shouldRun + triggeredOnStart: true + onTriggered: cpuInfoFile.reload() + } + + // Timer for load average + Timer { + id: loadAvgTimer + interval: root.loadAvgIntervalMs + repeat: true + running: root.shouldRun + triggeredOnStart: true + onTriggered: loadAvgFile.reload() + } + + // Timer for memory stats + Timer { + id: memoryTimer + interval: root.memIntervalMs + repeat: true + running: root.shouldRun + triggeredOnStart: true + onTriggered: { + memInfoFile.reload(); + zfsArcStatsFile.reload(); + } + } + + // Timer for disk usage + Timer { + id: diskTimer + interval: root.diskIntervalMs + repeat: true + running: root.shouldRun + triggeredOnStart: true + onTriggered: { + if (dfShell.running) { + dfShell.write("df --output=target,pcent,used,size,avail --block-size=1 -x efivarfs 2>/dev/null; echo '@@DF_END@@'\n"); + } + } + } + + // Timer for network speeds + Timer { + id: networkTimer + interval: root.networkIntervalMs + repeat: true + running: root.shouldRun + triggeredOnStart: true + onTriggered: netDevFile.reload() + } + + // Timer for GPU temperature + Timer { + id: gpuTempTimer + interval: root.gpuIntervalMs + repeat: true + running: root.shouldRun && root.gpuAvailable + triggeredOnStart: true + onTriggered: updateGpuTemperature() + } + + // -------------------------------------------- + // FileView components for reading system files + FileView { + id: memInfoFile + path: "/proc/meminfo" + onLoaded: parseMemoryInfo(text()) + } + + FileView { + id: cpuStatFile + path: "/proc/stat" + onLoaded: calculateCpuUsage(text()) + } + + FileView { + id: netDevFile + path: "/proc/net/dev" + onLoaded: calculateNetworkSpeed(text()) + } + + FileView { + id: loadAvgFile + path: "/proc/loadavg" + onLoaded: parseLoadAverage(text()) + } + + // ZFS ARC stats file (only exists on ZFS systems) + FileView { + id: zfsArcStatsFile + path: "/proc/spl/kstat/zfs/arcstats" + printErrors: false + onLoaded: parseZfsArcStats(text()) + onLoadFailed: { + // File doesn't exist (non-ZFS system), set ARC values to 0 + root.zfsArcSizeKb = 0; + root.zfsArcCminKb = 0; + } + } + + // -------------------------------------------- + // Persistent shell for disk usage queries (avoids fork+exec of large Quickshell process every poll) + // Uses 'df' aka 'disk free' + // "-x efivarfs" skips efivarfs mountpoints, for which the `statfs` syscall may cause system-wide stuttering + // --block-size=1 gives us bytes for precise GB calculation + // Timer writes commands to stdin; SplitParser reads output delimited by @@DF_END@@ + Process { + id: dfShell + command: ["sh"] + stdinEnabled: true + running: false + + onRunningChanged: { + if (!running && root.shouldRun) { + // Restart if it died unexpectedly while we still need it + Logger.w("SystemStat", "Disk shell exited unexpectedly, restarting"); + Qt.callLater(() => { + dfShell.running = true; + }); + } + } + + stdout: SplitParser { + splitMarker: "@@DF_END@@" + onRead: data => { + const lines = data.trim().split('\n'); + const newPercents = {}; + const newAvailPercents = {}; + const newUsedGb = {}; + const newSizeGb = {}; + const newAvailableGb = {}; + const bytesPerGb = 1000 * 1000 * 1000; + // Start from line 1 (skip header) + for (var i = 1; i < lines.length; i++) { + const parts = lines[i].trim().split(/\s+/); + if (parts.length >= 5) { + const target = parts[0]; + const percent = parseInt(parts[1].replace(/[^0-9]/g, '')) || 0; + const usedBytes = parseFloat(parts[2]) || 0; + const sizeBytes = parseFloat(parts[3]) || 0; + const availBytes = parseFloat(parts[4]) || 0; + const availPercent = sizeBytes > 0 ? (availBytes / sizeBytes) * 100 : 0; + newPercents[target] = percent; + newAvailPercents[target] = Math.round(availPercent); + newUsedGb[target] = usedBytes / bytesPerGb; + newSizeGb[target] = sizeBytes / bytesPerGb; + newAvailableGb[target] = availBytes / bytesPerGb; + } + } + root.diskPercents = newPercents; + root.diskAvailPercents = newAvailPercents; + root.diskUsedGb = newUsedGb; + root.diskSizeGb = newSizeGb; + root.diskAvailableGb = newAvailableGb; + root.pushDiskHistory(); + } + } + } + + // Process to get number of processors + Process { + id: nprocProcess + command: ["nproc"] + running: false + stdout: StdioCollector { + onStreamFinished: { + root.nproc = parseInt(text.trim()); + } + } + } + + // FileView to get avg cpu frequency (replaces subprocess spawn of `cat /proc/cpuinfo`) + FileView { + id: cpuInfoFile + path: "/proc/cpuinfo" + onLoaded: { + let txt = text(); + let matches = txt.match(/cpu MHz\s+:\s+([0-9.]+)/g); + if (matches && matches.length > 0) { + let totalFreq = 0.0; + for (let i = 0; i < matches.length; i++) { + totalFreq += parseFloat(matches[i].split(":")[1]); + } + let avgFreq = (totalFreq / matches.length) / 1000.0; + root.cpuFreq = avgFreq.toFixed(1) + "GHz"; + cpuMaxFreqFile.reload(); + if (avgFreq > root.cpuGlobalMaxFreq) + root.cpuGlobalMaxFreq = avgFreq; + if (root.cpuGlobalMaxFreq > 0) { + root.cpuFreqRatio = Math.min(1.0, avgFreq / root.cpuGlobalMaxFreq); + } + } + } + } + + // FileView to get maximum CPU frequency limit (replaces subprocess spawn) + // Reads cpu0's scaling_max_freq as representative value + FileView { + id: cpuMaxFreqFile + path: "/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq" + printErrors: false + onLoaded: { + let maxKHz = parseInt(text().trim()); + if (!isNaN(maxKHz) && maxKHz > 0) { + let newMaxFreq = maxKHz / 1000000.0; + if (Math.abs(root.cpuGlobalMaxFreq - newMaxFreq) > 0.01) { + root.cpuGlobalMaxFreq = newMaxFreq; + } + } + } + } + + // -------------------------------------------- + // -------------------------------------------- + // CPU Temperature + // It's more complex. + // ---- + // #1 - Find a common cpu sensor name ie: "coretemp", "k10temp", "zenpower" + FileView { + id: cpuTempNameReader + property int currentIndex: 0 + printErrors: false + + function checkNext() { + if (currentIndex >= 16) { + // No hwmon sensor found, try thermal_zone fallback (ARM SoCs, SCMI, etc.) + thermalZoneScanner.startScan(); + return; + } + + //Logger.i("SystemStat", "---- Probing: hwmon", currentIndex) + cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`; + cpuTempNameReader.reload(); + } + + onLoaded: { + const name = text().trim(); + if (root.supportedTempCpuSensorNames.includes(name)) { + root.cpuTempSensorName = name; + root.cpuTempHwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`; + Logger.i("SystemStat", `Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`); + } else { + currentIndex++; + Qt.callLater(() => { + // Qt.callLater is mandatory + checkNext(); + }); + } + } + + onLoadFailed: function (error) { + currentIndex++; + Qt.callLater(() => { + // Qt.callLater is mandatory + checkNext(); + }); + } + } + + // ---- + // #2 - Read sensor value + FileView { + id: cpuTempReader + printErrors: false + + onLoaded: { + const data = text().trim(); + if (root.cpuTempSensorName === "coretemp") { + // For Intel, collect all temperature values + const temp = parseInt(data) / 1000.0; + //console.log(temp, cpuTempReader.path) + root.intelTempValues.push(temp); + Qt.callLater(() => { + // Qt.callLater is mandatory + checkNextIntelTemp(); + }); + } else { + // For AMD sensors (k10temp and zenpower), directly set the temperature + root.cpuTemp = Math.round(parseInt(data) / 1000.0); + root.pushCpuTempHistory(); + } + } + onLoadFailed: function (error) { + Qt.callLater(() => { + // Qt.callLater is mandatory + checkNextIntelTemp(); + }); + } + } + + // -------------------------------------------- + // Thermal zone fallback for CPU and GPU temperature + // Used on ARM SoCs (e.g., SCMI sensors) where hwmon doesn't expose + // coretemp/k10temp/zenpower. Scans /sys/class/thermal/thermal_zoneN/type + // for CPU and GPU zone names, then reads temp from all matching zones. + // + // CPU: reads all cpu-*-thermal zones and reports the hottest core. + // GPU: prefers gpu-avg-thermal (firmware average), falls back to max of gpu[0-9]-thermal. + + FileView { + id: thermalZoneScanner + property int currentIndex: 0 + property var cpuZones: [] + property var gpuZones: [] + property string gpuAvgZonePath: "" + printErrors: false + + function startScan() { + currentIndex = 0; + cpuZones = []; + gpuZones = []; + gpuAvgZonePath = ""; + checkNext(); + } + + function checkNext() { + if (currentIndex >= 20) { + finishScan(); + return; + } + thermalZoneScanner.path = `/sys/class/thermal/thermal_zone${currentIndex}/type`; + thermalZoneScanner.reload(); + } + + onLoaded: { + const name = text().trim(); + const zonePath = `/sys/class/thermal/thermal_zone${currentIndex}`; + if (name.startsWith("cpu") && name.endsWith("thermal")) { + cpuZones.push({ + "type": name, + "path": zonePath + "/temp" + }); + } else if (name === "gpu-avg-thermal") { + gpuAvgZonePath = zonePath + "/temp"; + } else if (/^gpu[0-9]+-?thermal$/.test(name)) { + gpuZones.push({ + "type": name, + "path": zonePath + "/temp" + }); + } + currentIndex++; + Qt.callLater(() => { + checkNext(); + }); + } + + onLoadFailed: function (error) { + currentIndex++; + Qt.callLater(() => { + checkNext(); + }); + } + + function finishScan() { + // CPU thermal zones + if (cpuZones.length > 0) { + root.cpuTempSensorName = "thermal_zone"; + root.cpuThermalZonePaths = cpuZones.map(z => z.path); + const types = cpuZones.map(z => z.type).join(", "); + Logger.i("SystemStat", `Found ${cpuZones.length} CPU thermal zone(s): ${types}`); + } else if (root.cpuTempHwmonPath === "") { + Logger.w("SystemStat", "No supported temperature sensor found"); + } + + // GPU thermal zones + if (gpuAvgZonePath !== "") { + root.gpuThermalZonePath = gpuAvgZonePath; + root.gpuAvailable = true; + root.gpuType = "thermal_zone"; + Logger.i("SystemStat", `Found GPU thermal zone: gpu-avg-thermal`); + } else if (gpuZones.length > 0) { + root.gpuThermalZonePaths = gpuZones.map(z => z.path); + root.gpuThermalZonePath = gpuZones[0].path; // fallback single path + root.gpuAvailable = true; + root.gpuType = "thermal_zone"; + const types = gpuZones.map(z => z.type).join(", "); + Logger.i("SystemStat", `Found ${gpuZones.length} GPU thermal zone(s): ${types} (using max)`); + } + } + } + + // Thermal zone reader for CPU: reads all zones, reports max (hottest core) + FileView { + id: cpuThermalZoneReader + property int currentZoneIndex: 0 + property var collectedTemps: [] + printErrors: false + + onLoaded: { + const temp = parseInt(text().trim()) / 1000.0; + if (!isNaN(temp) && temp > 0) + collectedTemps.push(temp); + currentZoneIndex++; + Qt.callLater(() => { + readNextCpuThermalZone(); + }); + } + + onLoadFailed: function (error) { + currentZoneIndex++; + Qt.callLater(() => { + readNextCpuThermalZone(); + }); + } + } + + function readNextCpuThermalZone() { + if (cpuThermalZoneReader.currentZoneIndex >= root.cpuThermalZonePaths.length) { + if (cpuThermalZoneReader.collectedTemps.length > 0) { + root.cpuTemp = Math.round(Math.max(...cpuThermalZoneReader.collectedTemps)); + } else { + root.cpuTemp = 0; + } + root.pushCpuTempHistory(); + return; + } + cpuThermalZoneReader.path = root.cpuThermalZonePaths[cpuThermalZoneReader.currentZoneIndex]; + cpuThermalZoneReader.reload(); + } + + // Thermal zone reader for GPU: reads single zone (gpu-avg-thermal) or max of gpu[N] zones + FileView { + id: gpuThermalZoneReader + property int currentZoneIndex: 0 + property var collectedTemps: [] + printErrors: false + + onLoaded: { + const temp = parseInt(text().trim()) / 1000.0; + if (!isNaN(temp) && temp > 0) + collectedTemps.push(temp); + + // If we have multiple GPU zones (no gpu-avg), iterate and take max + if (root.gpuThermalZonePaths && root.gpuThermalZonePaths.length > 0) { + currentZoneIndex++; + if (currentZoneIndex < root.gpuThermalZonePaths.length) { + Qt.callLater(() => { + readNextGpuThermalZone(); + }); + return; + } + // All zones read, take max + root.gpuTemp = Math.round(Math.max(...collectedTemps)); + } else { + // Single gpu-avg-thermal zone + root.gpuTemp = Math.round(temp); + } + root.pushGpuHistory(); + } + } + + function readNextGpuThermalZone() { + gpuThermalZoneReader.path = root.gpuThermalZonePaths[gpuThermalZoneReader.currentZoneIndex]; + gpuThermalZoneReader.reload(); + } + + // Property to store multiple GPU thermal zone paths (when no gpu-avg is available) + property var gpuThermalZonePaths: [] + + // -------------------------------------------- + // -------------------------------------------- + // GPU Temperature + // On dual-GPU systems (e.g., Intel iGPU + NVIDIA dGPU, or AMD APU + AMD dGPU), + // we scan all hwmon entries, then select the best GPU based on priority. + // ---- + // #1 - Scan all hwmon entries to find GPU sensors + FileView { + id: gpuTempNameReader + property int currentIndex: 0 + printErrors: false + + function checkNext() { + if (currentIndex >= 16) { + // Finished scanning all hwmon entries + // Only check nvidia-smi if user has explicitly enabled dGPU monitoring (opt-in) + // because nvidia-smi wakes up the dGPU on laptops, draining battery + if (Settings.data.systemMonitor.enableDgpuMonitoring) { + Logger.d("SystemStat", `Found ${root.foundGpuSensors.length} sysfs GPU sensor(s), checking nvidia-smi (dGPU opt-in enabled)`); + nvidiaSmiCheck.running = true; + } else { + Logger.d("SystemStat", `Found ${root.foundGpuSensors.length} sysfs GPU sensor(s), skipping nvidia-smi (dGPU opt-in disabled)`); + root.gpuVramCheckIndex = 0; + checkNextGpuVram(); + } + return; + } + + gpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`; + gpuTempNameReader.reload(); + } + + onLoaded: { + const name = text().trim(); + if (root.supportedTempGpuSensorNames.includes(name)) { + // Collect this GPU sensor, don't stop - continue scanning for more + const hwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`; + const gpuType = name === "amdgpu" ? "amd" : "intel"; + root.foundGpuSensors.push({ + "hwmonPath": hwmonPath, + "type": gpuType, + "hasDedicatedVram": false // Will be checked later for AMD + }); + Logger.d("SystemStat", `Found ${name} GPU sensor at ${hwmonPath}`); + } + // Continue scanning regardless of whether we found a match + currentIndex++; + Qt.callLater(() => { + checkNext(); + }); + } + + onLoadFailed: function (error) { + currentIndex++; + Qt.callLater(() => { + checkNext(); + }); + } + } + + // ---- + // #2 - Read GPU sensor value (AMD/Intel via sysfs) + FileView { + id: gpuTempReader + printErrors: false + + onLoaded: { + const data = text().trim(); + root.gpuTemp = Math.round(parseInt(data) / 1000.0); + root.pushGpuHistory(); + } + } + + // ---- + // #3 - Check if nvidia-smi is available (for NVIDIA GPUs) + Process { + id: nvidiaSmiCheck + command: ["sh", "-c", "command -v nvidia-smi"] + running: false + stdout: StdioCollector { + onStreamFinished: { + if (text.trim().length > 0) { + // Add NVIDIA as a GPU option (always discrete, highest priority) + root.foundGpuSensors.push({ + "hwmonPath": "", + "type": "nvidia", + "hasDedicatedVram": true // NVIDIA is always discrete + }); + Logger.d("SystemStat", "Found NVIDIA GPU (nvidia-smi available)"); + } + // After NVIDIA check, check VRAM for AMD GPUs to distinguish dGPU from iGPU + root.gpuVramCheckIndex = 0; + checkNextGpuVram(); + } + } + } + + // ---- + // #4 - Check VRAM for AMD GPUs to distinguish dGPU from iGPU + // dGPUs have dedicated VRAM, iGPUs don't (use system RAM) + FileView { + id: gpuVramChecker + printErrors: false + + onLoaded: { + // File exists and has content = dGPU with dedicated VRAM + const vramSize = parseInt(text().trim()); + if (vramSize > 0) { + root.foundGpuSensors[root.gpuVramCheckIndex].hasDedicatedVram = true; + Logger.d("SystemStat", `GPU at ${root.foundGpuSensors[root.gpuVramCheckIndex].hwmonPath} has dedicated VRAM (dGPU)`); + } + root.gpuVramCheckIndex++; + Qt.callLater(() => { + checkNextGpuVram(); + }); + } + + onLoadFailed: function (error) { + // File doesn't exist = iGPU (no dedicated VRAM) + // hasDedicatedVram is already false by default + root.gpuVramCheckIndex++; + Qt.callLater(() => { + checkNextGpuVram(); + }); + } + } + + // ---- + // #4 - Read GPU temperature via nvidia-smi (NVIDIA only) + Process { + id: nvidiaTempProcess + command: ["nvidia-smi", "--query-gpu=temperature.gpu", "--format=csv,noheader,nounits"] + running: false + stdout: StdioCollector { + onStreamFinished: { + const temp = parseInt(text.trim()); + if (!isNaN(temp)) { + root.gpuTemp = temp; + root.pushGpuHistory(); + } + } + } + } + + // ------------------------------------------------------- + // ------------------------------------------------------- + // Parse ZFS ARC stats from /proc/spl/kstat/zfs/arcstats + function parseZfsArcStats(text) { + if (!text) + return; + const lines = text.split('\n'); + + // The file format is: name type data + // We need to find the lines with "size" and "c_min" and extract the values (third column) + let foundSize = false; + let foundCmin = false; + + for (const line of lines) { + const parts = line.trim().split(/\s+/); + if (parts.length >= 3) { + if (parts[0] === 'size') { + // The value is in bytes, convert to KB + const arcSizeBytes = parseInt(parts[2]) || 0; + root.zfsArcSizeKb = Math.floor(arcSizeBytes / 1024); + foundSize = true; + } else if (parts[0] === 'c_min') { + // The value is in bytes, convert to KB + const arcCminBytes = parseInt(parts[2]) || 0; + root.zfsArcCminKb = Math.floor(arcCminBytes / 1024); + foundCmin = true; + } + + // If we found both, we can return early + if (foundSize && foundCmin) { + return; + } + } + } + + // If fields not found, set to 0 + if (!foundSize) { + root.zfsArcSizeKb = 0; + } + if (!foundCmin) { + root.zfsArcCminKb = 0; + } + } + + // ------------------------------------------------------- + // Parse load average from /proc/loadavg + function parseLoadAverage(text) { + if (!text) + return; + const parts = text.trim().split(/\s+/); + if (parts.length >= 3) { + root.loadAvg1 = parseFloat(parts[0]); + root.loadAvg5 = parseFloat(parts[1]); + root.loadAvg15 = parseFloat(parts[2]); + } + } + + // ------------------------------------------------------- + // Parse memory info from /proc/meminfo + function parseMemoryInfo(text) { + if (!text) + return; + const lines = text.split('\n'); + let memTotal = 0; + let memAvailable = 0; + let swapTotal = 0; + let swapFree = 0; + + for (const line of lines) { + if (line.startsWith('MemTotal:')) { + memTotal = parseInt(line.split(/\s+/)[1]) || 0; + } else if (line.startsWith('MemAvailable:')) { + memAvailable = parseInt(line.split(/\s+/)[1]) || 0; + } else if (line.startsWith('SwapTotal:')) { + swapTotal = parseInt(line.split(/\s+/)[1]) || 0; + } else if (line.startsWith('SwapFree:')) { + swapFree = parseInt(line.split(/\s+/)[1]) || 0; + } + } + + if (memTotal > 0) { + // Calculate usage, adjusting for ZFS ARC cache if present + let usageKb = memTotal - memAvailable; + if (root.zfsArcSizeKb > 0) { + usageKb = Math.max(0, usageKb - root.zfsArcSizeKb + root.zfsArcCminKb); + } + root.memGb = (usageKb / 1048576).toFixed(1); // 1024*1024 = 1048576 + root.memPercent = Math.round((usageKb / memTotal) * 100); + root.memTotalGb = (memTotal / 1048576).toFixed(1); + root.pushMemHistory(); + } + + // Swap usage + root.swapTotalGb = (swapTotal / 1048576).toFixed(1); + if (swapTotal > 0) { + const swapUsedKb = swapTotal - swapFree; + root.swapGb = (swapUsedKb / 1048576).toFixed(1); + root.swapPercent = Math.round((swapUsedKb / swapTotal) * 100); + } else { + root.swapGb = 0; + root.swapPercent = 0; + } + } + + // ------------------------------------------------------- + // Calculate CPU usage from /proc/stat + + function calculateLineUsage(line) { + const parts = line.split(/\s+/); + const stats = { + "user": parseInt(parts[1]) || 0, + "nice": parseInt(parts[2]) || 0, + "system": parseInt(parts[3]) || 0, + "idle": parseInt(parts[4]) || 0, + "iowait": parseInt(parts[5]) || 0, + "irq": parseInt(parts[6]) || 0, + "softirq": parseInt(parts[7]) || 0, + "steal": parseInt(parts[8]) || 0, + "guest": parseInt(parts[9]) || 0, + "guestNice": parseInt(parts[10]) || 0 + }; + return stats; + } + + function computeUsage(prev, curr) { + if (!prev || !curr) + return -1; + const currTotalIdle = curr.idle + curr.iowait; + const currTotal = Object.values(curr).reduce((sum, val) => sum + val, 0); + const prevTotalIdle = prev.idle + prev.iowait; + const prevTotal = Object.values(prev).reduce((sum, val) => sum + val, 0); + + const diffTotal = currTotal - prevTotal; + const diffIdle = currTotalIdle - prevTotalIdle; + if (diffTotal > 0) { + return (((diffTotal - diffIdle) / diffTotal) * 100).toFixed(1); + } + return -1; + } + + function calculateCpuUsage(text) { + if (!text) + return; + const lines = text.split('\n'); + const cpuLine = lines[0]; + + // First line is total CPU + if (!cpuLine.startsWith('cpu ')) + return; + + const currCpuStats = calculateLineUsage(cpuLine); + const usage = computeUsage(root.prevCpuStats, currCpuStats); + + if (usage >= 0) { + root.cpuUsage = usage; + root.pushCpuHistory(); + } + root.prevCpuStats = currCpuStats; + + // Find the number of CPU cores + let nbCores = 0; + for (let i = 1; i < lines.length; i++) { + if (!lines[i].startsWith('cpu')) + break; + nbCores++; + } + + // Fallback if we did not find any cores + if (nbCores === 0) + return; + + // If we found more cores than before, we reset our stats + if (root.coresUsage.length < nbCores) + root.coresUsage = new Array(nbCores).fill(0); + + let coresStats = []; + let newCoresUsage = root.coresUsage.slice(); + for (let i = 0; i < nbCores; i++) { + const coreCpuLine = lines[i + 1]; + const currCoreStats = calculateLineUsage(coreCpuLine); + const coreUsage = computeUsage(root.prevCpuCoresStats?.[i], currCoreStats); + if (coreUsage >= 0) { + newCoresUsage[i] = coreUsage; + } + coresStats.push(currCoreStats); + } + root.coresUsage = newCoresUsage; + root.prevCpuCoresStats = coresStats; + } + + // ------------------------------------------------------- + // Check whether a network interface is virtual/tunnel/bridge. + // Only physical interfaces (eth*, en*, wl*, ww*) are kept so + // that traffic routed through VPNs, Docker bridges, etc. is + // not double-counted. + readonly property var _virtualPrefixes: ["lo", "docker", "veth", "br-", "virbr", "vnet", "tun", "tap", "wg", "tailscale", "nordlynx", "proton", "mullvad", "flannel", "cni", "cali", "vxlan", "genev", "gre", "sit", "ip6tnl", "dummy", "ifb", "nlmon", "bond"] + + function isVirtualInterface(name) { + for (let i = 0; i < _virtualPrefixes.length; ++i) { + if (name.startsWith(_virtualPrefixes[i])) + return true; + } + return false; + } + + // ------------------------------------------------------- + // Calculate RX and TX speed from /proc/net/dev + // Sums speeds of all physical interfaces + function calculateNetworkSpeed(text) { + if (!text) { + return; + } + + const currentTime = Date.now() / 1000; + const lines = text.split('\n'); + + let totalRx = 0; + let totalTx = 0; + + for (var i = 2; i < lines.length; i++) { + const line = lines[i].trim(); + if (!line) { + continue; + } + + const colonIndex = line.indexOf(':'); + if (colonIndex === -1) { + continue; + } + + const iface = line.substring(0, colonIndex).trim(); + if (isVirtualInterface(iface)) { + continue; + } + + const statsLine = line.substring(colonIndex + 1).trim(); + const stats = statsLine.split(/\s+/); + + const rxBytes = parseInt(stats[0], 10) || 0; + const txBytes = parseInt(stats[8], 10) || 0; + + totalRx += rxBytes; + totalTx += txBytes; + } + + // Compute only if we have a previous run to compare to. + if (root.prevTime > 0) { + const timeDiff = currentTime - root.prevTime; + + // Avoid division by zero if time hasn't passed. + if (timeDiff > 0) { + let rxDiff = totalRx - root.prevRxBytes; + let txDiff = totalTx - root.prevTxBytes; + + // Handle counter resets (e.g., WiFi reconnect), which would cause a negative value. + if (rxDiff < 0) { + rxDiff = 0; + } + if (txDiff < 0) { + txDiff = 0; + } + + root.rxSpeed = Math.round(rxDiff / timeDiff); // Speed in Bytes/s + root.txSpeed = Math.round(txDiff / timeDiff); + } + } + + root.prevRxBytes = totalRx; + root.prevTxBytes = totalTx; + root.prevTime = currentTime; + + // Update network history after speeds are computed + root.pushNetworkHistory(); + } + + // ------------------------------------------------------- + // Helper function to format network speeds + function formatSpeed(bytesPerSecond) { + const units = ["KB", "MB", "GB"]; + let value = bytesPerSecond / 1000; + let unitIndex = 0; + + while (value >= 1000 && unitIndex < units.length - 1) { + value /= 1000; + unitIndex++; + } + + const unit = units[unitIndex]; + const shortUnit = unit[0]; + const numStr = value < 10 ? value.toFixed(1) : Math.round(value).toString(); + + return (numStr + unit).length > 5 ? numStr + shortUnit : numStr + unit; + } + + // ------------------------------------------------------- + // Compact speed formatter for vertical bar display + function formatCompactSpeed(bytesPerSecond) { + if (!bytesPerSecond || bytesPerSecond <= 0) + return "0"; + const units = ["", "K", "M", "G"]; + let value = bytesPerSecond; + let unitIndex = 0; + while (value >= 1000 && unitIndex < units.length - 1) { + value = value / 1000.0; + unitIndex++; + } + // Promote at ~100 of current unit (e.g., 100k -> ~0.1M shown as 0.1M or 0M if rounded) + if (unitIndex < units.length - 1 && value >= 100) { + value = value / 1000.0; + unitIndex++; + } + const display = Math.round(value).toString(); + return display + units[unitIndex]; + } + + // ------------------------------------------------------- + // Smart formatter for memory values (GB) - max 4 chars + // Uses decimal for < 10GB, integer otherwise + function formatGigabytes(memGb) { + const value = parseFloat(memGb); + if (isNaN(value)) + return "0G"; + + if (value < 10) + return value.toFixed(1) + "G"; // "0.0G" to "9.9G" + return Math.round(value) + "G"; // "10G" to "999G" + } + + // ------------------------------------------------------- + // Formatting gigabytes with optional padding + function formatGigabytesDisplay(memGb, maxGb = null) { + const value = formatGigabytes(memGb === null ? 0 : memGb); + if (maxGb !== null) { + const padding = Math.max(4, formatGigabytes(maxGb).length); + return value.padStart(padding, " "); + } + return value; + } + + // ------------------------------------------------------- + // Formatting percentage with optional padding + function formatPercentageDisplay(value, padding = false) { + return `${Math.round(value === null ? 0 : value)}%`.padStart(padding ? 4 : 0, " "); + } + + // ------------------------------------------------------- + // Formatting disk usage + function formatDiskDisplay(diskPath, { + percent = false, + available = false, + padding = false +} = {}) { + if (percent) { + const raw = available ? root.diskAvailPercents[diskPath] : root.diskPercents[diskPath]; + return formatPercentageDisplay(raw, padding); + } else { + const rawGb = available ? root.diskAvailableGb[diskPath] : root.diskUsedGb[diskPath]; + const maxGb = padding ? root.diskSizeGb[diskPath] : null; + return formatGigabytesDisplay(rawGb, maxGb); + } + } + + // ------------------------------------------------------- + // Formatting ram usage + function formatRamDisplay({ + swap = false, + percent = false, + padding = false +} = {}) { + if (percent) { + const raw = swap ? swapPercent : memPercent; + return formatPercentageDisplay(raw, padding); + } else { + const rawGb = swap ? swapGb : memGb; + const maxGb = padding ? (swap ? swapTotalGb : memTotalGb) : null; + return formatGigabytesDisplay(rawGb, maxGb); + } + } + + // ------------------------------------------------------- + // Function to start fetching and computing the cpu temperature + function updateCpuTemperature() { + // For AMD sensors (k10temp and zenpower), only use Tctl sensor + // temp1_input corresponds to Tctl (Temperature Control) on these sensors + if (root.cpuTempSensorName === "k10temp" || root.cpuTempSensorName === "zenpower") { + cpuTempReader.path = `${root.cpuTempHwmonPath}/temp1_input`; + cpuTempReader.reload(); + } // For Intel coretemp, start averaging all available sensors/cores + else if (root.cpuTempSensorName === "coretemp") { + root.intelTempValues = []; + root.intelTempFilesChecked = 0; + checkNextIntelTemp(); + } // For thermal_zone fallback (ARM SoCs, SCMI, etc.), read all CPU zones and take max + else if (root.cpuTempSensorName === "thermal_zone") { + cpuThermalZoneReader.currentZoneIndex = 0; + cpuThermalZoneReader.collectedTemps = []; + readNextCpuThermalZone(); + } + } + + // ------------------------------------------------------- + // Function to check next Intel temperature sensor + function checkNextIntelTemp() { + if (root.intelTempFilesChecked >= root.intelTempMaxFiles) { + // Calculate average of all found temperatures + if (root.intelTempValues.length > 0) { + let sum = 0; + for (var i = 0; i < root.intelTempValues.length; i++) { + sum += root.intelTempValues[i]; + } + root.cpuTemp = Math.round(sum / root.intelTempValues.length); + root.pushCpuTempHistory(); + //Logger.i("SystemStat", `Averaged ${root.intelTempValues.length} CPU thermal sensors: ${root.cpuTemp}°C`) + } else { + Logger.w("SystemStat", "No temperature sensors found for coretemp"); + root.cpuTemp = 0; + root.pushCpuTempHistory(); + } + return; + } + + // Check next temperature file + root.intelTempFilesChecked++; + cpuTempReader.path = `${root.cpuTempHwmonPath}/temp${root.intelTempFilesChecked}_input`; + cpuTempReader.reload(); + } + + // ------------------------------------------------------- + // Function to check VRAM for each AMD GPU to determine if it's a dGPU + function checkNextGpuVram() { + // Skip non-AMD GPUs (NVIDIA and Intel Arc are always discrete) + while (root.gpuVramCheckIndex < root.foundGpuSensors.length) { + const gpu = root.foundGpuSensors[root.gpuVramCheckIndex]; + if (gpu.type === "amd") { + // Check for dedicated VRAM at hwmonPath/device/mem_info_vram_total + gpuVramChecker.path = `${gpu.hwmonPath}/device/mem_info_vram_total`; + gpuVramChecker.reload(); + return; + } + // Skip non-AMD GPUs + root.gpuVramCheckIndex++; + } + + // All VRAM checks complete, now select the best GPU + selectBestGpu(); + } + + // ------------------------------------------------------- + // Function to select the best GPU based on priority + // Priority (when dGPU monitoring enabled): NVIDIA > AMD dGPU > Intel Arc > AMD iGPU + // Priority (when dGPU monitoring disabled): AMD iGPU only (discrete GPUs skipped to preserve D3cold) + function selectBestGpu() { + const dgpuEnabled = Settings.data.systemMonitor.enableDgpuMonitoring; + + if (root.foundGpuSensors.length === 0) { + // No hwmon GPU sensors found, try thermal_zone fallback + if (dgpuEnabled && root.gpuThermalZonePath === "" && root.gpuThermalZonePaths.length === 0) { + // Thermal zone scanner hasn't found GPU zones yet; start a scan + thermalZoneScanner.startScan(); + } + return; + } + + let best = null; + + for (var i = 0; i < root.foundGpuSensors.length; i++) { + const gpu = root.foundGpuSensors[i]; + + // NVIDIA is always highest priority (always discrete) - skip if dGPU monitoring disabled + if (gpu.type === "nvidia") { + if (dgpuEnabled) { + best = gpu; + break; + } + continue; + } + + // AMD dGPU is second priority - skip if dGPU monitoring disabled (preserves D3cold power state) + if (gpu.type === "amd" && gpu.hasDedicatedVram) { + if (dgpuEnabled) { + best = gpu; + break; + } + continue; + } + + // Intel Arc is third priority (always discrete) - skip if dGPU monitoring disabled + if (gpu.type === "intel" && !best) { + if (dgpuEnabled) { + best = gpu; + } + continue; + } + + // AMD iGPU is lowest priority (fallback) - always allowed (no D3cold issue) + if (gpu.type === "amd" && !gpu.hasDedicatedVram && !best) { + best = gpu; + } + } + + if (best) { + root.gpuTempHwmonPath = best.hwmonPath; + root.gpuType = best.type; + root.gpuAvailable = true; + + const gpuDesc = best.type === "nvidia" ? "NVIDIA" : (best.type === "intel" ? "Intel Arc" : (best.hasDedicatedVram ? "AMD dGPU" : "AMD iGPU")); + Logger.i("SystemStat", `Selected ${gpuDesc} for temperature monitoring at ${best.hwmonPath || "nvidia-smi"}`); + } else if (!dgpuEnabled) { + Logger.d("SystemStat", "No iGPU found and dGPU monitoring is disabled"); + } + } + + // ------------------------------------------------------- + // Function to update GPU temperature + function updateGpuTemperature() { + if (root.gpuType === "nvidia") { + nvidiaTempProcess.running = true; + } else if (root.gpuType === "amd" || root.gpuType === "intel") { + gpuTempReader.path = `${root.gpuTempHwmonPath}/temp1_input`; + gpuTempReader.reload(); + } else if (root.gpuType === "thermal_zone") { + if (root.gpuThermalZonePaths && root.gpuThermalZonePaths.length > 0) { + // Multiple GPU zones (no gpu-avg), read all and take max + gpuThermalZoneReader.currentZoneIndex = 0; + gpuThermalZoneReader.collectedTemps = []; + readNextGpuThermalZone(); + } else { + // Single gpu-avg-thermal zone + gpuThermalZoneReader.path = root.gpuThermalZonePath; + gpuThermalZoneReader.reload(); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Theming/AppThemeService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Theming/AppThemeService.qml new file mode 100644 index 0000000..137e573 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Theming/AppThemeService.qml @@ -0,0 +1,90 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.UI + +Singleton { + id: root + + Connections { + target: WallpaperService + + // When the wallpaper changes, regenerate theme if necessary + function onWallpaperChanged(screenName, path) { + var effectiveMonitor = Settings.data.colorSchemes.monitorForColors; + if (effectiveMonitor === "" || effectiveMonitor === undefined) { + effectiveMonitor = Screen.name; + } + + if (screenName !== effectiveMonitor) + return; + + if (Settings.data.colorSchemes.useWallpaperColors) { + generateFromWallpaper(); + } else { + // Re-run predefined scheme templates so {{image}} reflects the new wallpaper path + ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme); + } + } + } + + Connections { + target: Settings.data.colorSchemes + function onDarkModeChanged() { + Logger.d("AppThemeService", "Detected dark mode change"); + generate(); + } + function onMonitorForColorsChanged() { + if (Settings.data.colorSchemes.useWallpaperColors) { + Logger.d("AppThemeService", "Monitor for colors changed to:", Settings.data.colorSchemes.monitorForColors); + generateFromWallpaper(); + } + } + function onGenerationMethodChanged() { + Logger.d("AppThemeService", "Generation method changed to:", Settings.data.colorSchemes.generationMethod); + generate(); + } + } + + // PUBLIC FUNCTIONS + function init() { + Logger.i("AppThemeService", "Service started"); + } + + function generate() { + if (Settings.data.colorSchemes.useWallpaperColors) { + generateFromWallpaper(); + } else { + // applyScheme will trigger template generation via schemeReader.onLoaded + ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme); + } + } + + function generateFromWallpaper() { + var effectiveMonitor = Settings.data.colorSchemes.monitorForColors; + if (effectiveMonitor === "" || effectiveMonitor === undefined) { + effectiveMonitor = Screen.name; + } + + const wp = WallpaperService.getWallpaper(effectiveMonitor); + if (!wp) { + Logger.e("AppThemeService", "No wallpaper found for monitor:", effectiveMonitor); + return; + } + const mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"; + TemplateProcessor.processWallpaperColors(wp, mode); + } + + function generateFromPredefinedScheme(schemeData) { + Logger.i("AppThemeService", "Generating templates from predefined color scheme"); + const mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"; + var effectiveMonitor = Settings.data.colorSchemes.monitorForColors; + if (effectiveMonitor === "" || effectiveMonitor === undefined) { + effectiveMonitor = Screen.name; + } + const wallpaperPath = WallpaperService.getWallpaper(effectiveMonitor) || ""; + TemplateProcessor.processPredefinedScheme(schemeData, mode, wallpaperPath); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Theming/ColorSchemeService.qml b/fedora/.config/quickshell/noctalia-shell/Services/Theming/ColorSchemeService.qml new file mode 100644 index 0000000..9348fe4 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Theming/ColorSchemeService.qml @@ -0,0 +1,286 @@ +pragma Singleton +import Qt.labs.folderlistmodel + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.Theming +import qs.Services.UI + +Singleton { + id: root + + property var schemes: [] + property bool scanning: false + property string schemesDirectory: Quickshell.shellDir + "/Assets/ColorScheme" + property string downloadedSchemesDirectory: Settings.configDir + "colorschemes" + property string colorsJsonFilePath: Settings.configDir + "colors.json" + readonly property string gtkRefreshScript: Quickshell.shellDir + "/Scripts/python/src/theming/gtk-refresh.py" + + // prefer-light/prefer-dark only; GTK template post_hook still runs full gtk-refresh. + function pushSystemColorScheme() { + if (!Settings.data.colorSchemes.syncGsettings) + return; + const mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"; + Quickshell.execDetached(["python3", gtkRefreshScript, "--appearance-only", mode]); + } + + Connections { + target: Settings.data.colorSchemes + function onDarkModeChanged() { + Logger.d("ColorScheme", "Detected dark mode change"); + if (!Settings.data.colorSchemes.useWallpaperColors && Settings.data.colorSchemes.predefinedScheme) { + // Re-apply current scheme to pick the right variant + applyScheme(Settings.data.colorSchemes.predefinedScheme); + } + root.pushSystemColorScheme(); + // Toast: dark/light mode switched + const enabled = !!Settings.data.colorSchemes.darkMode; + const label = enabled ? I18n.tr("tooltips.switch-to-dark-mode") : I18n.tr("tooltips.switch-to-light-mode"); + const description = I18n.tr("common.enabled"); + ToastService.showNotice(label, description, "dark-mode"); + } + } + + // -------------------------------- + function init() { + // does nothing but ensure the singleton is created + // do not remove + Logger.i("ColorScheme", "Service started"); + loadColorSchemes(); + } + + function loadColorSchemes() { + Logger.d("ColorScheme", "Load colorScheme"); + scanning = true; + schemes = []; + // Use find command to locate all scheme.json files in both directories + // First ensure the downloaded schemes directory exists + Quickshell.execDetached(["mkdir", "-p", downloadedSchemesDirectory]); + // Find in both preinstalled and downloaded directories + findProcess.command = ["find", "-L", schemesDirectory, downloadedSchemesDirectory, "-mindepth", "2", "-name", "*.json", "-type", "f"]; + findProcess.running = true; + } + + function getBasename(path) { + if (!path) + return ""; + var chunks = path.split("/"); + // Get the filename without extension + var filename = chunks[chunks.length - 1]; + var schemeName = filename.replace(".json", ""); + // Convert back to display names for special cases + if (schemeName === "Noctalia-default") { + return "Noctalia (default)"; + } else if (schemeName === "Noctalia-legacy") { + return "Noctalia (legacy)"; + } else if (schemeName === "Tokyo-Night") { + return "Tokyo Night"; + } else if (schemeName === "Rosepine") { + return "Rose Pine"; + } + return schemeName; + } + + function resolveSchemePath(nameOrPath) { + if (!nameOrPath) + return ""; + if (nameOrPath.indexOf("/") !== -1) { + return nameOrPath; + } + // Handle special cases for Noctalia schemes + var schemeName = nameOrPath.replace(".json", ""); + if (schemeName === "Noctalia (default)") { + schemeName = "Noctalia-default"; + } else if (schemeName === "Noctalia (legacy)") { + schemeName = "Noctalia-legacy"; + } else if (schemeName === "Tokyo Night") { + schemeName = "Tokyo-Night"; + } else if (schemeName === "Rose Pine") { + schemeName = "Rosepine"; + } + // Check preinstalled directory first, then downloaded directory + var preinstalledPath = schemesDirectory + "/" + schemeName + "/" + schemeName + ".json"; + var downloadedPath = downloadedSchemesDirectory + "/" + schemeName + "/" + schemeName + ".json"; + // Try to find the scheme in the loaded schemes list to determine which directory it's in + for (var i = 0; i < schemes.length; i++) { + if (schemes[i].indexOf("/" + schemeName + "/") !== -1 || schemes[i].indexOf("/" + schemeName + ".json") !== -1) { + return schemes[i]; + } + } + // Fallback: prefer preinstalled, then downloaded + return preinstalledPath; + } + + function applyScheme(nameOrPath) { + // Force reload by bouncing the path + var filePath = resolveSchemePath(nameOrPath); + schemeReader.path = ""; + schemeReader.path = filePath; + } + + function setPredefinedScheme(schemeName) { + Logger.i("ColorScheme", "Attempting to set predefined scheme to:", schemeName); + + var resolvedPath = resolveSchemePath(schemeName); + var basename = getBasename(schemeName); + + // Check if the scheme actually exists in the loaded schemes list + var schemeExists = false; + for (var i = 0; i < schemes.length; i++) { + if (getBasename(schemes[i]) === basename) { + schemeExists = true; + break; + } + } + + if (schemeExists) { + Settings.data.colorSchemes.predefinedScheme = basename; + applyScheme(schemeName); + ToastService.showNotice(I18n.tr("panels.color-scheme.title"), basename, "settings-color-scheme"); + } else { + Logger.e("ColorScheme", "Scheme not found:", schemeName); + ToastService.showError(I18n.tr("panels.color-scheme.title"), `'${basename}' ` + I18n.tr("common.not-found")); + } + } + + Process { + id: findProcess + running: false + + onExited: function (exitCode) { + if (exitCode === 0) { + var output = stdout.text.trim(); + var files = output.split('\n').filter(function (line) { + return line.length > 0; + }); + files.sort(function (a, b) { + var nameA = getBasename(a).toLowerCase(); + var nameB = getBasename(b).toLowerCase(); + return nameA.localeCompare(nameB); + }); + schemes = files; + scanning = false; + Logger.d("ColorScheme", "Listed", schemes.length, "schemes"); + // Normalize stored scheme to basename and re-apply if necessary + var stored = Settings.data.colorSchemes.predefinedScheme; + if (stored) { + var basename = getBasename(stored); + if (basename !== stored) { + Settings.data.colorSchemes.predefinedScheme = basename; + } + if (!Settings.data.colorSchemes.useWallpaperColors) { + applyScheme(basename); + } + } + } else { + Logger.e("ColorScheme", "Failed to find color scheme files"); + schemes = []; + scanning = false; + } + } + + stdout: StdioCollector {} + stderr: StdioCollector {} + } + + // Internal loader to read a scheme file + FileView { + id: schemeReader + onLoaded: { + try { + var data = JSON.parse(text()); + var variant = data; + // If scheme provides dark/light variants, pick based on settings + if (data && (data.dark || data.light)) { + if (Settings.data.colorSchemes.darkMode) { + variant = data.dark || data.light; + } else { + variant = data.light || data.dark; + } + } + writeColorsToDisk(variant); + Logger.i("ColorScheme", "Applying color scheme:", getBasename(path)); + + // Generate templates for predefined color schemes + if (hasEnabledTemplates() || Settings.data.templates.enableUserTheming) { + AppThemeService.generateFromPredefinedScheme(data); + } + } catch (e) { + Logger.e("ColorScheme", "Failed to parse scheme JSON:", path, e); + } + } + } + + // Check if any templates are enabled + function hasEnabledTemplates() { + const activeTemplates = Settings.data.templates.activeTemplates; + if (!activeTemplates || activeTemplates.length === 0) { + return false; + } + for (let i = 0; i < activeTemplates.length; i++) { + if (activeTemplates[i].enabled) { + return true; + } + } + return false; + } + + // Writer to colors.json using a JsonAdapter for safety + FileView { + id: colorsWriter + path: colorsJsonFilePath + printErrors: false + onSaved: + + // Logger.i("ColorScheme", "Colors saved") + {} + JsonAdapter { + id: out + property color mPrimary: "#000000" + property color mOnPrimary: "#000000" + property color mSecondary: "#000000" + property color mOnSecondary: "#000000" + property color mTertiary: "#000000" + property color mOnTertiary: "#000000" + property color mError: "#000000" + property color mOnError: "#000000" + property color mSurface: "#000000" + property color mOnSurface: "#000000" + property color mSurfaceVariant: "#000000" + property color mOnSurfaceVariant: "#000000" + property color mOutline: "#000000" + property color mShadow: "#000000" + property color mHover: "#000000" + property color mOnHover: "#000000" + } + } + + function writeColorsToDisk(obj) { + function pick(o, a, b, fallback) { + return (o && (o[a] || o[b])) || fallback; + } + out.mPrimary = pick(obj, "mPrimary", "primary", out.mPrimary); + out.mOnPrimary = pick(obj, "mOnPrimary", "onPrimary", out.mOnPrimary); + out.mSecondary = pick(obj, "mSecondary", "secondary", out.mSecondary); + out.mOnSecondary = pick(obj, "mOnSecondary", "onSecondary", out.mOnSecondary); + out.mTertiary = pick(obj, "mTertiary", "tertiary", out.mTertiary); + out.mOnTertiary = pick(obj, "mOnTertiary", "onTertiary", out.mOnTertiary); + out.mError = pick(obj, "mError", "error", out.mError); + out.mOnError = pick(obj, "mOnError", "onError", out.mOnError); + out.mSurface = pick(obj, "mSurface", "surface", out.mSurface); + out.mOnSurface = pick(obj, "mOnSurface", "onSurface", out.mOnSurface); + out.mSurfaceVariant = pick(obj, "mSurfaceVariant", "surfaceVariant", out.mSurfaceVariant); + out.mOnSurfaceVariant = pick(obj, "mOnSurfaceVariant", "onSurfaceVariant", out.mOnSurfaceVariant); + out.mOutline = pick(obj, "mOutline", "outline", out.mOutline); + out.mShadow = pick(obj, "mShadow", "shadow", out.mShadow); + out.mHover = pick(obj, "mHover", "hover", out.mHover); + out.mOnHover = pick(obj, "mOnHover", "onHover", out.mOnHover); + + // Force a rewrite by updating the path + colorsWriter.path = ""; + colorsWriter.path = colorsJsonFilePath; + colorsWriter.writeAdapter(); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Theming/TemplateProcessor.qml b/fedora/.config/quickshell/noctalia-shell/Services/Theming/TemplateProcessor.qml new file mode 100644 index 0000000..0339f38 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Theming/TemplateProcessor.qml @@ -0,0 +1,475 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +import qs.Commons +import qs.Services.System +import qs.Services.Theming +import qs.Services.UI + +Singleton { + id: root + + // Signal emitted when color generation completes successfully (for wallpaper-based theming) + signal colorsGenerated + + readonly property string dynamicConfigPath: Settings.cacheDir + "theming.dynamic.toml" + readonly property string templateProcessorScript: Quickshell.shellDir + "/Scripts/python/src/theming/template-processor.py" + + // Debounce state for wallpaper processing + property var pendingWallpaperRequest: null + property var pendingPredefinedRequest: null + + readonly property var schemeTypes: [ + { + "key": "tonal-spot", + "name": "M3-Tonal Spot" // Do not translate + }, + { + "key": "content", + "name": "M3-Content" // Do not translate + }, + { + "key": "fruit-salad", + "name": "M3-Fruit Salad" // Do not translate + }, + { + "key": "rainbow", + "name": "M3-Rainbow" // Do not translate + }, + { + "key": "monochrome", + "name": "M3-Monochrome" // Do not translate + }, + { + "key": "vibrant", + "name": I18n.tr("common.vibrant") + }, + { + "key": "faithful", + "name": I18n.tr("common.faithful") + }, + { + "key": "dysfunctional", + "name": I18n.tr("common.dysfunctional") + }, + { + "key": "muted", + "name": I18n.tr("common.color-muted") + }, + ] + + // Check if a template is enabled in the activeTemplates array + function isTemplateEnabled(templateId) { + const activeTemplates = Settings.data.templates.activeTemplates; + if (!activeTemplates) + return false; + for (let i = 0; i < activeTemplates.length; i++) { + if (activeTemplates[i].id === templateId && activeTemplates[i].enabled) { + return true; + } + } + return false; + } + + function escapeTomlString(value) { + if (!value) + return ""; + return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + } + + /** + * Process wallpaper colors using internal themer + * Dual-path architecture (wallpaper generation) + * Uses debouncing to prevent spawning multiple processes when spamming wallpaper changes + */ + function processWallpaperColors(wallpaperPath, mode) { + Logger.d("TemplateProcessor", `processWallpaperColors called: path=${wallpaperPath}, mode=${mode}`); + pendingWallpaperRequest = { + wallpaperPath: wallpaperPath, + mode: mode + }; + pendingPredefinedRequest = null; + debounceTimer.restart(); + } + + function executeWallpaperColors(wallpaperPath, mode) { + Logger.d("TemplateProcessor", `executeWallpaperColors: path=${wallpaperPath}, mode=${mode}`); + const content = buildThemeConfig(); + if (!content && !Settings.data.templates.enableUserTheming) { + Logger.d("TemplateProcessor", "executeWallpaperColors: no config content and no user theming, aborting"); + return; + } + const script = buildGenerationScript(content, wallpaperPath, mode); + + generateProcess.command = ["sh", "-c", script]; + generateProcess.running = true; + } + + readonly property string schemeJsonPath: Settings.cacheDir + "predefined-scheme.json" + readonly property string predefinedConfigPath: Settings.cacheDir + "theming.predefined.toml" + + /** + * Process predefined color scheme using Python template processor + * Uses --scheme flag to expand 14-color scheme to full 48-color palette + * Uses debouncing to prevent spawning multiple processes when spamming scheme changes + */ + function processPredefinedScheme(schemeData, mode, wallpaperPath) { + pendingPredefinedRequest = { + schemeData: schemeData, + mode: mode, + wallpaperPath: wallpaperPath || "" + }; + pendingWallpaperRequest = null; + debounceTimer.restart(); + } + + function executePredefinedScheme(schemeData, mode, wallpaperPath) { + // 1. Build TOML config for application templates (including terminals) + const tomlContent = buildPredefinedTemplateConfig(mode); + if (!tomlContent && !Settings.data.templates.enableUserTheming) { + Logger.d("TemplateProcessor", "No application templates enabled for predefined scheme"); + return; + } + + // 3. Build script to write files and run Python + const schemeJsonPathEsc = schemeJsonPath.replace(/'/g, "'\\''"); + + let script = ""; + + // Write scheme JSON (needed by both built-in and user templates) + const schemeDelimiter = "SCHEME_JSON_EOF_" + Math.random().toString(36).substr(2, 9); + script += `cat > '${schemeJsonPathEsc}' << '${schemeDelimiter}'\n`; + script += JSON.stringify(schemeData, null, 2) + "\n"; + script += `${schemeDelimiter}\n`; + + // Run built-in template processor only if there are templates configured + if (tomlContent) { + const configPathEsc = predefinedConfigPath.replace(/'/g, "'\\''"); + const tomlDelimiter = "TOML_CONFIG_EOF_" + Math.random().toString(36).substr(2, 9); + + // Write TOML config + script += `cat > '${configPathEsc}' << '${tomlDelimiter}'\n`; + script += tomlContent + "\n"; + script += `${tomlDelimiter}\n`; + + // Run Python template processor with --scheme flag + // Don't pass --mode so templates get both dark and light colors (e.g., zed.json needs both) + // Pass --default-mode so "default" in templates resolves to the current theme mode + // Pass wallpaper as positional arg so image_path is available in templates (no extraction occurs when --scheme is used) + const wpArg = wallpaperPath ? `'${wallpaperPath.replace(/'/g, "'\\''")}'` : ""; + script += `python3 "${templateProcessorScript}" ${wpArg} --scheme '${schemeJsonPathEsc}' --config '${configPathEsc}' --default-mode ${mode}\n`; + } + + // Add user templates if enabled + script += buildUserTemplateCommandForPredefined(schemeData, mode, wallpaperPath); + + generateProcess.command = ["sh", "-c", script]; + generateProcess.running = true; + } + + /** + * Build TOML config for predefined scheme templates (excludes terminal themes) + */ + function buildPredefinedTemplateConfig(mode) { + var lines = []; + const homeDir = Quickshell.env("HOME"); + + // Add terminal templates + TemplateRegistry.terminals.forEach(terminal => { + if (isTemplateEnabled(terminal.id)) { + lines.push(`\n[templates.${terminal.id}]`); + lines.push(`input_path = "${Quickshell.shellDir}/Assets/Templates/${terminal.predefinedTemplatePath}"`); + const outputPath = terminal.outputPath.replace("~", homeDir); + lines.push(`output_path = "${outputPath}"`); + const postHookEsc = escapeTomlString(terminal.postHook); + lines.push(`post_hook = "${postHookEsc}"`); + } + }); + + addApplicationTheming(lines, mode); + + if (lines.length > 0) { + return ["[config]"].concat(lines).join("\n") + "\n"; + } + return ""; + } + + // ================================================================================ + // WALLPAPER-BASED GENERATION + // ================================================================================ + function buildThemeConfig() { + var lines = []; + var mode = Settings.data.colorSchemes.darkMode ? "dark" : "light"; + + if (Settings.data.colorSchemes.useWallpaperColors) { + addWallpaperTheming(lines, mode); + } + + addApplicationTheming(lines, mode); + + if (lines.length > 0) { + return ["[config]"].concat(lines).join("\n") + "\n"; + } + return ""; + } + + function addWallpaperTheming(lines, mode) { + const homeDir = Quickshell.env("HOME"); + // Noctalia colors JSON + lines.push("[templates.noctalia]"); + lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Templates/noctalia.json"'); + lines.push('output_path = "' + Settings.configDir + 'colors.json"'); + + // Terminal templates + TemplateRegistry.terminals.forEach(terminal => { + if (isTemplateEnabled(terminal.id)) { + lines.push(`\n[templates.${terminal.id}]`); + lines.push(`input_path = "${Quickshell.shellDir}/Assets/Templates/${terminal.templatePath}"`); + const outputPath = terminal.outputPath.replace("~", homeDir); + lines.push(`output_path = "${outputPath}"`); + const postHookEsc = escapeTomlString(terminal.postHook); + lines.push(`post_hook = "${postHookEsc}"`); + } + }); + } + + function addApplicationTheming(lines, mode) { + const homeDir = Quickshell.env("HOME"); + TemplateRegistry.applications.forEach(app => { + if (app.id === "discord") { + // Handle Discord clients specially - multiple CSS themes + if (isTemplateEnabled("discord")) { + const inputs = Array.isArray(app.input) ? app.input : [app.input]; + inputs.forEach((inputFile, idx) => { + // Derive theme suffix from input filename: discord-midnight.css → midnight + const themeSuffix = inputFile.replace(/^discord-/, "").replace(/\.css$/, ""); + app.clients.forEach(client => { + if (isDiscordClientEnabled(client.name)) { + lines.push(`\n[templates.discord_${themeSuffix}_${client.name}]`); + lines.push(`input_path = "${Quickshell.shellDir}/Assets/Templates/${inputFile}"`); + // First input uses legacy name for backward compatibility + const outputFile = idx === 0 ? "noctalia.theme.css" : `noctalia-${themeSuffix}.theme.css`; + const outputPath = client.path.replace("~", homeDir) + `/themes/${outputFile}`; + lines.push(`output_path = "${outputPath}"`); + } + }); + }); + } + } else if (app.id === "code") { + // Handle Code clients specially + if (isTemplateEnabled("code")) { + app.clients.forEach(client => { + // Check if this specific client is detected + var resolvedPaths = TemplateRegistry.resolvedCodeClientPaths(client.name); + if (isCodeClientEnabled(client.name) && resolvedPaths.length > 0) { + resolvedPaths.forEach((resolvedPath, pathIndex) => { + var suffix = resolvedPaths.length > 1 ? `_${pathIndex}` : ""; + lines.push(`\n[templates.code_${client.name}${suffix}]`); + lines.push(`input_path = "${Quickshell.shellDir}/Assets/Templates/${app.input}"`); + lines.push(`output_path = "${resolvedPath}"`); + }); + } + }); + } + } else if (app.id === "emacs") { + if (isTemplateEnabled("emacs")) { + ProgramCheckerService.availableEmacsClients.forEach(client => { + lines.push(`\n[templates.emacs_${client.name}]`); + lines.push(`input_path = "${Quickshell.shellDir}/Assets/Templates/${app.input}"`); + const expandedPath = client.path.replace("~", homeDir) + "/themes/noctalia-theme.el"; + lines.push(`output_path = "${expandedPath}"`); + if (app.postProcess) { + const postHook = escapeTomlString(app.postProcess(mode)); + lines.push(`post_hook = "${postHook}"`); + } + }); + } + } else { + // Handle regular apps + if (isTemplateEnabled(app.id)) { + app.outputs.forEach((output, idx) => { + lines.push(`\n[templates.${app.id}_${idx}]`); + const inputFile = output.input || app.input; + lines.push(`input_path = "${Quickshell.shellDir}/Assets/Templates/${inputFile}"`); + const outputPath = output.path.replace("~", homeDir); + lines.push(`output_path = "${outputPath}"`); + if (app.postProcess) { + const postHook = escapeTomlString(app.postProcess(mode)); + lines.push(`post_hook = "${postHook}"`); + } + }); + } + } + }); + } + + function isDiscordClientEnabled(clientName) { + // Check ProgramCheckerService to see if client is detected + for (var i = 0; i < ProgramCheckerService.availableDiscordClients.length; i++) { + if (ProgramCheckerService.availableDiscordClients[i].name === clientName) { + return true; + } + } + return false; + } + + function isCodeClientEnabled(clientName) { + // Check ProgramCheckerService to see if client is detected + for (var i = 0; i < ProgramCheckerService.availableCodeClients.length; i++) { + if (ProgramCheckerService.availableCodeClients[i].name === clientName) { + return true; + } + } + return false; + } + + // Get scheme type, defaulting to tonal-spot if not a recognized value + function getSchemeType() { + const method = Settings.data.colorSchemes.generationMethod; + const validKeys = root.schemeTypes.map(scheme => scheme.key); + return validKeys.includes(method) ? method : "tonal-spot"; + } + + function buildGenerationScript(content, wallpaper, mode) { + const pathEsc = dynamicConfigPath.replace(/'/g, "'\\''"); + const wpDelimiter = "WALLPAPER_PATH_EOF_" + Math.random().toString(36).substr(2, 9); + + // Use heredoc for wallpaper path to avoid all escaping issues + let script = `NOCTALIA_WP_PATH=$(cat << '${wpDelimiter}'\n${wallpaper}\n${wpDelimiter}\n)\n`; + + // Run built-in template processor only if there are templates configured + if (content) { + const delimiter = "THEME_CONFIG_EOF_" + Math.random().toString(36).substr(2, 9); + script += `cat > '${pathEsc}' << '${delimiter}'\n${content}\n${delimiter}\n`; + + // Use template-processor.py (Python implementation) + // Don't pass --mode so templates get both dark and light colors (e.g., zed.json needs both) + // Pass --default-mode so "default" in templates resolves to the current theme mode + const schemeType = getSchemeType(); + script += `python3 "${templateProcessorScript}" "$NOCTALIA_WP_PATH" --scheme-type ${schemeType} --config '${pathEsc}' --default-mode ${mode}\n`; + } + + script += buildUserTemplateCommand("$NOCTALIA_WP_PATH", mode); + + return script + "\n"; + } + + // ================================================================================ + // USER TEMPLATES, advanced usage + // ================================================================================ + function buildUserTemplateCommand(input, mode) { + if (!Settings.data.templates.enableUserTheming) + return ""; + + const userConfigPath = getUserConfigPath(); + let script = "\n# Execute user config if it exists\n"; + script += `if [ -f '${userConfigPath}' ]; then\n`; + // If input is a shell variable (starts with $), use double quotes to allow expansion + // Otherwise, use single quotes for safety with file paths + const inputQuoted = input.startsWith("$") ? `"${input}"` : `'${input.replace(/'/g, "'\\''")}'`; + + const schemeType = getSchemeType(); + // Don't pass --mode so user templates get both dark and light colors + // Pass --default-mode so "default" in templates resolves to the current theme mode + script += ` python3 "${templateProcessorScript}" ${inputQuoted} --scheme-type ${schemeType} --config '${userConfigPath}' --default-mode ${mode}\n`; + script += "fi"; + + return script; + } + + function buildUserTemplateCommandForPredefined(schemeData, mode, wallpaperPath) { + if (!Settings.data.templates.enableUserTheming) + return ""; + + const userConfigPath = getUserConfigPath(); + + // Reuse the scheme JSON already written by processPredefinedScheme() + const schemeJsonPathEsc = schemeJsonPath.replace(/'/g, "'\\''"); + const wpArg = wallpaperPath ? `'${wallpaperPath.replace(/'/g, "'\\''")}'` : ""; + + let script = "\n# Execute user templates with predefined scheme colors\n"; + script += `if [ -f '${userConfigPath}' ]; then\n`; + // Use --scheme flag with the already-written scheme JSON + // Don't pass --mode so user templates get both dark and light colors + // Pass --default-mode so "default" in templates resolves to the current theme mode + // Pass wallpaper as positional arg so image_path is available in templates + script += ` python3 "${templateProcessorScript}" ${wpArg} --scheme '${schemeJsonPathEsc}' --config '${userConfigPath}' --default-mode ${mode}\n`; + script += "fi"; + + return script; + } + + function getUserConfigPath() { + return (Settings.configDir + "user-templates.toml").replace(/'/g, "'\\''"); + } + + // ================================================================================ + // DEBOUNCE TIMER + // ================================================================================ + function executePendingRequest() { + Logger.d("TemplateProcessor", `executePendingRequest: hasWallpaper=${!!pendingWallpaperRequest}, hasPredefined=${!!pendingPredefinedRequest}`); + if (pendingWallpaperRequest) { + const req = pendingWallpaperRequest; + pendingWallpaperRequest = null; + executeWallpaperColors(req.wallpaperPath, req.mode); + } else if (pendingPredefinedRequest) { + const req = pendingPredefinedRequest; + pendingPredefinedRequest = null; + executePredefinedScheme(req.schemeData, req.mode, req.wallpaperPath); + } else { + Logger.d("TemplateProcessor", "executePendingRequest: no pending request"); + } + } + + Timer { + id: debounceTimer + interval: 150 + repeat: false + onTriggered: { + Logger.d("TemplateProcessor", `debounceTimer fired: processRunning=${generateProcess.running}`); + // Kill any running process before starting new one + if (generateProcess.running) { + Logger.d("TemplateProcessor", "debounceTimer: stopping running process"); + generateProcess.running = false; + // executePendingRequest will be called from onExited + } else { + executePendingRequest(); + } + } + } + + // ================================================================================ + // PROCESSES + // ================================================================================ + Process { + id: generateProcess + workingDirectory: Quickshell.shellDir + running: false + + onExited: function (exitCode, exitStatus) { + // Execute any pending request (handles both kill case and debounce timer interval case) + if (pendingWallpaperRequest || pendingPredefinedRequest) { + Logger.d("TemplateProcessor", "generateProcess onExited: has pending request, executing"); + executePendingRequest(); + } else if (exitCode === 0) { + // No pending request and successful completion - emit signal + root.colorsGenerated(); + } + } + + stderr: StdioCollector { + onStreamFinished: { + const text = this.text.trim(); + if (text && text.includes("Template error:")) { + const errorLines = text.split("\n").filter(l => l.includes("Template error:")); + const errors = errorLines.slice(0, 3).join("\n") + (errorLines.length > 3 ? `\n... (+${errorLines.length - 3} more)` : ""); + Logger.w("TemplateProcessor", errors); + ToastService.showWarning(I18n.tr("toast.theming-processor-failed.title"), errors); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/Theming/TemplateRegistry.qml b/fedora/.config/quickshell/noctalia-shell/Services/Theming/TemplateRegistry.qml new file mode 100644 index 0000000..d9d022b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/Theming/TemplateRegistry.qml @@ -0,0 +1,619 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons + +Singleton { + id: root + + Component.onCompleted: { + if (Settings.data.templates.enableUserTheming) + writeUserTemplatesToml(); + } + + readonly property string templateApplyScript: Quickshell.shellDir + '/Scripts/bash/template-apply.sh' + readonly property string gtkRefreshScript: Quickshell.shellDir + '/Scripts/python/src/theming/gtk-refresh.py' + readonly property string vscodeHelperScript: Quickshell.shellDir + '/Scripts/python/src/theming/vscode-helper.py' + + // Dynamically resolved VSCode extension theme paths (all matching noctalia extensions) + property var resolvedCodePaths: [] + property var resolvedCodiumPaths: [] + + // Terminal configurations (for wallpaper-based templates) + // Each terminal must define a postHook that sets up config includes and triggers reload + readonly property var terminals: [ + { + "id": "foot", + "name": "Foot", + "templatePath": "terminal/foot", + "predefinedTemplatePath": "terminal/foot-predefined", + "outputPath": "~/.config/foot/themes/noctalia", + "postHook": `${templateApplyScript} foot` + }, + { + "id": "ghostty", + "name": "Ghostty", + "templatePath": "terminal/ghostty", + "predefinedTemplatePath": "terminal/ghostty-predefined", + "outputPath": "~/.config/ghostty/themes/noctalia", + "postHook": `${templateApplyScript} ghostty` + }, + { + "id": "kitty", + "name": "Kitty", + "templatePath": "terminal/kitty.conf", + "predefinedTemplatePath": "terminal/kitty-predefined.conf", + "outputPath": "~/.config/kitty/themes/noctalia.conf", + "postHook": `${templateApplyScript} kitty` + }, + { + "id": "alacritty", + "name": "Alacritty", + "templatePath": "terminal/alacritty.toml", + "predefinedTemplatePath": "terminal/alacritty-predefined.toml", + "outputPath": "~/.config/alacritty/themes/noctalia.toml", + "postHook": `${templateApplyScript} alacritty` + }, + { + "id": "wezterm", + "name": "Wezterm", + "templatePath": "terminal/wezterm.toml", + "predefinedTemplatePath": "terminal/wezterm-predefined.toml", + "outputPath": "~/.config/wezterm/colors/Noctalia.toml", + "postHook": `${templateApplyScript} wezterm` + } + ] + + // Application configurations - consolidated from Theming + AppThemeService + readonly property var applications: [ + { + "id": "gtk", + "name": "GTK", + "category": "system", + "input": "gtk4.css", + "outputs": [ + { + "path": "~/.config/gtk-3.0/noctalia.css", + "input": "gtk3.css" + }, + { + "path": "~/.config/gtk-4.0/noctalia.css", + "input": "gtk4.css" + } + ], + "postProcess": mode => `python3 ${gtkRefreshScript} ${mode}` + }, + { + "id": "qt", + "name": "Qt", + "category": "system", + "input": "qtct.conf", + "outputs": [ + { + "path": "~/.config/qt5ct/colors/noctalia.conf" + }, + { + "path": "~/.config/qt6ct/colors/noctalia.conf" + } + ] + }, + { + "id": "kcolorscheme", + "name": "KColorScheme", + "category": "system", + "input": "kcolorscheme.colors", + "outputs": [ + { + "path": "~/.local/share/color-schemes/noctalia.colors" + } + ], + "postProcess": () => "if command -v plasma-apply-colorscheme >/dev/null 2>&1; then plasma-apply-colorscheme BreezeDark; sleep 0.5; plasma-apply-colorscheme noctalia; fi" + }, + { + "id": "fuzzel", + "name": "Fuzzel", + "category": "launcher", + "input": "fuzzel.conf", + "outputs": [ + { + "path": "~/.config/fuzzel/themes/noctalia" + } + ], + "postProcess": () => `${templateApplyScript} fuzzel` + }, + { + "id": "vicinae", + "name": "Vicinae", + "category": "launcher", + "input": "vicinae.toml", + "outputs": [ + { + "path": "~/.local/share/vicinae/themes/noctalia.toml" + } + ], + "postProcess": () => `cp --update=none ${Quickshell.shellDir}/Assets/noctalia.svg ~/.local/share/vicinae/themes/noctalia.svg && ${templateApplyScript} vicinae` + }, + { + "id": "walker", + "name": "Walker", + "category": "launcher", + "input": "walker.css", + "outputs": [ + { + "path": "~/.config/walker/themes/noctalia/style.css" + } + ], + "postProcess": () => `${templateApplyScript} walker`, + "strict": true // Use strict mode for palette generation (preserves custom surface/outline values) + }, + { + "id": "pywalfox", + "name": "Pywalfox", + "category": "browser", + "input": "pywalfox.json", + "outputs": [ + { + "path": "~/.cache/wal/colors.json" + } + ], + "postProcess": mode => `${templateApplyScript} pywalfox ${mode}` + } // CONSOLIDATED DISCORD CLIENTS + , + { + "id": "discord", + "name": "Discord", + "category": "misc", + "input": ["discord-midnight.css", "discord-material.css"], + "clients": [ + { + "name": "vesktop", + "path": "~/.config/vesktop" + }, + { + "name": "webcord", + "path": "~/.config/webcord" + }, + { + "name": "armcord", + "path": "~/.config/armcord" + }, + { + "name": "equibop", + "path": "~/.config/equibop" + }, + { + "name": "equicord", + "path": "~/.config/Equicord" + }, + { + "name": "lightcord", + "path": "~/.config/lightcord" + }, + { + "name": "dorion", + "path": "~/.config/dorion" + }, + { + "name": "vencord", + "path": "~/.config/Vencord" + }, + { + "name": "vencord-flatpak", + "path": "~/.var/app/com.discordapp.Discord/config/Vencord" + }, + { + "name": "betterdiscord", + "path": "~/.config/BetterDiscord" + } + ] + }, + { + "id": "code", + "name": "VSCode", + "category": "editor", + "input": "code.json", + "clients": [ + { + "name": "code", + "path": "~/.vscode/extensions/noctalia.noctaliatheme-0.0.5/themes/NoctaliaTheme-color-theme.json" + }, + { + "name": "codium", + "path": "~/.vscode-oss/extensions/noctalia.noctaliatheme-0.0.5-universal/themes/NoctaliaTheme-color-theme.json" + } + ] + }, + { + "id": "zed", + "name": "Zed", + "category": "editor", + "input": "zed.json", + "outputs": [ + { + "path": "~/.config/zed/themes/noctalia.json" + } + ], + "dualMode": true // Template contains both dark and light theme patterns + }, + { + "id": "helix", + "name": "Helix", + "category": "editor", + "input": "helix.toml", + "outputs": [ + { + "path": "~/.config/helix/themes/noctalia.toml" + } + ] + }, + { + "id": "spicetify", + "name": "Spicetify", + "category": "audio", + "input": "spicetify.ini", + "outputs": [ + { + "path": "~/.config/spicetify/Themes/Comfy/color.ini" + } + ], + "postProcess": () => `spicetify -q apply --no-restart` + }, + { + "id": "telegram", + "name": "Telegram", + "category": "misc", + "input": "telegram.tdesktop-theme", + "outputs": [ + { + "path": "~/.config/telegram-desktop/themes/noctalia.tdesktop-theme" + } + ] + }, + { + "id": "zenBrowser", + "name": "Zen Browser", + "category": "browser", + "input": "zen-browser/zen-userChrome.css", + "outputs": [ + { + "path": "~/.cache/noctalia/zen-browser/zen-userChrome.css" + }, + { + "path": "~/.cache/noctalia/zen-browser/zen-userContent.css", + "input": "zen-browser/zen-userContent.css" + } + ], + "postProcess": () + => "sh -c 'CSS_CHROME=\"$HOME/.cache/noctalia/zen-browser/zen-userChrome.css\"; CSS_CONTENT=\"$HOME/.cache/noctalia/zen-browser/zen-userContent.css\"; LINE_CHROME=\"@import \\\"$CSS_CHROME\\\";\"; LINE_CONTENT=\"@import \\\"$CSS_CONTENT\\\";\"; find \"$HOME/.config/zen\" \"$HOME/.zen\" -mindepth 2 -maxdepth 2 -type d -name chrome -print0 2>/dev/null | while IFS= read -r -d \"\" dir; do USER_CHROME=\"$dir/userChrome.css\"; USER_CONTENT=\"$dir/userContent.css\"; mkdir -p \"$dir\"; touch \"$USER_CHROME\" \"$USER_CONTENT\"; sed -i \"/zen-browser\\/zen-userChrome\\.css/d\" \"$USER_CHROME\"; sed -i \"/zen-browser\\/zen-userContent\\.css/d\" \"$USER_CONTENT\"; if ! grep -Fq \"$LINE_CHROME\" \"$USER_CHROME\"; then printf \"%s\\n\" \"$LINE_CHROME\" >> \"$USER_CHROME\"; fi; if ! grep -Fq \"$LINE_CONTENT\" \"$USER_CONTENT\"; then printf \"%s\\n\" \"$LINE_CONTENT\" >> \"$USER_CONTENT\"; fi; done'" + }, + { + "id": "cava", + "name": "Cava", + "category": "audio", + "input": "cava.ini", + "outputs": [ + { + "path": "~/.config/cava/themes/noctalia" + } + ], + "postProcess": () => `${templateApplyScript} cava` + }, + { + "id": "yazi", + "name": "Yazi", + "category": "misc", + "input": "yazi.toml", + "outputs": [ + { + "path": "~/.config/yazi/flavors/noctalia.yazi/flavor.toml" + } + ], + "postProcess": () => `${templateApplyScript} yazi` + }, + { + "id": "emacs", + "name": "Emacs", + "category": "editor", + "input": "emacs.el", + "postProcess": () => `emacsclient -e "(load-theme 'noctalia t)"` + }, + { + "id": "labwc", + "name": "Labwc", + "category": "compositor", + "input": "labwc.conf", + "outputs": [ + { + "path": "~/.config/labwc/themerc-override" + } + ], + "postProcess": () => `${templateApplyScript} labwc` + }, + { + "id": "niri", + "name": "Niri", + "category": "compositor", + "input": "niri.kdl", + "outputs": [ + { + "path": "~/.config/niri/noctalia.kdl" + } + ], + "postProcess": () => `${templateApplyScript} niri` + }, + { + "id": "sway", + "name": "Sway", + "category": "compositor", + "input": "sway", + "outputs": [ + { + "path": "~/.config/sway/noctalia" + } + ], + "postProcess": () => `${templateApplyScript} sway` + }, + { + "id": "scroll", + "name": "Scroll", + "category": "compositor", + "input": "scroll", + "outputs": [ + { + "path": "~/.config/scroll/noctalia" + } + ], + "postProcess": () => `${templateApplyScript} scroll` + }, + { + "id": "hyprland", + "name": "Hyprland", + "category": "compositor", + "input": "hyprland.conf", + "outputs": [ + { + "path": "~/.config/hypr/noctalia/noctalia-colors.conf" + } + ], + "postProcess": () => `${templateApplyScript} hyprland` + }, + { + "id": "hyprtoolkit", + "name": "Hyprtoolkit", + "category": "system", + "input": "hyprtoolkit.conf", + "outputs": [ + { + "path": "~/.config/hypr/hyprtoolkit.conf" + } + ] + }, + { + "id": "mango", + "name": "Mango", + "category": "compositor", + "input": "mango.conf", + "outputs": [ + { + "path": "~/.config/mango/noctalia.conf" + } + ], + "postProcess": () => `${templateApplyScript} mango` + }, + { + "id": "btop", + "name": "btop", + "category": "misc", + "input": "btop.theme", + "outputs": [ + { + "path": "~/.config/btop/themes/noctalia.theme" + } + ], + "postProcess": () => `${templateApplyScript} btop` + }, + { + "id": "zathura", + "name": "Zathura", + "category": "misc", + "input": "zathurarc", + "outputs": [ + { + "path": "~/.config/zathura/noctaliarc" + } + ], + "postProcess": () => `${templateApplyScript} zathura` + }, + { + "id": "steam", + "name": "Steam", + "category": "misc", + "input": "steam.css", + "outputs": [ + { + "path": "~/.steam/steam/steamui/skins/Material-Theme/css/main/colors/matugen.css" + } + ] + } + ] + + // Extract Discord clients for ProgramCheckerService compatibility + readonly property var discordClients: { + var clients = []; + var discordApp = applications.find(app => app.id === "discord"); + if (discordApp && discordApp.clients) { + discordApp.clients.forEach(client => { + clients.push({ + "name": client.name, + "configPath": client.path, + "themePath": `${client.path}/themes/noctalia.theme.css` + }); + }); + } + return clients; + } + + // Get resolved theme paths for a code client (returns array of all matching paths) + function resolvedCodeClientPaths(clientName) { + if (clientName === "code") + return resolvedCodePaths; + if (clientName === "codium") + return resolvedCodiumPaths; + return []; + } + + // Extract Code clients for ProgramCheckerService compatibility + readonly property var codeClients: { + var clients = []; + var codeApp = applications.find(app => app.id === "code"); + if (codeApp && codeApp.clients) { + codeApp.clients.forEach(client => { + // Extract base config directory from theme path + var themePath = client.path; + var baseConfigDir = ""; + if (client.name === "code") { + // For VSCode: ~/.vscode/extensions/... -> ~/.vscode + baseConfigDir = "~/.vscode"; + } else if (client.name === "codium") { + // For VSCodium: ~/.vscode-oss/extensions/... -> ~/.vscode-oss + baseConfigDir = "~/.vscode-oss"; + } + clients.push({ + "name": client.name, + "configPath": baseConfigDir, + "themePath": "" // resolved dynamically via resolvedCodeClientPaths() + }); + }); + } + return clients; + } + + // Resolve VSCode extension paths dynamically + Process { + id: codeResolverProcess + command: ["python3", vscodeHelperScript, "~/.vscode/extensions"] + running: true + property var paths: [] + stdout: SplitParser { + onRead: data => { + var line = data.trim(); + if (line) + codeResolverProcess.paths.push(line); + } + } + onExited: { + root.resolvedCodePaths = paths; + } + } + + Process { + id: codiumResolverProcess + command: ["python3", vscodeHelperScript, "~/.vscode-oss/extensions"] + running: true + property var paths: [] + stdout: SplitParser { + onRead: data => { + var line = data.trim(); + if (line) + codiumResolverProcess.paths.push(line); + } + } + onExited: { + root.resolvedCodiumPaths = paths; + } + } + // Build user templates TOML content + function buildUserTemplatesToml() { + var lines = []; + lines.push("[config]"); + lines.push(""); + lines.push("[templates]"); + lines.push(""); + lines.push("# User-defined templates"); + lines.push("# Add your custom templates below"); + lines.push("# Example:"); + lines.push("# [templates.myapp]"); + lines.push("# input_path = \"~/.config/noctalia/templates/myapp.css\""); + lines.push("# output_path = \"~/.config/myapp/theme.css\""); + lines.push("# post_hook = \"myapp --reload-theme\""); + lines.push(""); + lines.push("# Remove this section and add your own templates"); + lines.push("#[templates.placeholder]"); + lines.push("#input_path = \"" + Quickshell.shellDir + "/Assets/Templates/noctalia.json\""); + lines.push("#output_path = \"" + Settings.cacheDir + "placeholder.json\""); + lines.push(""); + + return lines.join("\n") + "\n"; + } + + // Write user templates TOML file (moved from Theming) + function writeUserTemplatesToml() { + var userConfigPath = Settings.configDir + "user-templates.toml"; + + // Check if file already exists + fileCheckProcess.command = ["test", "-s", userConfigPath]; + fileCheckProcess.running = true; + } + + function doWriteUserTemplatesToml() { + var userConfigPath = Settings.configDir + "user-templates.toml"; + var configContent = buildUserTemplatesToml(); + var userConfigPathEsc = userConfigPath.replace(/'/g, "'\\''"); + var configDirEsc = Settings.configDir.replace(/'/g, "'\\''"); + + // Combine mkdir and write in a single script to avoid race condition + var script = `mkdir -p '${configDirEsc}' && cat > '${userConfigPathEsc}' << 'EOF'\n`; + script += configContent; + script += "EOF\n"; + fileWriteProcess.command = ["sh", "-c", script]; + fileWriteProcess.running = true; + } + + // Extract Emacs clients for ProgramCheckerService compatibility + readonly property var emacsClients: [ + { + "name": "doom", + "path": "~/.config/doom" + }, + { + "name": "modern", + "path": "~/.config/emacs" + }, + { + "name": "traditional", + "path": "~/.emacs.d" + } + ] + + // Process for checking if user templates file exists and is non-empty + Process { + id: fileCheckProcess + running: false + + onExited: function (exitCode) { + if (exitCode === 0) { + // File exists and is non-empty, skip creation + Logger.d("TemplateRegistry", "User templates config already exists, skipping creation"); + } else { + // File doesn't exist or is empty, create it + doWriteUserTemplatesToml(); + } + } + } + + // Process for writing user templates file with error reporting + Process { + id: fileWriteProcess + running: false + + onExited: function (exitCode) { + if (exitCode === 0) { + Logger.d("TemplateRegistry", "User templates config written to:", Settings.configDir + "user-templates.toml"); + } else { + Logger.e("TemplateRegistry", "Failed to write user templates config (exit code:", exitCode + ")"); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/UI/BarService.qml b/fedora/.config/quickshell/noctalia-shell/Services/UI/BarService.qml new file mode 100644 index 0000000..c16cd81 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/UI/BarService.qml @@ -0,0 +1,668 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.Compositor +import qs.Services.UI + +Singleton { + id: root + + property bool isVisible: true + + // Computed visibility that factors in compositor overview state + readonly property bool effectivelyVisible: { + if (!isVisible) { + return false; + } + if (Settings.data.bar.hideOnOverview && CompositorService.overviewActive) { + return false; + } + return true; + } + + property var readyBars: ({}) + + // Revision counter - increment when widget list structure changes (add/remove/reorder) + // This triggers Bar.qml to re-sync its ListModels + property int widgetsRevision: 0 + + // Registry to store actual widget instances + // Key format: "screenName|section|widgetId|index" + property var widgetInstances: ({}) + + signal activeWidgetsChanged + signal barReadyChanged(string screenName) + signal barAutoHideStateChanged(string screenName, bool hidden) + signal barHoverStateChanged(string screenName, bool hovered) + + // Track if a popup menu is open from the bar (prevents auto-hide) + property bool popupOpen: false + + // Auto-hide state per screen: { screenName: { hovered: bool, hidden: bool } } + property var screenAutoHideState: ({}) + + // Get or create auto-hide state for a screen + function getOrCreateAutoHideState(screenName) { + if (!screenAutoHideState[screenName]) { + screenAutoHideState[screenName] = { + "hovered": false, + "hidden": Settings.getBarDisplayModeForScreen(screenName) === "auto_hide" + }; + } + return screenAutoHideState[screenName]; + } + + // Set hover state for a screen + function setScreenHovered(screenName, hovered) { + var state = getOrCreateAutoHideState(screenName); + if (state.hovered !== hovered) { + state.hovered = hovered; + screenAutoHideState = Object.assign({}, screenAutoHideState); + barHoverStateChanged(screenName, hovered); + } + } + + // Set hidden state for a screen + function setScreenHidden(screenName, hidden) { + var state = getOrCreateAutoHideState(screenName); + if (state.hidden !== hidden) { + state.hidden = hidden; + screenAutoHideState = Object.assign({}, screenAutoHideState); + barAutoHideStateChanged(screenName, hidden); + } + } + + // Check if bar is hidden on a screen + function isBarHidden(screenName) { + var state = screenAutoHideState[screenName]; + return state ? state.hidden : false; + } + + // Check if bar is hovered on a screen + function isBarHovered(screenName) { + var state = screenAutoHideState[screenName]; + return state ? state.hovered : false; + } + + // Toggle bar visibility. In auto-hide mode, toggles the per-screen hidden + // state without touching isVisible (so hover-to-show still works). + // For non-auto-hide screens, toggles the global isVisible flag. + function toggleVisibility() { + // Check if any auto-hide screen is currently visible + var anyAutoHideVisible = false; + var hasAutoHideScreens = false; + for (var screenName in screenAutoHideState) { + if (Settings.getBarDisplayModeForScreen(screenName) === "auto_hide") { + hasAutoHideScreens = true; + if (!screenAutoHideState[screenName].hidden) { + anyAutoHideVisible = true; + break; + } + } + } + + // Toggle auto-hide screens (per-screen hidden state only) + if (hasAutoHideScreens) { + for (var screenName in screenAutoHideState) { + if (Settings.getBarDisplayModeForScreen(screenName) === "auto_hide") { + setScreenHidden(screenName, anyAutoHideVisible); + } + } + } + + // Only toggle global visibility when no auto-hide screens exist, + // otherwise it would permanently disable hover-to-show + if (!hasAutoHideScreens) { + isVisible = !isVisible; + } + } + + // Show bar. In auto-hide mode, un-hides on screens with auto-hide enabled. + // The bar stays visible until the user hovers and moves away. + function show() { + // Show auto-hide screens + for (var screenName in screenAutoHideState) { + if (Settings.getBarDisplayModeForScreen(screenName) === "auto_hide") { + setScreenHidden(screenName, false); + } + } + // Set global visibility (affects non-auto-hide screens) + isVisible = true; + } + + // Hide bar. In auto-hide mode, sets per-screen hidden state without touching + // isVisible so hover-to-show still works. For non-auto-hide screens, sets + // global visibility to false. + function hide() { + var hasAutoHideScreens = false; + for (var screenName in screenAutoHideState) { + if (Settings.getBarDisplayModeForScreen(screenName) === "auto_hide") { + setScreenHidden(screenName, true); + hasAutoHideScreens = true; + } + } + // Only set global visibility off when no auto-hide screens exist, + // otherwise it would permanently disable hover-to-show + if (!hasAutoHideScreens) { + isVisible = false; + } + } + + // Temporarily show the bar, then auto-hide after the configured delay. + // Uses the same pattern as workspace switch: show, then emit unhover + // to start the hide timer. + function peek() { + for (var screenName in screenAutoHideState) { + if (Settings.getBarDisplayModeForScreen(screenName) === "auto_hide") { + setScreenHidden(screenName, false); + if (!isBarHovered(screenName)) { + barHoverStateChanged(screenName, false); + } + } + } + } + + Component.onCompleted: { + Logger.i("BarService", "Service started"); + } + + // Bump widgetsRevision when settings are reloaded from an external file change + // so Bar.qml re-syncs its widget ListModels with the updated widget configuration + Connections { + target: Settings + function onSettingsReloaded() { + Logger.d("BarService", "Settings reloaded externally, bumping widgetsRevision"); + root.widgetsRevision++; + } + } + + // update bar's hidden state when mode changes + Connections { + target: Settings.data.bar + function onDisplayModeChanged() { + Logger.d("BarService", "Display mode changed to:", Settings.data.bar.displayMode); + + // Only affect screens without displayMode overrides + for (let screenName in screenAutoHideState) { + if (!Settings.hasScreenOverride(screenName, "displayMode")) { + var displayMode = Settings.getBarDisplayModeForScreen(screenName); + if (displayMode === "auto_hide") { + setScreenHidden(screenName, true); + } else { + if (screenAutoHideState[screenName].hidden) { + setScreenHidden(screenName, false); + } + } + } + } + } + + function onScreenOverridesChanged() { + Logger.d("BarService", "Screen overrides changed, re-evaluating auto-hide states"); + + // Re-evaluate auto-hide state for all screens + for (let screenName in screenAutoHideState) { + var displayMode = Settings.getBarDisplayModeForScreen(screenName); + if (displayMode === "auto_hide") { + if (!screenAutoHideState[screenName].hidden) { + setScreenHidden(screenName, true); + } + } else { + if (screenAutoHideState[screenName].hidden) { + setScreenHidden(screenName, false); + } + } + } + } + } + + // Track last workspace ID to detect actual workspace changes + property var lastWorkspaceId: null + + // Debounce rapid workspace switches to reduce load/unload races (SIGSEGV in QV4) + property string _pendingWorkspaceScreen: "" + + Timer { + id: workspaceDebounceTimer + interval: 80 + repeat: false + onTriggered: { + var screen = root._pendingWorkspaceScreen; + root._pendingWorkspaceScreen = ""; + if (screen) { + setScreenHidden(screen, false); + if (!root.isBarHovered(screen)) { + barHoverStateChanged(screen, false); + } + } + } + } + + // Workspace switch handler - directly show bar on the focused workspace screen + Connections { + target: CompositorService + function onWorkspaceChanged() { + if (!Settings.data.bar.showOnWorkspaceSwitch) + return; + if (Settings.data.bar.displayMode !== "auto_hide") + return; + + var ws = CompositorService.getCurrentWorkspace(); + if (!ws || !ws.output) { + return; + } + + // Only trigger if workspace actually changed + var currentWsId = ws.id; + if (currentWsId === root.lastWorkspaceId) { + return; + } + root.lastWorkspaceId = currentWsId; + + var screenName = ws.output || ""; + Logger.d("BarService", "Workspace switched to:", currentWsId, "on screen:", screenName); + + // Debounce: rapid switches (e.g. external monitor ↔ laptop) cause overlapping + // bar load/unload; 80ms delay coalesces them and reduces QV4 incubation races + root._pendingWorkspaceScreen = screenName; + workspaceDebounceTimer.restart(); + } + } + + // Function for the Bar to call when it's ready + function registerBar(screenName) { + if (!readyBars[screenName]) { + readyBars[screenName] = true; + Logger.d("BarService", "Bar is ready on screen:", screenName); + barReadyChanged(screenName); + } + } + + // Function for the Dock to check if the bar is ready + function isBarReady(screenName) { + return readyBars[screenName] || false; + } + + // Register a widget instance + function registerWidget(screenName, section, widgetId, index, instance) { + const key = [screenName, section, widgetId, index].join("|"); + widgetInstances[key] = { + "key": key, + "screenName": screenName, + "section": section, + "widgetId": widgetId, + "index": index, + "instance": instance + }; + + Logger.d("BarService", "Registered widget:", key); + root.activeWidgetsChanged(); + } + + // Unregister a widget instance + function unregisterWidget(screenName, section, widgetId, index) { + const key = [screenName, section, widgetId, index].join("|"); + delete widgetInstances[key]; + Logger.d("BarService", "Unregistered widget:", key); + root.activeWidgetsChanged(); + } + + // Lookup a specific widget instance (returns the actual QML instance) + function lookupWidget(widgetId, screenName = null, section = null, index = null) { + // If looking for a specific instance + if (screenName && section !== null) { + for (var key in widgetInstances) { + var widget = widgetInstances[key]; + if (!widget) + continue; + if (widget.widgetId === widgetId && widget.screenName === screenName && widget.section === section) { + if (index === null) { + return widget.instance; + } else if (widget.index == index) { + return widget.instance; + } + } + } + } + + // Return first match if no specific screen/section specified + for (var key in widgetInstances) { + var widget = widgetInstances[key]; + if (!widget) + continue; + if (widget.widgetId === widgetId) { + if (!screenName || widget.screenName === screenName) { + if (section === null || widget.section === section) { + return widget.instance; + } + } + } + } + + return undefined; + } + + // Get all instances of a widget type + function getAllWidgetInstances(widgetId = null, screenName = null, section = null) { + var instances = []; + + for (var key in widgetInstances) { + var widget = widgetInstances[key]; + if (!widget) + continue; + + var matches = true; + if (widgetId && widget.widgetId !== widgetId) + matches = false; + if (screenName && widget.screenName !== screenName) + matches = false; + if (section !== null && widget.section !== section) + matches = false; + + if (matches) { + instances.push(widget.instance); + } + } + + return instances; + } + + // Get widget with full metadata + function getWidgetWithMetadata(widgetId, screenName = null, section = null) { + for (var key in widgetInstances) { + var widget = widgetInstances[key]; + if (!widget) + continue; + if (widget.widgetId === widgetId) { + if (!screenName || widget.screenName === screenName) { + if (section === null || widget.section === section) { + return widget; + } + } + } + } + return undefined; + } + + // Get all widgets in a specific section + function getWidgetsBySection(section, screenName = null) { + var widgetEntries = []; + + for (var key in widgetInstances) { + var widget = widgetInstances[key]; + if (!widget) + continue; + if (widget.section === section) { + if (!screenName || widget.screenName === screenName) { + widgetEntries.push(widget); + } + } + } + + // Sort by index to maintain order + widgetEntries.sort(function (a, b) { + return (a.index || 0) - (b.index || 0); + }); + + // Return just the instances + return widgetEntries.map(function (w) { + return w.instance; + }); + } + + // Get all registered widgets (for debugging) + function getAllRegisteredWidgets() { + var result = []; + for (var key in widgetInstances) { + var widget = widgetInstances[key]; + if (!widget) + continue; + result.push({ + "key": key, + "widgetId": widget.widgetId, + "section": widget.section, + "screenName": widget.screenName, + "index": widget.index + }); + } + return result; + } + + // Check if a widget type exists in a section + function hasWidget(widgetId, section = null, screenName = null) { + for (var key in widgetInstances) { + var widget = widgetInstances[key]; + if (!widget) + continue; + if (widget.widgetId === widgetId) { + if (section === null || widget.section === section) { + if (!screenName || widget.screenName === screenName) { + return true; + } + } + } + } + return false; + } + + // Unregister all widget instances for a plugin (used during hot reload) + // Note: We don't destroy instances here - the Loader manages that when the component is unregistered + function destroyPluginWidgetInstances(pluginId) { + var widgetId = "plugin:" + pluginId; + var keysToRemove = []; + + // Find all instances of this plugin's widget + for (var key in widgetInstances) { + var widget = widgetInstances[key]; + if (widget && widget.widgetId === widgetId) { + keysToRemove.push(key); + Logger.d("BarService", "Unregistering plugin widget instance:", key); + } + } + + // Remove from registry + for (var i = 0; i < keysToRemove.length; i++) { + delete widgetInstances[keysToRemove[i]]; + } + + if (keysToRemove.length > 0) { + Logger.i("BarService", "Unregistered", keysToRemove.length, "instance(s) of plugin widget:", widgetId); + root.activeWidgetsChanged(); + } + } + + // Get pill direction for a widget instance + function getPillDirection(widgetInstance) { + try { + if (widgetInstance.section === "left") { + return false; + } else if (widgetInstance.section === "right") { + return true; + } else { + // middle section + if (widgetInstance.sectionWidgetIndex < widgetInstance.sectionWidgetsCount / 2) { + return true; + } else { + return false; + } + } + } catch (e) { + Logger.e(e); + } + return true; + } + + function getTooltipDirection(screenName) { + const position = Settings.getBarPositionForScreen(screenName); + switch (position) { + case "right": + return "left"; + case "left": + return "right"; + case "bottom": + return "top"; + default: + return "bottom"; + } + } + + // Helper to close any existing dialogs in a popup menu window + function closeExistingDialogs(popupMenuWindow) { + if (!popupMenuWindow || !popupMenuWindow.dialogParent) + return; + + var dialogParent = popupMenuWindow.dialogParent; + for (var i = dialogParent.children.length - 1; i >= 0; i--) { + var child = dialogParent.children[i]; + if (child && typeof child.close === "function") { + child.close(); + } + } + popupMenuWindow.hasDialog = false; + } + + // Open widget settings dialog for a bar widget + // Parameters: + // screen: The screen to show the dialog on + // section: Section id ("left", "center", "right") + // index: Widget index in section + // widgetId: Widget type id (e.g., "Volume") + // widgetData: Current widget settings object + function openWidgetSettings(screen, section, index, widgetId, widgetData) { + // Get the popup menu window to use as parent (avoids clipping issues with bar height) + var popupMenuWindow = PanelService.getPopupMenuWindow(screen); + if (!popupMenuWindow) { + Logger.e("BarService", "No popup menu window found for screen"); + return; + } + + // Close any existing dialogs first to prevent stacking + closeExistingDialogs(popupMenuWindow); + + if (PanelService.openedPanel) { + PanelService.openedPanel.close(); + } + + var component = Qt.createComponent(Quickshell.shellDir + "/Modules/Panels/Settings/Bar/BarWidgetSettingsDialog.qml"); + + function instantiateAndOpen() { + // Use dialogParent (Item) instead of window directly for proper Popup anchoring + var dialog = component.createObject(popupMenuWindow.dialogParent, { + "widgetIndex": index, + "widgetData": widgetData, + "widgetId": widgetId, + "sectionId": section, + "screen": screen + }); + + if (dialog) { + dialog.updateWidgetSettings.connect((sec, idx, settings) => { + var screenName = screen?.name || ""; + if (Settings.hasScreenOverride(screenName, "widgets")) { + var overrideWidgets = Settings.getBarWidgetsForScreen(screenName); + if (overrideWidgets && overrideWidgets[sec] && idx < overrideWidgets[sec].length) { + overrideWidgets[sec][idx] = Object.assign({}, overrideWidgets[sec][idx], settings); + Settings.setScreenOverride(screenName, "widgets", overrideWidgets); + } + } else { + var widgets = Settings.data.bar.widgets[sec]; + if (widgets && idx < widgets.length) { + widgets[idx] = Object.assign({}, widgets[idx], settings); + Settings.data.bar.widgets[sec] = widgets; + Settings.saveImmediate(); + } + } + }); + // Enable keyboard focus for the popup menu window when dialog is open + popupMenuWindow.hasDialog = true; + // Close the popup menu window when dialog closes + dialog.closed.connect(() => { + popupMenuWindow.hasDialog = false; + popupMenuWindow.close(); + dialog.destroy(); + }); + // Show the popup menu window and open the dialog + popupMenuWindow.open(); + dialog.open(); + } else { + Logger.e("BarService", "Failed to create widget settings dialog"); + } + } + + if (component.status === Component.Ready) { + instantiateAndOpen(); + } else if (component.status === Component.Error) { + Logger.e("BarService", "Error loading widget settings dialog:", component.errorString()); + } else { + component.statusChanged.connect(function () { + if (component.status === Component.Ready) { + instantiateAndOpen(); + } else if (component.status === Component.Error) { + Logger.e("BarService", "Error loading widget settings dialog:", component.errorString()); + } + }); + } + } + + // Open plugin settings dialog + // Parameters: + // screen: The screen to show the dialog on + // pluginManifest: The plugin's manifest object (must have entryPoints.settings) + function openPluginSettings(screen, pluginManifest) { + if (!pluginManifest || !pluginManifest.entryPoints || !pluginManifest.entryPoints.settings) { + Logger.e("BarService", "Cannot open plugin settings: no settings entry point"); + return; + } + + // Get the popup menu window to use as parent + var popupMenuWindow = PanelService.getPopupMenuWindow(screen); + if (!popupMenuWindow) { + Logger.e("BarService", "No popup menu window found for screen"); + return; + } + + // Close any existing dialogs first to prevent stacking + closeExistingDialogs(popupMenuWindow); + + var component = Qt.createComponent(Quickshell.shellDir + "/Widgets/NPluginSettingsPopup.qml"); + + function instantiateAndOpen() { + var dialog = component.createObject(popupMenuWindow.dialogParent, { + "showToastOnSave": true, + "screen": screen + }); + + if (dialog) { + // Enable keyboard focus for the popup menu window when dialog is open + popupMenuWindow.hasDialog = true; + // Close the popup menu window when dialog closes + dialog.closed.connect(() => { + popupMenuWindow.hasDialog = false; + popupMenuWindow.close(); + dialog.destroy(); + }); + // Show the popup menu window and open the dialog + popupMenuWindow.open(); + dialog.openPluginSettings(pluginManifest); + } else { + Logger.e("BarService", "Failed to create plugin settings dialog"); + } + } + + if (component.status === Component.Ready) { + instantiateAndOpen(); + } else if (component.status === Component.Error) { + Logger.e("BarService", "Error loading plugin settings dialog:", component.errorString()); + } else { + component.statusChanged.connect(function () { + if (component.status === Component.Ready) { + instantiateAndOpen(); + } else if (component.status === Component.Error) { + Logger.e("BarService", "Error loading plugin settings dialog:", component.errorString()); + } + }); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/UI/BarWidgetRegistry.qml b/fedora/.config/quickshell/noctalia-shell/Services/UI/BarWidgetRegistry.qml new file mode 100644 index 0000000..5c951d4 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/UI/BarWidgetRegistry.qml @@ -0,0 +1,503 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Modules.Bar.Widgets + +Singleton { + id: root + + signal pluginWidgetRegistryUpdated + + // Widget registry object mapping widget names to components + property var widgets: ({ + "ActiveWindow": activeWindowComponent, + "AudioVisualizer": audioVisualizerComponent, + "Battery": batteryComponent, + "Bluetooth": bluetoothComponent, + "Brightness": brightnessComponent, + "Clock": clockComponent, + "ControlCenter": controlCenterComponent, + "CustomButton": customButtonComponent, + "DarkMode": darkModeComponent, + "KeepAwake": keepAwakeComponent, + "KeyboardLayout": keyboardLayoutComponent, + "LockKeys": lockKeysComponent, + "Launcher": launcherComponent, + "MediaMini": mediaMiniComponent, + "Microphone": microphoneComponent, + "Network": networkComponent, + "NightLight": nightLightComponent, + "NoctaliaPerformance": noctaliaPerformanceComponent, + "NotificationHistory": notificationHistoryComponent, + "PowerProfile": powerProfileComponent, + "SessionMenu": sessionMenuComponent, + "Settings": settingsComponent, + "Spacer": spacerComponent, + "SystemMonitor": systemMonitorComponent, + "Taskbar": taskbarComponent, + "Tray": trayComponent, + "Volume": volumeComponent, + "VPN": vpnComponent, + "WallpaperSelector": wallpaperSelectorComponent, + "Workspace": workspaceComponent + }) + + property var widgetSettingsMap: ({ + "ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml", + "AudioVisualizer": "WidgetSettings/AudioVisualizerSettings.qml", + "Battery": "WidgetSettings/BatterySettings.qml", + "Bluetooth": "WidgetSettings/BluetoothSettings.qml", + "Brightness": "WidgetSettings/BrightnessSettings.qml", + "Clock": "WidgetSettings/ClockSettings.qml", + "ControlCenter": "WidgetSettings/ControlCenterSettings.qml", + "CustomButton": "WidgetSettings/CustomButtonSettings.qml", + "DarkMode": "WidgetSettings/DarkModeSettings.qml", + "KeepAwake": "WidgetSettings/KeepAwakeSettings.qml", + "KeyboardLayout": "WidgetSettings/KeyboardLayoutSettings.qml", + "Launcher": "WidgetSettings/LauncherSettings.qml", + "LockKeys": "WidgetSettings/LockKeysSettings.qml", + "MediaMini": "WidgetSettings/MediaMiniSettings.qml", + "Microphone": "WidgetSettings/MicrophoneSettings.qml", + "Network": "WidgetSettings/NetworkSettings.qml", + "NightLight": "WidgetSettings/NightLightSettings.qml", + "NoctaliaPerformance": "WidgetSettings/NoctaliaPerformanceSettings.qml", + "NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml", + "PowerProfile": "WidgetSettings/PowerProfileSettings.qml", + "SessionMenu": "WidgetSettings/SessionMenuSettings.qml", + "Settings": "WidgetSettings/SettingsSettings.qml", + "Spacer": "WidgetSettings/SpacerSettings.qml", + "SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml", + "Taskbar": "WidgetSettings/TaskbarSettings.qml", + "Tray": "WidgetSettings/TraySettings.qml", + "Volume": "WidgetSettings/VolumeSettings.qml", + "VPN": "WidgetSettings/VPNSettings.qml", + "WallpaperSelector": "WidgetSettings/WallpaperSelectorSettings.qml", + "Workspace": "WidgetSettings/WorkspaceSettings.qml" + }) + + property var widgetMetadata: ({ + "ActiveWindow": { + "showText": true, + "showIcon": true, + "hideMode": "hidden", + "scrollingMode": "hover", + "maxWidth": 145, + "useFixedWidth": false, + "colorizeIcons": false, + "textColor": "none" + }, + "AudioVisualizer": { + "width": 200, + "colorName": "primary", + "hideWhenIdle": false + }, + "Battery": { + "displayMode": "graphic-clean", + "deviceNativePath": "__default__", + "showPowerProfiles": false, + "showNoctaliaPerformance": false, + "hideIfNotDetected": true, + "hideIfIdle": false + }, + "Bluetooth": { + "displayMode": "onhover", + "iconColor": "none", + "textColor": "none" + }, + "Brightness": { + "displayMode": "onhover", + "iconColor": "none", + "textColor": "none", + "applyToAllMonitors": false + }, + "Clock": { + "clockColor": "none", + "useCustomFont": false, + "customFont": "", + "formatHorizontal": "HH:mm ddd, MMM dd", + "formatVertical": "HH mm - dd MM", + "tooltipFormat": "HH:mm ddd, MMM dd" + }, + "ControlCenter": { + "useDistroLogo": false, + "icon": "noctalia", + "customIconPath": "", + "colorizeDistroLogo": false, + "colorizeSystemIcon": "none", + "enableColorization": false + }, + "CustomButton": { + "icon": "heart", + "showIcon": true, + "showExecTooltip": true, + "showTextTooltip": true, + "generalTooltipText": "", + "hideMode": "alwaysExpanded", + "leftClickExec": "", + "leftClickUpdateText": false, + "rightClickExec": "", + "rightClickUpdateText": false, + "middleClickExec": "", + "middleClickUpdateText": false, + "textCommand": "", + "textStream": false, + "textIntervalMs": 3000, + "textCollapse": "", + "parseJson": false, + "wheelExec": "", + "wheelUpExec": "", + "wheelDownExec": "", + "wheelMode": "unified", + "wheelUpdateText": false, + "wheelUpUpdateText": false, + "wheelDownUpdateText": false, + "maxTextLength": { + "horizontal": 10, + "vertical": 10 + }, + "enableColorization": false, + "colorizeSystemIcon": "none", + "ipcIdentifier": "" + }, + "DarkMode": { + "iconColor": "none" + }, + "KeepAwake": { + "iconColor": "none", + "textColor": "none" + }, + "KeyboardLayout": { + "displayMode": "onhover", + "showIcon": true, + "iconColor": "none", + "textColor": "none" + }, + "LockKeys": { + "showCapsLock": true, + "showNumLock": true, + "showScrollLock": true, + "capsLockIcon": "letter-c", + "numLockIcon": "letter-n", + "scrollLockIcon": "letter-s", + "hideWhenOff": false + }, + "Launcher": { + "useDistroLogo": false, + "icon": "rocket", + "customIconPath": "", + "colorizeSystemIcon": "none", + "enableColorization": false, + "iconColor": "none" + }, + "MediaMini": { + "hideMode": "hidden", + "scrollingMode": "hover", + "maxWidth": 145, + "useFixedWidth": false, + "hideWhenIdle": false, + "showAlbumArt": true, + "showArtistFirst": true, + "showVisualizer": false, + "showProgressRing": true, + "visualizerType": "linear", + "textColor": "none", + "compactMode": false, + "panelShowAlbumArt": true + }, + "Microphone": { + "displayMode": "onhover", + "middleClickCommand": "pwvucontrol || pavucontrol", + "iconColor": "none", + "textColor": "none" + }, + "NotificationHistory": { + "showUnreadBadge": true, + "hideWhenZero": false, + "hideWhenZeroUnread": false, + "unreadBadgeColor": "primary", + "iconColor": "none" + }, + "SessionMenu": { + "iconColor": "error" + }, + "Settings": { + "iconColor": "none" + }, + "Spacer": { + "width": 20 + }, + "SystemMonitor": { + "compactMode": true, + "iconColor": "none", + "textColor": "none", + "useMonospaceFont": true, + "usePadding": false, + "showCpuUsage": true, + "showCpuCores": false, + "showCpuFreq": false, + "showCpuTemp": true, + "showGpuTemp": false, + "showLoadAverage": false, + "showMemoryUsage": true, + "showMemoryAsPercent": false, + "showSwapUsage": false, + "showNetworkStats": false, + "showDiskUsage": false, + "showDiskUsageAsPercent": false, + "showDiskAvailable": false, + "diskPath": "/" + }, + "Taskbar": { + "onlySameOutput": true, + "onlyActiveWorkspaces": true, + "hideMode": "hidden", + "colorizeIcons": false, + "showTitle": false, + "titleWidth": 120, + "showPinnedApps": true, + "smartWidth": true, + "maxTaskbarWidth": 40, + "iconScale": 0.8 + }, + "Tray": { + "blacklist": [], + "colorizeIcons": false, + "chevronColor": "none", + "pinned": [], + "drawerEnabled": true, + "hidePassive": false + }, + "VPN": { + "displayMode": "onhover", + "iconColor": "none", + "textColor": "none" + }, + "Network": { + "displayMode": "onhover", + "iconColor": "none", + "textColor": "none" + }, + "NightLight": { + "iconColor": "none" + }, + "NoctaliaPerformance": { + "iconColor": "none" + }, + "PowerProfile": { + "iconColor": "none" + }, + "Workspace": { + "labelMode": "index", + "followFocusedScreen": false, + "hideUnoccupied": false, + "characterCount": 2, + "showApplications": false, + "showApplicationsHover": false, + "showLabelsOnlyWhenOccupied": true, + "colorizeIcons": false, + "unfocusedIconsOpacity": 1.0, + "groupedBorderOpacity": 1.0, + "enableScrollWheel": true, + "iconScale": 0.8, + "focusedColor": "primary", + "occupiedColor": "secondary", + "emptyColor": "secondary", + "showBadge": true, + "pillSize": 0.6, + "fontWeight": "bold" + }, + "Volume": { + "displayMode": "onhover", + "middleClickCommand": "pwvucontrol || pavucontrol", + "iconColor": "none", + "textColor": "none" + }, + "WallpaperSelector": { + "iconColor": "none" + } + }) + + // Component definitions - these are loaded once at startup + property Component activeWindowComponent: Component { + ActiveWindow {} + } + property Component audioVisualizerComponent: Component { + AudioVisualizer {} + } + property Component batteryComponent: Component { + Battery {} + } + property Component bluetoothComponent: Component { + Bluetooth {} + } + property Component brightnessComponent: Component { + Brightness {} + } + property Component clockComponent: Component { + Clock {} + } + property Component customButtonComponent: Component { + CustomButton {} + } + property Component darkModeComponent: Component { + DarkMode {} + } + property Component keyboardLayoutComponent: Component { + KeyboardLayout {} + } + property Component keepAwakeComponent: Component { + KeepAwake {} + } + property Component lockKeysComponent: Component { + LockKeys {} + } + property Component launcherComponent: Component { + Launcher {} + } + property Component mediaMiniComponent: Component { + MediaMini {} + } + property Component microphoneComponent: Component { + Microphone {} + } + property Component nightLightComponent: Component { + NightLight {} + } + property Component noctaliaPerformanceComponent: Component { + NoctaliaPerformance {} + } + property Component notificationHistoryComponent: Component { + NotificationHistory {} + } + property Component powerProfileComponent: Component { + PowerProfile {} + } + property Component sessionMenuComponent: Component { + SessionMenu {} + } + property Component settingsComponent: Component { + Settings {} + } + property Component controlCenterComponent: Component { + ControlCenter {} + } + property Component spacerComponent: Component { + Spacer {} + } + property Component systemMonitorComponent: Component { + SystemMonitor {} + } + property Component trayComponent: Component { + Tray {} + } + property Component volumeComponent: Component { + Volume {} + } + property Component vpnComponent: Component { + VPN {} + } + property Component networkComponent: Component { + Network {} + } + property Component wallpaperSelectorComponent: Component { + WallpaperSelector {} + } + property Component workspaceComponent: Component { + Workspace {} + } + property Component taskbarComponent: Component { + Taskbar {} + } + function init() { + Logger.i("BarWidgetRegistry", "Service started"); + } + + // ------------------------------ + // Helper function to get widget component by name + function getWidget(id) { + return widgets[id] || null; + } + + // Helper function to check if widget exists + function hasWidget(id) { + return id in widgets; + } + + // Get list of available widget id + function getAvailableWidgets() { + return Object.keys(widgets); + } + + // Helper function to check if widget has user settings + function widgetHasUserSettings(id) { + return widgetMetadata[id] !== undefined; + } + + // ------------------------------ + // Plugin widget registration + + // Track plugin widgets separately + property var pluginWidgets: ({}) + property var pluginWidgetMetadata: ({}) + + // Register a plugin widget + function registerPluginWidget(pluginId, component, metadata) { + if (!pluginId || !component) { + Logger.e("BarWidgetRegistry", "Cannot register plugin widget: invalid parameters"); + return false; + } + + // Add plugin: prefix to avoid conflicts with core widgets + var widgetId = "plugin:" + pluginId; + + pluginWidgets[widgetId] = component; + pluginWidgetMetadata[widgetId] = metadata || {}; + + // Also add to main widgets object for unified access + widgets[widgetId] = component; + widgetMetadata[widgetId] = metadata || {}; + + Logger.i("BarWidgetRegistry", "Registered plugin widget:", widgetId); + root.pluginWidgetRegistryUpdated(); + return true; + } + + // Unregister a plugin widget + function unregisterPluginWidget(pluginId) { + var widgetId = "plugin:" + pluginId; + + if (!pluginWidgets[widgetId]) { + Logger.w("BarWidgetRegistry", "Plugin widget not registered:", widgetId); + return false; + } + + delete pluginWidgets[widgetId]; + delete pluginWidgetMetadata[widgetId]; + delete widgets[widgetId]; + delete widgetMetadata[widgetId]; + + Logger.i("BarWidgetRegistry", "Unregistered plugin widget:", widgetId); + root.pluginWidgetRegistryUpdated(); + return true; + } + + // Check if a widget is a plugin widget + function isPluginWidget(id) { + return id.startsWith("plugin:"); + } + + property var cpuIntensiveWidgets: ["AudioVisualizer"] + + function isCpuIntensive(id) { + if (pluginWidgetMetadata[id]?.cpuIntensive) + return true; + return cpuIntensiveWidgets.indexOf(id) >= 0; + } + + // Get list of plugin widget IDs + function getPluginWidgets() { + return Object.keys(pluginWidgets); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/UI/ControlCenterWidgetRegistry.qml b/fedora/.config/quickshell/noctalia-shell/Services/UI/ControlCenterWidgetRegistry.qml new file mode 100644 index 0000000..52bde99 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/UI/ControlCenterWidgetRegistry.qml @@ -0,0 +1,163 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Modules.Panels.ControlCenter.Widgets + +Singleton { + id: root + + // Widget registry object mapping widget names to components + property var widgets: ({ + "AirplaneMode": airplaneModeComponent, + "Bluetooth": bluetoothComponent, + "CustomButton": customButtonComponent, + "DarkMode": darkModeComponent, + "KeepAwake": keepAwakeComponent, + "NightLight": nightLightComponent, + "Notifications": notificationsComponent, + "PowerProfile": powerProfileComponent, + "WiFi": networkComponent, + "Network": networkComponent, + "NoctaliaPerformance": noctaliaPerformanceComponent, + "WallpaperSelector": wallpaperSelectorComponent + }) + + property var widgetMetadata: ({ + "CustomButton": { + "icon": "heart", + "onClicked": "", + "onRightClicked": "", + "onMiddleClicked": "", + "stateChecksJson": "[]", + "generalTooltipText": "", + "enableOnStateLogic": false, + "showExecTooltip": true + } + }) + + property var cpuIntensiveWidgets: ["SystemStat"] + + // Component definitions - these are loaded once at startup + property Component airplaneModeComponent: Component { + AirplaneMode {} + } + property Component bluetoothComponent: Component { + Bluetooth {} + } + property Component customButtonComponent: Component { + CustomButton {} + } + property Component darkModeComponent: Component { + DarkMode {} + } + property Component keepAwakeComponent: Component { + KeepAwake {} + } + property Component nightLightComponent: Component { + NightLight {} + } + property Component notificationsComponent: Component { + Notifications {} + } + property Component powerProfileComponent: Component { + PowerProfile {} + } + property Component networkComponent: Component { + Network {} + } + property Component noctaliaPerformanceComponent: Component { + NoctaliaPerformance {} + } + property Component wallpaperSelectorComponent: Component { + WallpaperSelector {} + } + + function init() { + Logger.i("ControlCenterWidgetRegistry", "Service started"); + } + + // ------------------------------ + // Helper function to get widget component by name + function getWidget(id) { + return widgets[id] || null; + } + + // Helper function to check if widget exists + function hasWidget(id) { + return id in widgets; + } + + // Get list of available widget id + function getAvailableWidgets() { + return Object.keys(widgets); + } + + // Helper function to check if widget has user settings + function widgetHasUserSettings(id) { + return widgetMetadata[id] !== undefined; + } + + // ------------------------------ + // Plugin widget registration + + // Track plugin widgets separately + property var pluginWidgets: ({}) + property var pluginWidgetMetadata: ({}) + + // Register a plugin widget + function registerPluginWidget(pluginId, component, metadata) { + if (!pluginId || !component) { + Logger.e("ControlCenterWidgetRegistry", "Cannot register plugin widget: invalid parameters"); + return false; + } + + // Add plugin: prefix to avoid conflicts with core widgets + var widgetId = "plugin:" + pluginId; + + pluginWidgets[widgetId] = component; + pluginWidgetMetadata[widgetId] = metadata || {}; + + // Also add to main widgets object for unified access + widgets[widgetId] = component; + widgetMetadata[widgetId] = metadata || {}; + + Logger.i("ControlCenterWidgetRegistry", "Registered plugin widget:", widgetId); + return true; + } + + // Unregister a plugin widget + function unregisterPluginWidget(pluginId) { + var widgetId = "plugin:" + pluginId; + + if (!pluginWidgets[widgetId]) { + Logger.w("ControlCenterWidgetRegistry", "Plugin widget not registered:", widgetId); + return false; + } + + delete pluginWidgets[widgetId]; + delete pluginWidgetMetadata[widgetId]; + delete widgets[widgetId]; + delete widgetMetadata[widgetId]; + + Logger.i("ControlCenterWidgetRegistry", "Unregistered plugin widget:", widgetId); + return true; + } + + // Check if a widget is a plugin widget + function isPluginWidget(id) { + return id.startsWith("plugin:"); + } + + // Get list of plugin widget IDs + function getPluginWidgets() { + return Object.keys(pluginWidgets); + } + + function isCpuIntensive(id) { + if (pluginWidgetMetadata[id]?.cpuIntensive) + return true; + return cpuIntensiveWidgets.indexOf(id) >= 0; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/UI/DesktopWidgetRegistry.qml b/fedora/.config/quickshell/noctalia-shell/Services/UI/DesktopWidgetRegistry.qml new file mode 100644 index 0000000..f40dcb5 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/UI/DesktopWidgetRegistry.qml @@ -0,0 +1,346 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Modules.DesktopWidgets.Widgets +import qs.Services.Noctalia + +Singleton { + id: root + + // Transient state - not persisted, resets on shell restart + property bool editMode: false + + // Signal emitted when plugin widgets are registered/unregistered + signal pluginWidgetRegistryUpdated + + // Component definitions + property Component clockComponent: Component { + DesktopClock {} + } + property Component mediaPlayerComponent: Component { + DesktopMediaPlayer {} + } + property Component weatherComponent: Component { + DesktopWeather {} + } + property Component systemStatComponent: Component { + DesktopSystemStat {} + } + property Component audioVisualizerComponent: Component { + DesktopAudioVisualizer {} + } + + // Widget registry object mapping widget names to components + // Created in Component.onCompleted to ensure Components are ready + property var widgets: ({}) + + // Explicit URLs for core widgets. Inline Component.url returns the parent + // file's URL, so we need these for Loader.setSource() with initial properties. + // Plugin widgets use their Component.url directly (loaded from actual files). + readonly property string _widgetsDir: Quickshell.shellDir + "/Modules/DesktopWidgets/Widgets/" + property var widgetUrls: ({}) + + Component.onCompleted: { + // Initialize widgets object after Components are ready + var widgetsObj = {}; + widgetsObj["Clock"] = clockComponent; + widgetsObj["MediaPlayer"] = mediaPlayerComponent; + widgetsObj["Weather"] = weatherComponent; + widgetsObj["SystemStat"] = systemStatComponent; + widgetsObj["AudioVisualizer"] = audioVisualizerComponent; + widgets = widgetsObj; + + var urlsObj = {}; + urlsObj["Clock"] = _widgetsDir + "DesktopClock.qml"; + urlsObj["MediaPlayer"] = _widgetsDir + "DesktopMediaPlayer.qml"; + urlsObj["Weather"] = _widgetsDir + "DesktopWeather.qml"; + urlsObj["SystemStat"] = _widgetsDir + "DesktopSystemStat.qml"; + urlsObj["AudioVisualizer"] = _widgetsDir + "DesktopAudioVisualizer.qml"; + widgetUrls = urlsObj; + + Logger.i("DesktopWidgetRegistry", "Service started"); + } + + property var widgetSettingsMap: ({ + "Clock": "WidgetSettings/ClockSettings.qml", + "MediaPlayer": "WidgetSettings/MediaPlayerSettings.qml", + "Weather": "WidgetSettings/WeatherSettings.qml", + "SystemStat": "WidgetSettings/SystemStatSettings.qml", + "AudioVisualizer": "WidgetSettings/AudioVisualizerSettings.qml" + }) + + property var widgetMetadata: ({ + "Clock": { + "showBackground": true, + "roundedCorners": true, + "clockStyle": "digital", + "clockColor": "none", + "useCustomFont": false, + "customFont": "", + "format": "HH:mm\\nd MMMM yyyy" + }, + "MediaPlayer": { + "showBackground": true, + "roundedCorners": true, + "visualizerType": "linear", + "hideMode": "visible", + "showButtons": true, + "showAlbumArt": true, + "showVisualizer": true + }, + "Weather": { + "showBackground": true, + "roundedCorners": true + }, + "SystemStat": { + "showBackground": true, + "roundedCorners": true, + "statType": "CPU", + "diskPath": "/", + "layout": "bottom" + }, + "AudioVisualizer": { + "showBackground": true, + "roundedCorners": true, + "width": 320, + "height": 72, + "visualizerType": "linear", + "hideWhenIdle": false, + "colorName": "primary" + } + }) + + property var cpuIntensiveWidgets: ["SystemStat", "AudioVisualizer"] + + // Plugin widget storage (mirroring BarWidgetRegistry pattern) + property var pluginWidgets: ({}) + property var pluginWidgetMetadata: ({}) + + function init() { + Logger.i("DesktopWidgetRegistry", "Service started"); + } + + // Helper function to get widget component by name + function getWidget(id) { + return widgets[id] || null; + } + + // Helper function to check if widget exists + function hasWidget(id) { + return id in widgets; + } + + // Get list of available widget ids + function getAvailableWidgets() { + var keys = Object.keys(widgets); + Logger.d("DesktopWidgetRegistry", "getAvailableWidgets() called, returning:", keys); + return keys; + } + + // Helper function to check if widget has user settings + function widgetHasUserSettings(id) { + return widgetMetadata[id] !== undefined; + } + + function isCpuIntensive(id) { + if (pluginWidgetMetadata[id]?.cpuIntensive) + return true; + return cpuIntensiveWidgets.indexOf(id) >= 0; + } + + // Check if a widget is a plugin widget + function isPluginWidget(id) { + return id.startsWith("plugin:"); + } + + // Get list of plugin widget IDs + function getPluginWidgets() { + return Object.keys(pluginWidgets); + } + + // Get display name for a widget ID + function getWidgetDisplayName(widgetId) { + if (widgetId.startsWith("plugin:")) { + var pluginId = widgetId.replace("plugin:", ""); + var manifest = PluginRegistry.getPluginManifest(pluginId); + return manifest ? manifest.name : pluginId; + } + // Core widgets - return as-is (Clock, MediaPlayer, Weather, SystemStat) + return widgetId; + } + + // Register a plugin desktop widget + function registerPluginWidget(pluginId, component, metadata) { + if (!pluginId || !component) { + Logger.e("DesktopWidgetRegistry", "Cannot register plugin widget: invalid parameters"); + return false; + } + + var widgetId = "plugin:" + pluginId; + + // Create new objects to trigger QML property change detection + var newPluginWidgets = Object.assign({}, pluginWidgets); + newPluginWidgets[widgetId] = component; + pluginWidgets = newPluginWidgets; + + var newPluginMetadata = Object.assign({}, pluginWidgetMetadata); + newPluginMetadata[widgetId] = metadata || {}; + pluginWidgetMetadata = newPluginMetadata; + + // Also add to main widgets object for unified access - reassign to trigger change + var newWidgets = Object.assign({}, widgets); + newWidgets[widgetId] = component; + widgets = newWidgets; + + var newMetadata = Object.assign({}, widgetMetadata); + newMetadata[widgetId] = Object.assign({}, { + "showBackground": true + }, metadata || {}); + widgetMetadata = newMetadata; + + Logger.i("DesktopWidgetRegistry", "Registered plugin widget:", widgetId); + root.pluginWidgetRegistryUpdated(); + return true; + } + + // Unregister a plugin desktop widget + function unregisterPluginWidget(pluginId) { + var widgetId = "plugin:" + pluginId; + + if (!pluginWidgets[widgetId]) { + Logger.w("DesktopWidgetRegistry", "Plugin widget not registered:", widgetId); + return false; + } + + // Create new objects without the widget to trigger QML property change detection + var newPluginWidgets = Object.assign({}, pluginWidgets); + delete newPluginWidgets[widgetId]; + pluginWidgets = newPluginWidgets; + + var newPluginMetadata = Object.assign({}, pluginWidgetMetadata); + delete newPluginMetadata[widgetId]; + pluginWidgetMetadata = newPluginMetadata; + + var newWidgets = Object.assign({}, widgets); + delete newWidgets[widgetId]; + widgets = newWidgets; + + var newMetadata = Object.assign({}, widgetMetadata); + delete newMetadata[widgetId]; + widgetMetadata = newMetadata; + + Logger.i("DesktopWidgetRegistry", "Unregistered plugin widget:", widgetId); + root.pluginWidgetRegistryUpdated(); + return true; + } + + function updateWidgetData(monitorName, widgetIndex, properties) { + if (widgetIndex < 0 || !monitorName) { + return; + } + + var monitorWidgets = Settings.data.desktopWidgets.monitorWidgets || []; + var newMonitorWidgets = monitorWidgets.slice(); + + for (var i = 0; i < newMonitorWidgets.length; i++) { + if (newMonitorWidgets[i].name === monitorName) { + var widgets = (newMonitorWidgets[i].widgets || []).slice(); + if (widgetIndex < widgets.length) { + widgets[widgetIndex] = Object.assign({}, widgets[widgetIndex], properties); + newMonitorWidgets[i] = Object.assign({}, newMonitorWidgets[i], { + "widgets": widgets + }); + Settings.data.desktopWidgets.monitorWidgets = newMonitorWidgets; + } + break; + } + } + } + + property var currentSettingsDialog: null + + function openWidgetSettings(screen, widgetIndex, widgetId, widgetData) { + if (!widgetId || !screen) { + return; + } + + if (root.currentSettingsDialog) { + root.currentSettingsDialog.close(); + root.currentSettingsDialog.destroy(); + root.currentSettingsDialog = null; + } + + var hasSettings = false; + if (root.isPluginWidget(widgetId)) { + var pluginId = widgetId.replace("plugin:", ""); + var manifest = PluginRegistry.getPluginManifest(pluginId); + if (manifest && manifest.entryPoints && (manifest.entryPoints.desktopWidgetSettings || manifest.entryPoints.settings)) { + hasSettings = true; + } + } else { + hasSettings = root.widgetSettingsMap[widgetId] !== undefined; + } + + if (!hasSettings) { + Logger.w("DesktopWidgetRegistry", "Widget does not have settings:", widgetId); + return; + } + + var popupMenuWindow = PanelService.getPopupMenuWindow(screen); + if (!popupMenuWindow) { + Logger.e("DesktopWidgetRegistry", "No popup menu window found for screen"); + return; + } + + if (popupMenuWindow.hideDynamicMenu) { + popupMenuWindow.hideDynamicMenu(); + } + + var component = Qt.createComponent(Quickshell.shellDir + "/Modules/Panels/Settings/DesktopWidgets/DesktopWidgetSettingsDialog.qml"); + + function instantiateAndOpen() { + var dialog = component.createObject(popupMenuWindow.dialogParent, { + "widgetIndex": widgetIndex, + "widgetData": widgetData, + "widgetId": widgetId, + "sectionId": screen.name, + "screen": screen + }); + + if (dialog) { + root.currentSettingsDialog = dialog; + dialog.updateWidgetSettings.connect((sec, idx, settings) => { + root.updateWidgetData(sec, idx, settings); + }); + popupMenuWindow.hasDialog = true; + dialog.closed.connect(() => { + popupMenuWindow.hasDialog = false; + popupMenuWindow.close(); + if (root.currentSettingsDialog === dialog) { + root.currentSettingsDialog = null; + } + dialog.destroy(); + }); + dialog.open(); + } else { + Logger.e("DesktopWidgetRegistry", "Failed to create widget settings dialog"); + } + } + + if (component.status === Component.Ready) { + instantiateAndOpen(); + } else if (component.status === Component.Error) { + Logger.e("DesktopWidgetRegistry", "Error loading settings dialog component:", component.errorString()); + } else { + component.statusChanged.connect(() => { + if (component.status === Component.Ready) { + instantiateAndOpen(); + } else if (component.status === Component.Error) { + Logger.e("DesktopWidgetRegistry", "Error loading settings dialog component:", component.errorString()); + } + }); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/UI/ImageCacheService.qml b/fedora/.config/quickshell/noctalia-shell/Services/UI/ImageCacheService.qml new file mode 100644 index 0000000..47d6976 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/UI/ImageCacheService.qml @@ -0,0 +1,784 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import "../../Helpers/sha256.js" as Checksum +import qs.Commons + +Singleton { + id: root + + // ------------------------------------------------- + // Public Properties + // ------------------------------------------------- + property bool imageMagickAvailable: false + property bool initialized: false + + // Cache directories + readonly property string baseDir: Settings.cacheDir + "images/" + readonly property string wpThumbDir: baseDir + "wallpapers/thumbnails/" + readonly property string wpLargeDir: baseDir + "wallpapers/large/" + readonly property string notificationsDir: baseDir + "notifications/" + readonly property string contributorsDir: baseDir + "contributors/" + + // Supported image formats - extended list when ImageMagick is available + readonly property var basicImageFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.bmp"] + readonly property var extendedImageFilters: ["*.jpg", "*.jpeg", "*.png", "*.gif", "*.bmp", "*.webp", "*.avif", "*.heic", "*.heif", "*.tiff", "*.tif", "*.pnm", "*.pgm", "*.ppm", "*.pbm", "*.svg", "*.svgz", "*.ico", "*.icns", "*.jxl", "*.jp2", "*.j2k", "*.exr", "*.hdr", "*.dds", "*.tga"] + readonly property var imageFilters: imageMagickAvailable ? extendedImageFilters : basicImageFilters + + // Check if a file format needs conversion (not natively supported by Qt) + function needsConversion(filePath) { + const ext = "*." + filePath.toLowerCase().split('.').pop(); + return !basicImageFilters.includes(ext); + } + + // ------------------------------------------------- + // Internal State + // ------------------------------------------------- + property var pendingRequests: ({}) + property var fallbackQueue: [] + property bool fallbackProcessing: false + + // Process queues to prevent "too many open files" errors + property var utilityProcessQueue: [] + property int runningUtilityProcesses: 0 + readonly property int maxConcurrentUtilityProcesses: 16 + + // Separate queue for heavy ImageMagick processing (lower concurrency) + property var imageMagickQueue: [] + property int runningImageMagickProcesses: 0 + readonly property int maxConcurrentImageMagickProcesses: 4 + + // ------------------------------------------------- + // Signals + // ------------------------------------------------- + signal cacheHit(string cacheKey, string cachedPath) + signal cacheMiss(string cacheKey) + signal processingComplete(string cacheKey, string cachedPath) + signal processingFailed(string cacheKey, string error) + + // ------------------------------------------------- + // Initialization + // ------------------------------------------------- + function init() { + Logger.i("ImageCache", "Service started"); + createDirectories(); + cleanupOldCache(); + checkMagickProcess.running = true; + } + + function createDirectories() { + Quickshell.execDetached(["mkdir", "-p", wpThumbDir]); + Quickshell.execDetached(["mkdir", "-p", wpLargeDir]); + Quickshell.execDetached(["mkdir", "-p", notificationsDir]); + Quickshell.execDetached(["mkdir", "-p", contributorsDir]); + } + + function cleanupOldCache() { + const dirs = [wpThumbDir, wpLargeDir, notificationsDir, contributorsDir]; + dirs.forEach(function (dir) { + Quickshell.execDetached(["find", dir, "-type", "f", "-mtime", "+15", "-delete"]); + }); + Logger.d("ImageCache", "Cleanup triggered for files older than 15 days"); + } + + // ------------------------------------------------- + // Public API: Get Thumbnail (384x384) + // ------------------------------------------------- + function getThumbnail(sourcePath, callback) { + if (!sourcePath || sourcePath === "") { + callback("", false); + return; + } + + getMtime(sourcePath, function (mtime) { + const cacheKey = generateThumbnailKey(sourcePath, mtime); + const cachedPath = wpThumbDir + cacheKey + ".png"; + + processRequest(cacheKey, cachedPath, sourcePath, callback, function () { + if (imageMagickAvailable) { + startThumbnailProcessing(sourcePath, cachedPath, cacheKey); + } else { + queueFallbackProcessing(sourcePath, cachedPath, cacheKey, 384); + } + }); + }); + } + + // ------------------------------------------------- + // Public API: Get Large Image (scaled to specified dimensions) + // ------------------------------------------------- + function getLarge(sourcePath, width, height, callback) { + if (!sourcePath || sourcePath === "") { + callback("", false); + return; + } + + if (Settings.data.wallpaper.useOriginalImages && !needsConversion(sourcePath)) { + callback(sourcePath, false); + return; + } + + if (!imageMagickAvailable) { + Logger.d("ImageCache", "ImageMagick not available, using original:", sourcePath); + callback(sourcePath, false); + return; + } + + // Fast dimension check - skip processing if image fits screen AND format is Qt-native + getImageDimensions(sourcePath, function (imgWidth, imgHeight) { + const fitsScreen = imgWidth > 0 && imgHeight > 0 && imgWidth <= width && imgHeight <= height; + + if (fitsScreen) { + // Only skip if format is natively supported by Qt + if (!needsConversion(sourcePath)) { + Logger.d("ImageCache", `Image ${imgWidth}x${imgHeight} fits screen ${width}x${height}, using original`); + callback(sourcePath, false); + return; + } + Logger.d("ImageCache", `Image needs conversion despite fitting screen`); + } + + // Use actual image dimensions if it fits (convert without upscaling), otherwise use screen dimensions + const targetWidth = fitsScreen ? imgWidth : width; + const targetHeight = fitsScreen ? imgHeight : height; + + getMtime(sourcePath, function (mtime) { + const cacheKey = generateLargeKey(sourcePath, width, height, mtime); + const cachedPath = wpLargeDir + cacheKey + ".png"; + + processRequest(cacheKey, cachedPath, sourcePath, callback, function () { + startLargeProcessing(sourcePath, cachedPath, targetWidth, targetHeight, cacheKey); + }); + }); + }); + } + + // ------------------------------------------------- + // Public API: Get Notification Icon (64x64) + // ------------------------------------------------- + function getNotificationIcon(imageUri, appName, summary, callback) { + if (!imageUri || imageUri === "") { + callback("", false); + return; + } + + // Resolve bare file path for temp check + const filePath = imageUri.startsWith("file://") ? imageUri.substring(7) : imageUri; + + // File paths in persistent locations are used directly, not cached + if ((imageUri.startsWith("/") || imageUri.startsWith("file://")) && !isTemporaryPath(filePath)) { + callback(imageUri, false); + return; + } + + const cacheKey = generateNotificationKey(imageUri, appName, summary); + const cachedPath = notificationsDir + cacheKey + ".png"; + + // Temporary file paths are copied to cache before the source is cleaned up + if (imageUri.startsWith("/") || imageUri.startsWith("file://")) { + processRequest(cacheKey, cachedPath, imageUri, callback, function () { + copyTempFileToCache(filePath, cachedPath, cacheKey); + }); + return; + } + + processRequest(cacheKey, cachedPath, imageUri, callback, function () { + // Notifications always use Qt fallback (image:// URIs can't be read by ImageMagick) + queueFallbackProcessing(imageUri, cachedPath, cacheKey, 64); + }); + } + + // Check if a path is in a temporary directory that may be cleaned up + function isTemporaryPath(path) { + return path.startsWith("/tmp/"); + } + + // Copy a temporary file to the cache directory + function copyTempFileToCache(sourcePath, destPath, cacheKey) { + const srcEsc = sourcePath.replace(/'/g, "'\\''"); + const dstEsc = destPath.replace(/'/g, "'\\''"); + + const processString = ` + import QtQuick + import Quickshell.Io + Process { + command: ["cp", "--", "${srcEsc}", "${dstEsc}"] + stdout: StdioCollector {} + stderr: StdioCollector {} + } + `; + + queueUtilityProcess({ + name: "CopyTempFile_" + cacheKey, + processString: processString, + onComplete: function (exitCode) { + if (exitCode === 0) { + Logger.d("ImageCache", "Temp file cached:", destPath); + notifyCallbacks(cacheKey, destPath, true); + } else { + Logger.w("ImageCache", "Failed to cache temp file:", sourcePath); + notifyCallbacks(cacheKey, "", false); + } + }, + onError: function () { + Logger.e("ImageCache", "Error caching temp file:", sourcePath); + notifyCallbacks(cacheKey, "", false); + } + }); + } + + // ------------------------------------------------- + // Public API: Get Circular Avatar (256x256) + // ------------------------------------------------- + function getCircularAvatar(url, username, callback) { + if (!url || !username) { + callback("", false); + return; + } + + const cacheKey = username; + const cachedPath = contributorsDir + username + "_circular.png"; + + processRequest(cacheKey, cachedPath, url, callback, function () { + if (imageMagickAvailable) { + downloadAndProcessAvatar(url, username, cachedPath, cacheKey); + } else { + // No fallback for circular avatars without ImageMagick + Logger.w("ImageCache", "Circular avatars require ImageMagick"); + notifyCallbacks(cacheKey, "", false); + } + }); + } + + // ------------------------------------------------- + // Cache Key Generation + // ------------------------------------------------- + function generateThumbnailKey(sourcePath, mtime) { + const keyString = sourcePath + "@384x384@" + (mtime || "unknown"); + return Checksum.sha256(keyString); + } + + function generateLargeKey(sourcePath, width, height, mtime) { + const keyString = sourcePath + "@" + width + "x" + height + "@" + (mtime || "unknown"); + return Checksum.sha256(keyString); + } + + function generateNotificationKey(imageUri, appName, summary) { + if (imageUri.startsWith("image://qsimage/")) { + return Checksum.sha256(appName + "|" + summary); + } + return Checksum.sha256(imageUri); + } + + // ------------------------------------------------- + // Request Processing (with coalescing) + // ------------------------------------------------- + function processRequest(cacheKey, cachedPath, sourcePath, callback, processFn) { + // Check if already processing this request + if (pendingRequests[cacheKey]) { + pendingRequests[cacheKey].callbacks.push(callback); + Logger.d("ImageCache", "Coalescing request for:", cacheKey); + return; + } + + // Check cache first + checkFileExists(cachedPath, function (exists) { + if (exists) { + Logger.d("ImageCache", "Cache hit:", cachedPath); + callback(cachedPath, true); + cacheHit(cacheKey, cachedPath); + return; + } + + // Re-check pendingRequests (race condition fix) + if (pendingRequests[cacheKey]) { + pendingRequests[cacheKey].callbacks.push(callback); + return; + } + + // Start new processing + Logger.d("ImageCache", "Cache miss, processing:", sourcePath); + cacheMiss(cacheKey); + pendingRequests[cacheKey] = { + callbacks: [callback], + sourcePath: sourcePath + }; + + processFn(); + }); + } + + function notifyCallbacks(cacheKey, path, success) { + const request = pendingRequests[cacheKey]; + if (request) { + request.callbacks.forEach(function (cb) { + cb(path, success); + }); + delete pendingRequests[cacheKey]; + } + + if (success) { + processingComplete(cacheKey, path); + } else { + processingFailed(cacheKey, "Processing failed"); + } + } + + // ------------------------------------------------- + // ImageMagick Processing: Thumbnail + // ------------------------------------------------- + function startThumbnailProcessing(sourcePath, outputPath, cacheKey) { + const srcEsc = sourcePath.replace(/'/g, "'\\''"); + const dstEsc = outputPath.replace(/'/g, "'\\''"); + + // Use Lanczos filter for high-quality downscaling, subtle unsharp mask, and PNG for lossless output + const command = `magick '${srcEsc}' -auto-orient -filter Lanczos -resize '384x384^' -gravity center -extent 384x384 -unsharp 0x0.5 '${dstEsc}'`; + + runProcess(command, cacheKey, outputPath, sourcePath); + } + + // ------------------------------------------------- + // ImageMagick Processing: Large + // ------------------------------------------------- + function startLargeProcessing(sourcePath, outputPath, width, height, cacheKey) { + const srcEsc = sourcePath.replace(/'/g, "'\\''"); + const dstEsc = outputPath.replace(/'/g, "'\\''"); + + // Use Lanczos filter for high-quality downscaling and PNG for lossless output + const command = `magick '${srcEsc}' -auto-orient -filter Lanczos -resize '${width}x${height}^' '${dstEsc}'`; + + runProcess(command, cacheKey, outputPath, sourcePath); + } + + // ------------------------------------------------- + // ImageMagick Processing: Circular Avatar + // ------------------------------------------------- + function downloadAndProcessAvatar(url, username, outputPath, cacheKey) { + const tempPath = contributorsDir + username + "_temp.png"; + const tempEsc = tempPath.replace(/'/g, "'\\''"); + const urlEsc = url.replace(/'/g, "'\\''"); + + // Download first (uses utility queue since curl/wget are lightweight) + const downloadCmd = `curl -L -s -o '${tempEsc}' '${urlEsc}' || wget -q -O '${tempEsc}' '${urlEsc}'`; + + const processString = ` + import QtQuick + import Quickshell.Io + Process { + command: ["sh", "-c", "${downloadCmd.replace(/"/g, '\\"')}"] + stdout: StdioCollector {} + stderr: StdioCollector {} + } + `; + + queueUtilityProcess({ + name: "DownloadProcess_" + cacheKey, + processString: processString, + onComplete: function (exitCode) { + if (exitCode !== 0) { + Logger.e("ImageCache", "Failed to download avatar for", username); + notifyCallbacks(cacheKey, "", false); + return; + } + // Now process with ImageMagick + processCircularAvatar(tempPath, outputPath, cacheKey); + }, + onError: function () { + notifyCallbacks(cacheKey, "", false); + } + }); + } + + function processCircularAvatar(inputPath, outputPath, cacheKey) { + const srcEsc = inputPath.replace(/'/g, "'\\''"); + const dstEsc = outputPath.replace(/'/g, "'\\''"); + + // ImageMagick command for circular crop with alpha + const command = `magick '${srcEsc}' -resize 256x256^ -gravity center -extent 256x256 -alpha set \\( +clone -channel A -evaluate set 0 +channel -fill white -draw 'circle 128,128 128,0' \\) -compose DstIn -composite '${dstEsc}'`; + + queueImageMagickProcess({ + command: command, + cacheKey: cacheKey, + onComplete: function (exitCode) { + // Clean up temp file + Quickshell.execDetached(["rm", "-f", inputPath]); + + if (exitCode !== 0) { + Logger.e("ImageCache", "Failed to create circular avatar"); + notifyCallbacks(cacheKey, "", false); + } else { + Logger.d("ImageCache", "Circular avatar created:", outputPath); + notifyCallbacks(cacheKey, outputPath, true); + } + }, + onError: function () { + Quickshell.execDetached(["rm", "-f", inputPath]); + notifyCallbacks(cacheKey, "", false); + } + }); + } + + // ------------------------------------------------- + // Generic Process Runner (with queue for ImageMagick) + // ------------------------------------------------- + + // Queue an ImageMagick process and run it when a slot is available + function queueImageMagickProcess(request) { + imageMagickQueue.push(request); + processImageMagickQueue(); + } + + // Process queued ImageMagick requests up to the concurrency limit + function processImageMagickQueue() { + while (runningImageMagickProcesses < maxConcurrentImageMagickProcesses && imageMagickQueue.length > 0) { + const request = imageMagickQueue.shift(); + runImageMagickProcess(request); + } + } + + // Actually run an ImageMagick process + function runImageMagickProcess(request) { + runningImageMagickProcesses++; + + const processString = ` + import QtQuick + import Quickshell.Io + Process { + command: ["sh", "-c", ""] + stdout: StdioCollector {} + stderr: StdioCollector {} + } + `; + + try { + const processObj = Qt.createQmlObject(processString, root, "ImageProcess_" + request.cacheKey); + processObj.command = ["sh", "-c", request.command]; + + processObj.exited.connect(function (exitCode) { + processObj.destroy(); + runningImageMagickProcesses--; + request.onComplete(exitCode, processObj); + processImageMagickQueue(); + }); + + processObj.running = true; + } catch (e) { + Logger.e("ImageCache", "Failed to create process:", e); + runningImageMagickProcesses--; + request.onError(e); + processImageMagickQueue(); + } + } + + function runProcess(command, cacheKey, outputPath, sourcePath) { + queueImageMagickProcess({ + command: command, + cacheKey: cacheKey, + onComplete: function (exitCode, proc) { + if (exitCode !== 0) { + const stderrText = proc.stderr.text || ""; + Logger.e("ImageCache", "Processing failed:", stderrText); + notifyCallbacks(cacheKey, sourcePath, false); + } else { + Logger.d("ImageCache", "Processing complete:", outputPath); + notifyCallbacks(cacheKey, outputPath, true); + } + }, + onError: function () { + notifyCallbacks(cacheKey, sourcePath, false); + } + }); + } + + // ------------------------------------------------- + // Qt Fallback Renderer + // ------------------------------------------------- + PanelWindow { + id: fallbackRenderer + implicitWidth: 0 + implicitHeight: 0 + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "noctalia-image-cache-renderer" + color: "transparent" + mask: Region {} + + Image { + id: fallbackImage + property string cacheKey: "" + property string destPath: "" + property int targetSize: 256 + + width: targetSize + height: targetSize + visible: true + cache: false + asynchronous: true + fillMode: Image.PreserveAspectCrop + mipmap: true + antialiasing: true + + onStatusChanged: { + if (!cacheKey) + return; + + if (status === Image.Ready) { + grabToImage(function (result) { + if (result.saveToFile(destPath)) { + Logger.d("ImageCache", "Fallback cache created:", destPath); + root.notifyCallbacks(cacheKey, destPath, true); + } else { + Logger.e("ImageCache", "Failed to save fallback cache"); + root.notifyCallbacks(cacheKey, "", false); + } + processNextFallback(); + }); + } else if (status === Image.Error) { + Logger.e("ImageCache", "Fallback image load failed"); + root.notifyCallbacks(cacheKey, "", false); + processNextFallback(); + } + } + + function processNextFallback() { + cacheKey = ""; + destPath = ""; + source = ""; + + if (fallbackQueue.length > 0) { + const next = fallbackQueue.shift(); + cacheKey = next.cacheKey; + destPath = next.destPath; + targetSize = next.size; + source = next.sourcePath; + } else { + fallbackProcessing = false; + } + } + } + } + + function queueFallbackProcessing(sourcePath, destPath, cacheKey, size) { + fallbackQueue.push({ + sourcePath: sourcePath, + destPath: destPath, + cacheKey: cacheKey, + size: size + }); + + if (!fallbackProcessing) { + fallbackProcessing = true; + const item = fallbackQueue.shift(); + fallbackImage.cacheKey = item.cacheKey; + fallbackImage.destPath = item.destPath; + fallbackImage.targetSize = item.size; + fallbackImage.source = item.sourcePath; + } + } + + // ------------------------------------------------- + // Utility Functions (with process queue to prevent fd exhaustion) + // ------------------------------------------------- + + // Queue a utility process and run it when a slot is available + function queueUtilityProcess(request) { + utilityProcessQueue.push(request); + processUtilityQueue(); + } + + // Process queued utility requests up to the concurrency limit + function processUtilityQueue() { + while (runningUtilityProcesses < maxConcurrentUtilityProcesses && utilityProcessQueue.length > 0) { + const request = utilityProcessQueue.shift(); + runUtilityProcess(request); + } + } + + // Actually run a utility process + function runUtilityProcess(request) { + runningUtilityProcesses++; + + try { + const processObj = Qt.createQmlObject(request.processString, root, request.name); + + processObj.exited.connect(function (exitCode) { + processObj.destroy(); + runningUtilityProcesses--; + request.onComplete(exitCode, processObj); + processUtilityQueue(); + }); + + processObj.running = true; + } catch (e) { + Logger.e("ImageCache", "Failed to create " + request.name + ":", e); + runningUtilityProcesses--; + request.onError(e); + processUtilityQueue(); + } + } + + function getMtime(filePath, callback) { + const pathEsc = filePath.replace(/'/g, "'\\''"); + const processString = ` + import QtQuick + import Quickshell.Io + Process { + command: ["stat", "-c", "%Y", "${pathEsc}"] + stdout: StdioCollector {} + stderr: StdioCollector {} + } + `; + + queueUtilityProcess({ + name: "MtimeProcess", + processString: processString, + onComplete: function (exitCode, proc) { + const mtime = exitCode === 0 ? proc.stdout.text.trim() : ""; + callback(mtime); + }, + onError: function () { + callback(""); + } + }); + } + + function checkFileExists(filePath, callback) { + const pathEsc = filePath.replace(/'/g, "'\\''"); + const processString = ` + import QtQuick + import Quickshell.Io + Process { + command: ["test", "-f", "${pathEsc}"] + stdout: StdioCollector {} + stderr: StdioCollector {} + } + `; + + queueUtilityProcess({ + name: "FileExistsProcess", + processString: processString, + onComplete: function (exitCode) { + callback(exitCode === 0); + }, + onError: function () { + callback(false); + } + }); + } + + function getImageDimensions(filePath, callback) { + const pathEsc = filePath.replace(/'/g, "'\\''"); + const processString = ` + import QtQuick + import Quickshell.Io + Process { + command: ["identify", "-ping", "-format", "%w %h", "${pathEsc}[0]"] + stdout: StdioCollector {} + stderr: StdioCollector {} + } + `; + + queueUtilityProcess({ + name: "IdentifyProcess", + processString: processString, + onComplete: function (exitCode, proc) { + let width = 0, height = 0; + if (exitCode === 0) { + const parts = proc.stdout.text.trim().split(" "); + if (parts.length >= 2) { + width = parseInt(parts[0], 10) || 0; + height = parseInt(parts[1], 10) || 0; + } + } + callback(width, height); + }, + onError: function () { + callback(0, 0); + } + }); + } + + // ------------------------------------------------- + // Cache Invalidation + // ------------------------------------------------- + function invalidateThumbnail(sourcePath) { + Logger.i("ImageCache", "Invalidating thumbnail for:", sourcePath); + // Since cache keys include hash, we'd need to track mappings + // For simplicity, clear all thumbnails + clearThumbnails(); + } + + function invalidateLarge(sourcePath) { + Logger.i("ImageCache", "Invalidating large for:", sourcePath); + clearLarge(); + } + + function invalidateNotification(imageId) { + const path = notificationsDir + imageId + ".png"; + Quickshell.execDetached(["rm", "-f", path]); + } + + function invalidateAvatar(username) { + const path = contributorsDir + username + "_circular.png"; + Quickshell.execDetached(["rm", "-f", path]); + } + + // ------------------------------------------------- + // Clear Cache Functions + // ------------------------------------------------- + function clearAll() { + Logger.i("ImageCache", "Clearing all cache"); + clearThumbnails(); + clearLarge(); + clearNotifications(); + clearContributors(); + } + + function clearThumbnails() { + Logger.i("ImageCache", "Clearing thumbnails cache"); + Quickshell.execDetached(["rm", "-rf", wpThumbDir]); + Quickshell.execDetached(["mkdir", "-p", wpThumbDir]); + } + + function clearLarge() { + Logger.i("ImageCache", "Clearing large cache"); + Quickshell.execDetached(["rm", "-rf", wpLargeDir]); + Quickshell.execDetached(["mkdir", "-p", wpLargeDir]); + } + + function clearNotifications() { + Logger.i("ImageCache", "Clearing notifications cache"); + Quickshell.execDetached(["rm", "-rf", notificationsDir]); + Quickshell.execDetached(["mkdir", "-p", notificationsDir]); + } + + function clearContributors() { + Logger.i("ImageCache", "Clearing contributors cache"); + Quickshell.execDetached(["rm", "-rf", contributorsDir]); + Quickshell.execDetached(["mkdir", "-p", contributorsDir]); + } + + // ------------------------------------------------- + // ImageMagick Detection + // ------------------------------------------------- + Process { + id: checkMagickProcess + command: ["sh", "-c", "command -v magick"] + running: false + + stdout: StdioCollector {} + stderr: StdioCollector {} + + onExited: function (exitCode) { + root.imageMagickAvailable = (exitCode === 0); + root.initialized = true; + if (root.imageMagickAvailable) { + Logger.i("ImageCache", "ImageMagick available"); + } else { + Logger.w("ImageCache", "ImageMagick not found, using Qt fallback"); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/UI/LauncherProviderRegistry.qml b/fedora/.config/quickshell/noctalia-shell/Services/UI/LauncherProviderRegistry.qml new file mode 100644 index 0000000..f5810ab --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/UI/LauncherProviderRegistry.qml @@ -0,0 +1,108 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.Noctalia + +Singleton { + id: root + + signal pluginProviderRegistryUpdated + + // Plugin provider storage + property var pluginProviders: ({}) // { "plugin:pluginId": component } + property var pluginProviderMetadata: ({}) // { "plugin:pluginId": metadata } + + // Persistent provider instances — survive LauncherCore destruction/recreation + // so plugins don't re-parse large datasets on every launcher open. + property var providerInstances: ({}) // { "plugin:pluginId": instance } + + function init() { + Logger.i("LauncherProviderRegistry", "Service started"); + } + + // Register a plugin launcher provider and instantiate it immediately + function registerPluginProvider(pluginId, component, metadata) { + if (!pluginId || !component) { + Logger.e("LauncherProviderRegistry", "Cannot register plugin provider: invalid parameters"); + return false; + } + + var providerId = "plugin:" + pluginId; + + pluginProviders[providerId] = component; + pluginProviderMetadata[providerId] = metadata || {}; + + // Instantiate immediately so data loading starts in the background + var pluginApi = PluginService.getPluginAPI(pluginId); + if (pluginApi) { + var instance = component.createObject(null, { + pluginApi: pluginApi + }); + if (instance) { + providerInstances[providerId] = instance; + if (instance.init) + instance.init(); + Logger.i("LauncherProviderRegistry", "Registered and instantiated plugin provider:", providerId); + } else { + Logger.e("LauncherProviderRegistry", "Failed to instantiate plugin provider:", providerId); + } + } + + root.pluginProviderRegistryUpdated(); + return true; + } + + // Unregister a plugin launcher provider + function unregisterPluginProvider(pluginId) { + var providerId = "plugin:" + pluginId; + + if (!pluginProviders[providerId]) { + Logger.w("LauncherProviderRegistry", "Plugin provider not registered:", providerId); + return false; + } + + if (providerInstances[providerId]) { + providerInstances[providerId].destroy(); + delete providerInstances[providerId]; + } + + delete pluginProviders[providerId]; + delete pluginProviderMetadata[providerId]; + + Logger.i("LauncherProviderRegistry", "Unregistered plugin provider:", providerId); + root.pluginProviderRegistryUpdated(); + return true; + } + + // Get list of registered plugin provider IDs + function getPluginProviders() { + return Object.keys(pluginProviders); + } + + // Get the live provider instance by ID + function getProviderInstance(providerId) { + return providerInstances[providerId] || null; + } + + // Get provider component by ID + function getProviderComponent(providerId) { + return pluginProviders[providerId] || null; + } + + // Get provider metadata by ID + function getProviderMetadata(providerId) { + return pluginProviderMetadata[providerId] || null; + } + + // Check if ID is a plugin provider + function isPluginProvider(id) { + return id.startsWith("plugin:"); + } + + // Check if provider exists + function hasProvider(providerId) { + return providerId in pluginProviders; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/UI/PanelService.qml b/fedora/.config/quickshell/noctalia-shell/Services/UI/PanelService.qml new file mode 100644 index 0000000..89993f9 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/UI/PanelService.qml @@ -0,0 +1,421 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.Compositor + +Singleton { + id: root + + // A ref. to the lockScreen, so it's accessible from anywhere. + property var lockScreen: null + + // Panels + property var registeredPanels: ({}) + property var openedPanel: null + property var closingPanel: null + property bool closedImmediately: false + + // Overlay launcher state (separate from normal panels) + property bool overlayLauncherOpen: false + property var overlayLauncherScreen: null + property var overlayLauncherCore: null // Reference to LauncherCore when overlay is active + // Brief window after panel opens where Exclusive keyboard is allowed on Hyprland + // This allows text inputs to receive focus, then switches to OnDemand for click-to-close + property bool isInitializingKeyboard: false + + // Global state for keybind recording components to block global shortcuts + property bool isKeybindRecording: false + + signal willOpen + signal didClose + + // Background slot assignments for dynamic panel background rendering + // Slot 0: currently opening/open panel, Slot 1: closing panel + property var backgroundSlotAssignments: [null, null] + signal slotAssignmentChanged(int slotIndex, var panel) + + function assignToSlot(slotIndex, panel) { + if (backgroundSlotAssignments[slotIndex] !== panel) { + var newAssignments = backgroundSlotAssignments.slice(); + newAssignments[slotIndex] = panel; + backgroundSlotAssignments = newAssignments; + slotAssignmentChanged(slotIndex, panel); + } + } + + // Popup menu windows (one per screen) - used for both tray menus and context menus + property var popupMenuWindows: ({}) + signal popupMenuWindowRegistered(var screen) + + // Register this panel (called after panel is loaded) + function registerPanel(panel) { + registeredPanels[panel.objectName] = panel; + Logger.d("PanelService", "Registered panel:", panel.objectName); + } + + // Register popup menu window for a screen + function registerPopupMenuWindow(screen, window) { + if (!screen || !window) + return; + var key = screen.name; + popupMenuWindows[key] = window; + Logger.d("PanelService", "Registered popup menu window for screen:", key); + popupMenuWindowRegistered(screen); + } + + // Unregister popup menu window for a screen (called on destruction) + function unregisterPopupMenuWindow(screen) { + if (!screen) + return; + var key = screen.name; + delete popupMenuWindows[key]; + Logger.d("PanelService", "Unregistered popup menu window for screen:", key); + } + + // Get popup menu window for a screen + function getPopupMenuWindow(screen) { + if (!screen) + return null; + return popupMenuWindows[screen.name] || null; + } + + // Show a context menu with proper handling for all compositors + // Optional targetItem: if provided, menu will be horizontally centered on this item instead of anchorItem + function showContextMenu(contextMenu, anchorItem, screen, targetItem) { + if (!contextMenu || !anchorItem) + return; + + // Close any previously opened context menu first + closeContextMenu(screen); + + var popupMenuWindow = getPopupMenuWindow(screen); + if (popupMenuWindow) { + popupMenuWindow.showContextMenu(contextMenu); + contextMenu.openAtItem(anchorItem, screen, targetItem); + } + } + + // Close any open context menu or popup menu window + function closeContextMenu(screen) { + var popupMenuWindow = getPopupMenuWindow(screen); + if (popupMenuWindow && popupMenuWindow.visible) { + popupMenuWindow.close(); + } + } + + // Show a tray menu with proper handling for all compositors + // Returns true if menu was shown successfully + function showTrayMenu(screen, trayItem, trayMenu, anchorItem, menuX, menuY, widgetSection, widgetIndex) { + if (!trayItem || !trayMenu || !anchorItem) + return false; + + // Close any previously opened menu first + closeContextMenu(screen); + + trayMenu.trayItem = trayItem; + trayMenu.widgetSection = widgetSection; + trayMenu.widgetIndex = widgetIndex; + + var popupMenuWindow = getPopupMenuWindow(screen); + if (popupMenuWindow) { + popupMenuWindow.open(); + trayMenu.showAt(anchorItem, menuX, menuY); + } else { + return false; + } + return true; + } + + // Close tray menu + function closeTrayMenu(screen) { + var popupMenuWindow = getPopupMenuWindow(screen); + if (popupMenuWindow) { + // This closes both the window and calls hideMenu on the tray menu + popupMenuWindow.close(); + } + } + + // Find a fallback screen, prioritizing 0x0 position (primary) + function findFallbackScreen() { + let primaryCandidate = null; + let firstScreen = null; + + for (let i = 0; i < Quickshell.screens.length; i++) { + const s = Quickshell.screens[i]; + if (s.x === 0 && s.y === 0) { + primaryCandidate = s; + } + if (!firstScreen) { + firstScreen = s; + } + } + + return primaryCandidate || firstScreen || null; + } + + // Returns a panel (loads it on-demand if not yet loaded) + // By default, if panel not found on screen, tries other screens (favoring 0x0) + // Pass fallback=false to disable this behavior + function getPanel(name, screen, fallback = true) { + if (!screen) { + Logger.d("PanelService", "missing screen for getPanel:", name); + // If no screen specified, return the first matching panel + for (var key in registeredPanels) { + if (key.startsWith(name + "-")) { + return registeredPanels[key]; + } + } + return null; + } + + var panelKey = `${name}-${screen.name}`; + + // Check if panel is already loaded + if (registeredPanels[panelKey]) { + return registeredPanels[panelKey]; + } + + // If fallback enabled, try to find panel on another screen + if (fallback) { + // First try the primary screen (0x0) + var fallbackScreen = findFallbackScreen(); + if (fallbackScreen && fallbackScreen.name !== screen.name) { + var fallbackKey = `${name}-${fallbackScreen.name}`; + if (registeredPanels[fallbackKey]) { + Logger.d("PanelService", "Panel fallback from", screen.name, "to", fallbackScreen.name); + return registeredPanels[fallbackKey]; + } + } + + // Try any other screen + for (var key in registeredPanels) { + if (key.startsWith(name + "-")) { + Logger.d("PanelService", "Panel fallback to first available:", key); + return registeredPanels[key]; + } + } + } + + Logger.w("PanelService", "Panel not found:", panelKey); + return null; + } + + // Check if a panel exists + function hasPanel(name) { + return name in registeredPanels; + } + + // Check if panels can be shown on a given screen (has bar enabled or allowPanelsOnScreenWithoutBar) + function canShowPanelsOnScreen(screen) { + const name = screen?.name || ""; + const monitors = Settings.data.bar.monitors || []; + const allowPanelsOnScreenWithoutBar = Settings.data.general.allowPanelsOnScreenWithoutBar; + return allowPanelsOnScreenWithoutBar || monitors.length === 0 || monitors.includes(name); + } + + // Find a screen that can show panels + function findScreenForPanels() { + for (let i = 0; i < Quickshell.screens.length; i++) { + if (canShowPanelsOnScreen(Quickshell.screens[i])) { + return Quickshell.screens[i]; + } + } + return null; + } + + // Timer to switch from Exclusive to OnDemand keyboard focus on Hyprland + Timer { + id: keyboardInitTimer + interval: 100 + repeat: false + onTriggered: { + root.isInitializingKeyboard = false; + } + } + + // Helper to keep only one panel open at any time + function willOpenPanel(panel) { + // Close overlay launcher if open + if (overlayLauncherOpen) { + overlayLauncherOpen = false; + overlayLauncherScreen = null; + } + + if (openedPanel && openedPanel !== panel) { + // Move current panel to closing slot before closing it + closingPanel = openedPanel; + assignToSlot(1, closingPanel); + openedPanel.close(); + } + + // Assign new panel to open slot + openedPanel = panel; + assignToSlot(0, panel); + + // Start keyboard initialization period (for Hyprland workaround) + if (panel && panel.exclusiveKeyboard) { + isInitializingKeyboard = true; + keyboardInitTimer.restart(); + } + + // emit signal + willOpen(); + } + + // Open launcher panel (handles both normal and overlay mode) + function openLauncher(screen) { + if (Settings.data.appLauncher.overviewLayer) { + // Close any regular panel first + if (openedPanel) { + closingPanel = openedPanel; + assignToSlot(1, closingPanel); + openedPanel.close(); + openedPanel = null; + } + // Open overlay launcher + overlayLauncherOpen = true; + overlayLauncherScreen = screen; + willOpen(); + } else { + // Normal mode - use the SmartPanel + var panel = getPanel("launcherPanel", screen); + if (panel) + panel.open(); + } + } + + // Toggle launcher panel + function toggleLauncher(screen) { + if (Settings.data.appLauncher.overviewLayer) { + if (overlayLauncherOpen && overlayLauncherScreen === screen) { + closeOverlayLauncher(); + } else { + openLauncher(screen); + } + } else { + var panel = getPanel("launcherPanel", screen); + if (panel) + panel.toggle(); + } + } + + // Close overlay launcher + function closeOverlayLauncher() { + if (overlayLauncherOpen) { + overlayLauncherOpen = false; + overlayLauncherScreen = null; + didClose(); + } + } + + // Close overlay launcher immediately (for app launches) + function closeOverlayLauncherImmediately() { + if (overlayLauncherOpen) { + closedImmediately = true; + overlayLauncherOpen = false; + overlayLauncherScreen = null; + didClose(); + } + } + + // ==================== Unified Launcher API ==================== + // These methods work for both normal (SmartPanel) and overlay modes + + function isLauncherOpen(screen) { + if (Settings.data.appLauncher.overviewLayer) { + return overlayLauncherOpen && overlayLauncherScreen === screen; + } else { + var panel = getPanel("launcherPanel", screen); + return panel ? panel.isPanelOpen : false; + } + } + + function getLauncherSearchText(screen) { + if (Settings.data.appLauncher.overviewLayer) { + return overlayLauncherCore ? overlayLauncherCore.searchText : ""; + } else { + var panel = getPanel("launcherPanel", screen); + return panel ? panel.searchText : ""; + } + } + + function setLauncherSearchText(screen, text) { + if (Settings.data.appLauncher.overviewLayer) { + if (overlayLauncherCore) + overlayLauncherCore.setSearchText(text); + } else { + var panel = getPanel("launcherPanel", screen); + if (panel) + panel.setSearchText(text); + } + } + + function openLauncherWithSearch(screen, searchText) { + if (Settings.data.appLauncher.overviewLayer) { + openLauncher(screen); + // Set search text after core is ready + Qt.callLater(() => { + if (overlayLauncherCore) + overlayLauncherCore.setSearchText(searchText); + }); + } else { + var panel = getPanel("launcherPanel", screen); + if (panel) { + panel.open(); + panel.setSearchText(searchText); + } + } + } + + function closeLauncher(screen) { + if (Settings.data.appLauncher.overviewLayer) { + closeOverlayLauncher(); + } else { + var panel = getPanel("launcherPanel", screen); + if (panel) + panel.close(); + } + } + + // Close any open panel (for general use) + function closePanel() { + if (overlayLauncherOpen) { + closeOverlayLauncher(); + } else if (openedPanel && openedPanel.close) { + openedPanel.close(); + } + } + + function closedPanel(panel) { + if (openedPanel && openedPanel === panel) { + openedPanel = null; + assignToSlot(0, null); + } + + if (closingPanel && closingPanel === panel) { + closingPanel = null; + assignToSlot(1, null); + } + + // Reset keyboard init state + isInitializingKeyboard = false; + keyboardInitTimer.stop(); + + // emit signal + didClose(); + } + + // Close panels when compositor overview opens (if setting is enabled) + Connections { + target: CompositorService + enabled: Settings.data.bar.hideOnOverview + + function onOverviewActiveChanged() { + if (CompositorService.overviewActive && root.openedPanel) { + root.openedPanel.close(); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/UI/SettingsPanelService.qml b/fedora/.config/quickshell/noctalia-shell/Services/UI/SettingsPanelService.qml new file mode 100644 index 0000000..fa561e2 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/UI/SettingsPanelService.qml @@ -0,0 +1,143 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Services.UI + +Singleton { + id: root + + // Track if the settings window is open + property bool isWindowOpen: false + + // Reference to the window (set by SettingsPanelWindow) + property var settingsWindow: null + + // Requested tab when opening + property int requestedTab: 0 + + // Requested subtab when opening (-1 means no specific subtab) + property int requestedSubTab: -1 + + // Requested entry for search navigation + property var requestedEntry: null + + signal windowOpened + signal windowClosed + + function openToEntry(entry, screen) { + if (Settings.data.ui.settingsPanelMode === "window") { + requestedEntry = entry; + if (settingsWindow) { + settingsWindow.visible = true; + isWindowOpen = true; + windowOpened(); + settingsWindow.navigateToEntry(entry); + } + } else { + if (!screen) { + Logger.w("SettingsPanelService", "Screen parameter required for panel mode"); + return; + } + var settingsPanel = PanelService.getPanel("settingsPanel", screen); + if (settingsPanel) { + settingsPanel.requestedEntry = entry; + settingsPanel.open(); + } + } + } + + // Unified function to open settings to a specific tab and subtab + // Respects user's settingsPanelMode setting (window vs panel) + // For panel mode, screen parameter is required + function openToTab(tab, subTab, screen) { + const tabId = tab !== undefined ? tab : 0; + const subTabId = subTab !== undefined ? subTab : -1; + + if (Settings.data.ui.settingsPanelMode === "window") { + requestedTab = tabId; + requestedSubTab = subTabId; + if (settingsWindow) { + settingsWindow.visible = true; + isWindowOpen = true; + windowOpened(); + settingsWindow.navigateTo(tabId, subTabId); + } + } else { + if (!screen) { + Logger.w("SettingsPanelService", "Screen parameter required for panel mode"); + return; + } + var settingsPanel = PanelService.getPanel("settingsPanel", screen); + if (settingsPanel) { + settingsPanel.openToTab(tabId, subTabId); + } + } + } + + function openWindow(tab) { + requestedTab = tab !== undefined ? tab : 0; + requestedSubTab = -1; + if (settingsWindow) { + settingsWindow.visible = true; + isWindowOpen = true; + windowOpened(); + settingsWindow.navigateTo(requestedTab, -1); + } + } + + function closeWindow() { + if (settingsWindow) { + settingsWindow.visible = false; + isWindowOpen = false; + windowClosed(); + } + } + + function toggleWindow(tab) { + if (isWindowOpen) { + closeWindow(); + } else { + openWindow(tab); + } + } + + // Unified toggle: opens to tab/subtab if closed, closes if open + // Respects settingsPanelMode setting + function toggle(tab, subTab, screen) { + const tabId = tab !== undefined ? tab : 0; + const subTabId = subTab !== undefined ? subTab : -1; + + if (Settings.data.ui.settingsPanelMode === "window") { + if (isWindowOpen) { + closeWindow(); + } else { + openToTab(tabId, subTabId); + } + } else { + if (!screen) { + Logger.w("SettingsPanelService", "Screen parameter required for panel mode"); + return; + } + var settingsPanel = PanelService.getPanel("settingsPanel", screen); + if (settingsPanel?.isPanelOpen) { + settingsPanel.close(); + } else { + settingsPanel?.openToTab(tabId, subTabId); + } + } + } + + // Unified close for both modes + function close(screen) { + if (Settings.data.ui.settingsPanelMode === "window") { + closeWindow(); + } else { + if (!screen) + return; + var settingsPanel = PanelService.getPanel("settingsPanel", screen); + settingsPanel?.close(); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/UI/SettingsSearchService.qml b/fedora/.config/quickshell/noctalia-shell/Services/UI/SettingsSearchService.qml new file mode 100644 index 0000000..50dc7ac --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/UI/SettingsSearchService.qml @@ -0,0 +1,158 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.Compositor +import qs.Services.Power +import qs.Services.System + +Singleton { + id: root + + property var searchIndex: [] + + FileView { + path: Quickshell.shellDir + "/Assets/settings-search-index.json" + watchChanges: false + printErrors: false + + onLoaded: { + try { + root.searchIndex = JSON.parse(text()); + } catch (e) { + root.searchIndex = []; + } + } + } + + readonly property var _roots: ({ + "CompositorService": CompositorService, + "Settings": Settings, + "Quickshell": Quickshell, + "IdleService": IdleService, + "SystemStatService": SystemStatService, + "SoundService": SoundService + }) + + function isEntryVisible(entry) { + if (!entry.visibleWhen || entry.visibleWhen.length === 0) + return true; + for (let i = 0; i < entry.visibleWhen.length; i++) { + if (!_evalCondition(entry.visibleWhen[i])) + return false; + } + return true; + } + + function _resolveValue(path) { + const parts = path.split("."); + const rootObj = _roots[parts[0]]; + if (rootObj === undefined) + return undefined; + + let obj = rootObj; + for (let i = 1; i < parts.length; i++) { + if (obj === undefined || obj === null) + return undefined; + let key = parts[i]; + if (key.endsWith("?")) + key = key.slice(0, -1); + obj = obj[key]; + } + return obj; + } + + function _splitAnd(expr) { + const parts = []; + let depth = 0; + let current = ""; + for (let i = 0; i < expr.length; i++) { + const ch = expr[i]; + if (ch === "(") + depth++; + else if (ch === ")") + depth--; + else if (depth === 0 && ch === "&" && i + 1 < expr.length && expr[i + 1] === "&") { + parts.push(current); + current = ""; + i++; + continue; + } + current += ch; + } + parts.push(current); + return parts; + } + + function _evalCondition(expr) { + expr = expr.trim(); + + // Strip outer parentheses + if (expr.startsWith("(") && expr.endsWith(")")) { + let depth = 0; + let allWrapped = true; + for (let i = 0; i < expr.length - 1; i++) { + if (expr[i] === "(") + depth++; + else if (expr[i] === ")") + depth--; + if (depth === 0) { + allWrapped = false; + break; + } + } + if (allWrapped) + return _evalCondition(expr.slice(1, -1)); + } + + // AND: all parts must be true + if (expr.includes("&&")) { + const parts = _splitAnd(expr); + if (parts.length > 1) { + for (let i = 0; i < parts.length; i++) { + if (!_evalCondition(parts[i])) + return false; + } + return true; + } + } + + // Negation + if (expr.startsWith("!")) + return !_evalCondition(expr.slice(1).trim()); + + // Literal false + if (expr === "false") + return false; + + // Strip nullish coalescing fallback + const nullishMatch = expr.match(/^(.+?)\s*\?\?\s*(?:false|true)\s*$/); + if (nullishMatch) + return _evalCondition(nullishMatch[1]); + + // === comparison + let m = expr.match(/^(.+?)\s*===\s*"([^"]*)"\s*$/); + if (m) + return _resolveValue(m[1].trim()) === m[2]; + + // !== comparison + m = expr.match(/^(.+?)\s*!==\s*"([^"]*)"\s*$/); + if (m) + return _resolveValue(m[1].trim()) !== m[2]; + + // > comparison + m = expr.match(/^(.+?)\s*>\s*(\d+)\s*$/); + if (m) + return _resolveValue(m[1].trim()) > parseInt(m[2]); + + // Simple property path — resolve and return truthiness + const val = _resolveValue(expr); + if (val !== undefined) + return !!val; + + // Unrecognized expression — assume visible + return true; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/UI/ToastService.qml b/fedora/.config/quickshell/noctalia-shell/Services/UI/ToastService.qml new file mode 100644 index 0000000..93126c3 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/UI/ToastService.qml @@ -0,0 +1,31 @@ +pragma Singleton + +import QtQuick +import Quickshell + +Singleton { + id: root + + // Simple signal-based notification system + // actionLabel: optional label for clickable action link + // actionCallback: optional function to call when action is clicked + signal notify(string title, string description, string icon, string type, int duration, string actionLabel, var actionCallback) + signal dismiss + + // Convenience methods + function showNotice(title, description = "", icon = "", duration = 3000, actionLabel = "", actionCallback = null) { + notify(title, description, icon, "notice", duration, actionLabel, actionCallback); + } + + function showWarning(title, description = "", duration = 4000, actionLabel = "", actionCallback = null) { + notify(title, description, "", "warning", duration, actionLabel, actionCallback); + } + + function showError(title, description = "", duration = 6000, actionLabel = "", actionCallback = null) { + notify(title, description, "", "error", duration, actionLabel, actionCallback); + } + + function dismissToast() { + dismiss(); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/UI/TooltipService.qml b/fedora/.config/quickshell/noctalia-shell/Services/UI/TooltipService.qml new file mode 100644 index 0000000..8325855 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/UI/TooltipService.qml @@ -0,0 +1,133 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons +import qs.Modules.Tooltip + +Singleton { + id: root + + property var activeTooltip: null + property var pendingTooltip: null // Track tooltip being created + + property Component tooltipComponent: Component { + Tooltip {} + } + + function show(target, content, direction, delay, fontFamily) { + if (!Settings.data.ui.tooltipsEnabled) { + return; + } + + // Don't create if no content + if (!target || !content || (Array.isArray(content) && content.length === 0)) { + Logger.i("Tooltip", "No target or content"); + return; + } + + // Cancel any pending tooltip (same or different target) + if (pendingTooltip) { + pendingTooltip.hideImmediately(); + pendingTooltip.destroy(); + pendingTooltip = null; + } + + // If we have an active tooltip for a different target, hide it + if (activeTooltip && activeTooltip.targetItem !== target) { + activeTooltip.hideImmediately(); + // Don't destroy immediately - let it clean itself up + activeTooltip = null; + } + + // If we already have a tooltip for this target, just update it + if (activeTooltip && activeTooltip.targetItem === target) { + activeTooltip.updateContent(content); + return activeTooltip; + } + + // Create new tooltip instance + const newTooltip = tooltipComponent.createObject(null); + + if (newTooltip) { + // Track as pending until it's visible + pendingTooltip = newTooltip; + + // Connect cleanup when tooltip hides + newTooltip.visibleChanged.connect(() => { + if (!newTooltip.visible) { + // Clean up after a delay to avoid interfering with new tooltips + Qt.callLater(() => { + if (newTooltip && !newTooltip.visible) { + if (activeTooltip === newTooltip) { + activeTooltip = null; + } + if (pendingTooltip === newTooltip) { + pendingTooltip = null; + } + newTooltip.destroy(); + } + }); + } else { + // Tooltip is now visible, move from pending to active + if (pendingTooltip === newTooltip) { + activeTooltip = newTooltip; + pendingTooltip = null; + } + } + }); + + // Show the tooltip + newTooltip.show(target, content, direction || "auto", delay || Style.tooltipDelay, fontFamily); + + return newTooltip; + } else { + Logger.e("Tooltip", "Failed to create tooltip instance"); + } + + return null; + } + + function hide(target) { + // If target is provided, only hide if tooltip belongs to that target + if (target) { + if (pendingTooltip && pendingTooltip.targetItem === target) { + pendingTooltip.hide(); + } + if (activeTooltip && activeTooltip.targetItem === target) { + activeTooltip.hide(); + } + } else { + if (pendingTooltip) { + pendingTooltip.hide(); + } + if (activeTooltip) { + activeTooltip.hide(); + } + } + } + + function hideImmediately() { + if (pendingTooltip) { + pendingTooltip.hideImmediately(); + pendingTooltip.destroy(); + pendingTooltip = null; + } + if (activeTooltip) { + activeTooltip.hideImmediately(); + activeTooltip.destroy(); + activeTooltip = null; + } + } + + function updateContent(newContent) { + if (activeTooltip) { + activeTooltip.updateContent(newContent); + } + } + + // Backward compatibility alias + function updateText(newText) { + updateContent(newText); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/UI/WallhavenService.qml b/fedora/.config/quickshell/noctalia-shell/Services/UI/WallhavenService.qml new file mode 100644 index 0000000..f10d84e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/UI/WallhavenService.qml @@ -0,0 +1,274 @@ +pragma Singleton + +import QtQuick +import Quickshell +import qs.Commons + +Singleton { + id: root + + // State + property bool fetching: false + property bool initialSearchScheduled: false + property var currentResults: [] + property var currentMeta: ({}) + property string lastError: "" + property string currentQuery: "" + property int currentPage: 1 + property int lastPage: 1 + + // Search parameters + property string categories: "111" // general,anime,people (all enabled by default) + property string purity: "100" // sfw + property string sorting: "relevance" // date_added, relevance, random, views, favorites, toplist + property string order: "desc" // desc, asc + property string topRange: "1M" // 1d, 3d, 1w, 1M, 3M, 6M, 1y + property string seed: "" // For random sorting + property string minResolution: "" // e.g., "1920x1080" + property string resolutions: "" // e.g., "1920x1080,1920x1200" + property string ratios: "" // e.g., "16x9,16x10" + property string colors: "" // Color hex codes + // API Key Priority: Environment Variable > Local Settings + readonly property string envApiKey: Quickshell.env("NOCTALIA_WALLHAVEN_API_KEY") || "" + readonly property string apiKey: envApiKey !== "" ? envApiKey : (Settings.data.wallpaper.wallhavenApiKey || "") + readonly property bool apiKeyManagedByEnv: envApiKey !== "" + + // Signals + signal searchCompleted(var results, var meta) + signal searchFailed(string error) + signal wallpaperDownloaded(string wallpaperId, string localPath) + + // Base API URL + readonly property string apiBaseUrl: "https://wallhaven.cc/api/v1" + + // ------------------------------------------------- + function search(query, page) { + if (fetching) { + return; + } + + // Reset initial search flag once we start a search + if (initialSearchScheduled) { + initialSearchScheduled = false; + } + + fetching = true; + lastError = ""; + currentQuery = query || ""; + currentPage = page || 1; + + var url = apiBaseUrl + "/search"; + var params = []; + + if (currentQuery) { + params.push("q=" + encodeURIComponent(currentQuery)); + } + + params.push("categories=" + categories); + // Safety: Force SFW if no purity selected to prevent API error + var safePurity = (purity === "000") ? "100" : purity; + params.push("purity=" + safePurity); + params.push("sorting=" + sorting); + params.push("order=" + order); + + if (sorting === "toplist") { + params.push("topRange=" + topRange); + } + + if (sorting === "random" && seed) { + params.push("seed=" + seed); + } + + if (minResolution) { + params.push("atleast=" + minResolution); + } + + if (resolutions) { + params.push("resolutions=" + resolutions); + } + + if (ratios) { + params.push("ratios=" + ratios); + } + + if (colors) { + params.push("colors=" + colors); + } + + if (apiKey) { + params.push("apikey=" + apiKey); + } + + params.push("page=" + currentPage); + + url += "?" + params.join("&"); + + Logger.d("Wallhaven", "Searching:", url); + + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function () { + if (xhr.readyState === XMLHttpRequest.DONE) { + fetching = false; + if (xhr.status === 200) { + try { + var response = JSON.parse(xhr.responseText); + if (response.data && Array.isArray(response.data)) { + currentResults = response.data; + currentMeta = response.meta || {}; + lastPage = currentMeta.last_page || 1; + + // Store seed for random sorting + if (currentMeta.seed) { + seed = currentMeta.seed; + } + + Logger.d("Wallhaven", "Search completed:", currentResults.length, "results, page", currentPage, "of", lastPage); + searchCompleted(currentResults, currentMeta); + } else { + var errorMsg = "Invalid API response"; + lastError = errorMsg; + Logger.e("Wallhaven", errorMsg); + searchFailed(errorMsg); + } + } catch (e) { + var errorMsg = "Failed to parse API response: " + e.toString(); + lastError = errorMsg; + Logger.e("Wallhaven", errorMsg); + searchFailed(errorMsg); + } + } else if (xhr.status === 429) { + var errorMsg = "Rate limit exceeded (45 requests/minute)"; + lastError = errorMsg; + Logger.w("Wallhaven", errorMsg); + searchFailed(errorMsg); + } else if (xhr.status === 401) { + var errorMsg = "Invalid API Key. Please check your settings."; + lastError = errorMsg; + Logger.e("Wallhaven", errorMsg); + searchFailed(errorMsg); + } else { + var errorMsg = "API error: " + xhr.status; + lastError = errorMsg; + Logger.e("Wallhaven", "Search failed:", errorMsg); + searchFailed(errorMsg); + } + } + }; + + xhr.open("GET", url); + xhr.send(); + } + + // ------------------------------------------------- + function getWallpaperUrl(wallpaper) { + // Use the 'path' field which contains the full resolution image URL + if (wallpaper.path) { + return wallpaper.path; + } + // Fallback to constructing URL from ID + if (wallpaper.id) { + var idPrefix = wallpaper.id.substring(0, 2); + return "https://w.wallhaven.cc/full/" + idPrefix + "/wallhaven-" + wallpaper.id + ".jpg"; + } + return ""; + } + + // ------------------------------------------------- + function getThumbnailUrl(wallpaper, size) { + // size: "small", "large", "original" + if (wallpaper.thumbs && wallpaper.thumbs[size]) { + return wallpaper.thumbs[size]; + } + // Fallback + if (wallpaper.id) { + var idPrefix = wallpaper.id.substring(0, 2); + var sizeMap = { + "small": "small", + "large": "lg", + "original": "orig" + }; + var sizePath = sizeMap[size] || "lg"; + return "https://th.wallhaven.cc/" + sizePath + "/" + idPrefix + "/" + wallpaper.id + ".jpg"; + } + return ""; + } + + // ------------------------------------------------- + function downloadWallpaper(wallpaper, callback) { + var url = getWallpaperUrl(wallpaper); + if (!url) { + Logger.e("Wallhaven", "No URL available for wallpaper", wallpaper.id); + if (callback) + callback(false, ""); + return; + } + + var wallpaperId = wallpaper.id; + + // Get the user's wallpaper directory + var wallpaperDir = Settings.preprocessPath(Settings.data.wallpaper.directory); + if (!wallpaperDir || wallpaperDir === "") { + wallpaperDir = Settings.defaultWallpapersDirectory; + } + + // Ensure directory ends with / + if (!wallpaperDir.endsWith("/")) { + wallpaperDir += "/"; + } + + var localPath = wallpaperDir + "wallhaven_" + wallpaperId + ".jpg"; + + Logger.d("Wallhaven", "Downloading wallpaper", wallpaperId, "to", localPath); + + // Use curl or wget to download the file, ensuring directory exists first + var downloadProcess = Qt.createQmlObject(` + import QtQuick + import Quickshell.Io + Process { + id: downloadProcess + command: ["sh", "-c", "mkdir -p '` + wallpaperDir + `' && (curl -L -s -o '` + localPath + `' '` + url + `' || wget -q -O '` + localPath + `' '` + url + `')"] + } + `, root, "DownloadProcess_" + wallpaperId); + + downloadProcess.exited.connect(function (exitCode) { + if (exitCode === 0) { + Logger.i("Wallhaven", "Wallpaper downloaded:", localPath); + wallpaperDownloaded(wallpaperId, localPath); + if (callback) + callback(true, localPath); + } else { + Logger.e("Wallhaven", "Failed to download wallpaper, exit code:", exitCode); + if (callback) + callback(false, ""); + } + downloadProcess.destroy(); + }); + + downloadProcess.running = true; + } + + // ------------------------------------------------- + function reset() { + currentResults = []; + currentMeta = {}; + currentQuery = ""; + currentPage = 1; + lastPage = 1; + seed = ""; + lastError = ""; + } + + // ------------------------------------------------- + function nextPage() { + if (currentPage < lastPage && !fetching) { + search(currentQuery, currentPage + 1); + } + } + + // ------------------------------------------------- + function previousPage() { + if (currentPage > 1 && !fetching) { + search(currentQuery, currentPage - 1); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Services/UI/WallpaperService.qml b/fedora/.config/quickshell/noctalia-shell/Services/UI/WallpaperService.qml new file mode 100644 index 0000000..fc8e849 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Services/UI/WallpaperService.qml @@ -0,0 +1,1683 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Services.Power +import qs.Services.Theming +import qs.Services.UI + +Singleton { + id: root + + readonly property ListModel fillModeModel: ListModel {} + readonly property string defaultDirectory: Settings.preprocessPath(Settings.data.wallpaper.directory) + readonly property string solidColorPrefix: "solid://" + + // All available wallpaper transitions + readonly property ListModel transitionsModel: ListModel {} + + // All transition keys but filter out "none" and "random" so we are left with the real transitions + readonly property var allTransitions: Array.from({ + "length": transitionsModel.count + }, (_, i) => transitionsModel.get(i).key).filter(key => key !== "random" && key != "none") + + property var wallpaperLists: ({}) + property int scanningCount: 0 + + // Cache for current wallpapers - can be updated directly since we use signals for notifications + property var currentWallpapers: ({}) + + // Track current alphabetical index for each screen + property var alphabeticalIndices: ({}) + + // Track used wallpapers for random mode (persisted across reboots) + property var usedRandomWallpapers: ({}) + + property bool isInitialized: false + property string wallpaperCacheFile: "" + + readonly property bool scanning: (scanningCount > 0) + readonly property string noctaliaDefaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png" + property string defaultWallpaper: noctaliaDefaultWallpaper + + // Signals for reactive UI updates + signal wallpaperChanged(string screenName, string path) + // Emitted when a wallpaper changes + signal wallpaperProcessingComplete(string screenName, string path, string cachedPath) + // Emitted when wallpaper processing (resize/cache) is complete. cachedPath is the resized version. + signal wallpaperDirectoryChanged(string screenName, string directory) + // Emitted when a monitor's directory changes + signal wallpaperListChanged(string screenName, int count) + + // Emitted when available wallpapers list changes + + // Browse mode: track current browse path per screen (separate from root directory) + property var currentBrowsePaths: ({}) + + // Wallpaper panel: which appearance slot (light/dark) new selections apply to — like picking a monitor tab + property string wallpaperSelectionAppearance: "light" + + // Bumped when favorites are added/removed so grid delegates can refresh star state + property int favoritesRevision: 0 + + // After favoriting, refresh snapshot once theme colors finish transitioning + property var pendingFavoriteSchemeRefresh: null + + // Signal emitted when browse path changes for a screen + signal browsePathChanged(string screenName, string path) + + Timer { + id: favoriteSchemeDebounceTimer + interval: 450 + repeat: false + property string pendingPath: "" + property string pendingSlot: "" + onTriggered: { + var p = pendingPath; + var s = pendingSlot; + pendingPath = ""; + pendingSlot = ""; + if (p && root.isFavorite(p)) { + root.updateFavoriteColorScheme(p, s); + } + } + } + + function scheduleFavoriteSchemeSnapshot(path, slot) { + Qt.callLater(function () { + if (root.isFavorite(path)) { + root.updateFavoriteColorScheme(path, slot); + } + }); + favoriteSchemeDebounceTimer.pendingPath = path; + favoriteSchemeDebounceTimer.pendingSlot = slot; + favoriteSchemeDebounceTimer.restart(); + if (Color.isTransitioning) { + root.pendingFavoriteSchemeRefresh = { + "path": path, + "slot": slot + }; + } + } + + Connections { + target: Settings.data.wallpaper + function onDirectoryChanged() { + root.usedRandomWallpapers = {}; + root.refreshWallpapersList(); + // Emit directory change signals for monitors using the default directory + if (!Settings.data.wallpaper.enableMultiMonitorDirectories) { + // All monitors use the main directory + for (var i = 0; i < Quickshell.screens.length; i++) { + root.wallpaperDirectoryChanged(Quickshell.screens[i].name, root.defaultDirectory); + } + } else { + // Only monitors without custom directories are affected + for (var i = 0; i < Quickshell.screens.length; i++) { + var screenName = Quickshell.screens[i].name; + var monitor = root.getMonitorConfig(screenName); + if (!monitor || !monitor.directory) { + root.wallpaperDirectoryChanged(screenName, root.defaultDirectory); + } + } + } + } + function onEnableMultiMonitorDirectoriesChanged() { + root.usedRandomWallpapers = {}; + root.refreshWallpapersList(); + // Notify all monitors about potential directory changes + for (var i = 0; i < Quickshell.screens.length; i++) { + var screenName = Quickshell.screens[i].name; + root.wallpaperDirectoryChanged(screenName, root.getMonitorDirectory(screenName)); + } + } + function onAutomationEnabledChanged() { + root.toggleRandomWallpaper(); + } + function onRandomIntervalSecChanged() { + root.restartRandomWallpaperTimer(); + } + function onWallpaperChangeModeChanged() { + // Reset alphabetical indices when mode changes + root.alphabeticalIndices = {}; + if (Settings.data.wallpaper.automationEnabled) { + root.restartRandomWallpaperTimer(); + root.setNextWallpaper(); + } + } + function onViewModeChanged() { + // Reset browse paths to root when mode changes + root.currentBrowsePaths = {}; + root.refreshWallpapersList(); + } + function onShowHiddenFilesChanged() { + root.refreshWallpapersList(); + } + function onUseSolidColorChanged() { + if (Settings.data.wallpaper.useSolidColor) { + var solidPath = root.createSolidColorPath(Settings.data.wallpaper.solidColor.toString()); + for (var i = 0; i < Quickshell.screens.length; i++) { + root.wallpaperChanged(Quickshell.screens[i].name, solidPath); + } + } else { + for (var i = 0; i < Quickshell.screens.length; i++) { + var screenName = Quickshell.screens[i].name; + root.wallpaperChanged(screenName, root.getWallpaper(screenName) || root.defaultWallpaper); + } + } + } + function onSolidColorChanged() { + if (Settings.data.wallpaper.useSolidColor) { + var solidPath = root.createSolidColorPath(Settings.data.wallpaper.solidColor.toString()); + for (var i = 0; i < Quickshell.screens.length; i++) { + root.wallpaperChanged(Quickshell.screens[i].name, solidPath); + } + } + } + function onSortOrderChanged() { + root.refreshWallpapersList(); + } + function onLinkLightAndDarkWallpapersChanged() { + if (Settings.data.wallpaper.linkLightAndDarkWallpapers) { + root.wallpaperSelectionAppearance = Settings.data.colorSchemes.darkMode ? "dark" : "light"; + root._syncWallpaperSlotsWhenLinking(); + } + root._notifyAllWallpapersChanged(); + } + } + + Connections { + target: Settings.data.colorSchemes + function onDarkModeChanged() { + if (Settings.data.wallpaper.linkLightAndDarkWallpapers) { + root.wallpaperSelectionAppearance = Settings.data.colorSchemes.darkMode ? "dark" : "light"; + } + // Restore scheme from favorite for this light/dark slot before wallpaper refresh + root.reapplyFavoriteThemeForActiveWallpaper(); + root._notifyAllWallpapersChanged(); + } + } + + Connections { + target: WallhavenService + function onWallpaperDownloaded() { + root.refreshWallpapersList(); + } + } + + // ------------------------------------------------- + function init() { + Logger.i("Wallpaper", "Service started"); + + translateModels(); + Qt.callLater(root._dedupeWallpaperFavoritesByPath); + + // Initialize cache file path + Qt.callLater(() => { + if (typeof Settings !== 'undefined' && Settings.cacheDir) { + wallpaperCacheFile = Settings.cacheDir + "wallpapers.json"; + wallpaperCacheView.path = wallpaperCacheFile; + } + }); + + // Note: isInitialized will be set to true in wallpaperCacheView.onLoaded + Logger.d("Wallpaper", "Triggering initial wallpaper scan"); + Qt.callLater(refreshWallpapersList); + } + + // Cache restore updates currentWallpapers without _setWallpaper, so wallpaperChanged does not fire. + function _scheduleThemeSyncFromCachedWallpaper() { + Qt.callLater(function () { + root.reapplyFavoriteThemeForActiveWallpaper(); + if (Settings.data.colorSchemes.useWallpaperColors) { + AppThemeService.generate(); + } + }); + } + + // ------------------------------------------------- + function translateModels() { + // Wait for i18n to be ready by retrying every time + if (!I18n.isLoaded) { + Qt.callLater(translateModels); + return; + } + + // Populate fillModeModel with translated names + fillModeModel.append({ + "key": "center", + "name": I18n.tr("positions.center"), + "uniform": 0.0 + }); + fillModeModel.append({ + "key": "crop", + "name": I18n.tr("wallpaper.fill-modes.crop"), + "uniform": 1.0 + }); + fillModeModel.append({ + "key": "fit", + "name": I18n.tr("wallpaper.fill-modes.fit"), + "uniform": 2.0 + }); + fillModeModel.append({ + "key": "stretch", + "name": I18n.tr("wallpaper.fill-modes.stretch"), + "uniform": 3.0 + }); + fillModeModel.append({ + "key": "repeat", + "name": I18n.tr("wallpaper.fill-modes.repeat"), + "uniform": 4.0 + }); + + // Populate transitionsModel with translated names + transitionsModel.append({ + "key": "none", + "name": I18n.tr("common.none") + }); + transitionsModel.append({ + "key": "random", + "name": I18n.tr("common.random") + }); + transitionsModel.append({ + "key": "fade", + "name": I18n.tr("wallpaper.transitions.fade") + }); + transitionsModel.append({ + "key": "disc", + "name": I18n.tr("wallpaper.transitions.disc") + }); + transitionsModel.append({ + "key": "stripes", + "name": I18n.tr("wallpaper.transitions.stripes") + }); + transitionsModel.append({ + "key": "wipe", + "name": I18n.tr("wallpaper.transitions.wipe") + }); + transitionsModel.append({ + "key": "pixelate", + "name": I18n.tr("wallpaper.transitions.pixelate") + }); + transitionsModel.append({ + "key": "honeycomb", + "name": I18n.tr("wallpaper.transitions.honeycomb") + }); + } + + // ------------------------------------------------------------------- + function getFillModeUniform() { + for (var i = 0; i < fillModeModel.count; i++) { + const mode = fillModeModel.get(i); + if (mode.key === Settings.data.wallpaper.fillMode) { + return mode.uniform; + } + } + // Fallback to crop + return 1.0; + } + + // ------------------------------------------------------------------- + // Solid color helpers + // ------------------------------------------------------------------- + function isSolidColorPath(path) { + return path && typeof path === "string" && path.startsWith(solidColorPrefix); + } + + function getSolidColor(path) { + if (!isSolidColorPath(path)) { + return null; + } + return path.substring(solidColorPrefix.length); + } + + function createSolidColorPath(colorString) { + return solidColorPrefix + colorString; + } + + function setSolidColor(colorString) { + Settings.data.wallpaper.solidColor = colorString; + Settings.data.wallpaper.useSolidColor = true; + } + + // ------------------------------------------------------------------- + // Per-screen wallpaper: persisted as { light, dark } (legacy string loads are normalized) + // ------------------------------------------------------------------- + function _isSplitWallpaperEntry(entry) { + if (!entry || typeof entry !== "object") { + return false; + } + return entry.light !== undefined || entry.dark !== undefined; + } + + function _pathsFromEntry(entry) { + if (!entry) { + return { + light: "", + dark: "" + }; + } + if (typeof entry === "string") { + return { + light: entry, + dark: entry + }; + } + return { + light: entry.light || "", + dark: entry.dark || "" + }; + } + + function _cloneWallpaperEntry(entry) { + if (typeof entry === "string") { + return { + light: entry, + dark: entry + }; + } + var p = _pathsFromEntry(entry); + return { + light: p.light, + dark: p.dark + }; + } + + function _entriesEqual(a, b) { + if (a === b) { + return true; + } + if (typeof a === "string" && typeof b === "string") { + return a === b; + } + if (typeof a === "string" || typeof b === "string") { + return false; + } + if (!_isSplitWallpaperEntry(a) || !_isSplitWallpaperEntry(b)) { + return false; + } + var pa = _pathsFromEntry(a); + var pb = _pathsFromEntry(b); + return pa.light === pb.light && pa.dark === pb.dark; + } + + function _entryToEffectivePath(entry) { + if (!entry) { + return ""; + } + if (typeof entry === "string") { + return entry; + } + var p = _pathsFromEntry(entry); + if (Settings.data.colorSchemes.darkMode) { + return p.dark || p.light || ""; + } + return p.light || p.dark || ""; + } + + function _normalizeAppearanceSlot(slot) { + return slot === "dark" ? "dark" : "light"; + } + + function _defaultAppearanceSlotForChange(slot) { + if (slot === "light" || slot === "dark") { + return slot; + } + return Settings.data.colorSchemes.darkMode ? "dark" : "light"; + } + + function getWallpaperPathForSlot(screenName, appearanceSlot) { + if (Settings.data.wallpaper.useSolidColor) { + return createSolidColorPath(Settings.data.wallpaper.solidColor.toString()); + } + var slot = _normalizeAppearanceSlot(appearanceSlot); + var entry = currentWallpapers[screenName]; + if (!entry) { + return ""; + } + var p = _pathsFromEntry(entry); + if (slot === "dark") { + return p.dark || p.light || ""; + } + return p.light || p.dark || ""; + } + + function getWallpapersEffectiveMap() { + var out = {}; + for (var i = 0; i < Quickshell.screens.length; i++) { + var n = Quickshell.screens[i].name; + out[n] = getWallpaper(n); + } + return out; + } + + function _ensureObjectWallpaperEntries() { + var names = {}; + Object.keys(currentWallpapers).forEach(function (k) { + names[k] = true; + }); + for (var i = 0; i < Quickshell.screens.length; i++) { + names[Quickshell.screens[i].name] = true; + } + Object.keys(names).forEach(function (name) { + var e = currentWallpapers[name]; + if (!e) { + return; + } + if (typeof e === "string") { + if (Settings.data.wallpaper.linkLightAndDarkWallpapers) { + currentWallpapers[name] = { + light: e, + dark: e + }; + } else { + currentWallpapers[name] = { + light: e, + dark: e + }; + } + } else if (_isSplitWallpaperEntry(e)) { + var p = _pathsFromEntry(e); + if (Settings.data.wallpaper.linkLightAndDarkWallpapers) { + currentWallpapers[name] = { + light: p.light || p.dark || "", + dark: p.dark || p.light || "" + }; + } else { + currentWallpapers[name] = { + light: p.light || "", + dark: p.dark || "" + }; + } + } + }); + saveTimer.restart(); + } + + function _notifyAllWallpapersChanged() { + for (var i = 0; i < Quickshell.screens.length; i++) { + var n = Quickshell.screens[i].name; + root.wallpaperChanged(n, getWallpaper(n)); + } + } + + // ------------------------------------------------------------------- + // Get specific monitor wallpaper data + function getMonitorConfig(screenName) { + var monitors = Settings.data.wallpaper.monitorDirectories; + if (monitors !== undefined) { + for (var i = 0; i < monitors.length; i++) { + if (monitors[i].name !== undefined && monitors[i].name === screenName) { + return monitors[i]; + } + } + } + } + + // ------------------------------------------------------------------- + // Get specific monitor directory + function getMonitorDirectory(screenName) { + if (!Settings.data.wallpaper.enableMultiMonitorDirectories) { + return root.defaultDirectory; + } + + var monitor = getMonitorConfig(screenName); + if (monitor !== undefined && monitor.directory !== undefined) { + return Settings.preprocessPath(monitor.directory); + } + + // Fall back to the main/single directory + return root.defaultDirectory; + } + + // ------------------------------------------------------------------- + // Set specific monitor directory + function setMonitorDirectory(screenName, directory) { + var monitors = Settings.data.wallpaper.monitorDirectories || []; + var found = false; + + // Create a new array with updated values + var newMonitors = monitors.map(function (monitor) { + if (monitor.name === screenName) { + found = true; + return { + "name": screenName, + "directory": directory, + "wallpaper": monitor.wallpaper || "" + }; + } + return monitor; + }); + + if (!found) { + newMonitors.push({ + "name": screenName, + "directory": directory, + "wallpaper": "" + }); + } + + // Update Settings with new array to ensure proper persistence + Settings.data.wallpaper.monitorDirectories = newMonitors.slice(); + root.wallpaperDirectoryChanged(screenName, Settings.preprocessPath(directory)); + } + + // ------------------------------------------------------------------- + // Get specific monitor wallpaper - now from cache + function getWallpaper(screenName) { + // Return solid color path when in solid color mode + if (Settings.data.wallpaper.useSolidColor) { + return createSolidColorPath(Settings.data.wallpaper.solidColor.toString()); + } + var entry = currentWallpapers[screenName]; + if (entry) { + var effective = _entryToEffectivePath(entry); + if (effective) { + return effective; + } + } + + // Try to inherit wallpaper from another active screen + var inherited = _inheritWallpaperFromExistingScreen(screenName); + if (inherited) { + return inherited; + } + + return root.defaultWallpaper; + } + + // ------------------------------------------------------------------- + function changeWallpaper(path, screenName, appearanceSlot) { + // Turn off solid color mode when selecting a wallpaper + if (Settings.data.wallpaper.useSolidColor) { + Settings.data.wallpaper.useSolidColor = false; + } + + var slot = _defaultAppearanceSlotForChange(appearanceSlot); + + // Save current favorite color schemes before switching away. + // This must happen before applyFavoriteTheme (called by the UI) + // overwrites the settings that _createFavoriteEntry reads. + _saveOutgoingFavorites(path, screenName, slot); + + if (screenName !== undefined) { + _setWallpaper(screenName, path, slot); + } else { + var allScreenNames = new Set(Object.keys(currentWallpapers)); + for (var i = 0; i < Quickshell.screens.length; i++) { + allScreenNames.add(Quickshell.screens[i].name); + } + allScreenNames.forEach(name => _setWallpaper(name, path, slot)); + } + } + + // ------------------------------------------------------------------- + // Save the color scheme of any favorited wallpapers that are about + // to be replaced, while the current settings still reflect them. + function _saveOutgoingFavorites(newPath, screenName, appearanceSlot) { + var paths = []; + var slot = _normalizeAppearanceSlot(appearanceSlot); + + function collectFromEntry(e) { + if (!e) { + return; + } + if (typeof e === "string") { + if (e && e !== newPath) { + paths.push(e); + } + return; + } + if (!_isSplitWallpaperEntry(e)) { + return; + } + var p = _pathsFromEntry(e); + if (Settings.data.wallpaper.linkLightAndDarkWallpapers) { + if (p.light && p.light !== newPath) { + paths.push(p.light); + } + if (p.dark && p.dark !== newPath && p.dark !== p.light) { + paths.push(p.dark); + } + } else { + var old = slot === "dark" ? p.dark : p.light; + if (old && old !== newPath) { + paths.push(old); + } + } + } + + if (screenName !== undefined) { + collectFromEntry(currentWallpapers[screenName]); + } else { + var names = new Set(Object.keys(currentWallpapers)); + for (var i = 0; i < Quickshell.screens.length; i++) { + names.add(Quickshell.screens[i].name); + } + names.forEach(function (name) { + collectFromEntry(currentWallpapers[name]); + }); + } + + var unique = []; + for (var j = 0; j < paths.length; j++) { + if (unique.indexOf(paths[j]) === -1) { + unique.push(paths[j]); + } + } + + unique.forEach(function (path) { + if (!path || path === newPath) { + return; + } + var favIdx = _findAnyFavoriteIndexForPath(path); + if (favIdx === _favoriteNotFound) { + return; + } + var app = _favoriteAppearanceSlot(Settings.data.wallpaper.favorites[favIdx]); + updateFavoriteColorScheme(path, app); + }); + } + + // ------------------------------------------------------------------- + function _inheritWallpaperFromExistingScreen(screenName) { + for (var i = 0; i < Quickshell.screens.length; i++) { + var otherName = Quickshell.screens[i].name; + if (otherName === screenName) { + continue; + } + var entry = currentWallpapers[otherName]; + if (!entry) { + continue; + } + var cloned = _cloneWallpaperEntry(entry); + if (_entriesEqual(currentWallpapers[screenName], cloned)) { + return _entryToEffectivePath(cloned); + } + currentWallpapers[screenName] = cloned; + saveTimer.restart(); + root.wallpaperChanged(screenName, _entryToEffectivePath(cloned)); + if (randomWallpaperTimer.running) { + randomWallpaperTimer.restart(); + } + return _entryToEffectivePath(cloned); + } + return ""; + } + + function _syncWallpaperSlotsWhenLinking() { + var names = new Set(Object.keys(currentWallpapers)); + for (var i = 0; i < Quickshell.screens.length; i++) { + names.add(Quickshell.screens[i].name); + } + names.forEach(function (name) { + var e = currentWallpapers[name]; + if (!e) { + return; + } + var eff = _entryToEffectivePath(e); + if (!eff) { + return; + } + var merged = { + light: eff, + dark: eff + }; + if (_entriesEqual(e, merged)) { + return; + } + currentWallpapers[name] = merged; + saveTimer.restart(); + root.wallpaperChanged(name, eff); + }); + if (randomWallpaperTimer.running) { + randomWallpaperTimer.restart(); + } + } + + function _setWallpaper(screenName, path, appearanceSlot) { + if (path === "" || path === undefined) { + return; + } + + if (screenName === undefined) { + Logger.w("Wallpaper", "setWallpaper", "no screen specified"); + return; + } + + var slot = _normalizeAppearanceSlot(appearanceSlot); + var oldEntry = currentWallpapers[screenName]; + var p = _pathsFromEntry(oldEntry); + var newEntry; + if (Settings.data.wallpaper.linkLightAndDarkWallpapers) { + newEntry = { + light: path, + dark: path + }; + } else if (slot === "dark") { + newEntry = { + light: p.light || "", + dark: path + }; + } else { + newEntry = { + light: path, + dark: p.dark || "" + }; + } + + if (_entriesEqual(oldEntry, newEntry)) { + return; + } + + currentWallpapers[screenName] = newEntry; + saveTimer.restart(); + root.wallpaperChanged(screenName, _entryToEffectivePath(newEntry)); + + if (randomWallpaperTimer.running) { + randomWallpaperTimer.restart(); + } + } + + // ------------------------------------------------------------------- + function setRandomWallpaper(screen) { + Logger.d("Wallpaper", "setRandomWallpaper"); + + if (Settings.data.wallpaper.enableMultiMonitorDirectories) { + if (screen === undefined) { + // Pick a random wallpaper per screen + for (var i = 0; i < Quickshell.screens.length; i++) { + var screenName = Quickshell.screens[i].name; + var wallpaperList = getWallpapersList(screenName); + + if (wallpaperList.length > 0) { + var randomPath = _pickUnusedRandom(screenName, wallpaperList); + changeWallpaper(randomPath, screenName); + } + } + } else { + // Pick a random wallpaper for the specified screen + var wallpaperList = getWallpapersList(screen); + if (wallpaperList.length > 0) { + var randomPath = _pickUnusedRandom(screen, wallpaperList); + changeWallpaper(randomPath, screen); + } + } + } else { + // Pick a random wallpaper common to all screens + // We can use any screenName here, so we just pick the primary one. + var wallpaperList = getWallpapersList(Screen.name); + if (wallpaperList.length > 0) { + var randomPath = _pickUnusedRandom("all", wallpaperList); + changeWallpaper(randomPath, screen); + } + } + } + + // ------------------------------------------------------------------- + // Pick a random wallpaper that hasn't been used yet in the current cycle. + // Once all wallpapers have been shown, resets the pool (keeping only the + // last-shown wallpaper to avoid an immediate repeat). + function _pickUnusedRandom(key, wallpaperList) { + var used = usedRandomWallpapers[key] || []; + + // Clean stale entries (files that were removed from the directory) + var wallpaperSet = new Set(wallpaperList); + used = used.filter(function (path) { + return wallpaperSet.has(path); + }); + + // Filter to wallpapers that haven't been used yet + var unused = wallpaperList.filter(function (path) { + return used.indexOf(path) === -1; + }); + + // If all have been used, reset but keep the last one to avoid immediate repeat + if (unused.length === 0) { + var lastUsed = used.length > 0 ? used[used.length - 1] : ""; + used = lastUsed ? [lastUsed] : []; + unused = wallpaperList.filter(function (path) { + return used.indexOf(path) === -1; + }); + // Edge case: only one wallpaper in the directory + if (unused.length === 0) { + unused = wallpaperList; + } + Logger.d("Wallpaper", "All wallpapers used for", key, "- resetting pool"); + } + + // Pick randomly from unused + var randomIndex = Math.floor(Math.random() * unused.length); + var picked = unused[randomIndex]; + + // Record as used + used.push(picked); + usedRandomWallpapers[key] = used; + + // Persist + saveTimer.restart(); + + return picked; + } + + // ------------------------------------------------------------------- + function setAlphabeticalWallpaper() { + Logger.d("Wallpaper", "setAlphabeticalWallpaper"); + + if (Settings.data.wallpaper.enableMultiMonitorDirectories) { + // Pick next alphabetical wallpaper per screen + for (var i = 0; i < Quickshell.screens.length; i++) { + var screenName = Quickshell.screens[i].name; + var wallpaperList = getWallpapersList(screenName); + + if (wallpaperList.length > 0) { + // Get or initialize index for this screen + if (alphabeticalIndices[screenName] === undefined) { + // Find current wallpaper in list to set initial index + var currentWallpaper = getWallpaper(screenName) || ""; + var foundIndex = wallpaperList.indexOf(currentWallpaper); + alphabeticalIndices[screenName] = (foundIndex >= 0) ? foundIndex : 0; + } + + // Get next index (wrap around) + var currentIndex = alphabeticalIndices[screenName]; + var nextIndex = (currentIndex + 1) % wallpaperList.length; + alphabeticalIndices[screenName] = nextIndex; + + var nextPath = wallpaperList[nextIndex]; + changeWallpaper(nextPath, screenName); + } + } + } else { + // Pick next alphabetical wallpaper common to all screens + var wallpaperList = getWallpapersList(Screen.name); + if (wallpaperList.length > 0) { + // Use primary screen name as key for single directory mode + var key = "all"; + if (alphabeticalIndices[key] === undefined) { + // Find current wallpaper in list to set initial index + var currentWallpaper = getWallpaper(Screen.name) || ""; + var foundIndex = wallpaperList.indexOf(currentWallpaper); + alphabeticalIndices[key] = (foundIndex >= 0) ? foundIndex : 0; + } + + // Get next index (wrap around) + var currentIndex = alphabeticalIndices[key]; + var nextIndex = (currentIndex + 1) % wallpaperList.length; + alphabeticalIndices[key] = nextIndex; + + var nextPath = wallpaperList[nextIndex]; + changeWallpaper(nextPath, undefined); + } + } + } + + // ------------------------------------------------------------------- + function toggleRandomWallpaper() { + Logger.d("Wallpaper", "toggleRandomWallpaper"); + if (Settings.data.wallpaper.automationEnabled) { + restartRandomWallpaperTimer(); + setNextWallpaper(); + } + } + + // ------------------------------------------------------------------- + function setNextWallpaper() { + var mode = Settings.data.wallpaper.wallpaperChangeMode || "random"; + if (mode === "alphabetical") { + setAlphabeticalWallpaper(); + } else { + setRandomWallpaper(); + } + } + + // ------------------------------------------------------------------- + function restartRandomWallpaperTimer() { + if (Settings.data.wallpaper.automationEnabled) { + randomWallpaperTimer.restart(); + } + } + + // ------------------------------------------------------------------- + function getWallpapersList(screenName) { + if (screenName != undefined && wallpaperLists[screenName] != undefined) { + return wallpaperLists[screenName]; + } + return []; + } + + // ------------------------------------------------------------------- + // Browse mode helper functions + // ------------------------------------------------------------------- + function getCurrentBrowsePath(screenName) { + if (currentBrowsePaths[screenName] !== undefined) { + var stored = currentBrowsePaths[screenName]; + var root = getMonitorDirectory(screenName); + if (root && stored.startsWith(root)) { + return stored; + } + // Stored path is outside the root directory, reset it + delete currentBrowsePaths[screenName]; + } + return getMonitorDirectory(screenName); + } + + function setBrowsePath(screenName, path) { + if (!screenName) + return; + currentBrowsePaths[screenName] = path; + browsePathChanged(screenName, path); + } + + function navigateUp(screenName) { + if (!screenName) + return; + var currentPath = getCurrentBrowsePath(screenName); + var rootPath = getMonitorDirectory(screenName); + + // Don't navigate if root is invalid or we're already at root + if (!rootPath || currentPath === rootPath) + return; + + // Get parent directory + var parentPath = currentPath.replace(/\/[^\/]+\/?$/, ""); + if (parentPath === "") + parentPath = rootPath; + + // Don't go above root + if (!parentPath.startsWith(rootPath)) { + parentPath = rootPath; + } + + setBrowsePath(screenName, parentPath); + } + + function navigateToRoot(screenName) { + if (!screenName) + return; + var rootPath = getMonitorDirectory(screenName); + setBrowsePath(screenName, rootPath); + } + + // Scan directory with optional directory listing (for browse mode) + // callback receives { files: [], directories: [] } + function scanDirectoryWithDirs(screenName, directory, callback) { + if (!directory || directory === "") { + callback({ + files: [], + directories: [] + }); + return; + } + + var result = { + files: [], + directories: [] + }; + var pendingScans = 2; + + function checkComplete() { + pendingScans--; + if (pendingScans === 0) { + // Files are already sorted by _scanDirectoryInternal according to sortOrder setting + // Only sort directories alphabetically + result.directories.sort(); + callback(result); + } + } + + // Scan for files + _scanDirectoryInternal(screenName, directory, false, false, function (files) { + result.files = files; + checkComplete(); + }); + + // Scan for directories + _scanForDirectories(directory, function (dirs) { + result.directories = dirs; + checkComplete(); + }); + } + + function _scanForDirectories(directory, callback) { + var findArgs = ["find", "-L", directory, "-maxdepth", "1", "-mindepth", "1", "-type", "d"]; + + var processString = ` + import QtQuick + import Quickshell.Io + Process { + id: process + command: ${JSON.stringify(findArgs)} + stdout: StdioCollector {} + stderr: StdioCollector {} + } + `; + + var processObject = Qt.createQmlObject(processString, root, "DirScan"); + + processObject.exited.connect(function (exitCode) { + var dirs = []; + if (exitCode === 0) { + var lines = processObject.stdout.text.split('\n'); + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (line !== '') { + var showHidden = Settings.data.wallpaper.showHiddenFiles; + var name = line.split('/').pop(); + if (showHidden || !name.startsWith('.')) { + dirs.push(line); + } + } + } + } + callback(dirs); + processObject.destroy(); + }); + + processObject.running = true; + } + + // ------------------------------------------------------------------- + function refreshWallpapersList() { + // Wait for imageMagickAvailable to be correctly set for ImageCacheService.imageFilters + if (!ImageCacheService.initialized) { + Qt.callLater(refreshWallpapersList); + return; + } + + var mode = Settings.data.wallpaper.viewMode; + Logger.d("Wallpaper", "refreshWallpapersList", "viewMode:", mode); + scanningCount = 0; + + if (mode === "recursive") { + // Use Process-based recursive search for all screens + for (var i = 0; i < Quickshell.screens.length; i++) { + var screenName = Quickshell.screens[i].name; + var directory = getMonitorDirectory(screenName); + scanDirectoryRecursive(screenName, directory); + } + } else if (mode === "browse") { + // Browse mode: scan current browse path (non-recursive) + // Note: The actual directory+subdirectory scanning happens in WallpaperPanel + // Here we just scan the current browse path for files + for (var i = 0; i < Quickshell.screens.length; i++) { + var screenName = Quickshell.screens[i].name; + var directory = getCurrentBrowsePath(screenName); + _scanDirectoryInternal(screenName, directory, false, true, null); + } + } else { + // Single directory mode (non-recursive) + for (var i = 0; i < Quickshell.screens.length; i++) { + var screenName = Quickshell.screens[i].name; + var directory = getMonitorDirectory(screenName); + _scanDirectoryInternal(screenName, directory, false, true, null); + } + } + } + + // Internal scan function + // recursive: whether to scan subdirectories + // updateList: whether to update wallpaperLists and emit signal + // callback: optional callback with files array + function _scanDirectoryInternal(screenName, directory, recursive, updateList, callback) { + if (!directory || directory === "") { + Logger.w("Wallpaper", "Empty directory for", screenName); + if (updateList) { + wallpaperLists[screenName] = []; + wallpaperListChanged(screenName, 0); + } + if (callback) + callback([]); + return; + } + + // Cancel any existing scan for this screen + if (recursiveProcesses[screenName]) { + Logger.d("Wallpaper", "Cancelling existing scan for", screenName); + recursiveProcesses[screenName].running = false; + recursiveProcesses[screenName].destroy(); + delete recursiveProcesses[screenName]; + if (updateList) + scanningCount--; + } + + if (updateList) + scanningCount++; + Logger.i("Wallpaper", "Starting scan for", screenName, "in", directory, "recursive:", recursive); + + // Build find command args dynamically from ImageCacheService filters + var filters = ImageCacheService.imageFilters; + var findArgs = ["find", "-L", directory]; + + // Add depth limit for non-recursive + if (!recursive) { + findArgs.push("-maxdepth", "1", "-mindepth", "1"); + } + + findArgs.push("-type", "f", "("); + for (var i = 0; i < filters.length; i++) { + if (i > 0) { + findArgs.push("-o"); + } + findArgs.push("-iname"); + findArgs.push(filters[i]); + } + findArgs.push(")"); + // Add printf to get modification time + findArgs.push("-printf", "%T@|%p\\n"); + + // Create Process component inline + var processString = ` + import QtQuick + import Quickshell.Io + Process { + id: process + command: ${JSON.stringify(findArgs)} + stdout: StdioCollector {} + stderr: StdioCollector {} + } + `; + + var processObject = Qt.createQmlObject(processString, root, "Scan_" + screenName); + + // Store reference to avoid garbage collection + if (updateList) { + recursiveProcesses[screenName] = processObject; + } + + var handler = function (exitCode) { + if (updateList) + scanningCount--; + Logger.d("Wallpaper", "Process exited with code", exitCode, "for", screenName); + + var files = []; + if (exitCode === 0) { + var lines = processObject.stdout.text.split('\n'); + var parsedFiles = []; + + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + if (line !== '') { + var parts = line.split('|'); + if (parts.length >= 2) { // Handle potential extra pipes in filename by joining rest + var timestamp = parseFloat(parts[0]); + var path = parts.slice(1).join('|'); + + var showHidden = Settings.data.wallpaper.showHiddenFiles; + var name = path.split('/').pop(); + if (showHidden || !name.startsWith('.')) { + parsedFiles.push({ + path: path, + time: timestamp, + name: name + }); + } + } else if (line.indexOf('|') === -1) { + // Fallback for unexpected output format or old find versions (unlikely but safe) + var path = line; + var showHidden = Settings.data.wallpaper.showHiddenFiles; + var name = path.split('/').pop(); + if (showHidden || !name.startsWith('.')) { + parsedFiles.push({ + path: path, + time: 0, + name: name + }); + } + } + } + } + // Sort files based on settings + var sortOrder = Settings.data.wallpaper.sortOrder || "name"; + + // Fischer-Yates shuffle + if (sortOrder === "random") { + for (let i = parsedFiles.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const temp = parsedFiles[i]; + parsedFiles[i] = parsedFiles[j]; + parsedFiles[j] = temp; + } + } else { + parsedFiles.sort(function (a, b) { + if (sortOrder === "date_desc") { // Newest first + return b.time - a.time; + } else if (sortOrder === "date_asc") { // Oldest first + return a.time - b.time; + } else if (sortOrder === "name_desc") { + return b.name.localeCompare(a.name); + } else { // name (asc) + return a.name.localeCompare(b.name); + } + }); + } + + // Map back to string array + files = parsedFiles.map(f => f.path); + + if (updateList) { + wallpaperLists[screenName] = files; + + // Reset alphabetical indices when list changes + if (alphabeticalIndices[screenName] !== undefined) { + var currentWallpaper = getWallpaper(screenName) || ""; + var foundIndex = files.indexOf(currentWallpaper); + alphabeticalIndices[screenName] = (foundIndex >= 0) ? foundIndex : 0; + } + + Logger.i("Wallpaper", "Scan completed for", screenName, "found", files.length, "files"); + wallpaperListChanged(screenName, files.length); + } + } else { + Logger.w("Wallpaper", "Scan failed for", screenName, "exit code:", exitCode, "(directory might not exist)"); + if (updateList) { + wallpaperLists[screenName] = []; + if (alphabeticalIndices[screenName] !== undefined) { + alphabeticalIndices[screenName] = 0; + } + wallpaperListChanged(screenName, 0); + } + } + + // Clean up + if (updateList) { + delete recursiveProcesses[screenName]; + } + + if (callback) + callback(files); + processObject.destroy(); + }; + + processObject.exited.connect(handler); + Logger.d("Wallpaper", "Starting process for", screenName); + processObject.running = true; + } + + // Process instances for scanning (one per screen) + property var recursiveProcesses: ({}) + + // ------------------------------------------------------------------- + function scanDirectoryRecursive(screenName, directory) { + _scanDirectoryInternal(screenName, directory, true, true, null); + } + + // ------------------------------------------------------------------- + // Favorites + // ------------------------------------------------------------------- + // TODO (~few weeks): Remove per-favorite `darkMode` (the boolean on each + // Settings.data.wallpaper.favorites[] entry). It duplicates `appearance` and is + // unrelated to Settings.data.colorSchemes.darkMode (global shell light/dark). + // Plan: one-time migration, then drop writes and the fallback in _favoriteAppearanceSlot. + // ------------------------------------------------------------------- + readonly property int _favoriteNotFound: -1 + + // ------------------------------------------------------------------- + function _favoriteAppearanceSlot(f) { + if (f.appearance === "light" || f.appearance === "dark") { + return f.appearance; + } + return f.darkMode ? "dark" : "light"; + } + + function _findFavoriteIndex(path, appearanceSlot) { + var favorites = Settings.data.wallpaper.favorites; + var searchPath = Settings.preprocessPath(path); + var slot = _normalizeAppearanceSlot(appearanceSlot); + for (var i = 0; i < favorites.length; i++) { + if (Settings.preprocessPath(favorites[i].path) !== searchPath) { + continue; + } + if (_favoriteAppearanceSlot(favorites[i]) === slot) { + return i; + } + } + return _favoriteNotFound; + } + + function _findAnyFavoriteIndexForPath(path) { + var favorites = Settings.data.wallpaper.favorites; + var searchPath = Settings.preprocessPath(path); + for (var i = 0; i < favorites.length; i++) { + if (Settings.preprocessPath(favorites[i].path) === searchPath) { + return i; + } + } + return _favoriteNotFound; + } + + function _dedupeWallpaperFavoritesByPath() { + var favorites = Settings.data.wallpaper.favorites; + if (!favorites || !favorites.length) { + return; + } + var seen = {}; + var out = []; + for (var i = 0; i < favorites.length; i++) { + var key = Settings.preprocessPath(favorites[i].path); + if (!key || seen[key]) { + continue; + } + seen[key] = true; + out.push(favorites[i]); + } + if (out.length !== favorites.length) { + Settings.data.wallpaper.favorites = out; + root.favoritesRevision++; + } + } + + // ------------------------------------------------------------------- + function _createFavoriteEntry(path, appearanceSlot) { + var app = _normalizeAppearanceSlot(appearanceSlot); + return { + "path": path, + "appearance": app, + "colorScheme": Settings.data.colorSchemes.predefinedScheme, + "darkMode": app === "dark" // TODO: remove per-favorite field (see Favorites section note) + , + "useWallpaperColors": Settings.data.colorSchemes.useWallpaperColors, + "generationMethod": Settings.data.colorSchemes.generationMethod, + "paletteColors": [Color.mPrimary.toString(), Color.mSecondary.toString(), Color.mTertiary.toString(), Color.mError.toString()] + }; + } + + // ------------------------------------------------------------------- + // Favorites are per (path, light|dark): at most one entry per path, tagged with the tab you starred from. + function isFavorite(path, appearanceSlot) { + if (appearanceSlot === undefined || appearanceSlot === null || appearanceSlot === "") { + return _findAnyFavoriteIndexForPath(path) !== _favoriteNotFound; + } + return _findFavoriteIndex(path, appearanceSlot) !== _favoriteNotFound; + } + + // Single favorite entry per path; use _favoriteAppearanceSlot(entry) for light vs dark it was starred under. + function favoriteEntryForPath(path) { + var idx = _findAnyFavoriteIndexForPath(path); + if (idx === _favoriteNotFound) { + return null; + } + return Settings.data.wallpaper.favorites[idx]; + } + + function getFavoriteForDisplay(path) { + return favoriteEntryForPath(path); + } + + // ------------------------------------------------------------------- + function toggleFavorite(path, appearanceSlot, screenName) { + var slot; + if (appearanceSlot !== undefined && appearanceSlot !== null && appearanceSlot !== "") { + slot = _normalizeAppearanceSlot(appearanceSlot); + } else { + slot = _normalizeAppearanceSlot(root.wallpaperSelectionAppearance); + } + var favorites = Settings.data.wallpaper.favorites.slice(); + var anyIdx = _findAnyFavoriteIndexForPath(path); + var applyWallpaperForSlot = false; + + if (anyIdx !== _favoriteNotFound) { + var existingSlot = _favoriteAppearanceSlot(favorites[anyIdx]); + if (existingSlot === slot) { + favorites.splice(anyIdx, 1); + Logger.d("Wallpaper", "Removed favorite:", path, slot); + if (favoriteSchemeDebounceTimer.pendingPath === path && favoriteSchemeDebounceTimer.pendingSlot === slot) { + favoriteSchemeDebounceTimer.stop(); + favoriteSchemeDebounceTimer.pendingPath = ""; + favoriteSchemeDebounceTimer.pendingSlot = ""; + } + if (root.pendingFavoriteSchemeRefresh && root.pendingFavoriteSchemeRefresh.path === path && root.pendingFavoriteSchemeRefresh.slot === slot) { + root.pendingFavoriteSchemeRefresh = null; + } + } else if (Settings.data.wallpaper.linkLightAndDarkWallpapers) { + favorites[anyIdx] = _createFavoriteEntry(path, slot); + Logger.d("Wallpaper", "Moved favorite to other appearance:", path, slot); + root.scheduleFavoriteSchemeSnapshot(path, slot); + applyWallpaperForSlot = true; + if (root.pendingFavoriteSchemeRefresh && root.pendingFavoriteSchemeRefresh.path === path) { + root.pendingFavoriteSchemeRefresh = { + "path": path, + "slot": slot + }; + } + } else { + // Separate light/dark wallpapers: star is remove-only (no sun/moon hint for "move" vs unfavorite). + favorites.splice(anyIdx, 1); + Logger.d("Wallpaper", "Removed favorite (star on other appearance tab, separate wallpapers):", path); + if (favoriteSchemeDebounceTimer.pendingPath === path) { + favoriteSchemeDebounceTimer.stop(); + favoriteSchemeDebounceTimer.pendingPath = ""; + favoriteSchemeDebounceTimer.pendingSlot = ""; + } + if (root.pendingFavoriteSchemeRefresh && root.pendingFavoriteSchemeRefresh.path === path) { + root.pendingFavoriteSchemeRefresh = null; + } + } + } else { + favorites.push(_createFavoriteEntry(path, slot)); + Logger.d("Wallpaper", "Added favorite:", path, slot); + root.scheduleFavoriteSchemeSnapshot(path, slot); + applyWallpaperForSlot = true; + } + + Settings.data.wallpaper.favorites = favorites; + root.favoritesRevision++; + + if (applyWallpaperForSlot) { + var scr; + if (Settings.data.wallpaper.setWallpaperOnAllMonitors) { + scr = undefined; + } else if (screenName !== undefined && screenName !== null && screenName !== "") { + scr = screenName; + } else if (Quickshell.screens.length > 0) { + scr = Quickshell.screens[0].name; + } else { + scr = undefined; + } + root.changeWallpaper(path, scr, slot); + root.applyFavoriteTheme(path, scr, slot); + } + + favoritesChanged(path); + } + + // Apply saved scheme from a favorite. Optional appearanceSlotOverride sets light vs dark target (UI tab or system mode). + function _applyFavoriteThemeFromEntry(favorite, appearanceSlotOverride) { + if (!favorite) { + return; + } + + var favApp; + if (appearanceSlotOverride !== undefined && appearanceSlotOverride !== null && appearanceSlotOverride !== "") { + favApp = _normalizeAppearanceSlot(appearanceSlotOverride); + } else { + favApp = _favoriteAppearanceSlot(favorite); + } + var targetDark = favApp === "dark"; + + var generationMethodChanging = Settings.data.colorSchemes.generationMethod !== favorite.generationMethod; + var darkModeChanging = Settings.data.colorSchemes.darkMode !== targetDark; + var useWallpaperColorsChanging = Settings.data.colorSchemes.useWallpaperColors !== favorite.useWallpaperColors; + + Settings.data.colorSchemes.useWallpaperColors = favorite.useWallpaperColors; + Settings.data.colorSchemes.predefinedScheme = favorite.colorScheme; + Settings.data.colorSchemes.generationMethod = favorite.generationMethod; + Settings.data.colorSchemes.darkMode = targetDark; + + // If nothing triggered AppThemeService via change handlers, regenerate once. + if (!generationMethodChanging && !darkModeChanging && !useWallpaperColorsChanging) { + AppThemeService.generate(); + } else if (!generationMethodChanging && !darkModeChanging && useWallpaperColorsChanging) { + AppThemeService.generate(); + } + } + + // When light/dark changes (or on startup), re-load scheme from the favorite for the wallpaper now shown for that slot. + function reapplyFavoriteThemeForActiveWallpaper() { + var effectiveMonitor = Settings.data.colorSchemes.monitorForColors; + if (effectiveMonitor === "" || effectiveMonitor === undefined) { + effectiveMonitor = Quickshell.screens.length > 0 ? Quickshell.screens[0].name : ""; + } + var wp = getWallpaper(effectiveMonitor); + if (!wp || isSolidColorPath(wp)) { + return; + } + var slot = Settings.data.colorSchemes.darkMode ? "dark" : "light"; + var favorite = favoriteEntryForPath(wp); + if (!favorite) { + return; + } + _applyFavoriteThemeFromEntry(favorite, slot); + } + + // ------------------------------------------------------------------- + function applyFavoriteTheme(path, screenName, appearanceSlot) { + // Only apply theme if the wallpaper is on the monitor driving colors + var effectiveMonitor = Settings.data.colorSchemes.monitorForColors; + if (effectiveMonitor === "" || effectiveMonitor === undefined) { + effectiveMonitor = Quickshell.screens.length > 0 ? Quickshell.screens[0].name : ""; + } + if (screenName !== undefined && screenName !== effectiveMonitor) { + return; + } + + var slot; + if (appearanceSlot !== undefined && appearanceSlot !== null && appearanceSlot !== "") { + slot = _normalizeAppearanceSlot(appearanceSlot); + } else { + slot = _normalizeAppearanceSlot(root.wallpaperSelectionAppearance); + } + var favorite = favoriteEntryForPath(path); + if (!favorite) { + return; + } + + var schemeSlot = slot; + if (Settings.data.wallpaper.linkLightAndDarkWallpapers) { + schemeSlot = _favoriteAppearanceSlot(favorite); + } + _applyFavoriteThemeFromEntry(favorite, schemeSlot); + } + + // ------------------------------------------------------------------- + function updateFavoriteColorScheme(path, appearanceSlot) { + var slot = _normalizeAppearanceSlot(appearanceSlot); + var existingIndex = _findFavoriteIndex(path, slot); + if (existingIndex === _favoriteNotFound) { + return; + } + + var favorites = Settings.data.wallpaper.favorites.slice(); + favorites[existingIndex] = _createFavoriteEntry(favorites[existingIndex].path, slot); + Settings.data.wallpaper.favorites = favorites; + Logger.d("Wallpaper", "Updated color scheme for favorite:", path, slot); + favoriteDataUpdated(path); + } + + signal favoritesChanged(string path) + signal favoriteDataUpdated(string path) + + // Auto-update favorite palette colors when theme colors finish transitioning + Connections { + target: Color + function onIsTransitioningChanged() { + if (!Color.isTransitioning) { + _updateCurrentWallpaperFavorites(); + } + } + } + + function _updateCurrentWallpaperFavorites() { + var effectiveMonitor = Settings.data.colorSchemes.monitorForColors; + if (effectiveMonitor === "" || effectiveMonitor === undefined) { + effectiveMonitor = Quickshell.screens.length > 0 ? Quickshell.screens[0].name : ""; + } + var wp = getWallpaper(effectiveMonitor); + if (!wp) { + return; + } + var favIdx = _findAnyFavoriteIndexForPath(wp); + if (favIdx === _favoriteNotFound) { + return; + } + var app = _favoriteAppearanceSlot(Settings.data.wallpaper.favorites[favIdx]); + updateFavoriteColorScheme(wp, app); + } + + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + // ------------------------------------------------------------------- + Timer { + id: randomWallpaperTimer + interval: Settings.data.wallpaper.randomIntervalSec * 1000 + running: Settings.data.wallpaper.automationEnabled && !PowerProfileService.noctaliaPerformanceMode + repeat: true + onTriggered: setNextWallpaper() + triggeredOnStart: false + } + + // ------------------------------------------------------------------- + // Cache file persistence + // ------------------------------------------------------------------- + FileView { + id: wallpaperCacheView + printErrors: false + watchChanges: false + + adapter: JsonAdapter { + id: wallpaperCacheAdapter + property var wallpapers: ({}) + property string defaultWallpaper: root.noctaliaDefaultWallpaper + property var usedRandomWallpapers: ({}) + } + + onLoaded: { + // Load wallpapers from cache file + root.currentWallpapers = wallpaperCacheAdapter.wallpapers || {}; + root.usedRandomWallpapers = wallpaperCacheAdapter.usedRandomWallpapers || {}; + + root._ensureObjectWallpaperEntries(); + + if (Settings.data.wallpaper.linkLightAndDarkWallpapers) { + root.wallpaperSelectionAppearance = Settings.data.colorSchemes.darkMode ? "dark" : "light"; + root._syncWallpaperSlotsWhenLinking(); + } + + // Load default wallpaper from cache if it exists, otherwise use Noctalia default + if (wallpaperCacheAdapter.defaultWallpaper && wallpaperCacheAdapter.defaultWallpaper !== "") { + root.defaultWallpaper = wallpaperCacheAdapter.defaultWallpaper; + Logger.d("Wallpaper", "Loaded default wallpaper from cache:", wallpaperCacheAdapter.defaultWallpaper); + } else { + root.defaultWallpaper = root.noctaliaDefaultWallpaper; + Logger.d("Wallpaper", "Using Noctalia default wallpaper"); + } + + Logger.d("Wallpaper", "Loaded wallpapers from cache file:", Object.keys(root.currentWallpapers).length, "screens"); + root.isInitialized = true; + root._scheduleThemeSyncFromCachedWallpaper(); + } + + onLoadFailed: error => { + // File doesn't exist yet or failed to load - initialize with empty state + root.currentWallpapers = {}; + Logger.d("Wallpaper", "Cache file doesn't exist or failed to load, starting with empty wallpapers"); + root.isInitialized = true; + root._scheduleThemeSyncFromCachedWallpaper(); + } + } + + Timer { + id: saveTimer + interval: 500 + repeat: false + onTriggered: { + wallpaperCacheAdapter.wallpapers = root.currentWallpapers; + wallpaperCacheAdapter.defaultWallpaper = root.defaultWallpaper; + wallpaperCacheAdapter.usedRandomWallpapers = root.usedRandomWallpapers; + wallpaperCacheView.writeAdapter(); + Logger.d("Wallpaper", "Saved wallpapers to cache file"); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/appicon_colorize.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/appicon_colorize.frag new file mode 100644 index 0000000..7cd2824 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/appicon_colorize.frag @@ -0,0 +1,40 @@ +#version 450 +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; +layout(binding = 1) uniform sampler2D source; +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + vec4 targetColor; + float colorizeMode; // 0.0 = dock mode (grayscale), 1.0 = tray mode (intensity), 2.0 = distro mode (luminance with better contrast) +} ubuf; + +void main() { + vec4 tex = texture(source, qt_TexCoord0); + + float intensity; + + if (ubuf.colorizeMode < 0.5) { + // Dock mode: Convert to grayscale using proper luminance weights + intensity = dot(tex.rgb, vec3(0.299, 0.587, 0.114)); + } else if (ubuf.colorizeMode < 1.5) { + // Tray mode: Use the maximum RGB channel value as intensity + intensity = max(max(tex.r, tex.g), tex.b); + + // Normalize intensity to make all icons more uniform + intensity = smoothstep(0.1, 0.9, intensity); + } else { + // Distro mode: Brightness boost with proper alpha handling + float maxChannel = max(max(tex.r, tex.g), tex.b); + + intensity = maxChannel * 1.5; + intensity = min(intensity, 1.0); + intensity = intensity * 0.7 + 0.3; + + intensity = intensity * tex.a; + + fragColor = vec4(ubuf.targetColor.rgb * intensity, tex.a) * ubuf.qt_Opacity; +} + + fragColor = vec4(ubuf.targetColor.rgb * intensity, tex.a) * ubuf.qt_Opacity; +} diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/color_picker.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/color_picker.frag new file mode 100644 index 0000000..3168a3b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/color_picker.frag @@ -0,0 +1,59 @@ +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf +{ + mat4 qt_Matrix; + vec4 params; // x=opacity, y=fixedVal, z=mode, w=padding +}; + +// Compatibility function for GLSL ES to avoid % operator issues +int mod_compat(float a, float b) { + return int(a - floor(a / b) * b); +} + +vec3 hsv2rgb(vec3 c) +{ + vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); + vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www); + return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); +} + +void main() +{ + float x = qt_TexCoord0.x; + float y = 1.0 - qt_TexCoord0.y; // Flip Y direction + + // Unpack vector + float opacity = params.x; + float fixedVal = params.y; + float mode = params.z + 0.1; // +0.1 safely handles float rounding + + // Build permutation matrices for swizzling + vec3 base = vec3(x, y, fixedVal); + + // Determine Mode Logic + // 0: (Red/Hue) 1: (Green/Sat) 2: (Blue/Val) + int perm = mod_compat(mode, 3.0); + + float isHSV = step(3.0, mode); + + // Branchless Selection + vec3 mask = vec3( + float(perm == 0), + float(perm == 1), + float(perm == 2)); + + // Swizzle fo shizzle + // If perm 0: base.zxy -> (fixedVal, x, y) + // If perm 1: base.xzy -> (x, fixedVal, y) + // If perm 2: base.xyz -> (x, y, fixedVal) + vec3 rgb_base = (base.zxy * mask.x) + (base.xzy * mask.y) + (base.xyz * mask.z); + + // Final mix + vec3 finalColor = mix(rgb_base, hsv2rgb(rgb_base), isHSV); + + fragColor = vec4(finalColor, 1.0) * opacity; +} diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/graph.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/graph.frag new file mode 100644 index 0000000..940f208 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/graph.frag @@ -0,0 +1,147 @@ +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D dataSource; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + vec4 lineColor1; + vec4 lineColor2; + float count1; + float count2; + float scroll1; + float scroll2; + float lineWidth; + float graphFillOpacity; + float texWidth; + float resY; + float aaSize; +}; + +// Sample normalized value from data texture +// channel 0 = primary (R), channel 1 = secondary (G) +float fetchData(float idx, int ch) { + float i = clamp(idx, 0.0, texWidth - 1.0); + float u = (floor(i) + 0.5) / texWidth; + vec4 t = texture(dataSource, vec2(u, 0.5)); + return ch == 0 ? t.r : t.g; +} + +// Cubic Hermite interpolation with reduced tangent scale for smooth curves +float cubicHermite(float y0, float y1, float y2, float y3, float t) { + float m1 = (y2 - y0) * 0.25; + float m2 = (y3 - y1) * 0.25; + float t2 = t * t; + float t3 = t2 * t; + return (2.0 * t3 - 3.0 * t2 + 1.0) * y1 + + (t3 - 2.0 * t2 + t) * m1 + + (-2.0 * t3 + 3.0 * t2) * y2 + + (t3 - t2) * m2; +} + +// Evaluate curve at fractional data index +float evalCurve(float dataIdx, int ch) { + float i = floor(dataIdx); + float t = dataIdx - i; + return cubicHermite( + fetchData(i - 1.0, ch), + fetchData(i, ch), + fetchData(i + 1.0, ch), + fetchData(i + 2.0, ch), + t + ); +} + +// Squared distance from point p to line segment a→b +float segDistSq(vec2 p, vec2 a, vec2 b) { + vec2 ab = b - a; + float len2 = dot(ab, ab); + float t = len2 > 0.0 ? clamp(dot(p - a, ab) / len2, 0.0, 1.0) : 0.0; + vec2 proj = a + t * ab; + vec2 d = p - proj; + return dot(d, d); +} + +// Minimum distance from fragment to curve via multi-segment sampling. +// Samples the curve at 9 half-pixel-spaced x-positions (±2px neighborhood) +// and returns the minimum distance to the 8 line segments between them. +float curveDistance(float dataIdx, float pixStep, float normY, int ch) { + vec2 frag = vec2(0.0, normY * resY); + + float px = -2.0; + float py = evalCurve(dataIdx - 2.0 * pixStep, ch) * resY; + vec2 d0 = frag - vec2(px, py); + float best = dot(d0, d0); + + for (int i = 1; i <= 8; i++) { + float cx = -2.0 + float(i) * 0.5; + float cy = evalCurve(dataIdx + cx * pixStep, ch) * resY; + best = min(best, segDistSq(frag, vec2(px, py), vec2(cx, cy))); + px = cx; + py = cy; + } + + return sqrt(best); +} + +// Premultiplied alpha over compositing +vec4 blendOver(vec4 src, vec4 dst) { + return src + dst * (1.0 - src.a); +} + +void main() { + vec2 uv = qt_TexCoord0; + float normY = 1.0 - uv.y; // 0 = bottom, 1 = top + + vec4 result = vec4(0.0); + float halfW = lineWidth * 0.5; + + // Primary line + if (count1 >= 4.0) { + float segs = count1 - 3.0; + float di = 2.0 + scroll1 + uv.x * segs; + float pixStep = dFdx(di); + float cy = evalCurve(di, 0); + float cyNext = evalCurve(di + pixStep, 0); + + // Fill below curve (gradient: opaque at top, transparent at bottom) + if (graphFillOpacity > 0.0 && normY <= cy) { + float a = graphFillOpacity * normY * lineColor1.a; + result = blendOver(vec4(lineColor1.rgb * a, a), result); + } + + // Multi-segment distance for accurate AA at peaks and steep sections. + // AA width derived analytically from curve slope: (|sinθ|+|cosθ|) + // gives the ideal SDF fwidth (~1.0–1.41) without GPU derivative noise. + float dist = curveDistance(di, pixStep, normY, 0); + float slope1 = (cyNext - cy) * resY; + float aa = (abs(slope1) + 1.0) * inversesqrt(slope1 * slope1 + 1.0) * aaSize * 2.0; + float sa = smoothstep(halfW + aa, halfW, dist) * lineColor1.a; + result = blendOver(vec4(lineColor1.rgb * sa, sa), result); + } + + // Secondary line + if (count2 >= 4.0) { + float segs = count2 - 3.0; + float di = 2.0 + scroll2 + uv.x * segs; + float pixStep = dFdx(di); + float cy = evalCurve(di, 1); + float cyNext = evalCurve(di + pixStep, 1); + + if (graphFillOpacity > 0.0 && normY <= cy) { + float a = graphFillOpacity * normY * lineColor2.a; + result = blendOver(vec4(lineColor2.rgb * a, a), result); + } + + float dist = curveDistance(di, pixStep, normY, 1); + float slope2 = (cyNext - cy) * resY; + float aa = (abs(slope2) + 1.0) * inversesqrt(slope2 * slope2 + 1.0) * aaSize * 2.0; + float sa = smoothstep(halfW + aa, halfW, dist) * lineColor2.a; + result = blendOver(vec4(lineColor2.rgb * sa, sa), result); + } + + fragColor = result * qt_Opacity; +} diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/progress_border.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/progress_border.frag new file mode 100644 index 0000000..331386c --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/progress_border.frag @@ -0,0 +1,67 @@ +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float progress; + float borderWidth; + vec4 progressColor; + vec4 borderColor; + vec4 backgroundColor; + float borderRadius; +} ubuf; + +void main() { + vec2 coord = qt_TexCoord0; + float p = clamp(ubuf.progress, 0.0, 1.0); + + if (ubuf.borderRadius > 0.0) { + // Circular progress + vec2 center = vec2(0.5, 0.5); + vec2 dir = coord - center; + float dist = length(dir); + + float outerRadius = 0.5; + float innerRadius = outerRadius - ubuf.borderWidth; + + float angle = atan(dir.y, dir.x) + radians(90.0); + if (angle < 0.0) angle += radians(360.0); + float maxAngle = radians(360.0) * p; + + bool inBorder = dist >= innerRadius && dist <= outerRadius; + bool inProgress = inBorder && angle <= maxAngle; + + if (inProgress) { + fragColor = ubuf.progressColor * ubuf.qt_Opacity; + } else if (inBorder) { + fragColor = ubuf.borderColor * ubuf.qt_Opacity; + } else if (dist < innerRadius) { + fragColor = ubuf.backgroundColor * ubuf.qt_Opacity; + } else { + fragColor = vec4(0.0, 0.0, 0.0, 0.0); + } + } else { + // Rectangular progress + bool inBorder = + coord.x < ubuf.borderWidth || + coord.x > (1.0 - ubuf.borderWidth) || + coord.y < ubuf.borderWidth || + coord.y > (1.0 - ubuf.borderWidth); + + float progressPos = p; + bool inProgress = inBorder && coord.x <= progressPos; + + if (inProgress) { + fragColor = ubuf.progressColor * ubuf.qt_Opacity; + } else if (inBorder) { + fragColor = ubuf.borderColor * ubuf.qt_Opacity; + } else { + fragColor = ubuf.backgroundColor * ubuf.qt_Opacity; + } + } +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/rounded_image.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/rounded_image.frag new file mode 100644 index 0000000..dc5c4f9 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/rounded_image.frag @@ -0,0 +1,101 @@ +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + // Custom properties with non-conflicting names + float itemWidth; + float itemHeight; + float sourceWidth; + float sourceHeight; + float cornerRadius; + float imageOpacity; + int fillMode; +} ubuf; + +// Function to calculate the signed distance from a point to a rounded box +float roundedBoxSDF(vec2 centerPos, vec2 boxSize, float radius) { + vec2 d = abs(centerPos) - boxSize + radius; + return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - radius; +} + +void main() { + // Get size from uniforms + vec2 itemSize = vec2(ubuf.itemWidth, ubuf.itemHeight); + vec2 sourceSize = vec2(ubuf.sourceWidth, ubuf.sourceHeight); + float cornerRadius = ubuf.cornerRadius; + float itemOpacity = ubuf.imageOpacity; + int fillMode = ubuf.fillMode; + + // Work in pixel space for accurate rounded rectangle calculation + vec2 pixelPos = qt_TexCoord0 * itemSize; + + // Calculate UV coordinates based on fill mode + vec2 imageUV = qt_TexCoord0; + + // fillMode constants from Qt: + // Image.Stretch = 0 + // Image.PreserveAspectFit = 1 + // Image.PreserveAspectCrop = 2 + // Image.Tile = 3 + // Image.TileVertically = 4 + // Image.TileHorizontally = 5 + // Image.Pad = 6 + + // Rounded corners always apply to full item bounds + vec2 roundedSize = itemSize; + vec2 roundedCenter = itemSize * 0.5; + + // Track if pixel is in letterbox area (for PreserveAspectFit) + bool inLetterbox = false; + + if (fillMode == 1) { // PreserveAspectFit + float itemAspect = itemSize.x / itemSize.y; + float sourceAspect = sourceSize.x / sourceSize.y; + + if (sourceAspect > itemAspect) { + // Image is wider than item, letterbox top/bottom + imageUV.y = (qt_TexCoord0.y - 0.5) * (sourceAspect / itemAspect) + 0.5; + } else { + // Image is taller than item, letterbox left/right + imageUV.x = (qt_TexCoord0.x - 0.5) * (itemAspect / sourceAspect) + 0.5; + } + + // Check if in letterbox area + inLetterbox = (imageUV.x < 0.0 || imageUV.x > 1.0 || imageUV.y < 0.0 || imageUV.y > 1.0); + } else if (fillMode == 2) { // PreserveAspectCrop + float itemAspect = itemSize.x / itemSize.y; + float sourceAspect = sourceSize.x / sourceSize.y; + + if (sourceAspect > itemAspect) { + // Image is wider than item, crop left/right. + imageUV.x = (qt_TexCoord0.x - 0.5) * (itemAspect / sourceAspect) + 0.5; + } else { + // Image is taller than item, crop top/bottom. + imageUV.y = (qt_TexCoord0.y - 0.5) * (sourceAspect / itemAspect) + 0.5; + } + } + // For Stretch (0) or other modes, use qt_TexCoord0 as-is + + // Calculate distance to rounded rectangle edge using the correct bounds + vec2 centerOffset = pixelPos - roundedCenter; + float distance = roundedBoxSDF(centerOffset, roundedSize * 0.5, cornerRadius); + + // Create smooth alpha mask for edge with anti-aliasing + float alpha = 1.0 - smoothstep(-0.5, 0.5, distance); + + // Sample the texture (or use transparent for letterbox) + // Clamp UV to prevent texture wrapping artifacts from floating-point imprecision + vec4 color = inLetterbox ? vec4(0.0) : texture(source, clamp(imageUV, vec2(0.0), vec2(1.0))); + + // Apply the rounded mask and opacity + // Qt textures use premultiplied alpha (color.rgb already contains rgb * alpha), + // so we only multiply by the external mask factors, not by color.a again + float mask = alpha * itemOpacity * ubuf.qt_Opacity; + fragColor = color * mask; +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wave_spectrum.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wave_spectrum.frag new file mode 100644 index 0000000..af1b905 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wave_spectrum.frag @@ -0,0 +1,82 @@ +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D dataSource; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + vec4 fillColor; + float count; + float texWidth; + float vertical; + float mirrored; +}; + +// Sample amplitude from data texture (R channel) +float fetchData(float idx) { + float i = clamp(idx, 0.0, texWidth - 1.0); + float u = (floor(i) + 0.5) / texWidth; + return texture(dataSource, vec2(u, 0.5)).r; +} + +// Cubic Hermite interpolation for smooth wave curves +float cubicHermite(float y0, float y1, float y2, float y3, float t) { + float m1 = (y2 - y0) * 0.25; + float m2 = (y3 - y1) * 0.25; + float t2 = t * t; + float t3 = t2 * t; + return (2.0 * t3 - 3.0 * t2 + 1.0) * y1 + + (t3 - 2.0 * t2 + t) * m1 + + (-2.0 * t3 + 3.0 * t2) * y2 + + (t3 - t2) * m2; +} + +// Evaluate interpolated amplitude at fractional data index +float evalCurve(float dataIdx) { + float i = floor(dataIdx); + float t = dataIdx - i; + return cubicHermite( + fetchData(i - 1.0), + fetchData(i), + fetchData(i + 1.0), + fetchData(i + 2.0), + t + ); +} + +void main() { + vec2 uv = qt_TexCoord0; + + // Swap axes for vertical mode + float axisPos = (vertical > 0.5) ? uv.y : uv.x; + float crossPos = (vertical > 0.5) ? uv.x : uv.y; + + // Map axis position to data index + float dataIdx; + if (mirrored > 0.5) { + // Mirror: value[0] at center, value[count-1] at edges + float distFromCenter = abs(axisPos - 0.5) * 2.0; + dataIdx = distFromCenter * max(count - 1.0, 1.0); + } else { + // Linear: value[0] at left/top, value[count-1] at right/bottom + dataIdx = axisPos * max(count - 1.0, 1.0); + } + + // Interpolated amplitude, clamped to valid range + float amplitude = clamp(evalCurve(dataIdx), 0.0, 1.0); + + // Wave fills center ± amplitude/2 in the cross axis + float halfAmp = amplitude * 0.5; + float distFromMid = abs(crossPos - 0.5); + + // Antialiased edge (~1px smooth transition) + float edge = fwidth(crossPos) * 1.5; + float mask = smoothstep(halfAmp + edge, halfAmp - edge, distFromMid); + + // Premultiplied alpha output + float a = mask * fillColor.a; + fragColor = vec4(fillColor.rgb * a, a) * qt_Opacity; +} diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/weather_cloud.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/weather_cloud.frag new file mode 100644 index 0000000..1991ce3 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/weather_cloud.frag @@ -0,0 +1,123 @@ +#version 450 +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float time; + float itemWidth; + float itemHeight; + vec4 bgColor; + float cornerRadius; + float alternative; +} ubuf; + +// Signed distance function for rounded rectangle +float roundedBoxSDF(vec2 center, vec2 size, float radius) { + vec2 q = abs(center) - size + radius; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius; +} + +float hash(vec2 p) { + p = fract(p * vec2(234.34, 435.345)); + p += dot(p, p + 34.23); + return fract(p.x * p.y); +} + +// Perlin-like noise +float noise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); // Smooth interpolation + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); +} + +// Turbulent noise for natural fog +float turbulence(vec2 p, float iTime) { + float t = 0.0; + float scale = 1.0; + for(int i = 0; i < 5; i++) { + t += abs(noise(p * scale + iTime * 0.1 * scale)) / scale; + scale *= 2.0; + } + return t; +} + +void main() { + vec2 uv = qt_TexCoord0; + + vec4 col = vec4(ubuf.bgColor.rgb, 1.0); + + // Different parameters for fog vs clouds + float timeSpeed, layerScale1, layerScale2, layerScale3; + float flowSpeed1, flowSpeed2; + float densityMin, densityMax; + float baseOpacity; + float pulseAmount; + + if (ubuf.alternative > 0.5) { + // Fog: slower, larger scale, more uniform + timeSpeed = 0.03; + layerScale1 = 1.0; + layerScale2 = 2.5; + layerScale3 = 2.0; + flowSpeed1 = 0.00; + flowSpeed2 = 0.02; + densityMin = 0.1; + densityMax = 0.9; + baseOpacity = 0.75; + pulseAmount = 0.05; + } else { + // Clouds: faster, smaller scale, puffier + timeSpeed = 0.08; + layerScale1 = 2.0; + layerScale2 = 4.0; + layerScale3 = 6.0; + flowSpeed1 = 0.03; + flowSpeed2 = 0.04; + densityMin = 0.35; + densityMax = 0.75; + baseOpacity = 0.4; + pulseAmount = 0.15; + } + + float iTime = ubuf.time * timeSpeed; + + // Create flowing patterns with multiple layers + vec2 flow1 = vec2(iTime * flowSpeed1, iTime * flowSpeed1 * 0.7); + vec2 flow2 = vec2(-iTime * flowSpeed2, iTime * flowSpeed2 * 0.8); + + float fog1 = noise(uv * layerScale1 + flow1); + float fog2 = noise(uv * layerScale2 + flow2); + float fog3 = turbulence(uv * layerScale3, iTime); + + float fogPattern = fog1 * 0.5 + fog2 * 0.3 + fog3 * 0.2; + float fogDensity = smoothstep(densityMin, densityMax, fogPattern); + + // Gentle pulsing + float pulse = sin(iTime * 0.4) * pulseAmount + (1.0 - pulseAmount); + fogDensity *= pulse; + + vec3 hazeColor = vec3(0.88, 0.90, 0.93); + float hazeOpacity = fogDensity * baseOpacity; + vec3 fogContribution = hazeColor * hazeOpacity; + float fogAlpha = hazeOpacity; + + vec3 resultRGB = fogContribution + col.rgb * (1.0 - fogAlpha); + float resultAlpha = fogAlpha + col.a * (1.0 - fogAlpha); + + // Calculate corner mask + vec2 pixelPos = qt_TexCoord0 * vec2(ubuf.itemWidth, ubuf.itemHeight); + vec2 center = pixelPos - vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5; + vec2 halfSize = vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5; + float dist = roundedBoxSDF(center, halfSize, ubuf.cornerRadius); + float cornerMask = 1.0 - smoothstep(-1.0, 0.0, dist); + + // Apply global opacity and corner mask + float finalAlpha = resultAlpha * ubuf.qt_Opacity * cornerMask; + fragColor = vec4(resultRGB * (finalAlpha / max(resultAlpha, 0.001)), finalAlpha); +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/weather_rain.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/weather_rain.frag new file mode 100644 index 0000000..bd3bc39 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/weather_rain.frag @@ -0,0 +1,84 @@ +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float time; + float itemWidth; + float itemHeight; + vec4 bgColor; + float cornerRadius; +} ubuf; + +// Signed distance function for rounded rectangle +float roundedBoxSDF(vec2 center, vec2 size, float radius) { + vec2 q = abs(center) - size + radius; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius; +} + +vec3 hash3(vec2 p) { + vec3 q = vec3(dot(p, vec2(127.1, 311.7)), + dot(p, vec2(269.5, 183.3)), + dot(p, vec2(419.2, 371.9))); + return fract(sin(q) * 43758.5453); +} + +float noise(vec2 x, float iTime) { + vec2 p = floor(x); + vec2 f = fract(x); + + float va = 0.0; + for (int j = -2; j <= 2; j++) { + for (int i = -2; i <= 2; i++) { + vec2 g = vec2(float(i), float(j)); + vec3 o = hash3(p + g); + vec2 r = g - f + o.xy; + float d = sqrt(dot(r, r)); + float ripple = max(mix(smoothstep(0.99, 0.999, max(cos(d - iTime * 2.0 + (o.x + o.y) * 5.0), 0.0)), 0.0, d), 0.0); + va += ripple; + } + } + + return va; +} + +void main() { + vec2 uv = qt_TexCoord0; + float iTime = ubuf.time * 0.07; + + // Aspect ratio correction for circular ripples + float aspect = ubuf.itemWidth / ubuf.itemHeight; + vec2 uvAspect = vec2(uv.x * aspect, uv.y); + + float f = noise(6.0 * uvAspect, iTime) * smoothstep(0.0, 0.2, sin(uv.x * 3.141592) * sin(uv.y * 3.141592)); + + // Calculate normal from noise for distortion + float normalScale = 0.5; + vec2 e = normalScale / vec2(ubuf.itemWidth, ubuf.itemHeight); + vec2 eAspect = vec2(e.x * aspect, e.y); + float cx = noise(6.0 * (uvAspect + eAspect), iTime) * smoothstep(0.0, 0.2, sin((uv.x + e.x) * 3.141592) * sin(uv.y * 3.141592)); + float cy = noise(6.0 * (uvAspect + eAspect.yx), iTime) * smoothstep(0.0, 0.2, sin(uv.x * 3.141592) * sin((uv.y + e.y) * 3.141592)); + vec2 n = vec2(cx - f, cy - f); + + // Scale distortion back to texture space (undo aspect correction for X) + vec2 distortion = vec2(n.x / aspect, n.y); + + // Sample source with distortion + vec4 col = texture(source, uv + distortion); + + // Apply rounded corner mask + vec2 pixelPos = qt_TexCoord0 * vec2(ubuf.itemWidth, ubuf.itemHeight); + vec2 center = pixelPos - vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5; + vec2 halfSize = vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5; + float dist = roundedBoxSDF(center, halfSize, ubuf.cornerRadius); + float cornerMask = 1.0 - smoothstep(-1.0, 0.0, dist); + + // Output with premultiplied alpha + float finalAlpha = col.a * ubuf.qt_Opacity * cornerMask; + fragColor = vec4(col.rgb * finalAlpha, finalAlpha); +} diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/weather_snow.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/weather_snow.frag new file mode 100644 index 0000000..e00a952 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/weather_snow.frag @@ -0,0 +1,75 @@ +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float time; + float itemWidth; + float itemHeight; + vec4 bgColor; + float cornerRadius; +} ubuf; + +// Signed distance function for rounded rectangle +float roundedBoxSDF(vec2 center, vec2 size, float radius) { + vec2 q = abs(center) - size + radius; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius; +} + +void main() { + // Aspect ratio correction + float aspect = ubuf.itemWidth / ubuf.itemHeight; + vec2 uv = qt_TexCoord0; + uv.x *= aspect; + uv.y = 1.0 - uv.y; + + float iTime = ubuf.time * 0.15; + + float snow = 0.0; + + for (int k = 0; k < 6; k++) { + for (int i = 0; i < 12; i++) { + float cellSize = 2.0 + (float(i) * 3.0); + float downSpeed = 0.3 + (sin(iTime * 0.4 + float(k + i * 20)) + 1.0) * 0.00008; + + vec2 uvAnim = uv + vec2( + 0.01 * sin((iTime + float(k * 6185)) * 0.6 + float(i)) * (5.0 / float(i + 1)), + downSpeed * (iTime + float(k * 1352)) * (1.0 / float(i + 1)) + ); + + vec2 uvStep = (ceil((uvAnim) * cellSize - vec2(0.5, 0.5)) / cellSize); + float x = fract(sin(dot(uvStep.xy, vec2(12.9898 + float(k) * 12.0, 78.233 + float(k) * 315.156))) * 43758.5453 + float(k) * 12.0) - 0.5; + float y = fract(sin(dot(uvStep.xy, vec2(62.2364 + float(k) * 23.0, 94.674 + float(k) * 95.0))) * 62159.8432 + float(k) * 12.0) - 0.5; + + float randomMagnitude1 = sin(iTime * 2.5) * 0.7 / cellSize; + float randomMagnitude2 = cos(iTime * 1.65) * 0.7 / cellSize; + + float d = 5.0 * distance((uvStep.xy + vec2(x * sin(y), y) * randomMagnitude1 + vec2(y, x) * randomMagnitude2), uvAnim.xy); + + float omiVal = fract(sin(dot(uvStep.xy, vec2(32.4691, 94.615))) * 31572.1684); + if (omiVal < 0.03) { + float newd = (x + 1.0) * 0.4 * clamp(1.9 - d * (15.0 + (x * 6.3)) * (cellSize / 1.4), 0.0, 1.0); + snow += newd; + } + } + } + + // Blend white snow over background color + float snowAlpha = clamp(snow * 2.0, 0.0, 1.0); + vec3 snowColor = vec3(1.0); + vec3 blended = mix(ubuf.bgColor.rgb, snowColor, snowAlpha); + + // Apply rounded corner mask + vec2 pixelPos = qt_TexCoord0 * vec2(ubuf.itemWidth, ubuf.itemHeight); + vec2 center = pixelPos - vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5; + vec2 halfSize = vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5; + float dist = roundedBoxSDF(center, halfSize, ubuf.cornerRadius); + float cornerMask = 1.0 - smoothstep(-1.0, 0.0, dist); + + // Output with premultiplied alpha + float finalAlpha = ubuf.qt_Opacity * cornerMask; + fragColor = vec4(blended * finalAlpha, finalAlpha); +} diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/weather_stars.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/weather_stars.frag new file mode 100644 index 0000000..1048cae --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/weather_stars.frag @@ -0,0 +1,130 @@ +#version 450 +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float time; + float itemWidth; + float itemHeight; + vec4 bgColor; + float cornerRadius; +} ubuf; + +// Signed distance function for rounded rectangle +float roundedBoxSDF(vec2 center, vec2 size, float radius) { + vec2 q = abs(center) - size + radius; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius; +} + +float hash(vec2 p) { + p = fract(p * vec2(234.34, 435.345)); + p += dot(p, p + 34.23); + return fract(p.x * p.y); +} + +vec2 hash2(vec2 p) { + p = fract(p * vec2(234.34, 435.345)); + p += dot(p, p + 34.23); + return fract(vec2(p.x * p.y, p.y * p.x)); +} + +float stars(vec2 uv, float density, float iTime) { + vec2 gridUV = uv * density; + vec2 gridID = floor(gridUV); + vec2 gridPos = fract(gridUV); + + float starField = 0.0; + + // Check neighboring cells for stars + for (int y = -1; y <= 1; y++) { + for (int x = -1; x <= 1; x++) { + vec2 offset = vec2(float(x), float(y)); + vec2 cellID = gridID + offset; + + // Random position within cell + vec2 starPos = hash2(cellID); + + // Only create a star for some cells (sparse distribution) + float starChance = hash(cellID + vec2(12.345, 67.890)); + if (starChance > 0.85) { + // Star position in grid space + vec2 toStar = (offset + starPos - gridPos); + float dist = length(toStar) * density; // Scale distance to pixel space + + float starSize = 1.5; + + // Star brightness variation + float brightness = hash(cellID + vec2(23.456, 78.901)) * 0.6 + 0.4; + + // Twinkling effect + float twinkleSpeed = hash(cellID + vec2(34.567, 89.012)) * 3.0 + 2.0; + float twinklePhase = iTime * twinkleSpeed + hash(cellID) * 6.28; + float twinkle = pow(sin(twinklePhase) * 0.5 + 0.5, 3.0); // Sharp on/off + + // Sharp star core + float star = 0.0; + if (dist < starSize) { + star = 1.0 * brightness * (0.3 + twinkle * 0.7); + + // Add tiny cross-shaped glow for brighter stars + if (brightness > 0.7) { + float crossGlow = max( + exp(-abs(toStar.x) * density * 5.0), + exp(-abs(toStar.y) * density * 5.0) + ) * 0.3 * twinkle; + star += crossGlow; + } + } + + starField += star; + } + } + } + + return starField; +} + +void main() { + vec2 uv = qt_TexCoord0; + float iTime = ubuf.time * 0.01; + + // Base background color + vec4 col = vec4(ubuf.bgColor.rgb, 1.0); + + // Aspect ratio for consistent stars + float aspect = ubuf.itemWidth / ubuf.itemHeight; + vec2 uvAspect = vec2(uv.x * aspect, uv.y); + + // Generate multiple layers of stars at different densities + float stars1 = stars(uvAspect, 40.0, iTime); // Tiny distant stars + float stars2 = stars(uvAspect + vec2(0.5, 0.3), 25.0, iTime * 1.3); // Small stars + float stars3 = stars(uvAspect + vec2(0.25, 0.7), 15.0, iTime * 0.9); // Bigger stars + + // Star colors with slight variation + vec3 starColor1 = vec3(0.85, 0.9, 1.0); // Faint blue-white + vec3 starColor2 = vec3(0.95, 0.97, 1.0); // White + vec3 starColor3 = vec3(1.0, 0.98, 0.95); // Warm white + + // Combine star layers + vec3 starsRGB = starColor1 * stars1 * 0.6 + + starColor2 * stars2 * 0.8 + + starColor3 * stars3 * 1.0; + + float starsAlpha = clamp(stars1 * 0.6 + stars2 * 0.8 + stars3, 0.0, 1.0); + + // Apply rounded corner mask + vec2 pixelPos = qt_TexCoord0 * vec2(ubuf.itemWidth, ubuf.itemHeight); + vec2 center = pixelPos - vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5; + vec2 halfSize = vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5; + float dist = roundedBoxSDF(center, halfSize, ubuf.cornerRadius); + float cornerMask = 1.0 - smoothstep(-1.0, 0.0, dist); + + // Add stars on top + vec3 resultRGB = starsRGB * starsAlpha + col.rgb * (1.0 - starsAlpha); + float resultAlpha = starsAlpha + col.a * (1.0 - starsAlpha); + + // Apply global opacity and corner mask + float finalAlpha = resultAlpha * ubuf.qt_Opacity * cornerMask; + fragColor = vec4(resultRGB * (finalAlpha / max(resultAlpha, 0.001)), finalAlpha); +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/weather_sun.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/weather_sun.frag new file mode 100644 index 0000000..bfc0cd9 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/weather_sun.frag @@ -0,0 +1,148 @@ +#version 450 +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float time; + float itemWidth; + float itemHeight; + vec4 bgColor; + float cornerRadius; +} ubuf; + +// Signed distance function for rounded rectangle +float roundedBoxSDF(vec2 center, vec2 size, float radius) { + vec2 q = abs(center) - size + radius; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius; +} + +float hash(vec2 p) { + p = fract(p * vec2(234.34, 435.345)); + p += dot(p, p + 34.23); + return fract(p.x * p.y); +} + +float noise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + f = f * f * (3.0 - 2.0 * f); + + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + + return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); +} + +// God rays originating from sun position +float sunRays(vec2 uv, vec2 sunPos, float iTime) { + vec2 toSun = uv - sunPos; + float angle = atan(toSun.y, toSun.x); + float dist = length(toSun); + + float rayCount = 7; + + // Radial pattern + float rays = sin(angle * rayCount + sin(iTime * 0.25)) * 0.5 + 0.5; + rays = pow(rays, 3.0); + + // Fade with distance + float falloff = 1.0 - smoothstep(0.0, 1.2, dist); + + return rays * falloff * 0.15; +} + +// Atmospheric shimmer / heat haze +float atmosphericShimmer(vec2 uv, float iTime) { + // Multiple layers of noise for complexity + float n1 = noise(uv * 5.0 + vec2(iTime * 0.1, iTime * 0.05)); + float n2 = noise(uv * 8.0 - vec2(iTime * 0.08, iTime * 0.12)); + float n3 = noise(uv * 12.0 + vec2(iTime * 0.15, -iTime * 0.1)); + + return (n1 * 0.5 + n2 * 0.3 + n3 * 0.2) * 0.15; +} + +float sunCore(vec2 uv, vec2 sunPos, float iTime) { + vec2 toSun = uv - sunPos; + float dist = length(toSun); + + // Main bright spot + float mainFlare = exp(-dist * 15.0) * 2.0; + + // Secondary reflection spots along the line + float flares = 0.0; + for (int i = 1; i <= 3; i++) { + vec2 flarePos = sunPos + toSun * float(i) * 0.3; + float flareDist = length(uv - flarePos); + float flareSize = 0.02 + float(i) * 0.01; + flares += smoothstep(flareSize * 2.0, flareSize * 0.5, flareDist) * (0.3 / float(i)); + } + + // Pulsing effect + float pulse = sin(iTime) * 0.1 + 0.9; + + return (mainFlare + flares) * pulse; +} + +void main() { + vec2 uv = qt_TexCoord0; + float iTime = ubuf.time * 0.08; + + // Sample the source + vec4 col = vec4(ubuf.bgColor.rgb, 1.0); + + vec2 sunPos = vec2(0.85, 0.2); + + // Aspect ratio correction + float aspect = ubuf.itemWidth / ubuf.itemHeight; + vec2 uvAspect = vec2(uv.x * aspect, uv.y); + vec2 sunPosAspect = vec2(sunPos.x * aspect, sunPos.y); + + // Generate sunny effects + float rays = sunRays(uvAspect, sunPosAspect, iTime); + float shimmerEffect = atmosphericShimmer(uv, iTime); + float flare = sunCore(uvAspect, sunPosAspect, iTime); + + // Warm sunny colors + vec3 sunColor = vec3(1.0, 0.95, 0.7); // Warm golden yellow + vec3 skyColor = vec3(0.9, 0.95, 1.0); // Light blue tint + vec3 shimmerColor = vec3(1.0, 0.98, 0.85); // Subtle warm shimmer + + // Apply rounded corner mask + vec2 pixelPos = qt_TexCoord0 * vec2(ubuf.itemWidth, ubuf.itemHeight); + vec2 center = pixelPos - vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5; + vec2 halfSize = vec2(ubuf.itemWidth, ubuf.itemHeight) * 0.5; + float dist = roundedBoxSDF(center, halfSize, ubuf.cornerRadius); + float cornerMask = 1.0 - smoothstep(-1.0, 0.0, dist); + + vec3 resultRGB = col.rgb; + float resultAlpha = col.a; + + // Add sun rays + vec3 raysContribution = sunColor * rays; + float raysAlpha = rays * 0.4; + resultRGB = raysContribution + resultRGB * (1.0 - raysAlpha); + resultAlpha = raysAlpha + resultAlpha * (1.0 - raysAlpha); + + // Add atmospheric shimmer + vec3 shimmerContribution = shimmerColor * shimmerEffect; + float shimmerAlpha = shimmerEffect * 0.1; + resultRGB = shimmerContribution + resultRGB * (1.0 - shimmerAlpha); + resultAlpha = shimmerAlpha + resultAlpha * (1.0 - shimmerAlpha); + + // Add bright sun core + vec3 flareContribution = sunColor * flare; + float flareAlpha = clamp(flare, 0.0, 1.0) * 0.6; + resultRGB = flareContribution + resultRGB * (1.0 - flareAlpha); + resultAlpha = flareAlpha + resultAlpha * (1.0 - flareAlpha); + + // Overall warm sunny tint + resultRGB = mix(resultRGB, resultRGB * vec3(1.08, 1.04, 0.98), 0.15); + + // Apply global opacity and corner mask + float finalAlpha = resultAlpha * ubuf.qt_Opacity * cornerMask; + fragColor = vec4(resultRGB * (finalAlpha / max(resultAlpha, 0.001)), finalAlpha); +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_disc.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_disc.frag new file mode 100644 index 0000000..3add2f1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_disc.frag @@ -0,0 +1,149 @@ +// ===== wp_disc.frag ===== +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source1; // Current wallpaper +layout(binding = 2) uniform sampler2D source2; // Next wallpaper + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float progress; // Transition progress (0.0 to 1.0) + float centerX; // X coordinate of disc center (0.0 to 1.0) + float centerY; // Y coordinate of disc center (0.0 to 1.0) + float smoothness; // Edge smoothness (0.0 to 1.0, 0=sharp, 1=very smooth) + float aspectRatio; // Width / Height of the screen + + // Fill mode parameters + float fillMode; // 0=center, 1=crop, 2=fit, 3=stretch, 4=repeat + float imageWidth1; // Width of source1 image + float imageHeight1; // Height of source1 image + float imageWidth2; // Width of source2 image + float imageHeight2; // Height of source2 image + float screenWidth; // Screen width + float screenHeight; // Screen height + vec4 fillColor; // Fill color for empty areas (default: black) + + // Solid color mode + float isSolid1; // 1.0 if source1 is solid color, 0.0 otherwise + float isSolid2; // 1.0 if source2 is solid color, 0.0 otherwise + vec4 solidColor1; // Solid color for source1 + vec4 solidColor2; // Solid color for source2 +} ubuf; + +// Calculate UV coordinates based on fill mode +vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { + float imageAspect = imgWidth / imgHeight; + float screenAspect = ubuf.screenWidth / ubuf.screenHeight; + vec2 transformedUV = uv; + + if (ubuf.fillMode < 0.5) { + // Mode 0: no (center) - No resize, center image at original size + // Convert UV to pixel coordinates, offset, then back to UV in image space + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imageOffset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(imgWidth, imgHeight)) * 0.5; + vec2 imagePixel = screenPixel - imageOffset; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 1.5) { + // Mode 1: crop (fill/cover) - Fill screen, crop excess (default) + float scale = max(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (scaledImageSize - vec2(ubuf.screenWidth, ubuf.screenHeight)) / scaledImageSize; + transformedUV = uv * (vec2(1.0) - offset) + offset * 0.5; + } + else if (ubuf.fillMode < 2.5) { + // Mode 2: fit (contain) - Fit inside screen, maintain aspect ratio + float scale = min(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - scaledImageSize) * 0.5; + + // Convert screen UV to pixel coordinates + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + // Adjust for offset and scale + vec2 imagePixel = (screenPixel - offset) / scale; + // Convert back to UV coordinates in image space + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 3.5) { + // Mode 3: stretch - Use original UV (stretches to fit) + // No transformation needed for stretch mode + } + else { + // Mode 4: repeat (tile) - Tile image at original size + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + transformedUV = screenPixel / vec2(imgWidth, imgHeight); + } + + return transformedUV; +} + +// Sample texture with fill mode and handle out-of-bounds +// If isSolid > 0.5, returns solidColor instead of sampling texture +vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float imgWidth, float imgHeight, float isSolid, vec4 solidColor) { + // Return solid color if in solid color mode + if (isSolid > 0.5) { + return solidColor; + } + + vec2 transformedUV = calculateUV(uv, imgWidth, imgHeight); + + // Mode 4 (repeat): use fract() to tile the image + if (ubuf.fillMode > 3.5) { + return texture(tex, fract(transformedUV)); + } + + // Check if UV is out of bounds + if (transformedUV.x < 0.0 || transformedUV.x > 1.0 || + transformedUV.y < 0.0 || transformedUV.y > 1.0) { + return ubuf.fillColor; + } + + return texture(tex, transformedUV); +} + +void main() { + vec2 uv = qt_TexCoord0; + + // Sample textures with fill mode (handles solid colors) + vec4 color1 = sampleWithFillMode(source1, uv, ubuf.imageWidth1, ubuf.imageHeight1, ubuf.isSolid1, ubuf.solidColor1); + vec4 color2 = sampleWithFillMode(source2, uv, ubuf.imageWidth2, ubuf.imageHeight2, ubuf.isSolid2, ubuf.solidColor2); + + // Map smoothness from 0.0-1.0 to 0.001-0.5 range + // Using a non-linear mapping for better control + float mappedSmoothness = mix(0.001, 0.5, ubuf.smoothness * ubuf.smoothness); + + // Adjust UV coordinates to compensate for aspect ratio + // This makes distances circular instead of elliptical + vec2 adjustedUV = vec2(uv.x * ubuf.aspectRatio, uv.y); + vec2 adjustedCenter = vec2(ubuf.centerX * ubuf.aspectRatio, ubuf.centerY); + + // Calculate distance in aspect-corrected space + float dist = distance(adjustedUV, adjustedCenter); + + // Calculate the maximum possible distance (corner to corner) + // This ensures the disc can cover the entire screen + float maxDistX = max(ubuf.centerX * ubuf.aspectRatio, + (1.0 - ubuf.centerX) * ubuf.aspectRatio); + float maxDistY = max(ubuf.centerY, 1.0 - ubuf.centerY); + float maxDist = length(vec2(maxDistX, maxDistY)); + + // Scale progress to cover the maximum distance + // Add extra range for smoothness to ensure complete coverage + // Adjust smoothness for aspect ratio to maintain consistent visual appearance + float adjustedSmoothness = mappedSmoothness * max(1.0, ubuf.aspectRatio); + + // Start the radius from -adjustedSmoothness, this ensures the disc is completely hidden when progress=0 + float totalDistance = maxDist + 2.0 * adjustedSmoothness; + float radius = -adjustedSmoothness + ubuf.progress * totalDistance; + + // Use smoothstep for a smooth edge transition + float factor = smoothstep(radius - adjustedSmoothness, radius + adjustedSmoothness, dist); + + // Mix the textures (factor = 0 inside disc, 1 outside) + fragColor = mix(color2, color1, factor); + + fragColor *= ubuf.qt_Opacity; +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_fade.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_fade.frag new file mode 100644 index 0000000..6fbc9a1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_fade.frag @@ -0,0 +1,112 @@ +// ===== wp_fade.frag ===== +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source1; +layout(binding = 2) uniform sampler2D source2; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float progress; + + // Fill mode parameters + float fillMode; // 0=center, 1=crop, 2=fit, 3=stretch, 4=repeat + float imageWidth1; // Width of source1 image + float imageHeight1; // Height of source1 image + float imageWidth2; // Width of source2 image + float imageHeight2; // Height of source2 image + float screenWidth; // Screen width + float screenHeight; // Screen height + vec4 fillColor; // Fill color for empty areas (default: black) + + // Solid color mode + float isSolid1; // 1.0 if source1 is solid color, 0.0 otherwise + float isSolid2; // 1.0 if source2 is solid color, 0.0 otherwise + vec4 solidColor1; // Solid color for source1 + vec4 solidColor2; // Solid color for source2 +} ubuf; + +// Calculate UV coordinates based on fill mode +vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { + float imageAspect = imgWidth / imgHeight; + float screenAspect = ubuf.screenWidth / ubuf.screenHeight; + vec2 transformedUV = uv; + + if (ubuf.fillMode < 0.5) { + // Mode 0: no (center) - No resize, center image at original size + // Convert UV to pixel coordinates, offset, then back to UV in image space + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imageOffset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(imgWidth, imgHeight)) * 0.5; + vec2 imagePixel = screenPixel - imageOffset; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 1.5) { + // Mode 1: crop (fill/cover) - Fill screen, crop excess (default) + float scale = max(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (scaledImageSize - vec2(ubuf.screenWidth, ubuf.screenHeight)) / scaledImageSize; + transformedUV = uv * (vec2(1.0) - offset) + offset * 0.5; + } + else if (ubuf.fillMode < 2.5) { + // Mode 2: fit (contain) - Fit inside screen, maintain aspect ratio + float scale = min(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - scaledImageSize) * 0.5; + + // Convert screen UV to pixel coordinates + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + // Adjust for offset and scale + vec2 imagePixel = (screenPixel - offset) / scale; + // Convert back to UV coordinates in image space + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 3.5) { + // Mode 3: stretch - Use original UV (stretches to fit) + // No transformation needed for stretch mode + } + else { + // Mode 4: repeat (tile) - Tile image at original size + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + transformedUV = screenPixel / vec2(imgWidth, imgHeight); + } + + return transformedUV; +} + +// Sample texture with fill mode and handle out-of-bounds +// If isSolid > 0.5, returns solidColor instead of sampling texture +vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float imgWidth, float imgHeight, float isSolid, vec4 solidColor) { + // Return solid color if in solid color mode + if (isSolid > 0.5) { + return solidColor; + } + + vec2 transformedUV = calculateUV(uv, imgWidth, imgHeight); + + // Mode 4 (repeat): use fract() to tile the image + if (ubuf.fillMode > 3.5) { + return texture(tex, fract(transformedUV)); + } + + // Check if UV is out of bounds + if (transformedUV.x < 0.0 || transformedUV.x > 1.0 || + transformedUV.y < 0.0 || transformedUV.y > 1.0) { + return ubuf.fillColor; + } + + return texture(tex, transformedUV); +} + +void main() { + vec2 uv = qt_TexCoord0; + + // Sample textures with fill mode (handles solid colors) + vec4 color1 = sampleWithFillMode(source1, uv, ubuf.imageWidth1, ubuf.imageHeight1, ubuf.isSolid1, ubuf.solidColor1); + vec4 color2 = sampleWithFillMode(source2, uv, ubuf.imageWidth2, ubuf.imageHeight2, ubuf.isSolid2, ubuf.solidColor2); + + // Mix the two textures based on progress value + fragColor = mix(color1, color2, ubuf.progress) * ubuf.qt_Opacity; +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_honeycomb.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_honeycomb.frag new file mode 100644 index 0000000..ef7d2ac --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_honeycomb.frag @@ -0,0 +1,170 @@ +// ===== wp_honeycomb.frag ===== +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source1; +layout(binding = 2) uniform sampler2D source2; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float progress; + float cellSize; // Size of hexagonal cells in UV space (default 0.04) + float centerX; // X coordinate of wave origin (0.0 to 1.0) + float centerY; // Y coordinate of wave origin (0.0 to 1.0) + float aspectRatio; // Width / Height of the screen + + // Fill mode parameters + float fillMode; // 0=center, 1=crop, 2=fit, 3=stretch, 4=repeat + float imageWidth1; // Width of source1 image + float imageHeight1; // Height of source1 image + float imageWidth2; // Width of source2 image + float imageHeight2; // Height of source2 image + float screenWidth; // Screen width + float screenHeight; // Screen height + vec4 fillColor; // Fill color for empty areas (default: black) + + // Solid color mode + float isSolid1; // 1.0 if source1 is solid color, 0.0 otherwise + float isSolid2; // 1.0 if source2 is solid color, 0.0 otherwise + vec4 solidColor1; // Solid color for source1 + vec4 solidColor2; // Solid color for source2 +} ubuf; + +// Calculate UV coordinates based on fill mode +vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { + float imageAspect = imgWidth / imgHeight; + float screenAspect = ubuf.screenWidth / ubuf.screenHeight; + vec2 transformedUV = uv; + + if (ubuf.fillMode < 0.5) { + // Mode 0: no (center) - No resize, center image at original size + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imageOffset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(imgWidth, imgHeight)) * 0.5; + vec2 imagePixel = screenPixel - imageOffset; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 1.5) { + // Mode 1: crop (fill/cover) - Fill screen, crop excess (default) + float scale = max(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (scaledImageSize - vec2(ubuf.screenWidth, ubuf.screenHeight)) / scaledImageSize; + transformedUV = uv * (vec2(1.0) - offset) + offset * 0.5; + } + else if (ubuf.fillMode < 2.5) { + // Mode 2: fit (contain) - Fit inside screen, maintain aspect ratio + float scale = min(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - scaledImageSize) * 0.5; + + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imagePixel = (screenPixel - offset) / scale; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 3.5) { + // Mode 3: stretch - Use original UV (stretches to fit) + } + else { + // Mode 4: repeat (tile) - Tile image at original size + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + transformedUV = screenPixel / vec2(imgWidth, imgHeight); + } + + return transformedUV; +} + +// Sample texture with fill mode and handle out-of-bounds +vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float imgWidth, float imgHeight, float isSolid, vec4 solidColor) { + if (isSolid > 0.5) { + return solidColor; + } + + vec2 transformedUV = calculateUV(uv, imgWidth, imgHeight); + + if (ubuf.fillMode > 3.5) { + return texture(tex, fract(transformedUV)); + } + + if (transformedUV.x < 0.0 || transformedUV.x > 1.0 || + transformedUV.y < 0.0 || transformedUV.y > 1.0) { + return ubuf.fillColor; + } + + return texture(tex, transformedUV); +} + +// Convert cartesian to axial hex coordinates and round to nearest hex center +vec2 hexRound(vec2 axial) { + // Convert axial (q,r) to cube (x,y,z) where x+y+z=0 + float x = axial.x; + float z = axial.y; + float y = -x - z; + + // Round each + float rx = round(x); + float ry = round(y); + float rz = round(z); + + // Fix rounding errors by resetting the component with largest diff + float dx = abs(rx - x); + float dy = abs(ry - y); + float dz = abs(rz - z); + + if (dx > dy && dx > dz) { + rx = -ry - rz; + } else if (dy > dz) { + ry = -rx - rz; + } else { + rz = -rx - ry; + } + + return vec2(rx, rz); +} + +void main() { + vec2 uv = qt_TexCoord0; + + // Sample both textures at original UV + vec4 color1 = sampleWithFillMode(source1, uv, ubuf.imageWidth1, ubuf.imageHeight1, ubuf.isSolid1, ubuf.solidColor1); + vec4 color2 = sampleWithFillMode(source2, uv, ubuf.imageWidth2, ubuf.imageHeight2, ubuf.isSolid2, ubuf.solidColor2); + + // Aspect-correct the UV for hex grid so cells appear as regular hexagons + vec2 aspectUV = vec2(uv.x * ubuf.aspectRatio, uv.y); + + // Convert to axial hex coordinates + // Hex grid: q axis along x, r axis at 60 degrees + float size = max(ubuf.cellSize, 0.01); + float q = (aspectUV.x * (2.0 / 3.0)) / size; + float r = ((-aspectUV.x / 3.0) + (sqrt(3.0) / 3.0) * aspectUV.y) / size; + + // Round to nearest hex center + vec2 hex = hexRound(vec2(q, r)); + + // Convert hex center back to aspect-corrected UV space + vec2 hexCenter; + hexCenter.x = size * (3.0 / 2.0) * hex.x; + hexCenter.y = size * (sqrt(3.0) * (hex.y + 0.5 * hex.x)); + + // Calculate distance from this cell's center to the wave origin (aspect-corrected) + vec2 origin = vec2(ubuf.centerX * ubuf.aspectRatio, ubuf.centerY); + float dist = distance(hexCenter, origin); + + // Maximum distance from origin to any corner (for normalization) + float maxDistX = max(ubuf.centerX * ubuf.aspectRatio, (1.0 - ubuf.centerX) * ubuf.aspectRatio); + float maxDistY = max(ubuf.centerY, 1.0 - ubuf.centerY); + float maxDist = length(vec2(maxDistX, maxDistY)); + + // Wave expansion (same approach as disc shader): + // Start radius behind the origin so the smoothstep zone is fully off-screen at progress=0 + float softEdge = 0.15 * maxDist; + float totalDistance = maxDist + 2.0 * softEdge; + float radius = -softEdge + ubuf.progress * totalDistance; + + // factor = 0 inside the wave (revealed), 1 outside (not yet reached) + float factor = smoothstep(radius - softEdge, radius + softEdge, dist); + float cellProgress = 1.0 - factor; + + fragColor = mix(color1, color2, cellProgress) * ubuf.qt_Opacity; +} diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_pixelate.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_pixelate.frag new file mode 100644 index 0000000..32ac40e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_pixelate.frag @@ -0,0 +1,119 @@ +// ===== wp_pixelate.frag ===== +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source1; +layout(binding = 2) uniform sampler2D source2; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float progress; + float maxBlockSize; // Maximum block size in pixels (default 64) + + // Fill mode parameters + float fillMode; // 0=center, 1=crop, 2=fit, 3=stretch, 4=repeat + float imageWidth1; // Width of source1 image + float imageHeight1; // Height of source1 image + float imageWidth2; // Width of source2 image + float imageHeight2; // Height of source2 image + float screenWidth; // Screen width + float screenHeight; // Screen height + vec4 fillColor; // Fill color for empty areas (default: black) + + // Solid color mode + float isSolid1; // 1.0 if source1 is solid color, 0.0 otherwise + float isSolid2; // 1.0 if source2 is solid color, 0.0 otherwise + vec4 solidColor1; // Solid color for source1 + vec4 solidColor2; // Solid color for source2 +} ubuf; + +// Calculate UV coordinates based on fill mode +vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { + float imageAspect = imgWidth / imgHeight; + float screenAspect = ubuf.screenWidth / ubuf.screenHeight; + vec2 transformedUV = uv; + + if (ubuf.fillMode < 0.5) { + // Mode 0: no (center) - No resize, center image at original size + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imageOffset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(imgWidth, imgHeight)) * 0.5; + vec2 imagePixel = screenPixel - imageOffset; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 1.5) { + // Mode 1: crop (fill/cover) - Fill screen, crop excess (default) + float scale = max(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (scaledImageSize - vec2(ubuf.screenWidth, ubuf.screenHeight)) / scaledImageSize; + transformedUV = uv * (vec2(1.0) - offset) + offset * 0.5; + } + else if (ubuf.fillMode < 2.5) { + // Mode 2: fit (contain) - Fit inside screen, maintain aspect ratio + float scale = min(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - scaledImageSize) * 0.5; + + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imagePixel = (screenPixel - offset) / scale; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 3.5) { + // Mode 3: stretch - Use original UV (stretches to fit) + } + else { + // Mode 4: repeat (tile) - Tile image at original size + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + transformedUV = screenPixel / vec2(imgWidth, imgHeight); + } + + return transformedUV; +} + +// Sample texture with fill mode and handle out-of-bounds +vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float imgWidth, float imgHeight, float isSolid, vec4 solidColor) { + if (isSolid > 0.5) { + return solidColor; + } + + vec2 transformedUV = calculateUV(uv, imgWidth, imgHeight); + + if (ubuf.fillMode > 3.5) { + return texture(tex, fract(transformedUV)); + } + + if (transformedUV.x < 0.0 || transformedUV.x > 1.0 || + transformedUV.y < 0.0 || transformedUV.y > 1.0) { + return ubuf.fillColor; + } + + return texture(tex, transformedUV); +} + +void main() { + vec2 uv = qt_TexCoord0; + + // Triangle wave: pixelation peaks at progress=0.5 + float pixelIntensity = 1.0 - abs(2.0 * ubuf.progress - 1.0); + + // Compute the sampling UV — snap to block grid when pixelating, pass through otherwise + vec2 sampleUV = uv; + if (pixelIntensity > 0.001) { + float blockSize = ubuf.maxBlockSize * pixelIntensity; + vec2 screenSize = vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 pixelCoord = uv * screenSize; + vec2 snappedPixel = floor(pixelCoord / blockSize) * blockSize + blockSize * 0.5; + sampleUV = snappedPixel / screenSize; + } + + // Sample both textures at the (possibly snapped) UV + vec4 color1 = sampleWithFillMode(source1, sampleUV, ubuf.imageWidth1, ubuf.imageHeight1, ubuf.isSolid1, ubuf.solidColor1); + vec4 color2 = sampleWithFillMode(source2, sampleUV, ubuf.imageWidth2, ubuf.imageHeight2, ubuf.isSolid2, ubuf.solidColor2); + + // Crossfade concentrated around the peak pixelation (progress ~0.5) + float blend = smoothstep(0.4, 0.6, ubuf.progress); + + fragColor = mix(color1, color2, blend) * ubuf.qt_Opacity; +} diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_stripes.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_stripes.frag new file mode 100644 index 0000000..1136bc6 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_stripes.frag @@ -0,0 +1,224 @@ +// ===== wp_stripes.frag ===== +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source1; // Current wallpaper +layout(binding = 2) uniform sampler2D source2; // Next wallpaper + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float progress; // Transition progress (0.0 to 1.0) + float stripeCount; // Number of stripes (default 12.0) + float angle; // Angle of stripes in degrees (default 30.0) + float smoothness; // Edge smoothness (0.0 to 1.0, 0=sharp, 1=very smooth) + float aspectRatio; // Width / Height of the screen + + // Fill mode parameters + float fillMode; // 0=center, 1=crop, 2=fit, 3=stretch, 4=repeat + float imageWidth1; // Width of source1 image + float imageHeight1; // Height of source1 image + float imageWidth2; // Width of source2 image + float imageHeight2; // Height of source2 image + float screenWidth; // Screen width + float screenHeight; // Screen height + vec4 fillColor; // Fill color for empty areas (default: black) + + // Solid color mode + float isSolid1; // 1.0 if source1 is solid color, 0.0 otherwise + float isSolid2; // 1.0 if source2 is solid color, 0.0 otherwise + vec4 solidColor1; // Solid color for source1 + vec4 solidColor2; // Solid color for source2 +} ubuf; + +// Calculate UV coordinates based on fill mode +vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { + float imageAspect = imgWidth / imgHeight; + float screenAspect = ubuf.screenWidth / ubuf.screenHeight; + vec2 transformedUV = uv; + + if (ubuf.fillMode < 0.5) { + // Mode 0: no (center) - No resize, center image at original size + // Convert UV to pixel coordinates, offset, then back to UV in image space + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imageOffset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(imgWidth, imgHeight)) * 0.5; + vec2 imagePixel = screenPixel - imageOffset; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 1.5) { + // Mode 1: crop (fill/cover) - Fill screen, crop excess (default) + float scale = max(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (scaledImageSize - vec2(ubuf.screenWidth, ubuf.screenHeight)) / scaledImageSize; + transformedUV = uv * (vec2(1.0) - offset) + offset * 0.5; + } + else if (ubuf.fillMode < 2.5) { + // Mode 2: fit (contain) - Fit inside screen, maintain aspect ratio + float scale = min(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - scaledImageSize) * 0.5; + + // Convert screen UV to pixel coordinates + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + // Adjust for offset and scale + vec2 imagePixel = (screenPixel - offset) / scale; + // Convert back to UV coordinates in image space + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 3.5) { + // Mode 3: stretch - Use original UV (stretches to fit) + // No transformation needed for stretch mode + } + else { + // Mode 4: repeat (tile) - Tile image at original size + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + transformedUV = screenPixel / vec2(imgWidth, imgHeight); + } + + return transformedUV; +} + +// Sample texture with fill mode and handle out-of-bounds +// If isSolid > 0.5, returns solidColor instead of sampling texture +vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float imgWidth, float imgHeight, float isSolid, vec4 solidColor) { + // Return solid color if in solid color mode + if (isSolid > 0.5) { + return solidColor; + } + + vec2 transformedUV = calculateUV(uv, imgWidth, imgHeight); + + // Mode 4 (repeat): use fract() to tile the image + if (ubuf.fillMode > 3.5) { + return texture(tex, fract(transformedUV)); + } + + // Check if UV is out of bounds + if (transformedUV.x < 0.0 || transformedUV.x > 1.0 || + transformedUV.y < 0.0 || transformedUV.y > 1.0) { + return ubuf.fillColor; + } + + return texture(tex, transformedUV); +} + +void main() { + vec2 uv = qt_TexCoord0; + + // Sample textures with fill mode (handles solid colors) + vec4 color1 = sampleWithFillMode(source1, uv, ubuf.imageWidth1, ubuf.imageHeight1, ubuf.isSolid1, ubuf.solidColor1); + vec4 color2 = sampleWithFillMode(source2, uv, ubuf.imageWidth2, ubuf.imageHeight2, ubuf.isSolid2, ubuf.solidColor2); + + // Map smoothness from 0.0-1.0 to 0.001-0.3 range + // Using a non-linear mapping for better control at low values + float mappedSmoothness = mix(0.001, 0.3, ubuf.smoothness * ubuf.smoothness); + + // Use values directly without forcing defaults + float stripes = (ubuf.stripeCount > 0.0) ? ubuf.stripeCount : 12.0; + float angleRad = radians(ubuf.angle); + float edgeSmooth = mappedSmoothness; + + // Create a coordinate system for stripes based on angle + // At 0°: vertical stripes (divide by x) + // At 45°: diagonal stripes + // At 90°: horizontal stripes (divide by y) + + // Transform coordinates based on angle + float cosA = cos(angleRad); + float sinA = sin(angleRad); + + // Project the UV position onto the stripe direction + // This gives us the position along the stripe direction + float stripeCoord = uv.x * cosA + uv.y * sinA; + + // Perpendicular coordinate (for edge movement) + float perpCoord = -uv.x * sinA + uv.y * cosA; + + // Calculate the range of perpCoord based on angle + // This determines how far edges need to travel to fully cover the screen + float minPerp = min(min(0.0 * -sinA + 0.0 * cosA, 1.0 * -sinA + 0.0 * cosA), + min(0.0 * -sinA + 1.0 * cosA, 1.0 * -sinA + 1.0 * cosA)); + float maxPerp = max(max(0.0 * -sinA + 0.0 * cosA, 1.0 * -sinA + 0.0 * cosA), + max(0.0 * -sinA + 1.0 * cosA, 1.0 * -sinA + 1.0 * cosA)); + + // Determine which stripe we're in + float stripePos = stripeCoord * stripes; + int stripeIndex = int(floor(stripePos)); + + // Determine if this is an odd or even stripe + bool isOddStripe = mod(float(stripeIndex), 2.0) != 0.0; + + // Calculate the progress for this specific stripe with wave delay + // Use absolute stripe position for consistent delay across all stripes + float normalizedStripePos = clamp(stripePos / stripes, 0.0, 1.0); + + // Increased delay and better distribution + float maxDelay = 0.1; + float stripeDelay = normalizedStripePos * maxDelay; + + // Better progress mapping that uses the full 0.0-1.0 range + // Map progress so that: + // - First stripe starts at progress = 0.0 + // - Last stripe finishes at progress = 1.0 + float stripeProgress; + if (ubuf.progress <= stripeDelay) { + stripeProgress = 0.0; + } else if (ubuf.progress >= (stripeDelay + (1.0 - maxDelay))) { + stripeProgress = 1.0; + } else { + // Scale the progress within the active window for this stripe + float activeStart = stripeDelay; + float activeEnd = stripeDelay + (1.0 - maxDelay); + stripeProgress = (ubuf.progress - activeStart) / (activeEnd - activeStart); + } + + // Use the perpendicular coordinate for edge comparison + float yPos = perpCoord; + + // Calculate edge position for this stripe + // Use the actual perpendicular coordinate range for this angle + float perpRange = maxPerp - minPerp; + float margin = edgeSmooth; // Just enough buffer for the smoothstep zone to clear the edge + float edgePosition; + if (isOddStripe) { + // Odd stripes: edge moves from max to min + edgePosition = maxPerp + margin - stripeProgress * (perpRange + margin * 2.0); + } else { + // Even stripes: edge moves from min to max + edgePosition = minPerp - margin + stripeProgress * (perpRange + margin * 2.0); + } + + // Determine which wallpaper to show based on rotated position + float mask; + if (isOddStripe) { + // Odd stripes reveal new wallpaper from bottom + mask = smoothstep(edgePosition - edgeSmooth, edgePosition + edgeSmooth, yPos); + } else { + // Even stripes reveal new wallpaper from top + mask = 1.0 - smoothstep(edgePosition - edgeSmooth, edgePosition + edgeSmooth, yPos); + } + + // Mix the wallpapers + fragColor = mix(color1, color2, mask); + + // Force exact values at start and end to prevent any bleed-through + if (ubuf.progress <= 0.0) { + fragColor = color1; // Only show old wallpaper at start + } else if (ubuf.progress >= 1.0) { + fragColor = color2; // Only show new wallpaper at end + } else { + // Add manga-style edge shadow only during transition + float edgeDist = abs(yPos - edgePosition); + float shadowStrength = 1.0 - smoothstep(0.0, edgeSmooth * 2.5, edgeDist); + shadowStrength *= 0.2 * (1.0 - abs(stripeProgress - 0.5) * 2.0); + fragColor.rgb *= (1.0 - shadowStrength); + + // Add slight vignette during transition for dramatic effect + float vignette = 1.0 - ubuf.progress * 0.1 * (1.0 - abs(stripeProgress - 0.5) * 2.0); + fragColor.rgb *= vignette; + } + + fragColor *= ubuf.qt_Opacity; +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_wipe.frag b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_wipe.frag new file mode 100644 index 0000000..19ab477 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Shaders/frag/wp_wipe.frag @@ -0,0 +1,151 @@ +// ===== wp_wipe.frag ===== +#version 450 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source1; // Current wallpaper +layout(binding = 2) uniform sampler2D source2; // Next wallpaper + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float progress; // Transition progress (0.0 to 1.0) + float direction; // 0=left, 1=right, 2=up, 3=down + float smoothness; // Edge smoothness (0.0 to 1.0, 0=sharp, 1=very smooth) + + // Fill mode parameters + float fillMode; // 0=center, 1=crop, 2=fit, 3=stretch, 4=repeat + float imageWidth1; // Width of source1 image + float imageHeight1; // Height of source1 image + float imageWidth2; // Width of source2 image + float imageHeight2; // Height of source2 image + float screenWidth; // Screen width + float screenHeight; // Screen height + vec4 fillColor; // Fill color for empty areas (default: black) + + // Solid color mode + float isSolid1; // 1.0 if source1 is solid color, 0.0 otherwise + float isSolid2; // 1.0 if source2 is solid color, 0.0 otherwise + vec4 solidColor1; // Solid color for source1 + vec4 solidColor2; // Solid color for source2 +} ubuf; + +// Calculate UV coordinates based on fill mode +vec2 calculateUV(vec2 uv, float imgWidth, float imgHeight) { + float imageAspect = imgWidth / imgHeight; + float screenAspect = ubuf.screenWidth / ubuf.screenHeight; + vec2 transformedUV = uv; + + if (ubuf.fillMode < 0.5) { + // Mode 0: no (center) - No resize, center image at original size + // Convert UV to pixel coordinates, offset, then back to UV in image space + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + vec2 imageOffset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - vec2(imgWidth, imgHeight)) * 0.5; + vec2 imagePixel = screenPixel - imageOffset; + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 1.5) { + // Mode 1: crop (fill/cover) - Fill screen, crop excess (default) + float scale = max(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (scaledImageSize - vec2(ubuf.screenWidth, ubuf.screenHeight)) / scaledImageSize; + transformedUV = uv * (vec2(1.0) - offset) + offset * 0.5; + } + else if (ubuf.fillMode < 2.5) { + // Mode 2: fit (contain) - Fit inside screen, maintain aspect ratio + float scale = min(ubuf.screenWidth / imgWidth, ubuf.screenHeight / imgHeight); + vec2 scaledImageSize = vec2(imgWidth, imgHeight) * scale; + vec2 offset = (vec2(ubuf.screenWidth, ubuf.screenHeight) - scaledImageSize) * 0.5; + + // Convert screen UV to pixel coordinates + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + // Adjust for offset and scale + vec2 imagePixel = (screenPixel - offset) / scale; + // Convert back to UV coordinates in image space + transformedUV = imagePixel / vec2(imgWidth, imgHeight); + } + else if (ubuf.fillMode < 3.5) { + // Mode 3: stretch - Use original UV (stretches to fit) + // No transformation needed for stretch mode + } + else { + // Mode 4: repeat (tile) - Tile image at original size + vec2 screenPixel = uv * vec2(ubuf.screenWidth, ubuf.screenHeight); + transformedUV = screenPixel / vec2(imgWidth, imgHeight); + } + + return transformedUV; +} + +// Sample texture with fill mode and handle out-of-bounds +// If isSolid > 0.5, returns solidColor instead of sampling texture +vec4 sampleWithFillMode(sampler2D tex, vec2 uv, float imgWidth, float imgHeight, float isSolid, vec4 solidColor) { + // Return solid color if in solid color mode + if (isSolid > 0.5) { + return solidColor; + } + + vec2 transformedUV = calculateUV(uv, imgWidth, imgHeight); + + // Mode 4 (repeat): use fract() to tile the image + if (ubuf.fillMode > 3.5) { + return texture(tex, fract(transformedUV)); + } + + // Check if UV is out of bounds + if (transformedUV.x < 0.0 || transformedUV.x > 1.0 || + transformedUV.y < 0.0 || transformedUV.y > 1.0) { + return ubuf.fillColor; + } + + return texture(tex, transformedUV); +} + +void main() { + vec2 uv = qt_TexCoord0; + + // Sample textures with fill mode (handles solid colors) + vec4 color1 = sampleWithFillMode(source1, uv, ubuf.imageWidth1, ubuf.imageHeight1, ubuf.isSolid1, ubuf.solidColor1); + vec4 color2 = sampleWithFillMode(source2, uv, ubuf.imageWidth2, ubuf.imageHeight2, ubuf.isSolid2, ubuf.solidColor2); + + // Map smoothness from 0.0-1.0 to 0.001-0.5 range + // Using a non-linear mapping for better control + float mappedSmoothness = mix(0.001, 0.5, ubuf.smoothness * ubuf.smoothness); + + float edge = 0.0; + float factor = 0.0; + + // Extend the progress range to account for smoothness + // This ensures the transition completes fully at the edges + float extendedProgress = ubuf.progress * (1.0 + 2.0 * mappedSmoothness) - mappedSmoothness; + + // Calculate edge position based on direction + // As progress goes from 0 to 1, we reveal source2 (new wallpaper) + if (ubuf.direction < 0.5) { + // Wipe from right to left (new image enters from right) + edge = 1.0 - extendedProgress; + factor = smoothstep(edge - mappedSmoothness, edge + mappedSmoothness, uv.x); + fragColor = mix(color1, color2, factor); + } + else if (ubuf.direction < 1.5) { + // Wipe from left to right (new image enters from left) + edge = extendedProgress; + factor = smoothstep(edge - mappedSmoothness, edge + mappedSmoothness, uv.x); + fragColor = mix(color2, color1, factor); + } + else if (ubuf.direction < 2.5) { + // Wipe from bottom to top (new image enters from bottom) + edge = 1.0 - extendedProgress; + factor = smoothstep(edge - mappedSmoothness, edge + mappedSmoothness, uv.y); + fragColor = mix(color1, color2, factor); + } + else { + // Wipe from top to bottom (new image enters from top) + edge = extendedProgress; + factor = smoothstep(edge - mappedSmoothness, edge + mappedSmoothness, uv.y); + fragColor = mix(color2, color1, factor); + } + + fragColor *= ubuf.qt_Opacity; +} \ No newline at end of file diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/appicon_colorize.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/appicon_colorize.frag.qsb new file mode 100644 index 0000000..ea0abbe Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/appicon_colorize.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/color_picker.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/color_picker.frag.qsb new file mode 100644 index 0000000..cfbbb50 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/color_picker.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/graph.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/graph.frag.qsb new file mode 100644 index 0000000..16cdee2 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/graph.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/progress_border.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/progress_border.frag.qsb new file mode 100644 index 0000000..60fda9f Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/progress_border.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/rounded_image.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/rounded_image.frag.qsb new file mode 100644 index 0000000..8942a67 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/rounded_image.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wave_spectrum.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wave_spectrum.frag.qsb new file mode 100644 index 0000000..6a680c5 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wave_spectrum.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/weather_cloud.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/weather_cloud.frag.qsb new file mode 100644 index 0000000..2fe9734 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/weather_cloud.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/weather_rain.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/weather_rain.frag.qsb new file mode 100644 index 0000000..5c06dd6 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/weather_rain.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/weather_snow.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/weather_snow.frag.qsb new file mode 100644 index 0000000..e466857 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/weather_snow.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/weather_stars.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/weather_stars.frag.qsb new file mode 100644 index 0000000..544c2b3 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/weather_stars.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/weather_sun.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/weather_sun.frag.qsb new file mode 100644 index 0000000..2b231fa Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/weather_sun.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_disc.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_disc.frag.qsb new file mode 100644 index 0000000..d324e00 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_disc.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_fade.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_fade.frag.qsb new file mode 100644 index 0000000..6778626 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_fade.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_honeycomb.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_honeycomb.frag.qsb new file mode 100644 index 0000000..4e3542c Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_honeycomb.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_pixelate.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_pixelate.frag.qsb new file mode 100644 index 0000000..9751a0f Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_pixelate.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_stripes.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_stripes.frag.qsb new file mode 100644 index 0000000..70b529f Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_stripes.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_wipe.frag.qsb b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_wipe.frag.qsb new file mode 100644 index 0000000..f878f35 Binary files /dev/null and b/fedora/.config/quickshell/noctalia-shell/Shaders/qsb/wp_wipe.frag.qsb differ diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/AudioSpectrum/NLinearSpectrum.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/AudioSpectrum/NLinearSpectrum.qml new file mode 100644 index 0000000..c30c834 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/AudioSpectrum/NLinearSpectrum.qml @@ -0,0 +1,49 @@ +import QtQuick +import qs.Commons + +Item { + id: root + property color fillColor: Color.mPrimary + property color strokeColor: Color.mOnSurface + property int strokeWidth: 0 + property var values: [] + property bool vertical: false + property string barPosition: "top" // "top", "bottom", "left", "right" + property bool mirrored: true + + // Minimum signal properties + property bool showMinimumSignal: false + property real minimumSignalValue: 0.01 // Default to 1% of height + + // Pre compute horizontal mirroring + readonly property int valuesCount: (values && values.length !== undefined) ? values.length : 0 + readonly property int totalBars: mirrored ? valuesCount * 2 : valuesCount + readonly property real barSlotSize: totalBars > 0 ? (vertical ? height : width) / totalBars : 0 + readonly property bool highQuality: (Settings.data.audio.visualizerType === "low") ? false : true + + Repeater { + model: root.totalBars + + Rectangle { + property int valueIndex: root.mirrored ? (index < root.valuesCount ? root.valuesCount - 1 - index : index - root.valuesCount) : index + + property real rawAmp: (root.values && root.values[valueIndex] !== undefined) ? root.values[valueIndex] : 0 + property real amp: (root.showMinimumSignal && rawAmp === 0) ? root.minimumSignalValue : rawAmp + + color: root.fillColor + border.color: root.strokeColor + border.width: root.strokeWidth + antialiasing: root.highQuality + smooth: root.highQuality + + // Only update when value actually changes - reduces GPU load + width: vertical ? root.width * amp : root.barSlotSize * 0.5 + height: vertical ? root.barSlotSize * 0.5 : root.height * amp + x: vertical ? (root.barPosition === "left" ? 0 : root.width - width) : index * root.barSlotSize + (root.barSlotSize * 0.25) + y: vertical ? index * root.barSlotSize + (root.barSlotSize * 0.25) : root.height - height + + // Disable updates when invisible to save GPU + visible: root.visible + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/AudioSpectrum/NMirroredSpectrum.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/AudioSpectrum/NMirroredSpectrum.qml new file mode 100644 index 0000000..658299a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/AudioSpectrum/NMirroredSpectrum.qml @@ -0,0 +1,51 @@ +import QtQuick +import qs.Commons + +Item { + id: root + property color fillColor: Color.mPrimary + property color strokeColor: Color.mOnSurface + property int strokeWidth: 0 + property var values: [] + property bool vertical: false + property bool mirrored: true + + // Minimum signal properties + property bool showMinimumSignal: false + property real minimumSignalValue: 0.01 // Default to 1% of height + + // Pre-compute mirroring + readonly property int valuesCount: (values && values.length !== undefined) ? values.length : 0 + readonly property int totalBars: mirrored ? valuesCount * 2 : valuesCount + readonly property real barSlotSize: totalBars > 0 ? (vertical ? height : width) / totalBars : 0 + readonly property bool highQuality: (Settings.data.audio.visualizerType === "low") ? false : true + readonly property real centerY: height / 2 + readonly property real centerX: width / 2 + + Repeater { + model: root.totalBars + + Rectangle { + property int valueIndex: root.mirrored ? (index < root.valuesCount ? root.valuesCount - 1 - index : index - root.valuesCount) : index + + property real rawAmp: (root.values && root.values[valueIndex] !== undefined) ? root.values[valueIndex] : 0 + property real amp: (root.showMinimumSignal && rawAmp === 0) ? root.minimumSignalValue : rawAmp + + property real barSize: (vertical ? root.width : root.height) * amp + + color: root.fillColor + border.color: root.strokeColor + border.width: root.strokeWidth + antialiasing: root.highQuality + smooth: root.highQuality + + width: vertical ? barSize : root.barSlotSize * 0.8 + height: vertical ? root.barSlotSize * 0.8 : barSize + x: vertical ? root.centerX - (barSize / 2) : index * root.barSlotSize + (root.barSlotSize * 0.25) + y: vertical ? index * root.barSlotSize + (root.barSlotSize * 0.25) : root.centerY - (barSize / 2) + + // Disable updates when invisible to save GPU + visible: root.visible + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/AudioSpectrum/NWaveSpectrum.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/AudioSpectrum/NWaveSpectrum.qml new file mode 100644 index 0000000..329b201 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/AudioSpectrum/NWaveSpectrum.qml @@ -0,0 +1,72 @@ +import QtQuick +import Quickshell +import qs.Commons + +Item { + id: root + property color fillColor: Color.mPrimary + property color strokeColor: Color.mOnSurface + property int strokeWidth: 0 + property var values: [] + property bool vertical: false + property bool mirrored: true + + // Minimum signal properties + property bool showMinimumSignal: false + property real minimumSignalValue: 0.01 // Default to 1% of height + + readonly property int valuesCount: (values && values.length !== undefined) ? values.length : 0 + readonly property bool hasData: valuesCount >= 2 + + // Data texture: one pixel per value, R channel = amplitude + Item { + id: dataRow + width: Math.max(root.valuesCount, 4) + height: 1 + + Repeater { + model: dataRow.width + + Rectangle { + required property int index + x: index + width: 1 + height: 1 + color: { + if (index >= root.valuesCount) + return Qt.rgba(0, 0, 0, 1); + var v = root.values[index]; + if (v === undefined || v === null || !isFinite(v)) + v = 0; + if (root.showMinimumSignal && v === 0) + v = root.minimumSignalValue; + return Qt.rgba(Math.max(0, Math.min(1, v)), 0, 0, 1); + } + } + } + } + + ShaderEffectSource { + id: dataTex + sourceItem: dataRow + textureSize: Qt.size(dataRow.width, 1) + live: true + smooth: false + hideSource: true + } + + ShaderEffect { + anchors.fill: parent + visible: root.hasData && root.width > 0 && root.height > 0 + + property variant dataSource: dataTex + property color fillColor: root.fillColor + property real count: root.valuesCount + property real texWidth: dataRow.width + property real vertical: root.vertical ? 1.0 : 0.0 + property real mirrored: root.mirrored ? 1.0 : 0.0 + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wave_spectrum.frag.qsb") + blending: true + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NBattery.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NBattery.qml new file mode 100644 index 0000000..fea02b3 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NBattery.qml @@ -0,0 +1,205 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +// Battery widget with Android 16 style rendering (horizontal or vertical) +Item { + id: root + + // Data (must be provided by parent) + required property real percentage + required property bool charging + required property bool pluggedIn + required property bool ready + required property bool low + required property bool critical + + // Sizing - baseSize controls overall scaleFactor for bar/panel usage + property real baseSize: Style.fontSizeM + + // Styling - no hardcoded colors, only theme colors + property color baseColor: Color.mOnSurface + property color lowColor: Color.mError + property color chargingColor: Color.mPrimary + property color textColor: Color.mSurface + + // Display options + property bool showPercentageText: true + property bool vertical: false + + // Alternating state icon display (toggles between percentage and icon when charging) + property bool showStateIcon: false + + onChargingChanged: { + if (!charging) + showStateIcon = false; + } + + // Internal sizing calculations based on baseSize + readonly property real scaleFactor: baseSize / Style.fontSizeM + readonly property real bodyWidth: { + const min = Style.toOdd(22 * scaleFactor); + if (!showPercentageText) { + return min; + } + + // increase length when showing 100% + if (percentage > 99) { + const max = Style.toOdd(30 * scaleFactor); + return max; + } + return min; + } + + readonly property real bodyHeight: Style.toOdd(14 * scaleFactor) + readonly property real terminalWidth: Math.round(2.5 * scaleFactor) + readonly property real terminalHeight: Math.round(7 * scaleFactor) + readonly property real cornerRadius: Math.round(3 * scaleFactor) + + // Total size is just body + terminal (no external icon) + readonly property real totalWidth: vertical ? bodyHeight : bodyWidth + terminalWidth + readonly property real totalHeight: vertical ? bodyWidth + terminalWidth : bodyHeight + + // Determine active color based on state + readonly property color activeColor: { + if (!ready) { + return Qt.alpha(baseColor, Style.opacityMedium); + } + if (charging) { + return chargingColor; + } + if (low || critical) { + return lowColor; + } + return baseColor; + } + + // Background color for empty portion (semi-transparent) + readonly property color emptyColor: Qt.alpha(baseColor, 0.66) + + // State icon logic + readonly property string stateIcon: { + if (!ready) + return "x"; + if (charging) + return "bolt-filled"; + if (pluggedIn) + return "plug-filled"; + return ""; + } + + // Animated percentage for smooth transitions + property real animatedPercentage: percentage + + Behavior on animatedPercentage { + enabled: !Settings.data.general.animationDisabled + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + // Timer to alternate between percentage text and state icon when charging/plugged + Timer { + id: alternateTimer + interval: 4000 + repeat: true + running: root.charging && root.showPercentageText + onTriggered: root.showStateIcon = !root.showStateIcon + } + + implicitWidth: Math.round(totalWidth) + implicitHeight: Math.round(totalHeight) + Layout.maximumWidth: implicitWidth + Layout.maximumHeight: implicitHeight + + // Battery body container + Item { + id: batteryBody + width: root.vertical ? root.bodyHeight : root.bodyWidth + root.terminalWidth + height: root.vertical ? root.bodyWidth + root.terminalWidth : root.bodyHeight + anchors.left: root.vertical ? undefined : parent.left + anchors.bottom: root.vertical ? parent.bottom : undefined + anchors.horizontalCenter: root.vertical ? parent.horizontalCenter : undefined + anchors.verticalCenter: root.vertical ? undefined : parent.verticalCenter + + // Battery body background + Rectangle { + id: bodyBackground + y: root.vertical ? root.terminalWidth : 0 + width: root.vertical ? root.bodyHeight : root.bodyWidth + height: root.vertical ? root.bodyWidth : root.bodyHeight + radius: root.cornerRadius + color: root.emptyColor + } + + // Terminal cap + Rectangle { + x: root.vertical ? (root.bodyHeight - root.terminalHeight) / 2 : root.bodyWidth + y: root.vertical ? 0 : (root.bodyHeight - root.terminalHeight) / 2 + width: root.vertical ? root.terminalHeight : root.terminalWidth + height: root.vertical ? root.terminalWidth : root.terminalHeight + radius: root.cornerRadius / 2 + color: root.critical ? root.lowColor : root.emptyColor + } + + // Fill level + Rectangle { + id: fillRect + visible: root.ready && (root.animatedPercentage > 0 || root.critical) + x: 0 + y: root.vertical ? root.terminalWidth + root.bodyWidth * (1 - (root.critical ? 1 : root.animatedPercentage / 100)) : 0 + width: root.vertical ? root.bodyHeight : root.bodyWidth * (root.critical ? 1 : root.animatedPercentage / 100) + height: root.vertical ? root.bodyWidth * (root.critical ? 1 : root.animatedPercentage / 100) : root.bodyHeight + radius: root.cornerRadius + color: root.activeColor + } + } + + // Percentage text overlaid on battery center + NText { + id: percentageText + visible: opacity > 0 + opacity: root.showPercentageText && root.ready && (root.charging ? !root.showStateIcon : !root.pluggedIn) ? 1 : 0 + x: batteryBody.x + Style.pixelAlignCenter(bodyBackground.width, width) + y: batteryBody.y + bodyBackground.y + Style.pixelAlignCenter(bodyBackground.height, height) + font.family: Settings.data.ui.fontFixed + font.weight: Style.fontWeightBold + text: root.vertical ? String(Math.round(root.animatedPercentage)).split('').join('\n') : Math.round(root.animatedPercentage) + pointSize: root.baseSize * (root.vertical ? 0.82 : 0.82) + color: Qt.alpha(root.textColor, 0.75) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + lineHeight: root.vertical ? 0.7 : 1.0 + lineHeightMode: Text.ProportionalHeight + + Behavior on opacity { + enabled: !Settings.data.general.animationDisabled + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + } + + // State icon centered inside battery body (shown when alternating) + NIcon { + id: stateIconOverlay + visible: opacity > 0 + opacity: !root.ready || (root.charging ? (root.showStateIcon || !root.showPercentageText) : root.pluggedIn) ? 1 : 0 + x: batteryBody.x + Style.pixelAlignCenter(bodyBackground.width, width) + y: batteryBody.y + bodyBackground.y + Style.pixelAlignCenter(bodyBackground.height, height) + icon: root.stateIcon + pointSize: Style.toOdd(root.baseSize) + color: Qt.alpha(root.textColor, 0.75) + + Behavior on opacity { + enabled: !Settings.data.general.animationDisabled + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NBox.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NBox.qml new file mode 100644 index 0000000..16a0107 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NBox.qml @@ -0,0 +1,30 @@ +import QtQuick +import qs.Commons + +// Rounded group container using the variant surface color. +// To be used in side panels and settings panes to group fields or buttons. +// Opacity is based on panelBackgroundOpacity but clamped to a minimum to avoid full transparency. + +Item { + id: root + + property color color: Color.mSurfaceVariant + property bool forceOpaque: false + property alias radius: bg.radius + property alias border: bg.border + + Rectangle { + id: bg + anchors.fill: parent + radius: Style.radiusM + border.color: Style.boxBorderColor + border.width: Style.borderS + color: { + if (forceOpaque) { + return root.color; + } + + return Color.smartAlpha(root.color); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NBusyIndicator.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NBusyIndicator.qml new file mode 100644 index 0000000..48413b9 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NBusyIndicator.qml @@ -0,0 +1,64 @@ +import QtQuick +import qs.Commons + +Item { + id: root + + property bool running: true + property color color: Color.mPrimary + property int size: Style.baseWidgetSize + property int strokeWidth: Style.borderL + property int duration: Style.animationSlow * 2 + + implicitWidth: size + implicitHeight: size + + // GPU-optimized spinner - draw once, rotate with GPU transform + Item { + id: spinner + anchors.fill: parent + + // Static canvas - drawn ONCE, then cached + Canvas { + id: canvas + anchors.fill: parent + renderStrategy: Canvas.Cooperative // Better performance than Threaded for simple shapes + renderTarget: Canvas.FramebufferObject // GPU texture + + // Enable layer caching - critical for performance! + layer.enabled: true + layer.smooth: true + + Component.onCompleted: { + requestPaint(); + } + + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + + var centerX = width / 2; + var centerY = height / 2; + var radius = Math.min(width, height) / 2 - strokeWidth / 2; + + ctx.strokeStyle = root.color; + ctx.lineWidth = Math.max(1, root.strokeWidth); + ctx.lineCap = "round"; + + // Draw arc with gap (270 degrees = 3/4 of circle) + ctx.beginPath(); + ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + Math.PI * 1.5); + ctx.stroke(); + } + } + + // Smooth rotation animation - uses GPU transform, NO canvas repaints! + RotationAnimation on rotation { + running: root.running + from: 0 + to: 360 + duration: root.duration + loops: Animation.Infinite + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NButton.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NButton.qml new file mode 100644 index 0000000..45a857e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NButton.qml @@ -0,0 +1,184 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.UI + +Item { + id: root + + // Public properties + property string text: "" + property string icon: "" + property var tooltipText + property color backgroundColor: Color.mPrimary + property color textColor: Color.mOnPrimary + property color hoverColor: Color.mHover + property color textHoverColor: Color.mOnHover + property real fontSize: Style.fontSizeM + property int fontWeight: Style.fontWeightSemiBold + property real iconSize: Style.fontSizeL + property bool outlined: false + property int horizontalAlignment: Qt.AlignHCenter + property real buttonRadius: Style.iRadiusS + + // Signals + signal clicked + signal rightClicked + signal middleClicked + signal entered + signal exited + + // Internal properties + property bool hovered: false + readonly property color contentColor: { + if (!root.enabled) { + return Color.mOnSurfaceVariant; + } + if (root.hovered) { + return root.textHoverColor; + } + if (root.outlined) { + return root.backgroundColor; + } + return root.textColor; + } + + // Dimensions - include margin so border renders cleanly at fractional scales + implicitWidth: bg.implicitWidth + 2 * Style.borderS + implicitHeight: bg.implicitHeight + 2 * Style.borderS + + opacity: enabled ? 1.0 : 0.6 + + Rectangle { + id: bg + anchors.fill: parent + anchors.margins: Style.borderS + + implicitWidth: contentRow.implicitWidth + (root.fontSize * 2) + implicitHeight: contentRow.implicitHeight + (root.fontSize) + + radius: root.buttonRadius + color: { + if (!root.enabled) + return root.outlined ? "transparent" : Qt.lighter(Color.mSurfaceVariant, 1.2); + if (root.hovered) + return root.hoverColor; + return root.outlined ? "transparent" : root.backgroundColor; + } + + border.width: root.outlined ? Style.borderS : 0 + border.color: { + if (!root.enabled) + return Color.mOutline; + if (root.hovered) + return root.hoverColor; + return root.outlined ? root.backgroundColor : "transparent"; + } + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + Behavior on border.color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + // Content + RowLayout { + id: contentRow + anchors.verticalCenter: parent.verticalCenter + anchors.left: root.horizontalAlignment === Qt.AlignLeft ? parent.left : undefined + anchors.horizontalCenter: root.horizontalAlignment === Qt.AlignHCenter ? parent.horizontalCenter : undefined + anchors.leftMargin: root.horizontalAlignment === Qt.AlignLeft ? Style.marginL : 0 + spacing: Style.marginXS + + // Icon (optional) + NIcon { + Layout.alignment: Qt.AlignVCenter + visible: root.icon !== "" + icon: root.icon + pointSize: root.iconSize + color: root.contentColor + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + // Text + NText { + Layout.alignment: Qt.AlignVCenter + visible: root.text !== "" + text: root.text + pointSize: root.fontSize + font.weight: root.fontWeight + color: root.contentColor + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + } + + // Mouse interaction + MouseArea { + id: mouseArea + anchors.fill: parent + enabled: root.enabled + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + + onEntered: { + root.hovered = root.enabled ? true : false; + root.entered(); + if (root.hovered && root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + TooltipService.show(root, root.tooltipText); + } + } + onExited: { + root.hovered = false; + root.exited(); + if (root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + TooltipService.hide(); + } + } + onPressed: mouse => { + if (root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + TooltipService.hide(); + } + if (mouse.button === Qt.LeftButton) { + root.clicked(); + } else if (mouse.button == Qt.RightButton) { + root.rightClicked(); + } else if (mouse.button == Qt.MiddleButton) { + root.middleClicked(); + } + } + + onCanceled: { + root.hovered = false; + if (root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + TooltipService.hide(); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NCheckbox.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NCheckbox.qml new file mode 100644 index 0000000..30e51d0 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NCheckbox.qml @@ -0,0 +1,87 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons + +RowLayout { + id: root + + // Public API + property string label: "" + property string description: "" + property bool checked: false + property bool hovering: false + property color activeColor: Color.mPrimary + property color activeOnColor: Color.mOnPrimary + property int baseSize: root.defaultSize + property real labelSize: Style.fontSizeL + + readonly property int defaultSize: Style.baseWidgetSize * 0.7 + + signal toggled(bool checked) + signal entered + signal exited + + Layout.fillWidth: true + + NLabel { + label: root.label + labelSize: root.labelSize + description: root.description + visible: root.label !== "" || root.description !== "" + } + + // Spacer to push the checkbox to the far right + Item { + Layout.fillWidth: true + } + + Rectangle { + id: box + + opacity: enabled ? 1.0 : 0.6 + Layout.margins: Style.borderS + implicitWidth: Style.toOdd(root.baseSize) + implicitHeight: Style.toOdd(root.baseSize) + radius: Style.iRadiusXS * (root.baseSize / root.defaultSize) + color: root.checked ? root.activeColor : Color.mSurface + border.color: Color.mOutline + border.width: Style.borderS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + NIcon { + visible: root.checked + x: Style.pixelAlignCenter(parent.width, width) + y: Style.pixelAlignCenter(parent.height, height) + icon: "check" + color: root.activeOnColor + pointSize: Style.toOdd(root.baseSize * 0.5) + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: { + hovering = true; + root.entered(); + } + onExited: { + hovering = false; + root.exited(); + } + onClicked: root.toggled(!root.checked) + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NCircleStat.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NCircleStat.qml new file mode 100644 index 0000000..cb3341a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NCircleStat.qml @@ -0,0 +1,164 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Services.UI +import qs.Widgets + +// Compact circular statistic display using Layout management +Item { + id: root + + property real ratio: 0 // 0..1 range + property string icon: "" + property string suffix: "%" + property real contentScale: 1.0 + property color fillColor: Color.mPrimary + property var tooltipText + property string tooltipDirection: "top" + + // Arc geometry constants + readonly property real _gaugeSize: 60 * contentScale + readonly property real _lineWidth: 6 * contentScale + readonly property real _arcRadius: _gaugeSize / 2 - 5 * contentScale + // Arc goes from 150° to 390° (30°), gap at bottom + // Bottom of arc is at y = center + radius * sin(30°) = center + radius * 0.5 + // Plus half line width for stroke + readonly property real _arcBottomY: _gaugeSize / 2 + _arcRadius * 0.5 + _lineWidth / 2 + // Height needs to include the icon which sits inside the arc gap + // Icon is ~12px tall, positioned 4px below text center, need ~4px more padding + readonly property real _contentHeight: _arcBottomY + 4 * contentScale + + implicitWidth: Math.round(_gaugeSize) + implicitHeight: Math.round(_contentHeight) + Layout.maximumWidth: implicitWidth + Layout.maximumHeight: implicitHeight + + // Animated ratio for smooth transitions - reduces repaint frequency + property real animatedRatio: ratio + + Behavior on animatedRatio { + enabled: !Settings.data.general.animationDisabled + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + // Repaint gauge when animated ratio changes (throttled by animation) + onAnimatedRatioChanged: { + if (!repaintTimer.running) { + repaintTimer.start(); + } + } + onFillColorChanged: gauge.requestPaint() + + // Throttle timer to limit repaint frequency during animation (~30 FPS) + Timer { + id: repaintTimer + interval: 33 + repeat: true + onTriggered: { + gauge.requestPaint(); + // Stop repeating once animation settles + if (Math.abs(root.animatedRatio - root.ratio) < 0.001) { + stop(); + } + } + } + + Canvas { + id: gauge + width: root._gaugeSize + height: root._gaugeSize + anchors.horizontalCenter: parent.horizontalCenter + y: 0 + + // Optimized Canvas settings for better GPU performance + renderStrategy: Canvas.Cooperative + renderTarget: Canvas.FramebufferObject + + // Enable layer caching - critical for performance! + layer.enabled: true + layer.smooth: true + + // Hide until first paint to avoid white FBO flash + visible: false + onPainted: visible = true + + Component.onCompleted: { + requestPaint(); + } + + onPaint: { + const ctx = getContext("2d"); + const w = width, h = height; + const cx = w / 2, cy = h / 2; + const r = root._arcRadius; + + // Rotated 90° to the right: gap at the bottom + // Start at 150° and end at 390° (30°) → bottom opening + const start = Math.PI * 5 / 6; // 150° + const endBg = Math.PI * 13 / 6; // 390° (equivalent to 30°) + + ctx.reset(); + ctx.lineWidth = root._lineWidth; + ctx.lineCap = Settings.data.general.iRadiusRatio > 0 ? "round" : "butt"; + + // Track uses outline for contrast against surfaceVariant backgrounds + ctx.strokeStyle = Color.mSurface; + ctx.beginPath(); + ctx.arc(cx, cy, r, start, endBg); + ctx.stroke(); + + // Value arc - only draw if ratio is meaningful (> 0.5%) + const r2 = Math.max(0, Math.min(1, root.animatedRatio)); + if (r2 > 0.005) { + const end = start + (endBg - start) * r2; + ctx.strokeStyle = root.fillColor; + ctx.beginPath(); + ctx.arc(cx, cy, r, start, end); + ctx.stroke(); + } + } + } + + // Percent centered in the circle + NText { + id: valueLabel + anchors.horizontalCenter: gauge.horizontalCenter + anchors.verticalCenter: gauge.verticalCenter + anchors.verticalCenterOffset: -4 * root.contentScale + text: `${Math.round(root.animatedRatio * 100)}${root.suffix}` + pointSize: Style.fontSizeM * root.contentScale * 0.9 + font.weight: Style.fontWeightBold + color: root.fillColor + horizontalAlignment: Text.AlignHCenter + } + + NIcon { + id: iconText + anchors.horizontalCenter: gauge.horizontalCenter + anchors.top: valueLabel.bottom + anchors.topMargin: 4 * root.contentScale + icon: root.icon + color: root.fillColor + pointSize: Style.fontSizeM * root.contentScale + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onEntered: { + if (root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + TooltipService.show(root, root.tooltipText, root.tooltipDirection); + } + } + onExited: { + if (root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + TooltipService.hide(); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NClock.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NClock.qml new file mode 100644 index 0000000..20bb30a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NClock.qml @@ -0,0 +1,442 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import "../Helpers/ColorsConvert.js" as ColorsConvert +import qs.Commons +import qs.Widgets + +Item { + id: root + + property var now: Time.now + + // Style: "analog" or "digital" + property string clockStyle: "analog" + + // Show seconds progress ring (digital only) + property bool showProgress: true + + // Color properties + property color backgroundColor: Color.mPrimary + property color clockColor: Color.mOnPrimary + + property color secondHandColor: { + var defaultColor = Color.mError; + var bestContrast = 1.0; // 1.0 is "no contrast" + var bestColor = defaultColor; + var candidates = [Color.mSecondary, Color.mTertiary, Color.mError]; + + const minContrast = 1.149; + + for (var i = 0; i < candidates.length; i++) { + var candidate = candidates[i]; + var contrastClock = ColorsConvert.getContrastRatio(candidate.toString(), clockColor.toString()); + if (contrastClock < minContrast) { + continue; + } + var contrastBg = ColorsConvert.getContrastRatio(candidate.toString(), backgroundColor.toString()); + if (contrastBg < minContrast) { + continue; + } + + var currentWorstContrast = Math.min(contrastBg, contrastClock); + + if (currentWorstContrast > bestContrast) { + bestContrast = currentWorstContrast; + bestColor = candidate; + } + } + + return bestColor; + } + + property color progressColor: root.secondHandColor + + // Font size properties for digital clock + property real hoursFontSize: Style.fontSizeXS + property real minutesFontSize: Style.fontSizeXXS + property int hoursFontWeight: Style.fontWeightBold + property int minutesFontWeight: Style.fontWeightBold + + // Scale ratio for canvas line widths (used by desktop widget scaling) + property real scaleRatio: Style.uiScaleRatio + + height: Math.round((Style.fontSizeXXXL * 1.9) / 2 * Style.uiScaleRatio) * 2 + width: root.height + + Loader { + id: clockLoader + anchors.fill: parent + + sourceComponent: { + if (root.clockStyle === "analog") + return analogClockComponent; + if (root.clockStyle === "binary") + return binaryClockComponent; + return digitalClockComponent; + } + + onLoaded: { + item.now = Qt.binding(function () { + return root.now; + }); + item.backgroundColor = Qt.binding(function () { + return root.backgroundColor; + }); + item.clockColor = Qt.binding(function () { + return root.clockColor; + }); + if (item.hasOwnProperty("secondHandColor")) { + item.secondHandColor = Qt.binding(function () { + return root.secondHandColor; + }); + } + if (item.hasOwnProperty("progressColor")) { + item.progressColor = Qt.binding(function () { + return root.progressColor; + }); + } + if (item.hasOwnProperty("hoursFontSize")) { + item.hoursFontSize = Qt.binding(function () { + return root.hoursFontSize; + }); + } + if (item.hasOwnProperty("minutesFontSize")) { + item.minutesFontSize = Qt.binding(function () { + return root.minutesFontSize; + }); + } + if ("hoursFontWeight" in item) { + item.hoursFontWeight = Qt.binding(function () { + return root.hoursFontWeight; + }); + } + if ("minutesFontWeight" in item) { + item.minutesFontWeight = Qt.binding(function () { + return root.minutesFontWeight; + }); + } + if (item.hasOwnProperty("scaleRatio")) { + item.scaleRatio = Qt.binding(function () { + return root.scaleRatio; + }); + } + if ("showProgress" in item) { + item.showProgress = Qt.binding(function () { + return root.showProgress; + }); + } + } + } + + // Analog Clock Component + component NClockAnalog: Item { + property var now + property color backgroundColor: Color.mPrimary + property color clockColor: Color.mOnPrimary + property color secondHandColor: Color.mError + property real scaleRatio: Style.uiScaleRatio + anchors.fill: parent + + Canvas { + id: clockCanvas + anchors.fill: parent + + Connections { + target: Time + function onNowChanged() { + clockCanvas.requestPaint(); + } + } + + onPaint: { + var currentTime = Time.now; + var hours = currentTime.getHours(); + var minutes = currentTime.getMinutes(); + var seconds = currentTime.getSeconds(); + + const markAlpha = 0.7; + var ctx = getContext("2d"); + ctx.reset(); + ctx.translate(width / 2, height / 2); + var radius = Math.min(width, height) / 2; + + // Hour marks + ctx.strokeStyle = Qt.alpha(clockColor, markAlpha); + ctx.lineWidth = 2 * scaleRatio; + var scaleFactor = 0.7; + + for (var i = 0; i < 12; i++) { + var scaleFactor = 0.8; + if (i % 3 === 0) { + scaleFactor = 0.65; + } + ctx.save(); + ctx.rotate(i * Math.PI / 6); + ctx.beginPath(); + ctx.moveTo(0, -radius * scaleFactor); + ctx.lineTo(0, -radius); + ctx.stroke(); + ctx.restore(); + } + + // Hour hand + ctx.save(); + var hourAngle = (hours % 12 + minutes / 60) * Math.PI / 6; + ctx.rotate(hourAngle); + ctx.strokeStyle = clockColor; + ctx.lineWidth = 3 * scaleRatio; + ctx.lineCap = "round"; + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, -radius * 0.6); + ctx.stroke(); + ctx.restore(); + + // Minute hand + ctx.save(); + var minuteAngle = (minutes + seconds / 60) * Math.PI / 30; + ctx.rotate(minuteAngle); + ctx.strokeStyle = clockColor; + ctx.lineWidth = 2 * scaleRatio; + ctx.lineCap = "round"; + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, -radius * 0.9); + ctx.stroke(); + ctx.restore(); + + // Second hand + ctx.save(); + var secondAngle = seconds * Math.PI / 30; + ctx.rotate(secondAngle); + ctx.strokeStyle = secondHandColor; + ctx.lineWidth = 1.6 * scaleRatio; + ctx.lineCap = "round"; + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(0, -radius); + ctx.stroke(); + ctx.restore(); + + // Center dot + ctx.beginPath(); + ctx.arc(0, 0, 3 * scaleRatio, 0, 2 * Math.PI); + ctx.fillStyle = clockColor; + ctx.fill(); + } + + Component.onCompleted: requestPaint() + } + } + + // Digital Clock Component + component NClockDigital: Item { + property var now + property color backgroundColor: Color.mPrimary + property color clockColor: Color.mOnPrimary + property color progressColor: Color.mError + property real hoursFontSize: Style.fontSizeXS + property real minutesFontSize: Style.fontSizeXXS + property int hoursFontWeight: Style.fontWeightBold + property int minutesFontWeight: Style.fontWeightBold + property real scaleRatio: Style.uiScaleRatio + property bool showProgress: true + + anchors.fill: parent + + // Digital clock's seconds circular progress + Canvas { + id: secondsProgress + anchors.fill: parent + visible: showProgress + property real progress: now.getSeconds() / 60 + onProgressChanged: requestPaint() + Connections { + target: Time + function onNowChanged() { + const total = now.getSeconds() * 1000 + now.getMilliseconds(); + secondsProgress.progress = total / 60000; + } + } + onPaint: { + var ctx = getContext("2d"); + var centerX = width / 2; + var centerY = height / 2; + var radius = Math.min(width, height) / 2 - 3 * scaleRatio; + ctx.reset(); + + // Background circle + ctx.beginPath(); + ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI); + ctx.lineWidth = 2.5 * scaleRatio; + ctx.strokeStyle = Qt.alpha(clockColor, 0.15); + ctx.stroke(); + + // Progress arc + ctx.beginPath(); + ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + progress * 2 * Math.PI); + ctx.lineWidth = 2.5 * scaleRatio; + ctx.strokeStyle = progressColor; + ctx.lineCap = "round"; + ctx.stroke(); + } + } + + // Digital clock + ColumnLayout { + anchors.centerIn: parent + spacing: -Style.marginXXS + + NText { + text: { + var t = Settings.data.location.use12hourFormat ? I18n.locale.toString(now, "hh AP") : I18n.locale.toString(now, "HH"); + return t.split(" ")[0]; + } + + pointSize: hoursFontSize + font.weight: hoursFontWeight + color: clockColor + family: Settings.data.ui.fontFixed + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: Qt.formatTime(now, "mm") + pointSize: minutesFontSize + font.weight: minutesFontWeight + color: clockColor + family: Settings.data.ui.fontFixed + Layout.alignment: Qt.AlignHCenter + } + } + } + + // Binary Clock Component + component NClockBinary: Item { + property var now + property color backgroundColor + property color clockColor: Color.mOnPrimary + + anchors.fill: parent + + readonly property int h: now.getHours() + readonly property int m: now.getMinutes() + readonly property int s: now.getSeconds() + + // BCD (Binary Coded Decimal) Format: + // H1 H2 : M1 M2 : S1 S2 + // H1 (tens): 0-2 (2 bits) + // H2 (ones): 0-9 (4 bits) + // M1 (tens): 0-5 (3 bits) + // M2 (ones): 0-9 (4 bits) + // S1 (tens): 0-5 (3 bits) + // S2 (ones): 0-9 (4 bits) + + RowLayout { + anchors.centerIn: parent + spacing: parent.width * 0.05 + + // Hours + RowLayout { + spacing: parent.parent.width * 0.02 + BinaryColumn { + value: Math.floor(h / 10) + bits: 2 + dotSize: root.width * 0.08 + activeColor: clockColor + Layout.alignment: Qt.AlignBottom + } + BinaryColumn { + value: h % 10 + bits: 4 + dotSize: root.width * 0.08 + activeColor: clockColor + Layout.alignment: Qt.AlignBottom + } + } + + // Minutes + RowLayout { + spacing: parent.parent.width * 0.02 + BinaryColumn { + value: Math.floor(m / 10) + bits: 3 + dotSize: root.width * 0.08 + activeColor: clockColor + Layout.alignment: Qt.AlignBottom + } + BinaryColumn { + value: m % 10 + bits: 4 + dotSize: root.width * 0.08 + activeColor: clockColor + Layout.alignment: Qt.AlignBottom + } + } + + // Seconds + RowLayout { + spacing: parent.parent.width * 0.02 + BinaryColumn { + value: Math.floor(s / 10) + bits: 3 + dotSize: root.width * 0.08 + activeColor: clockColor + Layout.alignment: Qt.AlignBottom + } + BinaryColumn { + value: s % 10 + bits: 4 + dotSize: root.width * 0.08 + activeColor: clockColor + Layout.alignment: Qt.AlignBottom + } + } + } + } + + component BinaryColumn: Column { + property int value: 0 + property int bits: 4 + property real dotSize: 10 + property color activeColor: "white" + + spacing: dotSize * 0.4 + + Repeater { + model: bits + + Rectangle { + property int bitIndex: (bits - 1) - index + property bool isActive: (value >> bitIndex) & 1 + + width: dotSize + height: dotSize + radius: dotSize / 2 + color: isActive ? activeColor : Qt.alpha(activeColor, 0.2) + + Behavior on color { + ColorAnimation { + duration: 200 + } + } + } + } + } + + Component { + id: analogClockComponent + NClockAnalog {} + } + + Component { + id: digitalClockComponent + NClockDigital {} + } + + Component { + id: binaryClockComponent + NClockBinary {} + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NCollapsible.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NCollapsible.qml new file mode 100644 index 0000000..de51933 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NCollapsible.qml @@ -0,0 +1,191 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons + +ColumnLayout { + id: root + + property string label: "" + property string description: "" + property bool expanded: false + property real contentSpacing: Style.marginM + property bool _userInteracted: false + + signal toggled(bool expanded) + + Layout.fillWidth: true + spacing: 0 + + // Default property to accept children + default property alias content: contentLayout.children + + // Header with clickable area + Rectangle { + id: headerContainer + Layout.fillWidth: true + Layout.preferredHeight: headerContent.implicitHeight + Style.margin2M + color: root.expanded ? Color.mSecondary : Color.mPrimary + radius: Style.iRadiusM + border.color: root.expanded ? Color.mOnSecondary : Color.mOutline + border.width: Style.borderS + + // Smooth color transitions + Behavior on color { + enabled: root._userInteracted + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + Behavior on border.color { + enabled: root._userInteracted + ColorAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + MouseArea { + id: headerArea + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + + onClicked: { + root._userInteracted = true; + root.expanded = !root.expanded; + root.toggled(root.expanded); + } + + // Hover effect overlay + Rectangle { + anchors.fill: parent + color: headerArea.containsMouse ? Color.mOnSurface : "transparent" + opacity: headerArea.containsMouse ? 0.08 : 0 + radius: headerContainer.radius // Reference the container's radius directly + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + } + + RowLayout { + id: headerContent + anchors.fill: parent + anchors.margins: Style.marginM + spacing: Style.marginM + + // Expand/collapse icon with rotation animation + NIcon { + id: chevronIcon + icon: "chevron-right" + pointSize: Style.fontSizeL + color: root.expanded ? Color.mOnSecondary : Color.mOnPrimary + Layout.alignment: Qt.AlignVCenter + + rotation: root.expanded ? 90 : 0 + Behavior on rotation { + enabled: root._userInteracted + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + Behavior on color { + enabled: root._userInteracted + ColorAnimation { + duration: Style.animationNormal + } + } + } + + // Header text content - properly contained + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: Style.marginL + + NText { + text: root.label + pointSize: Style.fontSizeL + font.weight: Style.fontWeightSemiBold + color: root.expanded ? Color.mOnSecondary : Color.mOnPrimary + wrapMode: Text.WordWrap + + Behavior on color { + enabled: root._userInteracted + ColorAnimation { + duration: Style.animationNormal + } + } + } + + NText { + text: root.description + pointSize: Style.fontSizeS + font.weight: Style.fontWeightRegular + color: root.expanded ? Color.mOnSecondary : Color.mOnPrimary + Layout.fillWidth: true + wrapMode: Text.WordWrap + visible: root.description !== "" + opacity: 0.87 + + Behavior on color { + enabled: root._userInteracted + ColorAnimation { + duration: Style.animationNormal + } + } + } + } + } + } + + // Collapsible content with Material 3 styling + Rectangle { + id: contentContainer + Layout.fillWidth: true + Layout.topMargin: Style.marginS + + visible: root.expanded + color: Color.mSurface + radius: Style.iRadiusL + border.color: Color.mOutline + border.width: Style.borderS + + // Dynamic height based on content + Layout.preferredHeight: expanded ? contentLayout.implicitHeight + Style.margin2L : 0 + + // Smooth height animation + Behavior on Layout.preferredHeight { + enabled: root._userInteracted + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + + // Content layout + ColumnLayout { + id: contentLayout + anchors.fill: parent + anchors.margins: Style.marginL + spacing: root.contentSpacing + } + + // Fade in animation for content + opacity: root.expanded ? 1.0 : 0.0 + Behavior on opacity { + enabled: root._userInteracted + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutCubic + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NColorChoice.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NColorChoice.qml new file mode 100644 index 0000000..6d8cd29 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NColorChoice.qml @@ -0,0 +1,89 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Services.UI + +RowLayout { + id: root + + property string label: I18n.tr("common.select-color") + property string description: I18n.tr("common.select-color-description") + property string tooltip: "" + property string currentKey: "" + property var defaultValue: undefined + property var noneColor: undefined // color declared as var so we can nullify + property var noneOnColor: undefined // color declared as var so we can nullify + + readonly property bool isValueChanged: (defaultValue !== undefined) && (currentKey !== defaultValue) + readonly property string indicatorTooltip: { + I18n.tr("panels.indicator.default-value", { + "value": defaultValue === "" ? "(empty)" : String(defaultValue) + }); + } + + readonly property int diameter: Style.baseWidgetSize * 0.9 * Style.uiScaleRatio + + signal selected(string key) + + NLabel { + label: root.label + description: root.description + showIndicator: root.isValueChanged + indicatorTooltip: root.indicatorTooltip + } + + RowLayout { + id: colourRow + + opacity: enabled ? 1.0 : 0.6 + Layout.minimumWidth: root.diameter * Color.colorKeyModel.length + + Repeater { + model: Color.colorKeyModel + + Rectangle { + id: colorCircle + + property bool isSelected: root.currentKey === modelData.key + property bool isHovered: circleMouseArea.containsMouse + + Layout.alignment: Qt.AlignHCenter + implicitWidth: root.diameter + implicitHeight: root.diameter + radius: root.diameter * 0.5 + color: (modelData.key === "none" && root.noneColor !== undefined) ? root.noneColor : Color.resolveColorKey(modelData.key) + border.color: (isSelected || isHovered) ? Color.mOnSurface : Color.mOutline + border.width: Style.borderM + + MouseArea { + id: circleMouseArea + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: TooltipService.show(parent, modelData.name) + onExited: TooltipService.hide() + onClicked: { + root.currentKey = modelData.key; + root.selected(modelData.key); + } + } + + NIcon { + anchors.centerIn: parent + icon: "check" + pointSize: Math.max(Style.fontSizeXS, colorCircle.width * 0.4) + color: (modelData.key === "none" && root.noneOnColor !== undefined) ? root.noneOnColor : Color.resolveOnColorKey(modelData.key) + font.weight: Style.fontWeightBold + visible: colorCircle.isSelected + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NColorPicker.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NColorPicker.qml new file mode 100644 index 0000000..c005240 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NColorPicker.qml @@ -0,0 +1,76 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +Rectangle { + id: root + + property var screen + property color selectedColor: "black" + + signal colorSelected(color color) + + Layout.margins: Style.borderS + implicitWidth: 150 + implicitHeight: Math.round(Style.baseWidgetSize * 1.1) + + radius: Style.iRadiusM + color: Color.mSurface + border.color: Color.mOutline + border.width: Style.borderS + + // Minimized Look + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + var dialog = Qt.createComponent("NColorPickerDialog.qml").createObject(root, { + "selectedColor": selectedColor, + "parent": Overlay.overlay, + "screen": root.screen + }); + // Connect the dialog's signal to the picker's signal + dialog.colorSelected.connect(function (color) { + root.selectedColor = color; + root.colorSelected(color); + }); + + dialog.open(); + } + + RowLayout { + anchors.fill: parent + anchors { + leftMargin: Style.marginL + rightMargin: Style.marginL + } + spacing: Style.marginS + + // Color preview circle + Rectangle { + Layout.preferredWidth: root.height * 0.6 + Layout.preferredHeight: root.height * 0.6 + radius: Math.min(Style.iRadiusL, Layout.preferredWidth / 2) + color: root.selectedColor + border.color: Color.mOutline + border.width: Style.borderS + } + + NText { + text: root.selectedColor.toString().toUpperCase() + family: Settings.data.ui.fontFixed + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + + NIcon { + icon: "color-picker" + color: Color.mOnSurfaceVariant + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NColorPickerDialog.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NColorPickerDialog.qml new file mode 100644 index 0000000..d731eb4 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NColorPickerDialog.qml @@ -0,0 +1,775 @@ +import QtQml +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import "../Helpers/ColorList.js" as ColorList +import qs.Commons +import qs.Services.UI +import qs.Widgets + +Popup { + id: root + + property var screen + property color selectedColor: "black" + + enum EditMode { + R, + G, + B, + H, + S, + V + } + property int editMode: NColorPickerDialog.EditMode.R + + // Code to deal with Hue when color is achromatic + property real stableHue: 0 + onSelectedColorChanged: { + if (selectedColor.hsvHue >= 0) { + stableHue = selectedColor.hsvHue; + } + if (liveMode && visible) { + colorSelected(selectedColor); + } + } + readonly property real displayHue: selectedColor.hsvHue < 0 ? stableHue : selectedColor.hsvHue + + signal colorSelected(color color) + + // When true: hides Cancel/Apply, emits colorSelected on every color change + property bool liveMode: false + + width: 580 + padding: Style.marginXL + + modal: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + onOpened: { + // Center on screen after popup is opened and parent position is known + if (screen && parent) { + var parentPos = parent.mapToItem(null, 0, 0); + x = Math.round((screen.width - width) / 2 - parentPos.x); + y = Math.round((screen.height - height) / 2 - parentPos.y); + } + } + + background: Rectangle { + color: Color.mSurface + radius: Style.iRadiusS + border.color: Color.mPrimary + border.width: Style.borderM + } + + contentItem: ColumnLayout { + id: mainContent + spacing: Style.marginL + + // Header + RowLayout { + Layout.fillWidth: true + + RowLayout { + spacing: Style.marginS + + NIcon { + icon: "color-picker" + pointSize: Style.fontSizeXXL + color: Color.mPrimary + } + + NText { + text: I18n.tr("widgets.color-picker.title") + pointSize: Style.fontSizeXL + font.weight: Style.fontWeightBold + color: Color.mPrimary + } + } + + Item { + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + onClicked: root.close() + } + } + + // Color preview section + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 80 + radius: Style.iRadiusS + color: root.selectedColor + border.color: Color.mOutline + border.width: Style.borderS + + ColumnLayout { + spacing: 0 + anchors.fill: parent + + Item { + Layout.fillHeight: true + } + + NText { + text: root.selectedColor.toString().toUpperCase() + family: Settings.data.ui.fontFixed + pointSize: Style.fontSizeL + font.weight: Style.fontWeightMedium + color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? "black" : "white" + Layout.alignment: Qt.AlignHCenter + } + + NText { + text: "RGB(" + Math.round(root.selectedColor.r * 255) + ", " + Math.round(root.selectedColor.g * 255) + ", " + Math.round(root.selectedColor.b * 255) + ")" + family: Settings.data.ui.fontFixed + pointSize: Style.fontSizeM + color: root.selectedColor.r + root.selectedColor.g + root.selectedColor.b > 1.5 ? "black" : "white" + Layout.alignment: Qt.AlignHCenter + } + + Item { + Layout.fillHeight: true + } + } + } + + // Main Box + NBox { + Layout.fillWidth: true + Layout.preferredHeight: controlsOutterColumn.implicitHeight + Style.margin2L + + ButtonGroup { + id: colorValues + } + + ColumnLayout { + id: controlsOutterColumn + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + RowLayout { + spacing: Style.marginL // Ensure nice gap between Left and Right groups + + // SpinBoxes Column + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignTop + Layout.preferredWidth: 3 + + // --- RED --- + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NRadioButton { + ButtonGroup.group: colorValues + text: "R" + font.weight: Style.fontWeightMedium + checked: true + onClicked: root.editMode = NColorPickerDialog.EditMode.R + Layout.fillWidth: false + } + + NSpinBox { + id: redSpinBox + from: 0 + to: 255 + + onValueChanged: { + if (!selectedSlider.pressed && !fieldMouse.pressed && !hexInput.activeFocus && value !== Math.round(root.selectedColor.r * 255)) { + root.selectedColor = Qt.rgba(value / 255, root.selectedColor.g, root.selectedColor.b, 1); + } + } + Binding { + target: redSpinBox + property: "value" + value: Math.round(root.selectedColor.r * 255) + } + } + } + + // --- GREEN --- + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NRadioButton { + ButtonGroup.group: colorValues + text: "G" + font.weight: Style.fontWeightMedium + onClicked: root.editMode = NColorPickerDialog.EditMode.G + Layout.fillWidth: false + } + + NSpinBox { + id: greenSpinBox + from: 0 + to: 255 + + onValueChanged: { + if (!selectedSlider.pressed && !fieldMouse.pressed && !hexInput.activeFocus && value !== Math.round(root.selectedColor.g * 255)) { + root.selectedColor = Qt.rgba(root.selectedColor.r, value / 255, root.selectedColor.b, 1); + } + } + Binding { + target: greenSpinBox + property: "value" + value: Math.round(root.selectedColor.g * 255) + } + } + } + + // --- BLUE --- + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NRadioButton { + ButtonGroup.group: colorValues + text: "B" + font.weight: Style.fontWeightMedium + onClicked: root.editMode = NColorPickerDialog.EditMode.B + Layout.fillWidth: false + } + + NSpinBox { + id: blueSpinBox + from: 0 + to: 255 + + onValueChanged: { + if (!selectedSlider.pressed && !fieldMouse.pressed && !hexInput.activeFocus && value !== Math.round(root.selectedColor.b * 255)) { + root.selectedColor = Qt.rgba(root.selectedColor.r, root.selectedColor.g, value / 255, 1); + } + } + Binding { + target: blueSpinBox + property: "value" + value: Math.round(root.selectedColor.b * 255) + } + } + } + + // Spacer + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + + // --- HUE --- + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NRadioButton { + ButtonGroup.group: colorValues + text: "H" + font.weight: Style.fontWeightMedium + checked: true + onClicked: root.editMode = NColorPickerDialog.EditMode.H + Layout.fillWidth: false + } + + NSpinBox { + id: hueSpinBox + from: 0 + to: 360 + + onValueChanged: { + if (!selectedSlider.pressed && !fieldMouse.pressed && !hexInput.activeFocus && value !== Math.round(root.displayHue * 360)) { + var newHue = value / 360; + root.selectedColor = Qt.hsva(newHue, root.selectedColor.hsvSaturation, root.selectedColor.hsvValue, 1); + root.stableHue = newHue; + } + } + + Binding { + target: hueSpinBox + property: "value" + value: Math.round(root.displayHue * 360) + } + } + } + + // --- SATURATION --- + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NRadioButton { + ButtonGroup.group: colorValues + text: "S" + font.weight: Style.fontWeightMedium + onClicked: root.editMode = NColorPickerDialog.EditMode.S + Layout.fillWidth: false + } + + NSpinBox { + id: satSpinBox + from: 0 + to: 100 + + onValueChanged: { + if (!selectedSlider.pressed && !fieldMouse.pressed && !hexInput.activeFocus && value !== Math.round(root.selectedColor.hsvSaturation * 100)) { + root.selectedColor = Qt.hsva(root.selectedColor.hsvHue, value / 100, root.selectedColor.hsvValue, 1); + } + } + Binding { + target: satSpinBox + property: "value" + value: Math.round(root.selectedColor.hsvSaturation * 100) + } + } + } + + // --- VALUE --- + RowLayout { + spacing: Style.marginM + + NRadioButton { + ButtonGroup.group: colorValues + text: "V" + font.weight: Style.fontWeightMedium + onClicked: root.editMode = NColorPickerDialog.EditMode.V + Layout.fillWidth: false + } + + NSpinBox { + id: valSpinBox + from: 0 + to: 100 + + onValueChanged: { + if (!selectedSlider.pressed && !fieldMouse.pressed && !hexInput.activeFocus && value !== Math.round(root.selectedColor.hsvValue * 100)) { + root.selectedColor = Qt.hsva(root.selectedColor.hsvHue, root.selectedColor.hsvSaturation, value / 100, 1); + } + } + Binding { + target: valSpinBox + property: "value" + value: Math.round(root.selectedColor.hsvValue * 100) + } + } + } + + // Spacer + Item { + Layout.fillHeight: true + Layout.fillWidth: true + } + + // Hex input + RowLayout { + id: hexInput + Layout.fillWidth: true + spacing: Style.marginM + + NLabel { + label: "Hex:" + Layout.alignment: Qt.AlignVCenter + } + + NTextInput { + text: root.selectedColor.toString().toUpperCase() + fontFamily: Settings.data.ui.fontFixed + Layout.fillWidth: true + Layout.minimumWidth: implicitWidth + onEditingFinished: { + if (/^#[0-9A-F]{6}$/i.test(text)) { + root.selectedColor = text; + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredWidth: 5 + Layout.alignment: Qt.AlignTop + spacing: Style.marginS + + Layout.topMargin: Math.round(2 * Style.uiScaleRatio) //Shim to try and line up colorField with SpinBoxes + + // --- SLIDER --- + NColorSlider { + id: selectedSlider + Layout.fillHeight: true + rainbowMode: root.editMode === NColorPickerDialog.EditMode.H + topColor: { + if (rainbowMode) + return "transparent"; + switch (root.editMode) { + case NColorPickerDialog.EditMode.R: + return "#FF0000"; + case NColorPickerDialog.EditMode.G: + return "#00FF00"; + case NColorPickerDialog.EditMode.B: + return "#0000FF"; + default: + return "#FFFFFF"; + } + } + + Binding { + target: selectedSlider + property: "value" + when: !selectedSlider.pressed // Only update from model when NOT dragging + value: { + switch (root.editMode) { + case NColorPickerDialog.EditMode.R: + return root.selectedColor.r; + case NColorPickerDialog.EditMode.G: + return root.selectedColor.g; + case NColorPickerDialog.EditMode.B: + return root.selectedColor.b; + case NColorPickerDialog.EditMode.H: + return root.displayHue; + case NColorPickerDialog.EditMode.S: + return root.selectedColor.hsvSaturation; + case NColorPickerDialog.EditMode.V: + return root.selectedColor.hsvValue; + default: + return 0; + } + } + } + + onMoved: { + var v = value; + switch (root.editMode) { + case NColorPickerDialog.EditMode.R: + root.selectedColor = Qt.rgba(v, root.selectedColor.g, root.selectedColor.b, 1); + break; + case NColorPickerDialog.EditMode.G: + root.selectedColor = Qt.rgba(root.selectedColor.r, v, root.selectedColor.b, 1); + break; + case NColorPickerDialog.EditMode.B: + root.selectedColor = Qt.rgba(root.selectedColor.r, root.selectedColor.g, v, 1); + break; + case NColorPickerDialog.EditMode.H: + root.selectedColor = Qt.hsva(v, root.selectedColor.hsvSaturation, root.selectedColor.hsvValue, 1); + root.stableHue = v; + break; + case NColorPickerDialog.EditMode.S: + root.selectedColor = Qt.hsva(root.selectedColor.hsvHue, v, root.selectedColor.hsvValue, 1); + break; + case NColorPickerDialog.EditMode.V: + root.selectedColor = Qt.hsva(root.selectedColor.hsvHue, root.selectedColor.hsvSaturation, v, 1); + break; + } + } + } + + // Color Field + Rectangle { + id: colorField + Layout.fillWidth: true + + Layout.preferredHeight: width + Layout.alignment: Qt.AlignTop + + radius: 0 + border.color: Color.mOutline + border.width: Style.borderS + clip: true + + ShaderEffect { + anchors.fill: parent + anchors.margins: 1 // Avoid drawing over the border + + fragmentShader: "../Shaders/qsb/color_picker.frag.qsb" + + // Pass which radio is selected + readonly property real fixedVal: { + switch (root.editMode) { + case NColorPickerDialog.EditMode.R: + return root.selectedColor.r; + case NColorPickerDialog.EditMode.G: + return root.selectedColor.g; + case NColorPickerDialog.EditMode.B: + return root.selectedColor.b; + case NColorPickerDialog.EditMode.H: + return root.displayHue; + case NColorPickerDialog.EditMode.S: + return root.selectedColor.hsvSaturation; + case NColorPickerDialog.EditMode.V: + return root.selectedColor.hsvValue; + default: + return 0; + } + } + + // Send as one vector because GPUs are so damn picky + property vector4d params: Qt.vector4d(1.0, fixedVal, root.editMode, 0) + } + + MouseArea { + id: fieldMouse + anchors.fill: parent + cursorShape: Qt.CrossCursor + + // Update color when clicking or dragging + function updateColor(mouse) { + // Normalize X and Y (0.0 to 1.0) + var xVal = Math.max(0, Math.min(1, mouse.x / width)); + var yVal = Math.max(0, Math.min(1, 1.0 - (mouse.y / height))); // Flip Y (0 at bottom) + + // Get the current fixed value (from the slider) + var fixed = 0.0; + + switch (root.editMode) { + case NColorPickerDialog.EditMode.R: + fixed = root.selectedColor.r; + root.selectedColor = Qt.rgba(fixed, xVal, yVal, 1); + break; + case NColorPickerDialog.EditMode.G: + fixed = root.selectedColor.g; + root.selectedColor = Qt.rgba(xVal, fixed, yVal, 1); + break; + case NColorPickerDialog.EditMode.B: + fixed = root.selectedColor.b; + root.selectedColor = Qt.rgba(xVal, yVal, fixed, 1); + break; + case NColorPickerDialog.EditMode.H: + // Use stableHue to prevent flipping to -1 + fixed = root.displayHue; + root.selectedColor = Qt.hsva(fixed, xVal, yVal, 1); + root.stableHue = fixed; + break; + case NColorPickerDialog.EditMode.S: + fixed = root.selectedColor.hsvSaturation; + root.selectedColor = Qt.hsva(xVal, fixed, yVal, 1); + // If we dragged Hue (xVal), update stableHue + root.stableHue = yVal; + break; + case NColorPickerDialog.EditMode.V: + fixed = root.selectedColor.hsvValue; + root.selectedColor = Qt.hsva(xVal, yVal, fixed, 1); + // If we dragged Hue (xVal), update stableHue + root.stableHue = xVal; + break; + } + } + onPressed: mouse => updateColor(mouse) + onPositionChanged: mouse => updateColor(mouse) + } + + // Color Indicator + Rectangle { + width: 10 + height: 10 + radius: Math.min(Style.iRadiusXS, width / 2) + color: "transparent" + border.color: root.selectedColor.hsvValue < 0.5 ? "white" : "black" + border.width: 1 + + // Find position based on the current color + readonly property point selectedPos: { + switch (root.editMode) { + case NColorPickerDialog.EditMode.R: + return Qt.point(root.selectedColor.g, root.selectedColor.b); + case NColorPickerDialog.EditMode.G: + return Qt.point(root.selectedColor.r, root.selectedColor.b); + case NColorPickerDialog.EditMode.B: + return Qt.point(root.selectedColor.r, root.selectedColor.g); + case NColorPickerDialog.EditMode.H: + return Qt.point(root.selectedColor.hsvSaturation, root.selectedColor.hsvValue); + case NColorPickerDialog.EditMode.S: + return Qt.point(root.displayHue, root.selectedColor.hsvValue); + case NColorPickerDialog.EditMode.V: + return Qt.point(root.displayHue, root.selectedColor.hsvSaturation); + default: + return Qt.point(0, 0); + } + } + + // Convert values to pixel position + x: (selectedPos.x * parent.width) - width / 2 + y: ((1.0 - selectedPos.y) * parent.height) - height / 2 + } + + // Redraw the border in case Color Indicator is near the edge + Rectangle { + anchors.fill: parent + color: "transparent" + border.color: Color.mOutline + border.width: Style.borderS + antialiasing: false + } + } + } + } + + NLabel { + label: I18n.tr("widgets.color-picker.palette-label") + Layout.fillWidth: true + } + + NScrollView { + id: paletteScrollView + Layout.fillWidth: true + Layout.preferredHeight: Math.min(paletteGrid.implicitHeight, 200) + verticalPolicy: paletteGrid.implicitHeight > 200 ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff + horizontalPolicy: ScrollBar.AlwaysOff + + GridLayout { + id: paletteGrid + width: paletteScrollView.availableWidth + columns: 17 + columnSpacing: 6 + rowSpacing: 6 + + NLabel { + Layout.columnSpan: 17 + Layout.fillWidth: true + description: I18n.tr("widgets.color-picker.palette-theme-colors") + } + + Repeater { + model: [ + { + name: "mPrimary", + color: Color.mPrimary + }, + { + name: "mSecondary", + color: Color.mSecondary + }, + { + name: "mTertiary", + color: Color.mTertiary + }, + { + name: "mError", + color: Color.mError + }, + { + name: "mSurface", + color: Color.mSurface + }, + { + name: "mSurfaceVariant", + color: Color.mSurfaceVariant + }, + { + name: "mOutline", + color: Color.mOutline + } + ] + + Rectangle { + width: 24 + height: 24 + radius: Style.iRadiusXXS + color: modelData.color + border.color: root.selectedColor.toString() === modelData.color.toString() ? Color.mPrimary : Color.mOutline + border.width: Math.max(1, root.selectedColor.toString() === modelData.color.toString() ? Style.borderM : Style.borderS) + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + + onEntered: { + TooltipService.show(parent, modelData.name + "\n" + parent.color.toString().toUpperCase()); + } + onExited: { + TooltipService.hide(); + } + onClicked: { + root.selectedColor = modelData.color; + TooltipService.hide(); + } + } + } + } + + NDivider { + Layout.columnSpan: 17 + Layout.fillWidth: true + Layout.topMargin: Style.marginXS + Layout.bottomMargin: 0 + } + + NLabel { + Layout.columnSpan: 17 + Layout.fillWidth: true + description: I18n.tr("widgets.color-picker.palette-description") + } + + Repeater { + model: ColorList.colors + + Rectangle { + width: 24 + height: 24 + radius: Math.min(Style.iRadiusXS, width / 2) + color: modelData.color + border.color: root.selectedColor.toString() === modelData.color.toString() ? Color.mPrimary : Color.mOutline + border.width: root.selectedColor.toString() === modelData.color.toString() ? 2 : 1 + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + + onEntered: { + TooltipService.show(parent, modelData.name + "\n" + parent.color.toString().toUpperCase(), "auto"); + } + onExited: { + TooltipService.hide(); + } + onClicked: { + root.selectedColor = modelData.color; + TooltipService.hide(); + } + } + } + } + } + } + } + } + + RowLayout { + visible: !root.liveMode + Layout.fillWidth: true + Layout.topMargin: 1 + Layout.bottomMargin: 1 + spacing: 10 + + Item { + Layout.fillWidth: true + } + + NButton { + id: cancelButton + text: I18n.tr("common.cancel") + outlined: cancelButton.hovered ? false : true + onClicked: { + root.close(); + } + } + + NButton { + text: I18n.tr("common.apply") + icon: "check" + onClicked: { + root.colorSelected(root.selectedColor); + // Delay close to prevent click propagation to elements behind the dialog + Qt.callLater(() => { + root.close(); + }); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NColorSlider.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NColorSlider.qml new file mode 100644 index 0000000..b08aaf3 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NColorSlider.qml @@ -0,0 +1,238 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Shapes +import qs.Commons +import qs.Services.UI + +Slider { + id: root + + property color fillColor: "transparent" + property var cutoutColor: Color.mSurface + property bool snapAlways: true + property real widthRatio: 0.7 + property var tooltipText + property string tooltipDirection: "auto" + property bool hovering: false + property color topColor: "white" + property color bottomColor: "black" + property bool rainbowMode: false + + readonly property real knobDiameter: Math.round((Style.baseWidgetSize * widthRatio * Style.uiScaleRatio) / 2) * 2 + readonly property real trackWidth: Math.round((knobDiameter * 0.4 * Style.uiScaleRatio) / 2) * 2 + readonly property real trackRadius: Math.min(Style.iRadiusL, trackWidth / 2) + readonly property real cutoutExtra: Math.round((Style.baseWidgetSize * 0.1 * Style.uiScaleRatio) / 2) * 2 + + orientation: Qt.Vertical + + padding: cutoutExtra / 2 + + snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease + implicitWidth: Math.max(trackWidth, knobDiameter) + + background: Item { + id: bgContainer + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + width: root.trackWidth + height: root.availableHeight + + LinearGradient { + id: standardLinearGradient + x1: 0 + y1: 0 + x2: 0 + y2: root.availableHeight + GradientStop { + position: 0.0 + color: root.topColor + } + GradientStop { + position: 1.0 + color: root.bottomColor + } + } + + LinearGradient { + id: rainbowLinearGradient + x1: 0 + y1: 0 + x2: 0 + y2: root.availableHeight + GradientStop { + position: 0.000 + color: "#FF0000" + } + GradientStop { + position: 0.167 + color: "#FF00FF" + } + GradientStop { + position: 0.333 + color: "#0000FF" + } + GradientStop { + position: 0.500 + color: "#00FFFF" + } + GradientStop { + position: 0.667 + color: "#00FF00" + } + GradientStop { + position: 0.833 + color: "#FFFF00" + } + GradientStop { + position: 1.000 + color: "#FF0000" + } + } + + Shape { + anchors.centerIn: parent + width: root.trackWidth + height: bgContainer.height + visible: root.trackWidth > 0 && bgContainer.height > 0 + preferredRendererType: Shape.CurveRenderer + asynchronous: true + + ShapePath { + id: trackPath + strokeColor: Qt.alpha(Color.mOutline, 0.5) + strokeWidth: Style.borderS + fillGradient: root.rainbowMode ? rainbowLinearGradient : standardLinearGradient + + readonly property real w: root.trackWidth + readonly property real h: root.availableHeight + readonly property real r: root.trackRadius + + startX: r + startY: 0 + + PathLine { + x: trackPath.w - trackPath.r + y: 0 + } + PathArc { + x: trackPath.w + y: trackPath.r + radiusX: trackPath.r + radiusY: trackPath.r + } + PathLine { + x: trackPath.w + y: trackPath.h - trackPath.r + } + PathArc { + x: trackPath.w - trackPath.r + y: trackPath.h + radiusX: trackPath.r + radiusY: trackPath.r + } + PathLine { + x: trackPath.r + y: trackPath.h + } + PathArc { + x: 0 + y: trackPath.h - trackPath.r + radiusX: trackPath.r + radiusY: trackPath.r + } + PathLine { + x: 0 + y: trackPath.r + } + PathArc { + x: trackPath.r + y: 0 + radiusX: trackPath.r + radiusY: trackPath.r + } + } + } + + // Circular cutout + Rectangle { + id: knobCutout + implicitWidth: root.knobDiameter + root.cutoutExtra + implicitHeight: root.knobDiameter + root.cutoutExtra + radius: Math.min(Style.iRadiusL, width / 2) + color: root.cutoutColor !== undefined ? root.cutoutColor : Color.mSurface + y: root.visualPosition * (root.availableHeight - root.knobDiameter) - root.cutoutExtra / 2 + anchors.horizontalCenter: parent.horizontalCenter + } + } + + handle: Item { + implicitWidth: root.knobDiameter + implicitHeight: root.knobDiameter + y: root.topPadding + root.visualPosition * (root.availableHeight - height) + anchors.horizontalCenter: parent.horizontalCenter + + Rectangle { + id: knob + implicitWidth: root.knobDiameter + implicitHeight: root.knobDiameter + radius: Math.min(Style.iRadiusL, width / 2) + color: { + if (root.rainbowMode) { + // Hue Logic: Map position (0.0 to 1.0) directly to Hue + return Qt.hsva(1 - root.visualPosition, 1, 1, 1); + } else { + // Linear Interpolation for Standard Gradients + // visualPosition 0.0 = Top, 1.0 = Bottom + var t = root.visualPosition; + var r = root.topColor.r * (1 - t) + root.bottomColor.r * t; + var g = root.topColor.g * (1 - t) + root.bottomColor.g * t; + var b = root.topColor.b * (1 - t) + root.bottomColor.b * t; + return Qt.rgba(r, g, b, 1); + } + } + + border.color: root.pressed ? Color.mHover : Color.mPrimary + border.width: Style.borderL + anchors.centerIn: parent + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + MouseArea { + enabled: true + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + acceptedButtons: Qt.NoButton // Don't accept any mouse buttons - only hover + propagateComposedEvents: true + + onEntered: { + root.hovering = true; + if (root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + TooltipService.show(knob, root.tooltipText, root.tooltipDirection); + } + } + + onExited: { + root.hovering = false; + if (root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + TooltipService.hide(); + } + } + } + + // Hide tooltip when slider is pressed (anywhere on the slider) + Connections { + target: root + function onPressedChanged() { + if (root.pressed && root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + TooltipService.hide(); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NComboBox.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NComboBox.qml new file mode 100644 index 0000000..a669696 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NComboBox.qml @@ -0,0 +1,342 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.UI +import qs.Widgets + +RowLayout { + id: root + + property real minimumWidth: 200 + property real popupHeight: 180 + + property string label: "" + property string description: "" + property string tooltip: "" + property var model + property string currentKey: "" + property string placeholder: "" + property var defaultValue: undefined + property string settingsPath: "" + property real baseSize: 1.0 + + readonly property real preferredHeight: Math.round(Style.baseWidgetSize * 1.1 * root.baseSize) + readonly property var comboBox: combo + + signal selected(string key) + + spacing: Style.marginL + + // Less strict comparison with != (instead of !==) so it can properly compare int vs string (ex for FPS: 30 and "30") + readonly property bool isValueChanged: (defaultValue !== undefined) && (currentKey != defaultValue) + + readonly property string indicatorTooltip: { + if (defaultValue === undefined) + return ""; + var displayValue = ""; + if (defaultValue === "") { + // Try to find the display name for empty key in the model + var found = false; + if (root.model) { + if (Array.isArray(root.model)) { + for (var i = 0; i < root.model.length; i++) { + var item = root.model[i]; + if (item && item.key === "") { + displayValue = item.name || I18n.tr("panels.indicator.system-default"); + found = true; + break; + } + } + } else if (typeof root.model.get === 'function') { + for (var i = 0; i < root.itemCount(); i++) { + var item = root.getItem(i); + if (item && item.key === "") { + displayValue = item.name || I18n.tr("panels.indicator.system-default"); + found = true; + break; + } + } + } + } + // If not found in model, show "System Default" instead of "(empty)" + if (!found) { + displayValue = I18n.tr("panels.indicator.system-default"); + } + } else { + // Try to find the display name for the default key in the model + var found = false; + if (root.model) { + if (Array.isArray(root.model)) { + for (var i = 0; i < root.model.length; i++) { + var item = root.model[i]; + if (item && item.key === defaultValue) { + displayValue = item.name || String(defaultValue); + found = true; + break; + } + } + } else if (typeof root.model.get === 'function') { + for (var i = 0; i < root.itemCount(); i++) { + var item = root.getItem(i); + if (item && item.key === defaultValue) { + displayValue = item.name || String(defaultValue); + found = true; + break; + } + } + } + } + if (!found) { + displayValue = String(defaultValue); + } + } + return I18n.tr("panels.indicator.default-value", { + "value": displayValue + }); + } + + function itemCount() { + if (!root.model) + return 0; + if (typeof root.model.count === 'number') + return root.model.count; + if (Array.isArray(root.model)) + return root.model.length; + return 0; + } + + function getItem(index) { + if (!root.model) + return null; + if (typeof root.model.get === 'function') + return root.model.get(index); + if (Array.isArray(root.model)) + return root.model[index]; + return null; + } + + function findIndexByKey(key) { + for (var i = 0; i < itemCount(); i++) { + var item = getItem(i); + if (item && item.key === key) + return i; + } + return -1; + } + + NLabel { + label: root.label + description: root.description + showIndicator: root.isValueChanged + indicatorTooltip: root.indicatorTooltip + } + + ComboBox { + id: combo + + opacity: enabled ? 1.0 : 0.6 + Layout.margins: Style.borderS + Layout.minimumWidth: Math.round(root.minimumWidth * Style.uiScaleRatio) + Layout.preferredHeight: Math.round(root.preferredHeight * Style.uiScaleRatio) + implicitWidth: Layout.minimumWidth + model: root.model + textRole: "name" + currentIndex: root.findIndexByKey(root.currentKey) + + onActivated: { + var item = root.getItem(combo.currentIndex); + if (item && item.key !== undefined) + root.selected(item.key); + } + + Keys.onUpPressed: event => { + if (combo.popup.visible) { + if (listView.currentIndex > 0) { + listView.currentIndex--; + listView.positionViewAtIndex(listView.currentIndex, ListView.Contain); + } + event.accepted = true; + } else { + event.accepted = false; + } + } + + Keys.onDownPressed: event => { + if (combo.popup.visible) { + if (listView.currentIndex < root.itemCount() - 1) { + listView.currentIndex++; + listView.positionViewAtIndex(listView.currentIndex, ListView.Contain); + } + event.accepted = true; + } else { + event.accepted = false; + } + } + + Keys.onReturnPressed: event => { + if (combo.popup.visible) { + var item = root.getItem(listView.currentIndex); + if (item && item.key !== undefined) { + root.selected(item.key); + combo.currentIndex = listView.currentIndex; + combo.popup.close(); + } + event.accepted = true; + } else { + event.accepted = false; + } + } + + Keys.onEnterPressed: event => { + combo.Keys.returnPressed(event); + } + + background: Rectangle { + implicitWidth: Math.round(Style.baseWidgetSize * 3.75 * Style.uiScaleRatio) + implicitHeight: Math.round(root.preferredHeight * Style.uiScaleRatio) + color: Color.mSurface + border.color: combo.activeFocus ? Color.mSecondary : Color.mOutline + border.width: Style.borderS + radius: Style.iRadiusM + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + onEntered: { + if (root.tooltip != "") { + TooltipService.show(root, root.tooltip); + } + } + onExited: { + if (root.tooltip != "") { + TooltipService.hide(); + } + } + } + } + + contentItem: NText { + leftPadding: Style.marginL + rightPadding: combo.indicator.width + Style.marginL + pointSize: Style.fontSizeM + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + color: combo.currentIndex >= 0 ? Color.mOnSurface : Color.mOnSurfaceVariant + text: { + if (combo.currentIndex >= 0 && combo.currentIndex < root.itemCount()) { + var item = root.getItem(combo.currentIndex); + return item ? item.name : root.placeholder; + } + return root.placeholder; + } + } + + indicator: NIcon { + x: combo.width - width - Style.marginM + y: combo.topPadding + (combo.availableHeight - height) / 2 + icon: "caret-down" + pointSize: Style.fontSizeL + } + + popup: Popup { + y: combo.height + Style.marginS + implicitWidth: combo.width + implicitHeight: Math.min(Math.round(root.popupHeight * Style.uiScaleRatio), listView.contentHeight + Style.margin2M) + padding: Style.marginM + + onOpened: { + listView.currentIndex = combo.currentIndex; + listView.positionViewAtIndex(combo.currentIndex, ListView.Beginning); + } + + contentItem: NListView { + id: listView + property var comboBox: combo + model: combo.popup.visible ? root.model : null + highlightMoveDuration: 0 + //showGradientMasks: false + + delegate: Rectangle { + id: delegateRect + required property int index + property bool isHighlighted: listView.currentIndex === index + + width: listView.availableWidth + height: delegateText.implicitHeight + Style.margin2S + radius: Style.iRadiusS + color: isHighlighted ? Color.mHover : "transparent" + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NText { + id: delegateText + anchors.fill: parent + anchors.leftMargin: Style.marginM + anchors.rightMargin: Style.marginM + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + pointSize: Style.fontSizeM + color: delegateRect.isHighlighted ? Color.mOnHover : Color.mOnSurface + text: { + var item = root.getItem(delegateRect.index); + return item && item.name ? item.name : ""; + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onContainsMouseChanged: { + if (containsMouse) { + listView.currentIndex = delegateRect.index; + } + } + onClicked: { + var item = root.getItem(delegateRect.index); + if (item && item.key !== undefined) { + root.selected(item.key); + listView.comboBox.currentIndex = delegateRect.index; + listView.comboBox.popup.close(); + } + } + } + } + } + + background: Rectangle { + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Style.borderS + radius: Style.iRadiusM + } + } + + Connections { + target: root + function onCurrentKeyChanged() { + combo.currentIndex = root.findIndexByKey(root.currentKey); + } + function onModelChanged() { + combo.currentIndex = root.findIndexByKey(root.currentKey); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NContextMenu.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NContextMenu.qml new file mode 100644 index 0000000..0975a12 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NContextMenu.qml @@ -0,0 +1,182 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons + +/* +* NContextMenu - Popup-based context menu for use inside panels and dialogs +* +* Use this component when you need a context menu inside: +* - Settings panels +* - Dialogs +* - Repeater delegates +* - Any nested component context +* +* For bar widgets and top-level window contexts, use NPopupContextMenu instead, +* which provides better screen boundary handling and compositor integration. +* +* Usage: +* NContextMenu { +* id: contextMenu +* parent: Overlay.overlay +* model: [ +* { "label": "Action 1", "action": "action1", "icon": "icon-name" }, +* { "label": "Action 2", "action": "action2" } +* ] +* onTriggered: action => { Logger.i("MyModule", "Selected:", action) } +* } +* +* MouseArea { +* onClicked: contextMenu.openAtItem(parent, mouse.x, mouse.y) +* } +*/ +Popup { + id: root + + property var model: [] + property real itemHeight: 36 + property real itemPadding: Style.marginM + property int verticalPolicy: ScrollBar.AsNeeded + property int horizontalPolicy: ScrollBar.AsNeeded + // Optional: explicit item whose bounds the menu must stay within. + // When unset, openAtItem auto-detects the nearest clipping ancestor. + property Item constrainTo: null + property Item _detectedConstraint: null + + signal triggered(string action) + + // Filter out hidden items to avoid spacing artifacts from zero-height items + readonly property var filteredModel: { + if (!model || model.length === 0) + return []; + var filtered = []; + for (var i = 0; i < model.length; i++) { + if (model[i].visible !== false) { + filtered.push(model[i]); + } + } + return filtered; + } + + width: 180 + padding: Style.marginS + + background: Rectangle { + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Style.borderS + radius: Style.iRadiusM + } + + contentItem: NListView { + id: listView + implicitHeight: Math.max(contentHeight, root.itemHeight) + spacing: Style.marginXXS + interactive: contentHeight > root.height + verticalPolicy: root.verticalPolicy + horizontalPolicy: root.horizontalPolicy + reserveScrollbarSpace: false + model: root.filteredModel + + delegate: ItemDelegate { + id: menuItem + width: listView.availableWidth + height: root.itemHeight + opacity: modelData.enabled !== false ? 1.0 : 0.5 + enabled: modelData.enabled !== false + + // Store reference to the popup + property var popup: root + + background: Rectangle { + color: menuItem.hovered && menuItem.enabled ? Color.mHover : "transparent" + radius: Style.iRadiusS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + contentItem: RowLayout { + spacing: Style.marginS + + // Optional icon + NIcon { + visible: modelData.icon !== undefined + icon: modelData.icon || "" + pointSize: Style.fontSizeM + color: menuItem.hovered && menuItem.enabled ? Color.mOnHover : Color.mOnSurface + Layout.leftMargin: root.itemPadding + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + NText { + text: modelData.label || modelData.text || "" + pointSize: Style.fontSizeM + color: menuItem.hovered && menuItem.enabled ? Color.mOnHover : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + Layout.fillWidth: true + Layout.leftMargin: modelData.icon === undefined ? root.itemPadding : 0 + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + + onClicked: { + if (enabled) { + popup.triggered(modelData.action || modelData.key || index.toString()); + popup.close(); + } + } + } + } + + // Helper function to open at mouse position + function openAt(x, y) { + if (root.parent) { + var menuWidth = root.width; + var itemCount = root.filteredModel.length; + var menuHeight = Math.max(itemCount * root.itemHeight + Math.max(0, itemCount - 1) * listView.spacing, root.itemHeight) + root.topPadding + root.bottomPadding; + var constraint = root.constrainTo || root._detectedConstraint; + if (constraint) { + var tl = constraint.mapToItem(root.parent, 0, 0); + x = Math.max(tl.x, Math.min(x, tl.x + constraint.width - menuWidth)); + y = Math.max(tl.y, Math.min(y, tl.y + constraint.height - menuHeight)); + } else { + x = Math.max(0, Math.min(x, root.parent.width - menuWidth)); + y = Math.max(0, Math.min(y, root.parent.height - menuHeight)); + } + } + root.x = x; + root.y = y; + root.open(); + } + + // Helper function to open at item + function openAtItem(item, mouseX, mouseY) { + if (!root.constrainTo) { + root._detectedConstraint = null; + var p = item; + while (p && p !== root.parent) { + if (p.clip && p.width > 0 && p.height > 0) { + root._detectedConstraint = p; + break; + } + p = p.parent; + } + } + var pos = item.mapToItem(root.parent, mouseX || 0, mouseY || 0); + openAt(pos.x, pos.y); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NDateTimeTokens.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NDateTimeTokens.qml new file mode 100644 index 0000000..496c70b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NDateTimeTokens.qml @@ -0,0 +1,393 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons + +Rectangle { + id: root + + property date sampleDate: new Date() // Dec 25, 2023, 2:30:45.123 PM + + signal tokenClicked(string token) + + Layout.margins: Style.borderS + color: Color.mSurface + border.color: Color.mOutline + border.width: Style.borderS + radius: Style.iRadiusM + + ColumnLayout { + id: column + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginS + + Flickable { + Layout.fillWidth: true + Layout.fillHeight: true + contentHeight: tokensColumn.implicitHeight + clip: true + + Column { + id: tokensColumn + width: parent.width + + Repeater { + model: [ + // Common format combinations + { + "category": "Common", + "token": "h:mm AP", + "description": I18n.tr("widgets.datetime-tokens.common-12hour-time-minutes"), + "example": "2:30 PM" + }, + { + "category": "Common", + "token": "HH:mm", + "description": I18n.tr("widgets.datetime-tokens.common-24hour-time-minutes"), + "example": "14:30" + }, + { + "category": "Common", + "token": "HH:mm:ss", + "description": I18n.tr("widgets.datetime-tokens.common-24hour-time-seconds"), + "example": "14:30:45" + }, + { + "category": "Common", + "token": "ddd MMM d", + "description": I18n.tr("widgets.datetime-tokens.common-weekday-month-day"), + "example": "Mon Dec 25" + }, + { + "category": "Common", + "token": "yyyy-MM-dd", + "description": I18n.tr("widgets.datetime-tokens.common-iso-date"), + "example": "2023-12-25" + }, + { + "category": "Common", + "token": "MM/dd/yyyy", + "description": I18n.tr("widgets.datetime-tokens.common-us-date"), + "example": "12/25/2023" + }, + { + "category": "Common", + "token": "dd.MM.yyyy", + "description": I18n.tr("widgets.datetime-tokens.common-european-date"), + "example": "25.12.2023" + }, + { + "category": "Common", + "token": "ddd, MMM dd", + "description": I18n.tr("widgets.datetime-tokens.common-weekday-date"), + "example": "Fri, Dec 12" + } // Hour tokens + , + { + "category": "Hour", + "token": "H", + "description": I18n.tr("widgets.datetime-tokens.hour-no-leading-zero"), + "example": "14" + }, + { + "category": "Hour", + "token": "HH", + "description": I18n.tr("widgets.datetime-tokens.hour-leading-zero"), + "example": "14" + } // Minute tokens + , + { + "category": "Minute", + "token": "m", + "description": I18n.tr("widgets.datetime-tokens.minute-no-leading-zero"), + "example": "30" + }, + { + "category": "Minute", + "token": "mm", + "description": I18n.tr("widgets.datetime-tokens.minute-leading-zero"), + "example": "30" + } // Second tokens + , + { + "category": "Second", + "token": "s", + "description": I18n.tr("widgets.datetime-tokens.second-no-leading-zero"), + "example": "45" + }, + { + "category": "Second", + "token": "ss", + "description": I18n.tr("widgets.datetime-tokens.second-leading-zero"), + "example": "45" + } // AM/PM tokens + , + { + "category": "AM/PM", + "token": "AP", + "description": I18n.tr("widgets.datetime-tokens.ampm-uppercase"), + "example": "PM" + }, + { + "category": "AM/PM", + "token": "ap", + "description": I18n.tr("widgets.datetime-tokens.ampm-lowercase"), + "example": "pm" + } // Timezone tokens + , + { + "category": "Timezone", + "token": "t", + "description": I18n.tr("widgets.datetime-tokens.timezone-abbreviation"), + "example": "UTC" + } // Year tokens + , + { + "category": "Year", + "token": "yy", + "description": I18n.tr("widgets.datetime-tokens.year-two-digit"), + "example": "23" + }, + { + "category": "Year", + "token": "yyyy", + "description": I18n.tr("widgets.datetime-tokens.year-four-digit"), + "example": "2023" + } // Month tokens + , + { + "category": "Month", + "token": "M", + "description": I18n.tr("widgets.datetime-tokens.month-number-no-zero"), + "example": "12" + }, + { + "category": "Month", + "token": "MM", + "description": I18n.tr("widgets.datetime-tokens.month-number-leading-zero"), + "example": "12" + }, + { + "category": "Month", + "token": "MMM", + "description": I18n.tr("widgets.datetime-tokens.month-abbreviated"), + "example": "Dec" + }, + { + "category": "Month", + "token": "MMMM", + "description": I18n.tr("widgets.datetime-tokens.month-full"), + "example": "December" + } // Day tokens + , + { + "category": "Day", + "token": "d", + "description": I18n.tr("widgets.datetime-tokens.day-no-leading-zero"), + "example": "25" + }, + { + "category": "Day", + "token": "dd", + "description": I18n.tr("widgets.datetime-tokens.day-leading-zero"), + "example": "25" + }, + { + "category": "Day", + "token": "ddd", + "description": I18n.tr("widgets.datetime-tokens.day-abbreviated"), + "example": "Mon" + }, + { + "category": "Day", + "token": "dddd", + "description": I18n.tr("widgets.datetime-tokens.day-full"), + "example": "Monday" + } + ] + + delegate: Rectangle { + id: tokenDelegate + width: tokensColumn.width + height: layout.implicitHeight + Style.marginS + radius: Style.iRadiusS + color: { + if (tokenMouseArea.containsMouse) { + return Qt.alpha(Color.mPrimary, 0.1); + } + return index % 2 === 0 ? Color.mSurfaceVariant : Qt.alpha(Color.mSurfaceVariant, 0.6); + } + + // Mouse area for the entire delegate + MouseArea { + id: tokenMouseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onClicked: { + root.tokenClicked(modelData.token); + clickAnimation.start(); + } + } + + // Click animation + SequentialAnimation { + id: clickAnimation + PropertyAnimation { + target: tokenDelegate + property: "color" + to: Qt.alpha(Color.mPrimary, 0.3) + duration: 100 + } + PropertyAnimation { + target: tokenDelegate + property: "color" + to: tokenMouseArea.containsMouse ? Qt.alpha(Color.mPrimary, 0.1) : (index % 2 === 0 ? Color.mSurface : Color.mSurfaceVariant) + duration: 200 + } + } + + RowLayout { + id: layout + anchors.fill: parent + anchors.margins: Style.marginXS + spacing: Style.marginM + + // Category badge + Rectangle { + Layout.alignment: Qt.AlignVCenter + width: 70 + height: 22 + color: getCategoryColor(modelData.category)[0] + radius: Style.iRadiusS + opacity: tokenMouseArea.containsMouse ? 0.9 : 1.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + + NText { + anchors.centerIn: parent + text: modelData.category + color: getCategoryColor(modelData.category)[1] + pointSize: Style.fontSizeXS + } + } + + // Token - Made more prominent and clickable + Rectangle { + id: tokenButton + Layout.alignment: Qt.AlignVCenter // Added this line + width: 100 + height: 22 + color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurface + radius: Style.iRadiusS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + NText { + anchors.centerIn: parent + text: modelData.token + color: tokenMouseArea.containsMouse ? Color.mOnPrimary : Color.mSurface + pointSize: Style.fontSizeS + font.weight: Style.fontWeightBold + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + + // Description + NText { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter // Added this line + text: modelData.description + color: tokenMouseArea.containsMouse ? Color.mOnSurface : Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + wrapMode: Text.WordWrap + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + // Live example + Rectangle { + Layout.alignment: Qt.AlignVCenter // Added this line + width: 90 + height: 22 + color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOnSurfaceVariant + radius: Style.iRadiusS + border.color: tokenMouseArea.containsMouse ? Color.mPrimary : Color.mOutline + border.width: Style.borderS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + NText { + anchors.centerIn: parent + text: I18n.locale.toString(root.sampleDate, modelData.token) + color: tokenMouseArea.containsMouse ? Color.mOnPrimary : Color.mSurfaceVariant + pointSize: Style.fontSizeS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + } + } + } + } + } + } + + function getCategoryColor(category) { + switch (category) { + case "Year": + return [Color.mPrimary, Color.mOnPrimary]; + case "Month": + return [Color.mSecondary, Color.mOnSecondary]; + case "Day": + return [Color.mTertiary, Color.mOnTertiary]; + case "Hour": + return [Color.mPrimary, Color.mOnPrimary]; + case "Minute": + return [Color.mSecondary, Color.mOnSecondary]; + case "Second": + return [Color.mTertiary, Color.mOnTertiary]; + case "AM/PM": + return [Color.mError, Color.mOnError]; + case "Timezone": + return [Color.mOnSurface, Color.mSurface]; + case "Common": + return [Color.mError, Color.mOnError]; + default: + return [Color.mOnSurfaceVariant, Color.mSurfaceVariant]; + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NDivider.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NDivider.qml new file mode 100644 index 0000000..29753ca --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NDivider.qml @@ -0,0 +1,30 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Commons + +Rectangle { + property bool vertical: false + + width: vertical ? Style.borderS : parent.width + height: vertical ? parent.height : Style.borderS + gradient: Gradient { + orientation: vertical ? Gradient.Vertical : Gradient.Horizontal + GradientStop { + position: 0.0 + color: "transparent" + } + GradientStop { + position: 0.1 + color: Color.mOutline + } + GradientStop { + position: 0.9 + color: Color.mOutline + } + GradientStop { + position: 1.0 + color: "transparent" + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NDropShadow.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NDropShadow.qml new file mode 100644 index 0000000..0a223f4 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NDropShadow.qml @@ -0,0 +1,31 @@ +import QtQuick +import QtQuick.Effects +import qs.Commons +import qs.Services.Power + +// Unified shadow system +Item { + id: root + + required property var source + + property bool autoPaddingEnabled: false + property real shadowHorizontalOffset: Settings.data.general.shadowOffsetX + property real shadowVerticalOffset: Settings.data.general.shadowOffsetY + property real shadowOpacity: Style.shadowOpacity + property color shadowColor: "black" + property real shadowBlur: Style.shadowBlur + + layer.enabled: Settings.data.general.enableShadows && !PowerProfileService.noctaliaPerformanceMode + layer.effect: MultiEffect { + source: root.source + shadowEnabled: true + blurMax: Style.shadowBlurMax + shadowBlur: root.shadowBlur + shadowOpacity: root.shadowOpacity + shadowColor: root.shadowColor + shadowHorizontalOffset: root.shadowHorizontalOffset + shadowVerticalOffset: root.shadowVerticalOffset + autoPaddingEnabled: root.autoPaddingEnabled + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NFilePicker.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NFilePicker.qml new file mode 100644 index 0000000..84395c1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NFilePicker.qml @@ -0,0 +1,817 @@ +import Qt.labs.folderlistmodel +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import qs.Commons +import qs.Widgets + +Popup { + id: root + + // Properties + property string title: I18n.tr("widget.file-picker.title") + property string initialPath: Quickshell.env("HOME") || "/home" + property string selectionMode: "files" // "files" or "folders" + property var nameFilters: ["*"] + property bool showDirs: true + property bool showHiddenFiles: false + property bool allowMultiSelection: false + + property var selectedPaths: [] + property string currentPath: initialPath + property bool shouldResetSelection: false + + // Signals + signal accepted(var paths) + signal cancelled + + function openFilePicker() { + if (!root.currentPath) + root.currentPath = root.initialPath; + shouldResetSelection = true; + open(); + } + + function getFileIcon(fileName) { + const ext = fileName.split('.').pop().toLowerCase(); + const iconMap = { + "txt": 'filepicker-file-text', + "md": 'filepicker-file-text', + "log": 'filepicker-file-text', + "jpg": 'filepicker-photo', + "jpeg": 'filepicker-photo', + "png": 'filepicker-photo', + "gif": 'filepicker-photo', + "bmp": 'filepicker-photo', + "svg": 'filepicker-photo', + "mp4": 'filepicker-video', + "avi": 'filepicker-video', + "mkv": 'filepicker-video', + "mov": 'filepicker-video', + "mp3": 'filepicker-music', + "wav": 'filepicker-music', + "flac": 'filepicker-music', + "ogg": 'filepicker-music', + "zip": 'filepicker-archive', + "tar": 'filepicker-archive', + "gz": 'filepicker-archive', + "rar": 'filepicker-archive', + "7z": 'filepicker-archive', + "pdf": 'filepicker-text', + "doc": 'filepicker-text', + "docx": 'filepicker-text', + "xls": 'filepicker-table', + "xlsx": 'filepicker-table', + "ppt": 'filepicker-presentation', + "pptx": 'filepicker-presentation', + "html": 'filepicker-code', + "htm": 'filepicker-code', + "css": 'filepicker-code', + "js": 'filepicker-code', + "json": 'filepicker-code', + "xml": 'filepicker-code', + "exe": 'filepicker-settings', + "app": 'filepicker-settings', + "deb": 'filepicker-settings', + "rpm": 'filepicker-settings' + }; + return iconMap[ext] || 'filepicker-file'; + } + + function formatFileSize(bytes) { + if (bytes === 0) + return "0 B"; + const k = 1024, sizes = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; + } + + function confirmSelection() { + if (filePickerPanel.currentSelection.length === 0) + return; + root.selectedPaths = filePickerPanel.currentSelection; + root.accepted(filePickerPanel.currentSelection); + root.close(); + } + + function updateFilteredModel() { + filteredModel.clear(); + const searchText = filePickerPanel.filterText.toLowerCase(); + + for (var i = 0; i < folderModel.count; i++) { + const fileName = folderModel.get(i, "fileName"); + const filePath = folderModel.get(i, "filePath"); + const fileIsDir = folderModel.get(i, "fileIsDir"); + const fileSize = folderModel.get(i, "fileSize"); + + // Skip hidden items if showHiddenFiles is false + // This additional check ensures hidden files are properly filtered + if (!root.showHiddenFiles && fileName.startsWith(".")) { + continue; + } + + // In folder mode, hide files + if (root.selectionMode === "folders" && !fileIsDir) + continue; + if (searchText === "" || fileName.toLowerCase().includes(searchText)) { + filteredModel.append({ + "fileName": fileName, + "filePath": filePath, + "fileIsDir": fileIsDir, + "fileSize": fileSize + }); + } + } + } + + width: 900 + height: 700 + modal: true + closePolicy: Popup.CloseOnEscape + anchors.centerIn: Overlay.overlay + + background: Rectangle { + color: Color.mSurfaceVariant + radius: Style.iRadiusL + border.color: Color.mOutline + border.width: Style.borderS + } + + Rectangle { + id: filePickerPanel + anchors.fill: parent + anchors.margins: Style.marginL + color: "transparent" + + property string filterText: "" + property var currentSelection: [] + property bool viewMode: true // true = grid, false = list + property string searchText: "" + property bool showSearchBar: false + + focus: true + + Keys.onPressed: event => { + if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_F) { + filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar; + if (filePickerPanel.showSearchBar) + Qt.callLater(() => searchInput.forceActiveFocus()); + event.accepted = true; + } else if (event.key === Qt.Key_Escape && filePickerPanel.showSearchBar) { + filePickerPanel.showSearchBar = false; + filePickerPanel.searchText = ""; + filePickerPanel.filterText = ""; + root.updateFilteredModel(); + event.accepted = true; + } + } + + // Function when an item is clicked, either a folder or a file since both have the same functionality, reduces repetitive code + function itemClicked(modifiers, path) { + var list = currentSelection.slice(); + const index = list.indexOf(path); + if (root.allowMultiSelection && (modifiers & Qt.ShiftModifier) && list.length > 0) { + if (index > -1) { + list.splice(index, 1); + currentSelection = list; + } else { + var i = 0; + var toggle = false; + const firstItemPath = list[0]; + while (i < filteredModel.count) { + const itemPath = filteredModel.get(i).filePath; + + // This should be called twice, when we get to the item selected and when we get to the starting item. + if (itemPath === firstItemPath || itemPath === path) { + toggle = !toggle; + } + + // Add all files between the starting item and the item selected. + if (toggle) { + if (!list.includes(itemPath)) { + list.push(itemPath); + } + } + + i++; + } + + // Add the path selected as well since it's skipped in the while loop + if (!list.includes(path)) { + list.push(path); + } + currentSelection = list; + } + } else if (root.allowMultiSelection && (modifiers & Qt.ControlModifier)) { + if (index > -1) { + list.splice(index, 1); + currentSelection = list; + } else { + list.push(path); + currentSelection = list; + } + } else { + currentSelection = [path]; + } + } + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginM + + // Header + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NIcon { + icon: "filepicker-folder" + color: Color.mPrimary + pointSize: Style.fontSizeXXL + } + NText { + text: root.title + pointSize: Style.fontSizeXL + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + } + + // "Select Current" button only visible in folder selection mode + NButton { + text: I18n.tr("widgets.file-picker.select-current") + icon: "filepicker-folder-current" + visible: root.selectionMode === "folders" + onClicked: { + filePickerPanel.currentSelection = [root.currentPath]; + root.confirmSelection(); + } + } + + NIconButton { + icon: "filepicker-refresh" + tooltipText: I18n.tr("common.refresh") + onClicked: { + // Force a proper refresh by resetting the folder + const currentFolder = folderModel.folder; + folderModel.folder = ""; + folderModel.folder = currentFolder; + Qt.callLater(root.updateFilteredModel); + } + } + NIconButton { + icon: "filepicker-close" + tooltipText: I18n.tr("common.close") + onClicked: { + root.cancelled(); + root.close(); + } + } + } + + NDivider { + Layout.fillWidth: true + } + + // Navigation toolbar + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 45 + color: Color.mSurfaceVariant + radius: Style.iRadiusS + border.color: Color.mOutline + border.width: Style.borderS + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Style.marginS + anchors.rightMargin: Style.marginS + spacing: Style.marginS + + NIconButton { + icon: "filepicker-arrow-up" + tooltipText: I18n.tr("tooltips.up") + baseSize: Style.baseWidgetSize * 0.8 + enabled: folderModel.folder.toString() !== "file:///" + onClicked: { + const parentPath = folderModel.parentFolder.toString().replace("file://", ""); + folderModel.folder = "file://" + parentPath; + root.currentPath = parentPath; + } + } + + NIconButton { + icon: "filepicker-home" + tooltipText: I18n.tr("tooltips.home") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + const homePath = Quickshell.env("HOME") || "/home"; + folderModel.folder = "file://" + homePath; + root.currentPath = homePath; + } + } + + NIconButton { + icon: filePickerPanel.showSearchBar ? "filepicker-x" : "filepicker-search" + tooltipText: filePickerPanel.showSearchBar ? I18n.tr("tooltips.search-close") : I18n.tr("common.search") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + filePickerPanel.showSearchBar = !filePickerPanel.showSearchBar; + if (!filePickerPanel.showSearchBar) { + filePickerPanel.searchText = ""; + filePickerPanel.filterText = ""; + root.updateFilteredModel(); + } + } + } + + NTextInput { + id: locationInput + text: root.currentPath + placeholderText: I18n.tr("placeholders.enter-path") + Layout.fillWidth: true + + visible: !filePickerPanel.showSearchBar + enabled: !filePickerPanel.showSearchBar + + onEditingFinished: { + const newPath = text.trim(); + if (newPath !== "" && newPath !== root.currentPath) { + folderModel.folder = "file://" + newPath; + root.currentPath = newPath; + } else { + text = root.currentPath; + } + } + Connections { + target: root + function onCurrentPathChanged() { + if (!locationInput.activeFocus) + locationInput.text = root.currentPath; + } + } + } + + // Search bar + NTextInput { + id: searchInput + inputIconName: "search" + placeholderText: I18n.tr("placeholders.search") + Layout.fillWidth: true + + visible: filePickerPanel.showSearchBar + enabled: filePickerPanel.showSearchBar + + text: filePickerPanel.searchText + onTextChanged: { + filePickerPanel.searchText = text; + filePickerPanel.filterText = text; + root.updateFilteredModel(); + } + Keys.onEscapePressed: { + filePickerPanel.showSearchBar = false; + filePickerPanel.searchText = ""; + filePickerPanel.filterText = ""; + root.updateFilteredModel(); + } + } + + NIconButton { + icon: filePickerPanel.viewMode ? "filepicker-list" : "filepicker-layout-grid" + tooltipText: filePickerPanel.viewMode ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: filePickerPanel.viewMode = !filePickerPanel.viewMode + } + NIconButton { + icon: root.showHiddenFiles ? "filepicker-eye-off" : "filepicker-eye" + tooltipText: root.showHiddenFiles ? I18n.tr("tooltips.hidden-files-hide") : I18n.tr("tooltips.hidden-files-hide") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + root.showHiddenFiles = !root.showHiddenFiles; + // Force model refresh by resetting the folder + const currentFolder = folderModel.folder; + folderModel.folder = ""; + folderModel.folder = currentFolder; + Qt.callLater(root.updateFilteredModel); + } + } + } + } + + // File list area + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Color.mSurface + radius: Style.iRadiusM + border.color: Color.mOutline + border.width: Style.borderS + + FolderListModel { + id: folderModel + folder: "file://" + root.currentPath + // Use wildcard filters including hidden files when showHiddenFiles is true + nameFilters: root.showHiddenFiles ? ["*", ".*"] : root.nameFilters + showDirs: root.showDirs + showHidden: true // Always true, we'll filter in updateFilteredModel + showDotAndDotDot: false + sortField: FolderListModel.Name + sortReversed: false + + onFolderChanged: { + root.currentPath = folder.toString().replace("file://", ""); + filePickerPanel.currentSelection = []; + Qt.callLater(root.updateFilteredModel); + } + + onStatusChanged: { + if (status === FolderListModel.Error) { + if (root.currentPath !== Quickshell.env("HOME")) { + folder = "file://" + Quickshell.env("HOME"); + root.currentPath = Quickshell.env("HOME"); + } + } else if (status === FolderListModel.Ready) { + root.updateFilteredModel(); + } + } + } + + // Update nameFilters when showHiddenFiles changes + Connections { + target: root + function onShowHiddenFilesChanged() { + folderModel.nameFilters = root.showHiddenFiles ? ["*", ".*"] : root.nameFilters; + } + } + + ListModel { + id: filteredModel + } + + // Grid view + NGridView { + id: gridView + anchors.fill: parent + anchors.margins: Style.marginM + model: filteredModel + visible: filePickerPanel.viewMode + reuseItems: true + gradientColor: Color.mSurface + + property int columns: Math.max(1, Math.floor(availableWidth / 120)) + property int itemSize: Math.floor((availableWidth - leftMargin - rightMargin - (columns * Style.marginS)) / columns) + + cellWidth: Math.floor((availableWidth - leftMargin - rightMargin) / columns) + cellHeight: Math.floor(itemSize * 0.8) + Style.marginXS + Style.fontSizeS + Style.marginM + + leftMargin: Style.marginS + rightMargin: Style.marginS + topMargin: Style.marginS + bottomMargin: Style.marginS + + delegate: Rectangle { + id: gridItem + width: gridView.itemSize + height: gridView.cellHeight + color: "transparent" + radius: Style.iRadiusM + + property bool isSelected: filePickerPanel.currentSelection.includes(model.filePath) + + Rectangle { + anchors.fill: parent + color: "transparent" + radius: parent.radius + border.color: isSelected ? Color.mSecondary : Color.mSurface + border.width: Style.borderL + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + Rectangle { + anchors.fill: parent + color: (mouseArea.containsMouse && !isSelected) ? Color.mHover : "transparent" + radius: parent.radius + border.color: (mouseArea.containsMouse && !isSelected) ? Color.mHover : "transparent" + border.width: Style.borderS + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginXS + + Rectangle { + id: iconContainer + Layout.fillWidth: true + Layout.preferredHeight: Math.round(gridView.itemSize * 0.67) + color: "transparent" + + property bool isImage: { + if (model.fileIsDir) + return false; + const ext = model.fileName.split('.').pop().toLowerCase(); + return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(ext); + } + + Image { + id: thumbnail + anchors.fill: parent + anchors.margins: Style.marginXS + source: iconContainer.isImage ? "file://" + model.filePath : "" + fillMode: Image.PreserveAspectFit + visible: iconContainer.isImage && status === Image.Ready + smooth: false + cache: true + asynchronous: true + sourceSize.width: 120 + sourceSize.height: 120 + onStatusChanged: { + if (status === Image.Error) + visible = false; + } + + Rectangle { + anchors.fill: parent + color: Color.mSurfaceVariant + radius: Style.iRadiusS + visible: thumbnail.status === Image.Loading + NIcon { + icon: "filepicker-photo" + pointSize: Style.fontSizeL + color: Color.mOnSurfaceVariant + anchors.centerIn: parent + } + } + } + + NIcon { + icon: model.fileIsDir ? "filepicker-folder" : root.getFileIcon(model.fileName) + pointSize: Style.fontSizeXXL * 2 + color: { + if (isSelected) + return Color.mSecondary; + else if (mouseArea.containsMouse) + return Color.mOnHover; + else + return model.fileIsDir ? Color.mPrimary : Color.mOnSurfaceVariant; + } + anchors.centerIn: parent + visible: !iconContainer.isImage || thumbnail.status !== Image.Ready + } + + Rectangle { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Style.marginS + width: 24 + height: 24 + radius: Math.min(Style.iRadiusL, width / 2) + color: Color.mSecondary + border.color: Color.mOutline + border.width: Style.borderS + visible: isSelected + NIcon { + icon: "filepicker-check" + pointSize: Style.fontSizeS + color: Color.mOnSecondary + anchors.centerIn: parent + } + } + } + + NText { + text: model.fileName + color: { + if (isSelected) + return Color.mSecondary; + else if (mouseArea.containsMouse) + return Color.mOnHover; + else + return Color.mOnSurfaceVariant; + } + pointSize: Style.fontSizeS + font.weight: isSelected ? Style.fontWeightBold : Style.fontWeightRegular + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WrapAnywhere + elide: Text.ElideRight + maximumLineCount: 2 + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + if (model.fileIsDir) { + // In folder mode, single click selects the folder + if (root.selectionMode === "folders") { + filePickerPanel.itemClicked(mouse.modifiers, model.filePath); + } + // In file mode, single click on folder does nothing (must double-click to enter) + } else { + // Single click on file selects it (only in file mode) + if (root.selectionMode === "files") { + filePickerPanel.itemClicked(mouse.modifiers, model.filePath); + } + } + } + } + + onDoubleClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + if (model.fileIsDir) { + // Double-click on folder always navigates into it + folderModel.folder = "file://" + model.filePath; + root.currentPath = model.filePath; + } else { + // Double-click on file selects and confirms (only in file mode) + if (root.selectionMode === "files") { + filePickerPanel.currentSelection = [model.filePath]; + root.confirmSelection(); + } + } + } + } + } + } + } + + // List view + NListView { + id: listView + anchors.fill: parent + anchors.margins: Style.marginS + model: filteredModel + visible: !filePickerPanel.viewMode + gradientColor: Color.mSurface + + delegate: Rectangle { + id: listItem + width: listView.width + height: 40 + color: { + if (filePickerPanel.currentSelection.includes(model.filePath)) + return Color.mSecondary; + if (mouseArea.containsMouse) + return Color.mHover; + return "transparent"; + } + radius: Style.iRadiusS + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginM + anchors.rightMargin: Style.marginM + spacing: Style.marginM + + NIcon { + icon: model.fileIsDir ? "filepicker-folder" : root.getFileIcon(model.fileName) + pointSize: Style.fontSizeL + color: model.fileIsDir ? (filePickerPanel.currentSelection.includes(model.filePath) ? Color.mOnSecondary : Color.mPrimary) : Color.mOnSurfaceVariant + } + + NText { + text: model.fileName + color: filePickerPanel.currentSelection.includes(model.filePath) ? Color.mOnSecondary : Color.mOnSurface + pointSize: Style.fontSizeM + font.weight: filePickerPanel.currentSelection.includes(model.filePath) ? Style.fontWeightBold : Style.fontWeightRegular + Layout.fillWidth: true + elide: Text.ElideRight + } + + NText { + text: model.fileIsDir ? "" : root.formatFileSize(model.fileSize) + color: filePickerPanel.currentSelection.includes(model.filePath) ? Color.mOnSecondary : Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + visible: !model.fileIsDir + Layout.preferredWidth: implicitWidth + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + if (model.fileIsDir) { + // In folder mode, single click selects the folder + if (root.selectionMode === "folders") { + filePickerPanel.itemClicked(mouse.modifiers, model.filePath); + } + // In file mode, single click on folder does nothing (must double-click to enter) + } else { + // Single click on file selects it (only in file mode) + if (root.selectionMode === "files") { + filePickerPanel.itemClicked(mouse.modifiers, model.filePath); + } + } + } + } + + onDoubleClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + if (model.fileIsDir) { + // Double-click on folder always navigates into it + folderModel.folder = "file://" + model.filePath; + root.currentPath = model.filePath; + } else { + // Double-click on file selects and confirms (only in file mode) + if (root.selectionMode === "files") { + filePickerPanel.currentSelection = [model.filePath]; + root.confirmSelection(); + } + } + } + } + } + } + } + } + + // Footer + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NText { + text: { + if (filePickerPanel.searchText.length > 0) { + return "Searching for: \"" + filePickerPanel.searchText + "\" (" + filteredModel.count + " matches)"; + } else if (filePickerPanel.currentSelection.length > 0) { + const selectedName = filePickerPanel.currentSelection[0].split('/').pop(); + return I18n.tr("widgets.file-picker.selected") + " " + selectedName; + } else { + return filteredModel.count + " " + (filteredModel.count === 1 ? I18n.tr("widgets.file-picker.item") : I18n.tr("widgets.file-picker.items")); + } + } + color: filePickerPanel.searchText.length > 0 ? Color.mPrimary : Color.mOnSurfaceVariant + pointSize: Style.fontSizeS + Layout.fillWidth: true + } + + NButton { + text: I18n.tr("common.cancel") + outlined: true + onClicked: { + root.cancelled(); + root.close(); + } + } + + NButton { + text: root.selectionMode === "folders" ? I18n.tr("widgets.file-picker.select-folder") : I18n.tr("widgets.file-picker.select-file") + icon: "filepicker-check" + enabled: filePickerPanel.currentSelection.length > 0 + onClicked: root.confirmSelection() + } + } + } + + Connections { + target: root + function onShouldResetSelectionChanged() { + if (root.shouldResetSelection) { + filePickerPanel.currentSelection = []; + root.shouldResetSelection = false; + } + } + } + + Component.onCompleted: { + if (!root.currentPath) + root.currentPath = root.initialPath; + folderModel.folder = "file://" + root.currentPath; + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NGraph.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NGraph.qml new file mode 100644 index 0000000..839229d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NGraph.qml @@ -0,0 +1,234 @@ +import QtQuick +import Quickshell +import qs.Commons + +Item { + id: root + clip: true + + // Primary line + property var values: [] + property color color: Color.mPrimary + + // Optional secondary line + property var values2: [] + property color color2: Color.mError + + // Range settings for primary line + property real minValue: 0 + property real maxValue: 100 + + // Range settings for secondary line (defaults to primary range) + property real minValue2: minValue + property real maxValue2: maxValue + + // Style settings + property real strokeWidth: 1 + property bool fill: true + property real fillOpacity: 0.15 + property real antialiasing: 0.5 + + // Smooth scrolling interval (how often data updates) + property int updateInterval: 1000 + + // Animate scale changes (for network graphs with dynamic max) + property bool animateScale: false + + // Vertical padding (percentage of range) to keep values from touching edges + readonly property real curvePadding: 0.12 + + readonly property bool hasData: values.length >= 4 + readonly property bool hasData2: values2.length >= 4 + + // Scale animation state + property real _targetMax1: maxValue + property real _targetMax2: maxValue2 + property real _animMax1: maxValue + property real _animMax2: maxValue2 + + onMaxValueChanged: { + _targetMax1 = maxValue; + if (animateScale && _ready1) { + _scaleTimer.start(); + } else { + _animMax1 = maxValue; + } + } + + onMaxValue2Changed: { + _targetMax2 = maxValue2; + if (animateScale && _ready2) { + _scaleTimer.start(); + } else { + _animMax2 = maxValue2; + } + } + + // Effective max values (animated or direct) + readonly property real _effectiveMax1: animateScale ? _animMax1 : maxValue + readonly property real _effectiveMax2: animateScale ? _animMax2 : maxValue2 + + // Scroll state (driven by NumberAnimation) + property real _t1: 1.0 + property bool _ready1: false + property real _pred1: 0 + + property real _t2: 1.0 + property bool _ready2: false + property real _pred2: 0 + + // Frame-accurate scroll animations tied to Qt's render loop + NumberAnimation { + id: _scrollAnim1 + target: root + property: "_t1" + from: 0 + to: 1 + duration: root.updateInterval + } + + NumberAnimation { + id: _scrollAnim2 + target: root + property: "_t2" + from: 0 + to: 1 + duration: root.updateInterval + } + + onValuesChanged: { + if (values.length < 4) + return; + + const last = values[values.length - 1]; + const prev = values[values.length - 2]; + _pred1 = Math.max(minValue, last + (last - prev) * 0.5); + + if (!_ready1) + _ready1 = true; + _scrollAnim1.restart(); + } + + onValues2Changed: { + if (values2.length < 4) + return; + + const last = values2[values2.length - 1]; + const prev = values2[values2.length - 2]; + _pred2 = Math.max(minValue2, last + (last - prev) * 0.5); + + if (!_ready2) + _ready2 = true; + _scrollAnim2.restart(); + } + + // Scale animation timer (only needed for animateScale mode) + Timer { + id: _scaleTimer + interval: 16 + repeat: true + + onTriggered: { + const scaleLerp = 0.15; + const threshold = 0.5; + let stillAnimating = false; + + if (Math.abs(root._animMax1 - root._targetMax1) > threshold) { + root._animMax1 += (root._targetMax1 - root._animMax1) * scaleLerp; + stillAnimating = true; + } else if (root._animMax1 !== root._targetMax1) { + root._animMax1 = root._targetMax1; + } + + if (Math.abs(root._animMax2 - root._targetMax2) > threshold) { + root._animMax2 += (root._targetMax2 - root._animMax2) * scaleLerp; + stillAnimating = true; + } else if (root._animMax2 !== root._targetMax2) { + root._animMax2 = root._targetMax2; + } + + if (!stillAnimating) + stop(); + } + } + + // Normalize a value to [0, 1] with padding applied + function _normalize(val, minVal, maxVal) { + let range = maxVal - minVal; + if (range <= 0) + return 0.5; + let padding = range * curvePadding; + let paddedMin = minVal - padding; + let paddedRange = (maxVal + padding) - paddedMin; + return Math.max(0, Math.min(1, (val - paddedMin) / paddedRange)); + } + + // Data texture built from Rectangles instead of Canvas. + // Each Rectangle is one data point, color-coded with normalized values. + // R channel = primary, G channel = secondary. + Item { + id: _dataRow + width: Math.max(root.values.length + 1, root.values2.length + 1, 4) + height: 1 + + Repeater { + model: _dataRow.width + + Rectangle { + required property int index + x: index + width: 1 + height: 1 + color: { + let r = 0, g = 0; + let n1 = root.values.length; + let n2 = root.values2.length; + let eMax1 = root._effectiveMax1; + let eMax2 = root._effectiveMax2; + + if (index < n1) + r = root._normalize(root.values[index], root.minValue, eMax1); + else if (n1 > 0) + r = root._normalize(root._pred1, root.minValue, eMax1); + + if (index < n2) + g = root._normalize(root.values2[index], root.minValue2, eMax2); + else if (n2 > 0) + g = root._normalize(root._pred2, root.minValue2, eMax2); + + return Qt.rgba(r, g, 0, 1); + } + } + } + } + + ShaderEffectSource { + id: _dataTex + sourceItem: _dataRow + textureSize: Qt.size(_dataRow.width, 1) + live: true + smooth: false + hideSource: true + } + + ShaderEffect { + anchors.fill: parent + visible: (root.hasData || root.hasData2) && width > 0 && height > 0 + + property variant dataSource: _dataTex + property color lineColor1: root.color + property color lineColor2: root.color2 + property real count1: root.values.length + property real count2: root.values2.length + property real scroll1: root._t1 + property real scroll2: root._t2 + property real lineWidth: root.strokeWidth + property real graphFillOpacity: root.fill ? root.fillOpacity : 0.0 + property real texWidth: _dataRow.width + property real resY: height + property real aaSize: root.antialiasing + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/graph.frag.qsb") + blending: true + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NGridView.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NGridView.qml new file mode 100644 index 0000000..b436281 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NGridView.qml @@ -0,0 +1,430 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Templates as T +import qs.Commons + +Item { + id: root + + // Signal for key press events when keyNavigationEnabled is true + signal keyPressed(var event) + + property color handleColor: Qt.alpha(Color.mHover, 0.8) + property color handleHoverColor: handleColor + property color handlePressedColor: handleColor + property color trackColor: "transparent" + property real handleWidth: 6 + property real handleRadius: Style.iRadiusM + property int verticalPolicy: ScrollBar.AsNeeded + property int horizontalPolicy: ScrollBar.AlwaysOff + readonly property bool verticalScrollBarActive: { + if (gridView.ScrollBar.vertical.policy === ScrollBar.AlwaysOff) + return false; + return gridView.contentHeight > gridView.height; + } + readonly property bool contentOverflows: gridView.contentHeight > gridView.height + + // Gradient properties + property bool showGradientMasks: true + property color gradientColor: Color.mSurfaceVariant + property int gradientHeight: 16 + property bool reserveScrollbarSpace: true + + // Keep scrollbars visible whenever overflow exists (without forcing visibility when not scrollable) + property bool showScrollbarWhenScrollable: Settings.data.ui.scrollbarAlwaysVisible + + // Available width for content (excludes scrollbar space when reserveScrollbarSpace is true) + // Note: Always reserves space when enabled to avoid binding loops with cellWidth calculations + readonly property real availableWidth: width - (reserveScrollbarSpace ? handleWidth + Style.marginXS : 0) + + // Expose activeFocus from internal gridView + readonly property bool hasActiveFocus: gridView.activeFocus + + // Forward GridView properties + property alias model: gridView.model + property alias delegate: gridView.delegate + property alias cellWidth: gridView.cellWidth + property alias cellHeight: gridView.cellHeight + property alias leftMargin: gridView.leftMargin + property alias rightMargin: gridView.rightMargin + property alias topMargin: gridView.topMargin + property alias bottomMargin: gridView.bottomMargin + property alias currentIndex: gridView.currentIndex + property alias count: gridView.count + property alias contentHeight: gridView.contentHeight + property alias contentWidth: gridView.contentWidth + property alias contentY: gridView.contentY + property alias contentX: gridView.contentX + property alias currentItem: gridView.currentItem + property alias highlightItem: gridView.highlightItem + property alias highlightFollowsCurrentItem: gridView.highlightFollowsCurrentItem + property alias preferredHighlightBegin: gridView.preferredHighlightBegin + property alias preferredHighlightEnd: gridView.preferredHighlightEnd + property alias highlightRangeMode: gridView.highlightRangeMode + property alias snapMode: gridView.snapMode + property alias keyNavigationEnabled: gridView.keyNavigationEnabled + property alias keyNavigationWraps: gridView.keyNavigationWraps + property alias cacheBuffer: gridView.cacheBuffer + property alias displayMarginBeginning: gridView.displayMarginBeginning + property alias displayMarginEnd: gridView.displayMarginEnd + property alias layoutDirection: gridView.layoutDirection + property alias effectiveLayoutDirection: gridView.effectiveLayoutDirection + property alias flow: gridView.flow + property alias boundsBehavior: gridView.boundsBehavior + property alias flickableDirection: gridView.flickableDirection + property alias interactive: gridView.interactive + property alias moving: gridView.moving + property alias flicking: gridView.flicking + property alias dragging: gridView.dragging + property alias horizontalVelocity: gridView.horizontalVelocity + property alias verticalVelocity: gridView.verticalVelocity + property alias reuseItems: gridView.reuseItems + + // Animate items when the model is reordered (e.g. ListModel.move()) + property bool animateMovement: false + + // Scroll speed multiplier for mouse wheel (1.0 = default, higher = faster) + property real wheelScrollMultiplier: 2.0 + property int smoothWheelAnimationDuration: Style.animationNormal + property real _wheelTargetY: 0 + + function clampScrollY(value) { + return Math.max(0, Math.min(value, gridView.contentHeight - gridView.height)); + } + + function applyWheelScroll(delta) { + if (!root.contentOverflows) + return; + + const step = delta * root.wheelScrollMultiplier; + + if (!Settings.data.general.smoothScrollEnabled || Settings.data.general.animationDisabled) { + gridView.contentY = root.clampScrollY(gridView.contentY - step); + root._wheelTargetY = gridView.contentY; + return; + } + + if (!wheelScrollAnimation.running) + root._wheelTargetY = gridView.contentY; + + root._wheelTargetY = root.clampScrollY(root._wheelTargetY - step); + wheelScrollAnimation.to = root._wheelTargetY; + wheelScrollAnimation.restart(); + } + + function animateToContentY(targetY) { + const clampedY = root.clampScrollY(targetY); + + if (!Settings.data.general.smoothScrollEnabled || Settings.data.general.animationDisabled || gridView.dragging || gridView.flicking) { + gridView.contentY = clampedY; + root._wheelTargetY = clampedY; + return; + } + + root._wheelTargetY = clampedY; + wheelScrollAnimation.to = clampedY; + wheelScrollAnimation.restart(); + } + + // Track selection index for gradient visibility (set externally) + property int trackedSelectionIndex: -1 + + // Check if selection is on first visible row + readonly property bool selectionOnFirstVisibleRow: { + if (trackedSelectionIndex < 0 || cellHeight <= 0 || cellWidth <= 0) + return false; + + // Calculate columns per row + var cols = Math.floor(gridView.width / cellWidth); + if (cols <= 0) + cols = 1; + + // Calculate the row of the selection + var selectionRow = Math.round(trackedSelectionIndex / cols); + + // Calculate the first visible row + var firstVisibleRow = Math.round(gridView.contentY / cellHeight); + + return selectionRow === firstVisibleRow; + } + + // Check if selection is on last visible row + readonly property bool selectionOnLastVisibleRow: { + if (trackedSelectionIndex < 0 || cellHeight <= 0 || cellWidth <= 0) + return false; + + // Calculate columns per row + var cols = Math.floor(gridView.width / cellWidth); + if (cols <= 0) + cols = 1; + + // Calculate the row of the selection + var selectionRow = Math.round(trackedSelectionIndex / cols); + + // Calculate the last visible row (might be partially visible) + var lastVisibleRow = Math.round((gridView.contentY + gridView.height - 1) / cellHeight); + + return selectionRow === lastVisibleRow; + } + + // Forward GridView methods + function positionViewAtIndex(index, mode) { + const shouldAnimate = mode === GridView.Contain; + if (!shouldAnimate) { + gridView.positionViewAtIndex(index, mode); + root._wheelTargetY = gridView.contentY; + return; + } + + const previousY = gridView.contentY; + gridView.positionViewAtIndex(index, mode); + const targetY = root.clampScrollY(gridView.contentY); + + if (Math.abs(targetY - previousY) < 0.5) { + root._wheelTargetY = targetY; + return; + } + + gridView.contentY = previousY; + root._wheelTargetY = previousY; + root.animateToContentY(targetY); + } + + function positionViewAtBeginning() { + gridView.positionViewAtBeginning(); + } + + function positionViewAtEnd() { + gridView.positionViewAtEnd(); + } + + function forceLayout() { + gridView.forceLayout(); + } + + function forceActiveFocus() { + gridView.forceActiveFocus(); + } + + function cancelFlick() { + gridView.cancelFlick(); + } + + function flick(xVelocity, yVelocity) { + gridView.flick(xVelocity, yVelocity); + } + + function incrementCurrentIndex() { + gridView.incrementCurrentIndex(); + } + + function decrementCurrentIndex() { + gridView.decrementCurrentIndex(); + } + + function indexAt(x, y) { + return gridView.indexAt(x, y); + } + + function itemAt(x, y) { + return gridView.itemAt(x, y); + } + + function itemAtIndex(index) { + return gridView.itemAtIndex(index); + } + + function moveCurrentIndexUp() { + gridView.moveCurrentIndexUp(); + } + + function moveCurrentIndexDown() { + gridView.moveCurrentIndexDown(); + } + + function moveCurrentIndexLeft() { + gridView.moveCurrentIndexLeft(); + } + + function moveCurrentIndexRight() { + gridView.moveCurrentIndexRight(); + } + + // Set reasonable implicit sizes for Layout usage + implicitWidth: 200 + implicitHeight: 200 + + Component.onCompleted: { + _wheelTargetY = gridView.contentY; + createGradients(); + } + + // Dynamically create gradient overlays + function createGradients() { + if (!showGradientMasks) + return; + + Qt.createQmlObject(` + import QtQuick + import qs.Commons + Rectangle { + x: 0 + y: 0 + width: root.availableWidth + height: root.gradientHeight + z: 1 + visible: root.showGradientMasks && root.contentOverflows + opacity: (gridView.contentY <= 1 || root.selectionOnFirstVisibleRow) ? 0 : 1 + Behavior on opacity { + NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad } + } + gradient: Gradient { + GradientStop { position: 0.0; color: root.gradientColor } + GradientStop { position: 1.0; color: "transparent" } + } + } + `, root, "topGradient"); + + Qt.createQmlObject(` + import QtQuick + import qs.Commons + Rectangle { + x: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: -1 + width: root.availableWidth + height: root.gradientHeight + 1 + z: 1 + visible: root.showGradientMasks && root.contentOverflows + opacity: ((gridView.contentY + gridView.height >= gridView.contentHeight - 1) || root.selectionOnLastVisibleRow) ? 0 : 1 + Behavior on opacity { + NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad } + } + gradient: Gradient { + GradientStop { position: 0.0; color: "transparent" } + GradientStop { position: 1.0; color: root.gradientColor } + } + } + `, root, "bottomGradient"); + } + + GridView { + id: gridView + anchors.fill: parent + anchors.rightMargin: root.reserveScrollbarSpace ? root.handleWidth + Style.marginXS : 0 + + move: root.animateMovement ? moveTransitionImpl : null + displaced: root.animateMovement ? displacedTransitionImpl : null + + Transition { + id: moveTransitionImpl + NumberAnimation { + properties: "x,y" + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + Transition { + id: displacedTransitionImpl + NumberAnimation { + properties: "x,y" + duration: Style.animationNormal + easing.type: Easing.InOutQuad + } + } + + // Enable clipping to keep content within bounds + clip: true + + // Enable flickable for smooth scrolling + boundsBehavior: Flickable.StopAtBounds + + NumberAnimation { + id: wheelScrollAnimation + target: gridView + property: "contentY" + duration: root.smoothWheelAnimationDuration + easing.type: Easing.OutCubic + } + + onDraggingChanged: { + if (dragging) { + wheelScrollAnimation.stop(); + root._wheelTargetY = contentY; + } + } + + onFlickingChanged: { + if (flicking) { + wheelScrollAnimation.stop(); + root._wheelTargetY = contentY; + } + } + + onContentHeightChanged: root._wheelTargetY = root.clampScrollY(root._wheelTargetY) + onHeightChanged: root._wheelTargetY = root.clampScrollY(root._wheelTargetY) + + // Focus handling depends on keyNavigationEnabled + focus: keyNavigationEnabled + activeFocusOnTab: keyNavigationEnabled + + // Emit keyPressed signal for custom key handling + Keys.onPressed: event => { + if (keyNavigationEnabled) { + root.keyPressed(event); + } + } + + WheelHandler { + enabled: root.wheelScrollMultiplier !== 1.0 + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onWheel: event => { + const delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y : event.angleDelta.y / 2; + root.applyWheelScroll(delta); + event.accepted = true; + } + } + + ScrollBar.vertical: ScrollBar { + parent: root + x: root.mirrored ? 0 : root.width - width + y: 0 + height: root.height + policy: root.verticalPolicy + + contentItem: Rectangle { + implicitWidth: root.handleWidth + implicitHeight: 100 + radius: root.handleRadius + color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor + opacity: parent.policy === ScrollBar.AlwaysOn ? 1.0 : root.verticalScrollBarActive ? ((root.showScrollbarWhenScrollable || parent.active) ? 1.0 : 0.0) : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + background: Rectangle { + implicitWidth: root.handleWidth + implicitHeight: 100 + color: root.trackColor + opacity: parent.policy === ScrollBar.AlwaysOn ? 0.3 : root.verticalScrollBarActive ? ((root.showScrollbarWhenScrollable || parent.active) ? 0.3 : 0.0) : 0.0 + radius: root.handleRadius / 2 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NHeader.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NHeader.qml new file mode 100644 index 0000000..629c344 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NHeader.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons + +ColumnLayout { + id: root + + property string label: "" + property string description: "" + property bool enableDescriptionRichText: false + + opacity: enabled ? 1.0 : 0.6 + spacing: Style.marginXXS + Layout.fillWidth: true + Layout.bottomMargin: Style.marginM + + NText { + text: root.label + pointSize: Style.fontSizeXL + font.weight: Style.fontWeightSemiBold + color: Color.mPrimary + visible: root.label !== "" + } + + NText { + text: root.description + pointSize: Style.fontSizeM + color: Color.mOnSurfaceVariant + wrapMode: Text.WordWrap + Layout.fillWidth: true + visible: root.description !== "" + richTextEnabled: root.enableDescriptionRichText + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NIcon.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NIcon.qml new file mode 100644 index 0000000..834702a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NIcon.qml @@ -0,0 +1,30 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +Text { + id: root + + property string icon: Icons.defaultIcon + property real pointSize: Style.fontSizeL + property bool applyUiScale: true + + visible: (icon !== undefined) && (icon !== "") + text: { + if ((icon === undefined) || (icon === "")) { + return ""; + } + if (Icons.get(icon) === undefined) { + Logger.w("Icon", `"${icon}"`, "doesn't exist in the icons font"); + Logger.callStack(); + return Icons.get(Icons.defaultIcon); + } + return Icons.get(icon); + } + font.family: Icons.fontFamily + font.pointSize: Math.max(1, applyUiScale ? root.pointSize * Style.uiScaleRatio : root.pointSize) + color: Color.mOnSurface + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NIconButton.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NIconButton.qml new file mode 100644 index 0000000..52f4355 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NIconButton.qml @@ -0,0 +1,133 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Commons +import qs.Services.UI + +Item { + id: root + + property real baseSize: Style.baseWidgetSize + property bool applyUiScale: true + + property string icon + property var tooltipText + property string tooltipDirection: "auto" + property bool allowClickWhenDisabled: false + property bool handleWheel: false + property bool hovering: false + + property color colorBg: Color.smartAlpha(Color.mSurfaceVariant) + property color colorFg: Color.mPrimary + property color colorBgHover: Color.mHover + property color colorFgHover: Color.mOnHover + property color colorBorder: Color.mOutline + property color colorBorderHover: Color.mOutline + property real customRadius: -1 // -1 means use default (iRadiusL), otherwise use this value + + // Expose border properties for backwards compatibility (aliases to visualButton) + property alias border: visualButton.border + property alias radius: visualButton.radius + property alias color: visualButton.color + + signal entered + signal exited + signal clicked + signal rightClicked + signal middleClicked + signal wheel(int angleDelta) + + // Calculate button size based on settings + readonly property real buttonSize: applyUiScale ? Style.toOdd(baseSize * Style.uiScaleRatio) : Style.toOdd(baseSize) + + // Size: use implicit width/height which layout can override + // BarWidgetLoader sets explicit width/height to extend click area + implicitWidth: buttonSize + implicitHeight: buttonSize + + opacity: enabled ? 1.0 : 0.6 + + // Visual button - stays at buttonSize, centered in parent + Rectangle { + id: visualButton + width: root.buttonSize + height: root.buttonSize + anchors.centerIn: parent + + color: root.enabled && root.hovering ? colorBgHover : colorBg + radius: Math.min((customRadius >= 0 ? customRadius : Style.iRadiusL), width / 2) + border.color: root.enabled && root.hovering ? colorBorderHover : colorBorder + border.width: Style.borderS + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + + NIcon { + icon: root.icon + pointSize: Style.toOdd(visualButton.width * 0.48) + applyUiScale: root.applyUiScale + color: root.enabled && root.hovering ? colorFgHover : colorFg + // Pixel-perfect centering + x: Style.pixelAlignCenter(visualButton.width, width) + y: Style.pixelAlignCenter(visualButton.height, contentHeight) + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + } + } + + // MouseArea fills root (extends beyond visual button for bar click area) + MouseArea { + // Always enabled to allow hover/tooltip even when the button is disabled + enabled: true + anchors.fill: parent + cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + hoverEnabled: true + onEntered: { + hovering = root.enabled ? true : false; + if (hovering && tooltipText && (!Array.isArray(tooltipText) || tooltipText.length > 0)) { + TooltipService.show(root, tooltipText, tooltipDirection); + } + root.entered(); + } + onExited: { + hovering = false; + if (tooltipText && (!Array.isArray(tooltipText) || tooltipText.length > 0)) { + TooltipService.hide(root); + } + root.exited(); + } + onClicked: mouse => { + if (tooltipText && (!Array.isArray(tooltipText) || tooltipText.length > 0)) { + TooltipService.hide(root); + } + if (!root.enabled && !allowClickWhenDisabled) { + return; + } + if (mouse.button === Qt.LeftButton) { + root.clicked(); + } else if (mouse.button === Qt.RightButton) { + root.rightClicked(); + } else if (mouse.button === Qt.MiddleButton) { + root.middleClicked(); + } + } + onWheel: wheel => { + if (root.handleWheel) { + root.wheel(wheel.angleDelta.y); + } + wheel.accepted = false; + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NIconButtonHot.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NIconButtonHot.qml new file mode 100644 index 0000000..39fdb92 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NIconButtonHot.qml @@ -0,0 +1,161 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import qs.Commons +import qs.Services.UI + +Rectangle { + id: root + + // Public properties + property real baseSize: Style.baseWidgetSize + property bool applyUiScale: true + property string icon + property var tooltipText + property string tooltipDirection: "auto" + property bool allowClickWhenDisabled: false + property bool hot: false + + // Internal properties + property bool hovering: false + property bool pressed: false + + // Color properties + property color colorBg: Color.smartAlpha(Color.mSurfaceVariant) + property color colorFg: Color.mPrimary + property color colorBgHover: Color.mHover + property color colorFgHover: Color.mOnHover + property color colorBorder: Color.mOutline + property color colorBorderHover: Color.mOutline + + // Hot state colors + property color colorBgHot: Color.mPrimary + property color colorFgHot: Color.mOnPrimary + + // Signals + signal entered + signal exited + signal clicked + signal rightClicked + signal middleClicked + + // Dimensions + implicitWidth: applyUiScale ? Math.round(baseSize * Style.uiScaleRatio) : Math.round(baseSize) + implicitHeight: applyUiScale ? Math.round(baseSize * Style.uiScaleRatio) : Math.round(baseSize) + + // Appearance + opacity: enabled ? 1.0 : 0.6 + color: { + if (root.enabled && root.hovering || pressed) { + return colorBgHover; + } + + if (hot) { + return colorBgHot; + } + return colorBg; + } + radius: Math.min(Style.iRadiusL, width / 2) + border.color: root.enabled && root.hovering ? colorBorderHover : colorBorder + border.width: Style.borderS + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + + // Icon + NIcon { + icon: root.icon + pointSize: Math.max(1, Math.round(root.width * 0.48)) + applyUiScale: root.applyUiScale + color: { + if (root.enabled && root.hovering || pressed) { + return colorFgHover; + } + if (hot) { + return colorFgHot; + } + return colorFg; + } + // Center horizontally + x: (root.width - width) / 2 + // Center vertically accounting for font metrics + y: (root.height - height) / 2 + (height - contentHeight) / 2 + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.InOutQuad + } + } + } + + MouseArea { + // Always enabled to allow hover/tooltip even when the button is disabled + enabled: true + anchors.fill: parent + cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + hoverEnabled: true + + onEntered: { + hovering = root.enabled ? true : false; + if (hovering && tooltipText && (!Array.isArray(tooltipText) || tooltipText.length > 0)) { + TooltipService.show(parent, tooltipText, tooltipDirection); + } + root.entered(); + } + + onExited: { + hovering = false; + if (tooltipText && (!Array.isArray(tooltipText) || tooltipText.length > 0)) { + TooltipService.hide(); + } + root.exited(); + } + + onPressed: function (mouse) { + if (root.enabled) { + root.pressed = true; + } + if (tooltipText && (!Array.isArray(tooltipText) || tooltipText.length > 0)) { + TooltipService.hide(); + } + } + + onReleased: function (mouse) { + root.scale = 1.0; + root.pressed = false; + + if (!root.enabled && !allowClickWhenDisabled) { + return; + } + + // Only trigger actions if released while hovering + if (root.hovering) { + if (mouse.button === Qt.LeftButton) { + root.clicked(); + } else if (mouse.button === Qt.RightButton) { + root.rightClicked(); + } else if (mouse.button === Qt.MiddleButton) { + root.middleClicked(); + } + } + } + + onCanceled: { + root.hovering = false; + root.pressed = false; + root.scale = 1.0; + if (tooltipText && (!Array.isArray(tooltipText) || tooltipText.length > 0)) { + TooltipService.hide(); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NIconPicker.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NIconPicker.qml new file mode 100644 index 0000000..20ee31e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NIconPicker.qml @@ -0,0 +1,170 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import qs.Commons +import qs.Widgets + +Popup { + id: root + modal: true + + property string selectedIcon: "" + property string initialIcon: "" + + signal iconSelected(string iconName) + + width: Math.round(900 * Style.uiScaleRatio) + height: Math.round(700 * Style.uiScaleRatio) + anchors.centerIn: Overlay.overlay + padding: Style.marginXL + + property string query: "" + property var allIcons: Object.keys(Icons.icons) + property var filteredIcons: { + if (query === "") + return allIcons; + var q = query.toLowerCase(); + return allIcons.filter(name => name.toLowerCase().includes(q)); + } + readonly property int columns: 6 + readonly property int cellW: Math.floor(grid.width / columns) + readonly property int cellH: Math.round(cellW * 0.7 + 36) + + onOpened: { + selectedIcon = initialIcon; + query = initialIcon; + searchInput.forceActiveFocus(); + } + + background: Rectangle { + color: Color.mSurface + radius: Style.iRadiusL + border.color: Color.mPrimary + border.width: Style.borderM + } + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginM + + // Title row + RowLayout { + Layout.fillWidth: true + NText { + text: I18n.tr("widgets.icon-picker.title") + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + } + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + onClicked: root.close() + } + } + + NDivider { + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginS + NTextInput { + id: searchInput + Layout.fillWidth: true + label: I18n.tr("common.search") + placeholderText: I18n.tr("placeholders.search-icons") + text: root.query + onTextChanged: root.query = text.trim().toLowerCase() + } + } + + // Icon grid + NGridView { + id: grid + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: Style.marginM + cellWidth: root.cellW + cellHeight: root.cellH + model: root.filteredIcons + reserveScrollbarSpace: false + gradientColor: Color.mSurface + + delegate: Rectangle { + width: grid.cellWidth + height: grid.cellHeight + radius: Style.iRadiusS + + color: (root.selectedIcon === modelData) ? Qt.alpha(Color.mPrimary, 0.15) : "transparent" + border.color: (root.selectedIcon === modelData) ? Color.mPrimary : "transparent" + border.width: (root.selectedIcon === modelData) ? Style.borderS : 0 + + MouseArea { + anchors.fill: parent + onClicked: root.selectedIcon = modelData + onDoubleClicked: { + root.selectedIcon = modelData; + root.iconSelected(root.selectedIcon); + root.close(); + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginS + spacing: Style.marginS + Item { + Layout.fillHeight: true + Layout.preferredHeight: 4 + } + NIcon { + Layout.alignment: Qt.AlignHCenter + icon: modelData + pointSize: 42 + } + NText { + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.topMargin: Style.marginXS + elide: Text.ElideRight + wrapMode: Text.NoWrap + maximumLineCount: 1 + horizontalAlignment: Text.AlignHCenter + color: Color.mOnSurfaceVariant + pointSize: Style.fontSizeXS + text: modelData + } + Item { + Layout.fillHeight: true + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + Item { + Layout.fillWidth: true + } + NButton { + text: I18n.tr("common.cancel") + outlined: true + onClicked: root.close() + } + NButton { + text: I18n.tr("common.apply") + icon: "check" + enabled: root.selectedIcon !== "" + onClicked: { + root.iconSelected(root.selectedIcon); + root.close(); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NImageRounded.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NImageRounded.qml new file mode 100644 index 0000000..a33ffcf --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NImageRounded.qml @@ -0,0 +1,103 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Widgets +import qs.Commons + +Item { + id: root + + property real radius: 0 + property string imagePath: "" + property string fallbackIcon: "" + property real fallbackIconSize: Style.fontSizeXXL + property real borderWidth: 0 + property color borderColor: "transparent" + property int imageFillMode: Image.PreserveAspectCrop + + readonly property bool _isAnimated: imagePath.toLowerCase().endsWith(".gif") + readonly property Item imageSource: imageSourceLoader.item + readonly property bool showFallback: fallbackIcon !== "" && (imagePath === "" || (imageSource && imageSource.status === Image.Error)) + readonly property int status: imageSource ? imageSource.status : Image.Null + + Rectangle { + anchors.fill: parent + radius: root.radius + color: "transparent" + border.width: root.borderWidth + border.color: root.borderColor + + Loader { + id: imageSourceLoader + anchors.fill: parent + anchors.margins: root.borderWidth + active: root.imagePath !== "" + sourceComponent: root._isAnimated ? animatedComponent : staticComponent + } + + Component { + id: staticComponent + Image { + visible: false + source: root.imagePath + mipmap: true + smooth: true + asynchronous: true + antialiasing: true + fillMode: root.imageFillMode + } + } + + Component { + id: animatedComponent + AnimatedImage { + visible: false + source: root.imagePath + mipmap: true + smooth: true + asynchronous: true + antialiasing: true + fillMode: root.imageFillMode + playing: true + } + } + + // Fallback texture provider to avoid null source warnings + ShaderEffectSource { + id: _safeFallback + sourceItem: Rectangle { + width: 1 + height: 1 + color: "transparent" + } + visible: false + live: false + } + + ShaderEffect { + anchors.fill: parent + anchors.margins: root.borderWidth + visible: !root.showFallback && root.imageSource !== null && root.status === Image.Ready + property var source: root.imageSource ?? _safeFallback + property real itemWidth: width + property real itemHeight: height + property real sourceWidth: root.imageSource?.sourceSize.width ?? 0 + property real sourceHeight: root.imageSource?.sourceSize.height ?? 0 + property real cornerRadius: Math.max(0, root.radius - root.borderWidth) + property real imageOpacity: 1.0 + property int fillMode: root.imageFillMode + + fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/rounded_image.frag.qsb") + supportsAtlasTextures: false + blending: true + } + + NIcon { + anchors.fill: parent + anchors.margins: root.borderWidth + visible: root.showFallback + icon: root.fallbackIcon + pointSize: root.fallbackIconSize + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NInputAction.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NInputAction.qml new file mode 100644 index 0000000..53025ed --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NInputAction.qml @@ -0,0 +1,52 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +// Input and button row +RowLayout { + id: root + + // Public properties + property string label: "" + property string description: "" + property string placeholderText: "" + property string text: "" + property string actionButtonText: I18n.tr("common.test") + property string actionButtonIcon: "media-play" + property bool actionButtonEnabled: text !== "" + + // Signals + signal editingFinished + signal actionClicked + + // Internal properties + spacing: Style.marginM + + NTextInput { + id: textInput + label: root.label + description: root.description + placeholderText: root.placeholderText + text: root.text + onTextChanged: root.text = text + onEditingFinished: root.editingFinished() + Layout.fillWidth: true + } + + NButton { + Layout.fillWidth: false + Layout.alignment: Qt.AlignBottom + + text: root.actionButtonText + icon: root.actionButtonIcon + backgroundColor: Color.mSecondary + textColor: Color.mOnSecondary + hoverColor: Color.mHover + enabled: root.actionButtonEnabled + + onClicked: { + root.actionClicked(); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NKeybindRecorder.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NKeybindRecorder.qml new file mode 100644 index 0000000..c19343d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NKeybindRecorder.qml @@ -0,0 +1,334 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.UI +import qs.Widgets + +Item { + id: root + + property string label: "" + property string description: "" + property var currentKeybinds: [] + property string defaultKeybind: "" + property bool allowEmpty: false + property color labelColor: Color.mOnSurface + property color descriptionColor: Color.mOnSurfaceVariant + property string settingsPath: "" + + property int maxKeybinds: 2 + property bool requireModifierForNormalKeys: true + signal keybindsChanged(var newKeybinds) + + implicitHeight: contentLayout.implicitHeight + + // -1 = not recording, >= 0 = re-recording at index, -2 = adding new + property int recordingIndex: -1 + property bool hasConflict: false + + onRecordingIndexChanged: { + PanelService.isKeybindRecording = recordingIndex !== -1; + if (recordingIndex !== -1) { + hasConflict = false; + } + } + + readonly property real _pillHeight: Style.baseWidgetSize * 1.1 * Style.uiScaleRatio + + function _applyKeybind(keyStr) { + if (!keyStr) + return; + + // 1. Internal duplicate check (same action) + for (let i = 0; i < root.currentKeybinds.length; i++) { + if (i !== root.recordingIndex && String(root.currentKeybinds[i]).toLowerCase() === keyStr.toLowerCase()) { + hasConflict = true; + ToastService.showWarning(I18n.tr("panels.general.keybinds-conflict-title"), I18n.tr("panels.general.keybinds-conflict-description", { + "action": root.label || "This action" + })); + conflictTimer.restart(); + return; + } + } + + // 2. External conflict check (other actions) + const conflict = Keybinds.getKeybindConflict(keyStr, root.settingsPath, Settings.data); + if (conflict) { + hasConflict = true; + ToastService.showWarning(I18n.tr("panels.general.keybinds-conflict-title"), I18n.tr("panels.general.keybinds-conflict-description", { + "action": conflict + })); + conflictTimer.restart(); + return; + } + + var newKeybinds = Array.from(root.currentKeybinds); + if (recordingIndex >= 0) { + newKeybinds[recordingIndex] = keyStr; + } + // Ensure array is dense and limited to maxKeybinds + newKeybinds = newKeybinds.filter(k => k !== undefined && k !== "").slice(0, root.maxKeybinds); + recordingIndex = -1; + root.keybindsChanged(newKeybinds); + } + + Timer { + id: conflictTimer + interval: 2000 + onTriggered: { + hasConflict = false; + recordingIndex = -1; + } + } + + RowLayout { + id: contentLayout + width: parent.width + spacing: Style.marginL + + // Label and Description (optional) + NLabel { + id: labelContainer + label: root.label + description: root.description + labelColor: root.labelColor + descriptionColor: root.descriptionColor + visible: label !== "" || description !== "" + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + } + + RowLayout { + id: slotsRow + spacing: Style.marginS + Layout.alignment: Qt.AlignVCenter | (labelContainer.visible ? Qt.AlignRight : Qt.AlignLeft) + + Repeater { + model: root.maxKeybinds + delegate: MouseArea { + id: slotArea + width: Math.round(180 * Style.uiScaleRatio) + height: root._pillHeight + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + readonly property bool isOccupied: index < root.currentKeybinds.length + readonly property bool isRecordingThis: root.recordingIndex === index + readonly property string keybindText: isRecordingThis ? I18n.tr("placeholders.keybind-recording") : (isOccupied ? root.currentKeybinds[index] : I18n.tr("placeholders.add-new-keybind")) + + onClicked: { + if (isRecordingThis) { + root.recordingIndex = -1; + } else { + root.recordingIndex = index; + keybindInput.forceActiveFocus(); + } + } + + Rectangle { + id: slotBg + anchors.fill: parent + radius: Style.iRadiusS + color: root.hasConflict && slotArea.isRecordingThis ? Color.mError : (slotArea.isRecordingThis ? Color.mSecondary : (slotArea.containsMouse ? Qt.alpha(Color.mSecondary, 0.15) : Color.mSurface)) + border.color: root.hasConflict && slotArea.isRecordingThis ? Color.mError : (slotArea.isRecordingThis ? Color.mPrimary : (slotArea.containsMouse ? Color.mSecondary : Color.mOutline)) + border.width: Style.borderS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginM + anchors.rightMargin: Style.marginS + spacing: Style.marginXS + + NIcon { + icon: root.hasConflict && slotArea.isRecordingThis ? "alert-circle" : (slotArea.isRecordingThis ? "circle-dot" : "keyboard") + color: slotArea.isRecordingThis ? Color.mOnSecondary : (slotArea.isOccupied ? Color.mOnSurfaceVariant : Qt.alpha(Color.mOnSurfaceVariant, 0.4)) + opacity: 0.8 + visible: !slotArea.isRecordingThis || root.hasConflict + } + + NText { + Layout.fillWidth: true + text: slotArea.keybindText + color: slotArea.isRecordingThis ? Color.mOnSecondary : (slotArea.isOccupied ? Color.mOnSurface : Color.mOnSurfaceVariant) + font.family: slotArea.isOccupied && !slotArea.isRecordingThis ? Settings.data.ui.fontFixed : Settings.data.ui.fontDefault + font.pointSize: slotArea.isOccupied ? Style.fontSizeM : Style.fontSizeS + font.weight: slotArea.isOccupied ? Style.fontWeightBold : Style.fontWeightRegular + elide: Text.ElideRight + opacity: slotArea.isOccupied || slotArea.isRecordingThis ? 1.0 : 0.6 + } + + Item { + Layout.preferredWidth: Math.round(root._pillHeight * 0.7) + Layout.fillHeight: true + visible: slotArea.isOccupied && root.recordingIndex === -1 + + NIconButton { + anchors.centerIn: parent + visible: root.recordingIndex === -1 && (root.currentKeybinds.length > 1 || root.allowEmpty) + icon: "x" + colorBg: "transparent" + colorBgHover: Qt.alpha(Color.mError, 0.1) + colorFg: Color.mOnSurfaceVariant + colorFgHover: Color.mError + border.width: 0 + baseSize: Style.baseWidgetSize * 0.7 + onClicked: { + var newKeybinds = Array.from(root.currentKeybinds); + newKeybinds.splice(index, 1); + root.keybindsChanged(newKeybinds); + } + } + } + } + } + } + } + } + + // Hidden Item to capture keys + Item { + id: keybindInput + width: 0 + height: 0 + focus: true + + Keys.onPressed: event => { + if (root.recordingIndex === -1 || root.hasConflict) + return; + + // Handle Escape specifically to ensure it doesn't close the panel + if (event.key === Qt.Key_Escape) { + event.accepted = true; + root._applyKeybind("Esc"); + return; + } + + // Ignore modifier keys by themselves + if (event.key === Qt.Key_Control || event.key === Qt.Key_Shift || event.key === Qt.Key_Alt || event.key === Qt.Key_Meta) { + event.accepted = true; // Consume modifiers too while listening + return; + } + + let keyStr = ""; + if (event.modifiers & Qt.ControlModifier) + keyStr += "Ctrl+"; + if (event.modifiers & Qt.AltModifier) + keyStr += "Alt+"; + if (event.modifiers & Qt.ShiftModifier) + keyStr += "Shift+"; + + let keyName = ""; + let rawText = event.text; + + if (event.key >= Qt.Key_A && event.key <= Qt.Key_Z || event.key >= Qt.Key_0 && event.key <= Qt.Key_9) { + keyName = String.fromCharCode(event.key); + } else if (event.key >= Qt.Key_F1 && event.key <= Qt.Key_F12) { + keyName = "F" + (event.key - Qt.Key_F1 + 1); + } else if (rawText && rawText.length > 0 && rawText.charCodeAt(0) > 31 && rawText.charCodeAt(0) !== 127) { + keyName = rawText.toUpperCase(); + + // Handle shifted digits + if (event.modifiers & Qt.ShiftModifier) { + const shiftMap = { + "!": "1", + "\"": "2", + "§": "3", + "$": "4", + "%": "5", + "&": "6", + "/": "7", + "(": "8", + ")": "9", + "=": "0", + "@": "2", + "#": "3", + "^": "6", + "*": "8" + }; + if (shiftMap[keyName]) + keyName = shiftMap[keyName]; + } + } else { + switch (event.key) { + case Qt.Key_Return: + keyName = "Return"; + break; + case Qt.Key_Enter: + keyName = "Enter"; + break; + case Qt.Key_Tab: + keyName = "Tab"; + break; + case Qt.Key_Backspace: + keyName = "Backspace"; + break; + case Qt.Key_Delete: + keyName = "Del"; + break; + case Qt.Key_Insert: + keyName = "Ins"; + break; + case Qt.Key_Home: + keyName = "Home"; + break; + case Qt.Key_End: + keyName = "End"; + break; + case Qt.Key_PageUp: + keyName = "PgUp"; + break; + case Qt.Key_PageDown: + keyName = "PgDn"; + break; + case Qt.Key_Left: + keyName = "Left"; + break; + case Qt.Key_Right: + keyName = "Right"; + break; + case Qt.Key_Up: + keyName = "Up"; + break; + case Qt.Key_Down: + keyName = "Down"; + break; + } + } + + if (keyName) { + // Enforce modifier requirement (Ctrl or Alt) for "normal" keys unless explicitly disabled + // Allow Arrows, Nav, Function, and System keys without modifiers + const isSpecialKey = (event.key >= Qt.Key_F1 && event.key <= Qt.Key_F35) || (event.key >= Qt.Key_Left && event.key <= Qt.Key_Down) || (event.key === Qt.Key_Home || event.key === Qt.Key_End || event.key === Qt.Key_PageUp || event.key === Qt.Key_PageDown) || (event.key === Qt.Key_Insert || event.key === Qt.Key_Delete || event.key + === Qt.Key_Backspace) || (event.key === Qt.Key_Tab || event.key + === Qt.Key_Return || event.key === Qt.Key_Enter + || event.key === Qt.Key_Escape || event.key + === Qt.Key_Space); + + const hasModifier = (event.modifiers & Qt.ControlModifier) || (event.modifiers & Qt.AltModifier); + + if (root.requireModifierForNormalKeys && !hasModifier && !isSpecialKey) { + hasConflict = true; + ToastService.showWarning(I18n.tr("panels.general.keybinds-modifier-title"), I18n.tr("panels.general.keybinds-modifier-description")); + conflictTimer.restart(); + return; + } + + root._applyKeybind(keyStr + keyName); + } + event.accepted = true; + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NLabel.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NLabel.qml new file mode 100644 index 0000000..71073fc --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NLabel.qml @@ -0,0 +1,69 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + + property string label: "" + property string description: "" + property string icon: "" + property color labelColor: Color.mOnSurface + property color descriptionColor: Color.mOnSurfaceVariant + property color iconColor: Color.mOnSurface + property bool showIndicator: false + property string indicatorTooltip: "" + property real labelSize: Style.fontSizeL + + opacity: enabled ? 1.0 : 0.6 + spacing: Style.marginXXS + visible: root.label != "" || root.description != "" + + Layout.fillWidth: true + + RowLayout { + spacing: Style.marginXS + Layout.fillWidth: true + visible: root.label !== "" + + NIcon { + visible: root.icon !== "" + icon: root.icon + pointSize: Style.fontSizeXXL + color: root.iconColor + Layout.rightMargin: Style.marginS + } + + NText { + id: labelText + Layout.fillWidth: true + text: root.label + pointSize: root.labelSize + font.weight: Style.fontWeightSemiBold + color: labelColor + wrapMode: Text.WordWrap + + // Settings indicator dot positioned right after the text content + Loader { + active: root.showIndicator + x: labelText.contentWidth + Style.marginXS + anchors.verticalCenter: parent.verticalCenter + sourceComponent: NSettingsIndicator { + show: true + tooltipText: root.indicatorTooltip || "" + } + } + } + } + + NText { + visible: root.description !== "" + Layout.fillWidth: true + text: root.description + pointSize: Style.fontSizeS + color: root.descriptionColor + wrapMode: Text.WordWrap + textFormat: Text.StyledText + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NLinearGauge.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NLinearGauge.qml new file mode 100644 index 0000000..93a0398 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NLinearGauge.qml @@ -0,0 +1,28 @@ +import QtQuick +import qs.Commons +import qs.Services.UI + +Rectangle { + id: root + + // Mandatory properties for gauges + required property int orientation // Qt.Vertical || Qt.Horizontal + required property real ratio // 0..1 + + radius: orientation === Qt.Vertical ? width / 2 : height / 2 + color: Color.mOutline + property color fillColor: Color.mPrimary + + // Fill that grows from bottom if vertical and left if horizontal. + // Snap to zero if the computed pixel length is sub-pixel (< 1px). + Rectangle { + readonly property real clampedRatio: Math.min(1, Math.max(0, root.ratio)) + readonly property real rawFill: (orientation === Qt.Vertical ? root.height : root.width) * clampedRatio + width: orientation === Qt.Vertical ? root.width : (rawFill < 1 ? 0 : rawFill) + height: orientation === Qt.Vertical ? (rawFill < 1 ? 0 : rawFill) : root.height + radius: root.radius + color: root.fillColor + anchors.bottom: orientation === Qt.Vertical ? parent.bottom : undefined + anchors.left: parent.left + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NListView.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NListView.qml new file mode 100644 index 0000000..7a2ae41 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NListView.qml @@ -0,0 +1,340 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Templates as T +import qs.Commons + +Item { + id: root + + property color handleColor: Qt.alpha(Color.mHover, 0.8) + property color handleHoverColor: handleColor + property color handlePressedColor: handleColor + property color trackColor: "transparent" + property real handleWidth: Math.round(6 * Style.uiScaleRatio) + property real handleRadius: Style.iRadiusM + property int verticalPolicy: ScrollBar.AsNeeded + property int horizontalPolicy: ScrollBar.AlwaysOff + readonly property bool verticalScrollBarActive: { + if (listView.ScrollBar.vertical.policy === ScrollBar.AlwaysOff) + return false; + return listView.contentHeight > listView.height; + } + readonly property bool contentOverflows: listView.contentHeight > listView.height + + property bool showGradientMasks: true + property color gradientColor: Color.mSurfaceVariant + property int gradientHeight: 16 + property bool reserveScrollbarSpace: true + + // Keep scrollbars visible whenever overflow exists (without forcing visibility when not scrollable) + property bool showScrollbarWhenScrollable: Settings.data.ui.scrollbarAlwaysVisible + + // Available width for content (excludes scrollbar space when reserveScrollbarSpace is true) + readonly property real availableWidth: width - (reserveScrollbarSpace ? handleWidth + Style.marginXS : 0) + + // Forward ListView properties + property alias model: listView.model + property alias delegate: listView.delegate + property alias spacing: listView.spacing + property alias orientation: listView.orientation + property alias currentIndex: listView.currentIndex + property alias count: listView.count + property alias contentHeight: listView.contentHeight + property alias contentWidth: listView.contentWidth + property alias contentY: listView.contentY + property alias contentX: listView.contentX + property alias currentItem: listView.currentItem + property alias highlightItem: listView.highlightItem + property alias headerItem: listView.headerItem + property alias footerItem: listView.footerItem + property alias section: listView.section + property alias highlightFollowsCurrentItem: listView.highlightFollowsCurrentItem + property alias highlightMoveDuration: listView.highlightMoveDuration + property alias highlightMoveVelocity: listView.highlightMoveVelocity + property alias preferredHighlightBegin: listView.preferredHighlightBegin + property alias preferredHighlightEnd: listView.preferredHighlightEnd + property alias highlightRangeMode: listView.highlightRangeMode + property alias snapMode: listView.snapMode + property alias keyNavigationWraps: listView.keyNavigationWraps + property alias cacheBuffer: listView.cacheBuffer + property alias displayMarginBeginning: listView.displayMarginBeginning + property alias displayMarginEnd: listView.displayMarginEnd + property alias layoutDirection: listView.layoutDirection + property alias effectiveLayoutDirection: listView.effectiveLayoutDirection + property alias verticalLayoutDirection: listView.verticalLayoutDirection + property alias boundsBehavior: listView.boundsBehavior + property alias flickableDirection: listView.flickableDirection + property alias interactive: listView.interactive + property alias moving: listView.moving + property alias flicking: listView.flicking + property alias dragging: listView.dragging + property alias horizontalVelocity: listView.horizontalVelocity + property alias verticalVelocity: listView.verticalVelocity + + // Scroll speed multiplier for mouse wheel (1.0 = default, higher = faster) + property real wheelScrollMultiplier: 2.0 + property int smoothWheelAnimationDuration: Style.animationNormal + property real _wheelTargetY: 0 + + function clampScrollY(value) { + return Math.max(0, Math.min(value, listView.contentHeight - listView.height)); + } + + function applyWheelScroll(delta) { + if (!root.contentOverflows) + return; + + const step = delta * root.wheelScrollMultiplier; + + if (!Settings.data.general.smoothScrollEnabled || Settings.data.general.animationDisabled) { + listView.contentY = root.clampScrollY(listView.contentY - step); + root._wheelTargetY = listView.contentY; + return; + } + + if (!wheelScrollAnimation.running) + root._wheelTargetY = listView.contentY; + + root._wheelTargetY = root.clampScrollY(root._wheelTargetY - step); + wheelScrollAnimation.to = root._wheelTargetY; + wheelScrollAnimation.restart(); + } + + function animateToContentY(targetY) { + const clampedY = root.clampScrollY(targetY); + + if (!Settings.data.general.smoothScrollEnabled || Settings.data.general.animationDisabled || listView.dragging || listView.flicking) { + listView.contentY = clampedY; + root._wheelTargetY = clampedY; + return; + } + + root._wheelTargetY = clampedY; + wheelScrollAnimation.to = clampedY; + wheelScrollAnimation.restart(); + } + + // Forward ListView methods + function positionViewAtIndex(index, mode) { + const shouldAnimate = mode === ListView.Contain; + if (!shouldAnimate) { + listView.positionViewAtIndex(index, mode); + root._wheelTargetY = listView.contentY; + return; + } + + const previousY = listView.contentY; + listView.positionViewAtIndex(index, mode); + const targetY = root.clampScrollY(listView.contentY); + + if (Math.abs(targetY - previousY) < 0.5) { + root._wheelTargetY = targetY; + return; + } + + listView.contentY = previousY; + root._wheelTargetY = previousY; + root.animateToContentY(targetY); + } + + function positionViewAtBeginning() { + listView.positionViewAtBeginning(); + } + + function positionViewAtEnd() { + listView.positionViewAtEnd(); + } + + function forceLayout() { + listView.forceLayout(); + } + + function cancelFlick() { + listView.cancelFlick(); + } + + function flick(xVelocity, yVelocity) { + listView.flick(xVelocity, yVelocity); + } + + function incrementCurrentIndex() { + listView.incrementCurrentIndex(); + } + + function decrementCurrentIndex() { + listView.decrementCurrentIndex(); + } + + function indexAt(x, y) { + return listView.indexAt(x, y); + } + + function itemAt(x, y) { + return listView.itemAt(x, y); + } + + function itemAtIndex(index) { + return listView.itemAtIndex(index); + } + + // Set reasonable implicit sizes for Layout usage + implicitWidth: 200 + implicitHeight: 200 + + Component.onCompleted: { + _wheelTargetY = listView.contentY; + createGradients(); + } + + // Dynamically create gradient overlays + function createGradients() { + if (!showGradientMasks) + return; + + Qt.createQmlObject(` + import QtQuick + import qs.Commons + Rectangle { + x: 0 + y: 0 + width: root.availableWidth + height: root.gradientHeight + z: 1 + visible: root.showGradientMasks && root.contentOverflows + opacity: { + if (listView.contentY <= 1) return 0; + if (listView.currentItem && listView.currentItem.y - listView.contentY < root.gradientHeight) return 0; + return 1; + } + Behavior on opacity { + NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad } + } + gradient: Gradient { + GradientStop { position: 0.0; color: root.gradientColor } + GradientStop { position: 1.0; color: "transparent" } + } + } + `, root, "topGradient"); + + Qt.createQmlObject(` + import QtQuick + import qs.Commons + Rectangle { + x: 0 + anchors.bottom: parent.bottom + anchors.bottomMargin: -1 + width: root.availableWidth + height: root.gradientHeight + 1 + z: 1 + visible: root.showGradientMasks && root.contentOverflows + opacity: { + if (listView.contentY + listView.height >= listView.contentHeight - 1) return 0; + if (listView.currentItem && listView.currentItem.y + listView.currentItem.height > listView.contentY + listView.height - root.gradientHeight) return 0; + return 1; + } + Behavior on opacity { + NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad } + } + gradient: Gradient { + GradientStop { position: 0.0; color: "transparent" } + GradientStop { position: 1.0; color: root.gradientColor } + } + } + `, root, "bottomGradient"); + } + + ListView { + id: listView + anchors.fill: parent + anchors.rightMargin: root.reserveScrollbarSpace ? root.handleWidth + Style.marginXS : 0 + + clip: true + boundsBehavior: Flickable.StopAtBounds + + NumberAnimation { + id: wheelScrollAnimation + target: listView + property: "contentY" + duration: root.smoothWheelAnimationDuration + easing.type: Easing.OutCubic + } + + onDraggingChanged: { + if (dragging) { + wheelScrollAnimation.stop(); + root._wheelTargetY = contentY; + } + } + + onFlickingChanged: { + if (flicking) { + wheelScrollAnimation.stop(); + root._wheelTargetY = contentY; + } + } + + onContentHeightChanged: root._wheelTargetY = root.clampScrollY(root._wheelTargetY) + onHeightChanged: root._wheelTargetY = root.clampScrollY(root._wheelTargetY) + + WheelHandler { + enabled: !root.contentOverflows + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onWheel: event => { + event.accepted = true; + } + } + + WheelHandler { + enabled: root.wheelScrollMultiplier !== 1.0 && root.contentOverflows + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onWheel: event => { + const delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y : event.angleDelta.y / 2; + root.applyWheelScroll(delta); + event.accepted = true; + } + } + + ScrollBar.vertical: ScrollBar { + parent: root + x: root.mirrored ? 0 : root.width - width + y: 0 + height: root.height + policy: root.verticalPolicy + visible: policy === ScrollBar.AlwaysOn || root.verticalScrollBarActive + + contentItem: Rectangle { + implicitWidth: root.handleWidth + implicitHeight: 100 + radius: root.handleRadius + color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor + opacity: parent.policy === ScrollBar.AlwaysOn ? 1.0 : root.verticalScrollBarActive ? ((root.showScrollbarWhenScrollable || parent.active) ? 1.0 : 0.0) : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + background: Rectangle { + implicitWidth: root.handleWidth + implicitHeight: 100 + color: root.trackColor + opacity: parent.policy === ScrollBar.AlwaysOn ? 0.3 : root.verticalScrollBarActive ? ((root.showScrollbarWhenScrollable || parent.active) ? 0.3 : 0.0) : 0.0 + radius: root.handleRadius / 2 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NPluginSettingsPopup.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NPluginSettingsPopup.qml new file mode 100644 index 0000000..5088681 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NPluginSettingsPopup.qml @@ -0,0 +1,152 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.Noctalia +import qs.Services.UI +import qs.Widgets + +Popup { + id: root + modal: true + dim: false + anchors.centerIn: parent + property var screen: null + readonly property real maxHeight: (screen ? screen.height : (parent ? parent.height : 800)) * 0.8 + + property real _minWidth: 600 * Style.uiScaleRatio + width: _minWidth + height: Math.min(settingsContent.implicitHeight + padding * 2, maxHeight) + padding: Style.marginXL + + property var currentPlugin: null + property var currentPluginApi: null + property bool showToastOnSave: false + + background: Rectangle { + color: Color.mSurface + radius: Style.radiusL + border.color: Color.mPrimary + border.width: Style.borderM + } + + contentItem: FocusScope { + focus: true + + ColumnLayout { + id: settingsContent + anchors.fill: parent + spacing: Style.marginM + + // Header + RowLayout { + Layout.fillWidth: true + + NText { + text: I18n.tr("panels.plugins.plugin-settings-title", { + "plugin": root.currentPlugin?.name || "" + }) + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mPrimary + Layout.fillWidth: true + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("common.close") + onClicked: root.close() + } + } + + // Separator + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: Color.mOutline + } + + // Settings loader - pluginApi is passed via setSource() in openPluginSettings() + NScrollView { + id: settingsScrollView + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 100 + horizontalPolicy: ScrollBar.AlwaysOff + gradientColor: Color.mSurface + + Loader { + id: settingsLoader + width: settingsScrollView.availableWidth + } + } + + // Action buttons + RowLayout { + Layout.fillWidth: true + Layout.topMargin: Style.marginM + spacing: Style.marginM + + Item { + Layout.fillWidth: true + } + + NButton { + text: I18n.tr("common.close") + outlined: true + onClicked: root.close() + } + + NButton { + text: I18n.tr("common.apply") + icon: "check" + onClicked: { + if (settingsLoader.item && settingsLoader.item.saveSettings) { + settingsLoader.item.saveSettings(); + if (root.showToastOnSave) { + ToastService.showNotice(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.settings-saved")); + } + } + } + } + } + } + } + + onClosed: { + // Clear both source and sourceComponent to ensure full cleanup + settingsLoader.sourceComponent = null; + settingsLoader.source = ""; + currentPlugin = null; + currentPluginApi = null; + } + + function openPluginSettings(pluginManifest, settingsEntryPoint) { + currentPlugin = pluginManifest; + + // Use composite key if available (for custom plugins), otherwise use manifest ID (for official plugins) + var pluginId = pluginManifest.compositeKey || pluginManifest.id; + + currentPluginApi = PluginService.getPluginAPI(pluginId); + if (!currentPluginApi) { + Logger.e("NPluginSettingsPopup", "Cannot open settings: plugin not loaded:", pluginId); + if (showToastOnSave) { + ToastService.showError(I18n.tr("panels.plugins.title"), I18n.tr("panels.plugins.settings-error-not-loaded")); + } + return; + } + + // Get plugin directory + var pluginDir = PluginRegistry.getPluginDir(pluginId); + var settingsEntry = settingsEntryPoint ? pluginManifest.entryPoints[settingsEntryPoint] : pluginManifest.entryPoints.settings; + var settingsPath = pluginDir + "/" + settingsEntry; + + settingsLoader.setSource("file://" + settingsPath, { + "pluginApi": currentPluginApi + }); + + var preferred = (settingsLoader.item && settingsLoader.item.preferredWidth !== undefined) ? settingsLoader.item.preferredWidth + padding * 2 : 0; + width = Math.max(preferred, _minWidth); + open(); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NPopupContextMenu.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NPopupContextMenu.qml new file mode 100644 index 0000000..cb1cde8 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NPopupContextMenu.qml @@ -0,0 +1,372 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import qs.Commons + +// Simple context menu PopupWindow (similar to TrayMenu) +// Designed to be rendered inside a PopupMenuWindow for click-outside-to-close +// Automatically positions itself to respect screen boundaries +PopupWindow { + id: root + + property alias model: repeater.model + property real itemHeight: 28 // Match TrayMenu + property real itemPadding: Style.marginM + property int verticalPolicy: ScrollBar.AsNeeded + property int horizontalPolicy: ScrollBar.AsNeeded + + property var anchorItem: null + property ShellScreen screen: null + property real minWidth: 120 + property real calculatedWidth: 180 + + // Explicit offset for centering on target item (computed from targetItem in openAtItem) + property real targetOffsetX: 0 + property real targetOffsetY: 0 + property real targetWidth: 0 + property real targetHeight: 0 + + readonly property string barPosition: Settings.getBarPositionForScreen(screen?.name) + readonly property real barHeight: Style.getBarHeightForScreen(screen?.name) + + signal triggered(string action, var item) + + implicitWidth: calculatedWidth + implicitHeight: Math.min(600, flickable.contentHeight + Style.margin2S) + visible: false + color: "transparent" + + NText { + id: textMeasure + visible: false + pointSize: Style.fontSizeS + wrapMode: Text.NoWrap + elide: Text.ElideNone + width: undefined + } + + NIcon { + id: iconMeasure + visible: false + icon: "bell" + pointSize: Style.fontSizeS + applyUiScale: false + } + + onModelChanged: { + Qt.callLater(calculateWidth); + } + + function calculateWidth() { + let maxWidth = 0; + if (model && model.length) { + for (let i = 0; i < model.length; i++) { + const item = model[i]; + if (item && item.visible !== false) { + const label = item.label || item.text || ""; + textMeasure.text = label; + textMeasure.forceLayout(); + + let itemWidth = textMeasure.contentWidth + 8; + + if (item.icon !== undefined) { + itemWidth += iconMeasure.width + Style.marginS; + } + + itemWidth += Style.margin2M; + + if (itemWidth > maxWidth) { + maxWidth = itemWidth; + } + } + } + } + calculatedWidth = Math.max(maxWidth + Style.margin2S, minWidth); + } + + anchor.item: anchorItem + + anchor.rect.x: { + if (anchorItem && screen) { + const anchorGlobalPos = anchorItem.mapToItem(null, 0, 0); + + // Use stored targetOffsetX and targetWidth for positioning + const effectiveWidth = targetWidth > 0 ? targetWidth : anchorItem.width; + const targetGlobalX = anchorGlobalPos.x + targetOffsetX; + + // For right bar: position menu to the left of target + if (root.barPosition === "right") { + let baseX = targetOffsetX - implicitWidth - Style.marginM; + return baseX; + } + + // For left bar: position menu to the right of target + if (root.barPosition === "left") { + let baseX = targetOffsetX + effectiveWidth + Style.marginM; + return baseX; + } + + // For top/bottom bar: center horizontally on target + const targetCenterScreenX = targetGlobalX + (effectiveWidth / 2); + const menuScreenX = targetCenterScreenX - (implicitWidth / 2); + let baseX = menuScreenX - anchorGlobalPos.x; + + const menuRight = menuScreenX + implicitWidth; + + // Adjust if menu would clip on the right + if (menuRight > screen.width - Style.marginM) { + const overflow = menuRight - (screen.width - Style.marginM); + return baseX - overflow; + } + // Adjust if menu would clip on the left + if (menuScreenX < Style.marginM) { + return baseX + (Style.marginM - menuScreenX); + } + return baseX; + } + return 0; + } + anchor.rect.y: { + if (anchorItem && screen) { + // Check if using absolute positioning (small anchor point item) + const isAbsolutePosition = anchorItem.width <= 1 && anchorItem.height <= 1; + + if (isAbsolutePosition) { + // For absolute positioning, show menu directly at anchor Y + // Only adjust if menu would clip at bottom + const anchorGlobalPos = anchorItem.mapToItem(null, 0, 0); + const menuBottom = anchorGlobalPos.y + implicitHeight; + + if (menuBottom > screen.height - Style.marginM) { + // Position above the click point instead + return -implicitHeight; + } + return 0; + } + + const anchorGlobalPos = anchorItem.mapToItem(null, 0, 0); + + // Use target offset/height for vertical bars, anchor dimensions otherwise + const effectiveHeight = targetHeight > 0 ? targetHeight : anchorItem.height; + const effectiveOffsetY = targetOffsetY; + + // Calculate base Y position based on bar orientation + let baseY; + if (root.barPosition === "bottom") { + // For bottom bar: position menu above the bar + baseY = -(implicitHeight + Style.marginS); + } else if (root.barPosition === "top") { + // For top bar: position menu below bar at consistent height + // Compensate for anchor's Y position to ensure menu top is always at (barHeight + margin) + baseY = barHeight + Style.marginS - anchorGlobalPos.y; + } else { + // For left/right bar: vertically center on target item + const targetCenterY = effectiveOffsetY + (effectiveHeight / 2); + baseY = targetCenterY - (implicitHeight / 2); + } + + const menuScreenY = anchorGlobalPos.y + baseY; + const menuBottom = menuScreenY + implicitHeight; + + // Define clipping boundaries based on bar position + const topLimit = Style.marginM; + const bottomLimit = root.barPosition === "bottom" ? screen.height - barHeight - Style.marginS : screen.height - Style.marginM; + + // Adjust if menu would clip at top (skip for bottom bar - don't push menu down over bar) + if (menuScreenY < topLimit && root.barPosition !== "bottom") { + const adjustment = topLimit - menuScreenY; + return baseY + adjustment; + } + + // Adjust if menu would clip at bottom (or overlap bar for bottom bar) + if (menuBottom > bottomLimit) { + const overflow = menuBottom - bottomLimit; + return baseY - overflow; + } + + return baseY; + } + + // Fallback if no screen + if (root.barPosition === "bottom") { + return -implicitHeight - Style.marginS; + } + return barHeight; + } + + Component.onCompleted: { + Qt.callLater(calculateWidth); + } + + Item { + anchors.fill: parent + focus: true + Keys.onEscapePressed: root.close() + } + + Rectangle { + id: menuBackground + anchors.fill: parent + color: Color.mSurface + border.color: Color.mOutline + border.width: Style.borderS + radius: Style.radiusM + opacity: root.visible ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + } + + Flickable { + id: flickable + anchors.fill: parent + anchors.margins: Style.marginS + contentHeight: columnLayout.implicitHeight + interactive: true + opacity: root.visible ? 1.0 : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + + ColumnLayout { + id: columnLayout + width: flickable.width + spacing: 0 + + Repeater { + id: repeater + + delegate: Rectangle { + id: menuItem + required property var modelData + required property int index + + Layout.preferredWidth: parent.width + Layout.preferredHeight: modelData.visible !== false ? root.itemHeight : 0 + visible: modelData.visible !== false + color: "transparent" + + Rectangle { + id: innerRect + anchors.fill: parent + color: mouseArea.containsMouse ? Color.mHover : "transparent" + radius: Style.radiusS + opacity: modelData.enabled !== false ? 1.0 : 0.5 + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Style.marginM + anchors.rightMargin: Style.marginM + spacing: Style.marginS + + NIcon { + visible: modelData.icon !== undefined + icon: modelData.icon || "" + pointSize: Style.fontSizeS + applyUiScale: false + color: mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + NText { + text: modelData.label || modelData.text || "" + pointSize: Style.fontSizeS + color: mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + Layout.fillWidth: true + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + enabled: (modelData.enabled !== false) && root.visible + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (menuItem.modelData.enabled !== false) { + root.triggered(menuItem.modelData.action || menuItem.modelData.key || menuItem.index.toString(), menuItem.modelData); + // Don't call root.close() here - let the parent PopupMenuWindow handle closing + } + } + } + } + } + } + } + } + + // Helper function to open context menu anchored to an item + // Position is calculated automatically based on bar position and screen boundaries + // Optional centerOnItem: if provided, menu will be horizontally centered on this item instead of anchorItem + function openAtItem(item, itemScreen, centerOnItem) { + if (!item) { + Logger.w("NPopupContextMenu", "anchorItem is undefined, won't show menu."); + return; + } + + // Set anchor and screen first + anchorItem = item; + screen = itemScreen || null; + + // Compute target offset and dimensions from centerOnItem + if (centerOnItem && centerOnItem !== item) { + const relPos = centerOnItem.mapToItem(item, 0, 0); + targetOffsetX = relPos.x; + targetOffsetY = relPos.y; + targetWidth = centerOnItem.width; + targetHeight = centerOnItem.height; + } else { + targetOffsetX = 0; + targetOffsetY = 0; + targetWidth = 0; + targetHeight = 0; + } + + // Calculate menu width after anchor is set + calculateWidth(); + + visible = true; + + // Force anchor recalculation after showing + Qt.callLater(() => { + anchor.updateAnchor(); + }); + } + + function close() { + visible = false; + } + + function closeMenu() { + close(); + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NRadioButton.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NRadioButton.qml new file mode 100644 index 0000000..326955d --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NRadioButton.qml @@ -0,0 +1,53 @@ +import QtQuick +import QtQuick.Controls +import qs.Commons +import qs.Widgets + +RadioButton { + id: root + + property real pointSize: Style.fontSizeM + + implicitWidth: outerCircle.implicitWidth + Style.marginS + contentItem.implicitWidth + + indicator: Rectangle { + id: outerCircle + + implicitWidth: Style.baseWidgetSize * 0.625 * pointSize / Style.fontSizeM + implicitHeight: Style.baseWidgetSize * 0.625 * pointSize / Style.fontSizeM + radius: Math.min(Style.iRadiusL, width / 2) + color: "transparent" + border.color: root.checked ? Color.mPrimary : Color.mOnSurface + border.width: Style.borderM + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + anchors.fill: parent + anchors.margins: parent.width * 0.3 + + radius: Math.min(Style.iRadiusL, width / 2) + color: Qt.alpha(Color.mPrimary, root.checked ? 1 : 0) + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + contentItem: NText { + text: root.text + pointSize: root.pointSize + anchors.verticalCenter: parent.verticalCenter + anchors.left: outerCircle.right + anchors.right: parent.right + anchors.leftMargin: Style.marginS + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NReorderCheckboxes.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NReorderCheckboxes.qml new file mode 100644 index 0000000..494015e --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NReorderCheckboxes.qml @@ -0,0 +1,310 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons + +Item { + id: root + + // Public API + property var model: [] + property var disabledIds: [] + property color activeColor: Color.mPrimary + property color activeOnColor: Color.mOnPrimary + property color dragHandleColor: Color.mOutline + property int baseSize: Style.baseWidgetSize * 0.7 + property int spacing: Style.marginM + + signal itemToggled(int index, bool enabled) + signal itemsReordered(int fromIndex, int toIndex) + signal dragPotentialStarted + signal dragPotentialEnded + + readonly property real itemHeight: root.baseSize + readonly property real contentHeight: root.model.length > 0 ? root.model.length * itemHeight + (root.model.length - 1) * root.spacing : 0 + implicitHeight: contentHeight + + function toggleItem(index) { + if (index < 0 || index >= root.model.length) + return; + var item = root.model[index]; + if (item.required) + return; + + // Create a new array to trigger binding update + var newModel = root.model.slice(); + newModel[index] = Object.assign({}, item, { + "enabled": !item.enabled + }); + root.model = newModel; + + root.itemToggled(index, newModel[index].enabled); + } + + function moveItem(fromIndex, toIndex) { + if (fromIndex === toIndex) + return; + if (fromIndex < 0 || fromIndex >= root.model.length) + return; + if (toIndex < 0 || toIndex >= root.model.length) + return; + + // Create a new array with item moved + var newModel = root.model.slice(); + var item = newModel.splice(fromIndex, 1)[0]; + newModel.splice(toIndex, 0, item); + root.model = newModel; + + root.itemsReordered(fromIndex, toIndex); + } + + Item { + id: itemsContainer + anchors.fill: parent + clip: true + + Repeater { + id: repeater + model: root.model + + delegate: Item { + id: delegateItem + + width: itemsContainer.width + height: checkboxRow.height + + required property int index + required property var modelData + + property string text: modelData.text || "" + property bool itemEnabled: modelData.enabled || false + property bool required: modelData.required || false + readonly property bool isDisabled: (root.disabledIds || []).indexOf(modelData.id) !== -1 + readonly property bool canDrag: !delegateItem.isDisabled + property bool dragging: false + property int dragStartY: 0 + property int dragStartIndex: -1 + property int dragTargetIndex: -1 + property int itemSpacing: root.spacing + + RowLayout { + id: checkboxRow + + width: parent.width + spacing: Style.marginS + opacity: isDisabled ? 0.5 : 1.0 + + // Drag handle + Rectangle { + id: dragHandle + + Layout.preferredWidth: root.baseSize + Layout.preferredHeight: root.baseSize + radius: Style.iRadiusXS + color: dragHandleMouseArea.containsMouse ? Color.mSurfaceVariant : "transparent" + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + ColumnLayout { + anchors.centerIn: parent + spacing: Style.marginS + + Repeater { + model: 3 + Rectangle { + Layout.preferredWidth: root.baseSize * 0.4 + Layout.preferredHeight: 2 + radius: 1 + color: root.dragHandleColor + } + } + } + + MouseArea { + id: dragHandleMouseArea + + anchors.fill: parent + cursorShape: delegateItem.canDrag ? Qt.SizeVerCursor : Qt.ArrowCursor + hoverEnabled: true + preventStealing: false + enabled: delegateItem.canDrag + z: 1000 + + onPressed: mouse => { + if (!delegateItem.canDrag) { + return; + } + delegateItem.dragStartIndex = delegateItem.index; + delegateItem.dragTargetIndex = delegateItem.index; + delegateItem.dragStartY = delegateItem.y; + delegateItem.dragging = true; + delegateItem.z = 999; + + // Signal that interaction started (prevents panel close) + preventStealing = true; + root.dragPotentialStarted(); + } + + onPositionChanged: mouse => { + if (delegateItem.dragging) { + var dy = mouse.y - dragHandle.height / 2; + var newY = delegateItem.y + dy; + + // Constrain within bounds + newY = Math.max(0, Math.min(newY, root.contentHeight - delegateItem.height)); + delegateItem.y = newY; + + // Calculate target index (but don't apply yet) + var targetIndex = Math.floor((newY + delegateItem.height / 2) / (delegateItem.height + delegateItem.itemSpacing)); + targetIndex = Math.max(0, Math.min(targetIndex, repeater.count - 1)); + + delegateItem.dragTargetIndex = targetIndex; + } + } + + onReleased: { + // Always signal end of interaction + preventStealing = false; + root.dragPotentialEnded(); + + // Apply the model change now that drag is complete + if (delegateItem.dragStartIndex !== -1 && delegateItem.dragTargetIndex !== -1 && delegateItem.dragStartIndex !== delegateItem.dragTargetIndex) { + root.moveItem(delegateItem.dragStartIndex, delegateItem.dragTargetIndex); + } + + delegateItem.dragging = false; + delegateItem.dragStartIndex = -1; + delegateItem.dragTargetIndex = -1; + delegateItem.z = 0; + } + + onCanceled: { + // Handle cancel (e.g., ESC key pressed during drag) + preventStealing = false; + root.dragPotentialEnded(); + + delegateItem.dragging = false; + delegateItem.dragStartIndex = -1; + delegateItem.dragTargetIndex = -1; + delegateItem.z = 0; + } + } + } + + // Checkbox + Rectangle { + id: box + + Layout.preferredWidth: root.baseSize + Layout.preferredHeight: root.baseSize + radius: Style.iRadiusXS + color: delegateItem.itemEnabled ? root.activeColor : Color.mSurface + border.color: delegateItem.required ? root.activeColor : Color.mOutline + border.width: Style.borderS + opacity: delegateItem.required ? 0.7 : 1.0 + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + NIcon { + visible: delegateItem.itemEnabled + anchors.centerIn: parent + anchors.horizontalCenterOffset: -1 + icon: "check" + color: root.activeOnColor + pointSize: Math.max(Style.fontSizeXS, root.baseSize * 0.5) + } + + MouseArea { + anchors.fill: parent + cursorShape: (!delegateItem.required && !delegateItem.isDisabled) ? Qt.PointingHandCursor : Qt.ArrowCursor + enabled: !delegateItem.required && !delegateItem.isDisabled + + onClicked: { + if (!delegateItem.required && !delegateItem.isDisabled) { + root.toggleItem(delegateItem.index); + } + } + } + } + + // Label + NText { + Layout.fillWidth: true + text: delegateItem.text + color: Color.mOnSurface + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + // Required indicator + NText { + visible: delegateItem.required + text: I18n.tr("common.required") + color: Color.mOnSurfaceVariant + verticalAlignment: Text.AlignVCenter + } + } + + // Position binding for non-dragging state + y: { + if (delegateItem.dragging) { + return delegateItem.y; + } + + // Check if any item is being dragged + var draggedIndex = -1; + var targetIndex = -1; + for (var i = 0; i < repeater.count; i++) { + var item = repeater.itemAt(i); + if (item && item.dragging) { + draggedIndex = item.dragStartIndex; + targetIndex = item.dragTargetIndex; + break; + } + } + + // If an item is being dragged, adjust positions + if (draggedIndex !== -1 && targetIndex !== -1 && draggedIndex !== targetIndex) { + var currentIndex = delegateItem.index; + + if (draggedIndex < targetIndex) { + // Dragging down: shift items up between draggedIndex and targetIndex + if (currentIndex > draggedIndex && currentIndex <= targetIndex) { + return (currentIndex - 1) * (delegateItem.height + delegateItem.itemSpacing); + } + } else { + // Dragging up: shift items down between targetIndex and draggedIndex + if (currentIndex >= targetIndex && currentIndex < draggedIndex) { + return (currentIndex + 1) * (delegateItem.height + delegateItem.itemSpacing); + } + } + } + + return delegateItem.index * (delegateItem.height + delegateItem.itemSpacing); + } + + Behavior on y { + enabled: !delegateItem.dragging + NumberAnimation { + duration: Style.animationNormal + easing.type: Easing.OutQuad + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NScrollText.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NScrollText.qml new file mode 100644 index 0000000..0360ea7 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NScrollText.qml @@ -0,0 +1,218 @@ +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import qs.Commons + +/* +NScrollText { +NText { +pointSize: Style.fontSizeS +// here any NText properties can be used +} +maxWidth: 200 +text: "Some long long long text" +scrollMode: NScrollText.ScrollMode.Always +} +*/ + +Item { + id: root + + required property string text + default property Component delegate: NText { + pointSize: Style.fontSizeS + } + + property real maxWidth: Infinity + + enum ScrollMode { + Never = 0, + Always = 1, + Hover = 2 + } + + property int scrollMode: NScrollText.ScrollMode.Never + property bool alwaysMaxWidth: false + property bool forcedHover: false + property int cursorShape: Qt.ArrowCursor + + property real waitBeforeScrolling: 1000 + property real scrollCycleDuration: Math.max(4000, root.text.length * 120) + property real resettingDuration: 300 + + // Fade controls (fadeExtent: 0.0–0.5, fraction of width that fades) + property real fadeExtent: 0.1 + property real fadeCornerRadius: 0 + property bool fadeRoundLeftCorners: true + + readonly property real contentWidth: { + if (!titleText.item) + return 0; + const implicit = titleText.item.implicitWidth; + return implicit > 0 ? implicit : titleText.item.width; + } + readonly property real measuredWidth: scrollContainer.width + + implicitWidth: alwaysMaxWidth ? maxWidth : Math.min(maxWidth, contentWidth) + implicitHeight: titleText.height + + layer.enabled: contentWidth > maxWidth + layer.effect: MultiEffect { + maskEnabled: true + maskThresholdMin: 0.5 + maskSpreadAtMin: 1.0 + maskSource: fadeMask + } + + enum ScrollState { + None = 0, + Scrolling = 1, + Resetting = 2 + } + + property int state: NScrollText.ScrollState.None + + onTextChanged: { + if (titleText.item) + titleText.item.text = text; + if (loopingText.item) + loopingText.item.text = text; + + // reset state + resetState(); + } + onMaxWidthChanged: resetState() + onContentWidthChanged: root.updateState() + onForcedHoverChanged: updateState() + + function resetState() { + root.state = NScrollText.ScrollState.None; + scrollContainer.x = 0; + scrollTimer.restart(); + root.updateState(); + } + + Timer { + id: scrollTimer + interval: root.waitBeforeScrolling + onTriggered: { + root.state = NScrollText.ScrollState.Scrolling; + root.updateState(); + } + } + + MouseArea { + id: hoverArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + onEntered: root.updateState() + onExited: root.updateState() + cursorShape: root.cursorShape + } + + function ensureReset() { + if (state === NScrollText.ScrollState.Scrolling) + state = NScrollText.ScrollState.Resetting; + } + + function updateState() { + if (contentWidth <= root.maxWidth || scrollMode === NScrollText.ScrollMode.Never) { + state = NScrollText.ScrollState.None; + return; + } + if (scrollMode === NScrollText.ScrollMode.Always) { + if (hoverArea.containsMouse) { + ensureReset(); + } else { + scrollTimer.restart(); + } + } else if (scrollMode === NScrollText.ScrollMode.Hover) { + if (hoverArea.containsMouse || forcedHover) + state = NScrollText.ScrollState.Scrolling; + else + ensureReset(); + } + } + + RowLayout { + id: scrollContainer + height: parent.height + x: 0 + spacing: 50 + + Loader { + id: titleText + sourceComponent: root.delegate + Layout.fillHeight: true + onLoaded: { + this.item.text = root.text; + // Bind height to container to enable vertical centering of overly high text + this.item.height = Qt.binding(() => titleText.height); + } + } + + Loader { + id: loopingText + sourceComponent: root.delegate + Layout.fillHeight: true + visible: root.state !== NScrollText.ScrollState.None + onLoaded: { + this.item.text = root.text; + this.item.height = Qt.binding(() => loopingText.height); + } + } + + NumberAnimation on x { + running: root.state === NScrollText.ScrollState.Resetting + to: 0 + duration: root.resettingDuration + easing.type: Easing.OutQuad + onFinished: { + root.state = NScrollText.ScrollState.None; + root.updateState(); + } + } + + NumberAnimation on x { + running: root.state === NScrollText.ScrollState.Scrolling + to: -(titleText.width + scrollContainer.spacing) + duration: root.scrollCycleDuration + loops: Animation.Infinite + easing.type: Easing.Linear + } + } + + // Transparency Fade Rectangle + Rectangle { + id: fadeMask + width: root.width + height: root.height + topLeftRadius: fadeRoundLeftCorners ? fadeCornerRadius : 0 + bottomLeftRadius: fadeRoundLeftCorners ? fadeCornerRadius : 0 + topRightRadius: fadeCornerRadius + bottomRightRadius: fadeCornerRadius + gradient: Gradient { + GradientStop { + position: 0.0 + color: "transparent" + } + GradientStop { + position: fadeExtent + color: "white" + } + GradientStop { + position: 1 - fadeExtent + color: "white" + } + GradientStop { + position: 1.0 + color: "transparent" + } + orientation: Gradient.Horizontal + } + layer.enabled: true + layer.smooth: true + opacity: 0 + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NScrollView.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NScrollView.qml new file mode 100644 index 0000000..d0d1556 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NScrollView.qml @@ -0,0 +1,280 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Templates as T +import qs.Commons + +ScrollView { + id: root + + property color handleColor: Qt.alpha(Color.mHover, 0.8) + property color handleHoverColor: handleColor + property color handlePressedColor: handleColor + property color trackColor: "transparent" + property real handleWidth: Math.round(6 * Style.uiScaleRatio) + property real handleRadius: Style.iRadiusM + property int verticalPolicy: ScrollBar.AsNeeded + property int horizontalPolicy: ScrollBar.AsNeeded + property bool preventHorizontalScroll: horizontalPolicy === ScrollBar.AlwaysOff + property int boundsBehavior: Flickable.StopAtBounds + readonly property bool verticalScrollable: (contentItem.contentHeight > contentItem.height) || (verticalPolicy == ScrollBar.AlwaysOn) + readonly property bool horizontalScrollable: (contentItem.contentWidth > contentItem.width) || (horizontalPolicy == ScrollBar.AlwaysOn) + property bool showGradientMasks: true + property color gradientColor: Color.mSurfaceVariant + property int gradientHeight: 16 + property bool reserveScrollbarSpace: true + property real userRightPadding: 0 + // Keep scrollbars visible whenever overflow exists (without forcing visibility when not scrollable) + property bool showScrollbarWhenScrollable: Settings.data.ui.scrollbarAlwaysVisible + + // Scroll speed multiplier for mouse wheel (1.0 = default, higher = faster) + property real wheelScrollMultiplier: 2.0 + property int smoothWheelAnimationDuration: Style.animationNormal + property real _wheelTargetY: 0 + + function clampScrollY(value) { + if (!root._internalFlickable) + return 0; + const flickable = root._internalFlickable; + return Math.max(0, Math.min(value, flickable.contentHeight - flickable.height)); + } + + function applyWheelScroll(delta) { + if (!root._internalFlickable) + return; + + const flickable = root._internalFlickable; + const step = delta * root.wheelScrollMultiplier; + + if (!Settings.data.general.smoothScrollEnabled || Settings.data.general.animationDisabled) { + flickable.contentY = root.clampScrollY(flickable.contentY - step); + root._wheelTargetY = flickable.contentY; + return; + } + + if (!wheelScrollAnimation.running) + root._wheelTargetY = flickable.contentY; + + root._wheelTargetY = root.clampScrollY(root._wheelTargetY - step); + wheelScrollAnimation.to = root._wheelTargetY; + wheelScrollAnimation.restart(); + } + + rightPadding: userRightPadding + (reserveScrollbarSpace && verticalScrollable ? handleWidth + Style.marginXS : 0) + + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding) + implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding) + + // Configure the internal flickable when it becomes available + Component.onCompleted: { + configureFlickable(); + createGradients(); + } + + // Dynamically create gradient overlays to avoid interfering with ScrollView content management + function createGradients() { + if (!showGradientMasks) + return; + + Qt.createQmlObject(` + import QtQuick + import qs.Commons + Rectangle { + x: root.leftPadding + y: root.topPadding + width: root.availableWidth + height: root.gradientHeight + z: 1 + visible: root.showGradientMasks && root.verticalScrollable + opacity: root.contentItem.contentY <= 1 ? 0 : 1 + Behavior on opacity { + NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad } + } + gradient: Gradient { + GradientStop { position: 0.0; color: root.gradientColor } + GradientStop { position: 1.0; color: "transparent" } + } + } + `, root, "topGradient"); + + Qt.createQmlObject(` + import QtQuick + import qs.Commons + Rectangle { + x: root.leftPadding + y: root.height - root.bottomPadding - height + 1 + width: root.availableWidth + height: root.gradientHeight + 1 + z: 1 + visible: root.showGradientMasks && root.verticalScrollable + opacity: (root.contentItem.contentY + root.contentItem.height >= root.contentItem.contentHeight - 1) ? 0 : 1 + Behavior on opacity { + NumberAnimation { duration: Style.animationFast; easing.type: Easing.InOutQuad } + } + gradient: Gradient { + GradientStop { position: 0.0; color: "transparent" } + GradientStop { position: 1.0; color: root.gradientColor } + } + } + `, root, "bottomGradient"); + } + + // Reference to the internal Flickable for wheel handling + property Flickable _internalFlickable: null + + NumberAnimation { + id: wheelScrollAnimation + target: root._internalFlickable + property: "contentY" + duration: root.smoothWheelAnimationDuration + easing.type: Easing.OutCubic + } + + Connections { + target: root._internalFlickable + + function onDraggingChanged() { + if (!root._internalFlickable || !root._internalFlickable.dragging) + return; + wheelScrollAnimation.stop(); + root._wheelTargetY = root._internalFlickable.contentY; + } + + function onFlickingChanged() { + if (!root._internalFlickable || !root._internalFlickable.flicking) + return; + wheelScrollAnimation.stop(); + root._wheelTargetY = root._internalFlickable.contentY; + } + + function onContentHeightChanged() { + root._wheelTargetY = root.clampScrollY(root._wheelTargetY); + } + + function onHeightChanged() { + root._wheelTargetY = root.clampScrollY(root._wheelTargetY); + } + } + + // Function to configure the underlying Flickable + function configureFlickable() { + // Find the internal Flickable (it's usually the first child) + for (var i = 0; i < children.length; i++) { + var child = children[i]; + if (child.toString().indexOf("Flickable") !== -1) { + // Configure the flickable to prevent horizontal scrolling + child.boundsBehavior = root.boundsBehavior; + root._internalFlickable = child; + + if (root.preventHorizontalScroll) { + child.flickableDirection = Flickable.VerticalFlick; + child.contentWidth = Qt.binding(() => child.width); + } + + root._wheelTargetY = child.contentY; + break; + } + } + } + + WheelHandler { + enabled: root.wheelScrollMultiplier !== 1.0 && root._internalFlickable !== null + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + onWheel: event => { + const delta = event.pixelDelta.y !== 0 ? event.pixelDelta.y : event.angleDelta.y / 2; + root.applyWheelScroll(delta); + event.accepted = true; + } + } + + // Watch for changes in horizontalPolicy + onHorizontalPolicyChanged: { + preventHorizontalScroll = (horizontalPolicy === ScrollBar.AlwaysOff); + configureFlickable(); + } + + ScrollBar.vertical: ScrollBar { + parent: root + x: root.mirrored ? 0 : root.width - width + y: root.topPadding + height: root.availableHeight + policy: root.verticalPolicy + interactive: root.verticalScrollable + + contentItem: Rectangle { + implicitWidth: root.handleWidth + implicitHeight: 100 + radius: root.handleRadius + color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor + opacity: parent.policy === ScrollBar.AlwaysOn ? 1.0 : root.verticalScrollable ? ((root.showScrollbarWhenScrollable || parent.active) ? 1.0 : 0.0) : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + background: Rectangle { + implicitWidth: root.handleWidth + implicitHeight: 100 + color: root.trackColor + opacity: parent.policy === ScrollBar.AlwaysOn ? 0.3 : root.verticalScrollable ? ((root.showScrollbarWhenScrollable || parent.active) ? 0.3 : 0.0) : 0.0 + radius: root.handleRadius / 2 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + } + + ScrollBar.horizontal: ScrollBar { + parent: root + x: root.leftPadding + y: root.height - height + width: root.availableWidth + policy: root.horizontalPolicy + interactive: root.horizontalScrollable + + contentItem: Rectangle { + implicitWidth: 100 + implicitHeight: root.handleWidth + radius: root.handleRadius + color: parent.pressed ? root.handlePressedColor : parent.hovered ? root.handleHoverColor : root.handleColor + opacity: parent.policy === ScrollBar.AlwaysOn ? 1.0 : root.horizontalScrollable ? ((root.showScrollbarWhenScrollable || parent.active) ? 1.0 : 0.0) : 0.0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + background: Rectangle { + implicitWidth: 100 + implicitHeight: root.handleWidth + color: root.trackColor + opacity: parent.policy === ScrollBar.AlwaysOn ? 0.3 : root.horizontalScrollable ? ((root.showScrollbarWhenScrollable || parent.active) ? 0.3 : 0.0) : 0.0 + radius: root.handleRadius / 2 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NSearchableComboBox.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NSearchableComboBox.qml new file mode 100644 index 0000000..7e58930 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NSearchableComboBox.qml @@ -0,0 +1,446 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +RowLayout { + id: root + + property real minimumWidth: 280 + property real popupHeight: 180 + + property bool selectOnNavigation: true + property string label: "" + property string description: "" + property ListModel model: {} + property string currentKey: "" + property string placeholder: "" + property string searchPlaceholder: I18n.tr("placeholders.search") + property Component delegate: null + property var defaultValue: undefined + property string settingsPath: "" + + readonly property real preferredHeight: Math.round(Style.baseWidgetSize * 1.1) + + signal selected(string key) + + spacing: Style.marginL + Layout.fillWidth: true + + readonly property bool isValueChanged: (defaultValue !== undefined) && (currentKey !== defaultValue) + readonly property string indicatorTooltip: { + if (defaultValue === undefined) + return ""; + var displayValue = ""; + if (defaultValue === "") { + // Try to find the display name for empty key in the model + if (model && model.count > 0) { + for (var i = 0; i < model.count; i++) { + var item = model.get(i); + if (item && item.key === "") { + displayValue = item.name || I18n.tr("panels.indicator.system-default"); + break; + } + } + // If not found in model, show "System Default" instead of "(empty)" + if (displayValue === "") { + displayValue = I18n.tr("panels.indicator.system-default"); + } + } else { + displayValue = I18n.tr("panels.indicator.system-default"); + } + } else { + // Try to find the display name for the default key in the model + if (model && model.count > 0) { + for (var i = 0; i < model.count; i++) { + var item = model.get(i); + if (item && item.key === defaultValue) { + displayValue = item.name || String(defaultValue); + break; + } + } + if (displayValue === "") { + displayValue = String(defaultValue); + } + } else { + displayValue = String(defaultValue); + } + } + return I18n.tr("panels.indicator.default-value", { + "value": displayValue + }); + } + + // Filtered model for search results + property ListModel filteredModel: ListModel {} + property string searchText: "" + + function findIndexByKey(key) { + if (!root.model) + return -1; + for (var i = 0; i < root.model.count; i++) { + if (root.model.get(i).key === key) { + return i; + } + } + return -1; + } + + // The active model used for the popup list (source model or filtered results) + readonly property var activeModel: isFiltered ? filteredModel : root.model + + function findIndexInActiveModel(key) { + if (!activeModel || activeModel.count === undefined) + return -1; + for (var i = 0; i < activeModel.count; i++) { + if (activeModel.get(i).key === key) { + return i; + } + } + return -1; + } + + // Whether we're using filtered results or the source model directly + property bool isFiltered: false + + function filterModel() { + // Check if model exists and has items + if (!root.model || root.model.count === undefined || root.model.count === 0) { + filteredModel.clear(); + isFiltered = false; + return; + } + + var query = searchText.trim(); + if (query === "") { + // No search text - use source model directly, don't copy + filteredModel.clear(); + isFiltered = false; + return; + } + + // We have search text - need to filter + isFiltered = true; + filteredModel.clear(); + + // Convert ListModel to array for fuzzy search + var items = []; + for (var i = 0; i < root.model.count; i++) { + items.push(root.model.get(i)); + } + + // Use fuzzy search if available, fallback to simple search + if (typeof FuzzySort !== 'undefined') { + var fuzzyResults = FuzzySort.go(query, items, { + "key": "name", + "limit": 50 + }); + + // Add results in order of relevance + for (var j = 0; j < fuzzyResults.length; j++) { + filteredModel.append(fuzzyResults[j].obj); + } + } else { + // Fallback to simple search + var searchLower = query.toLowerCase(); + for (var i = 0; i < items.length; i++) { + var item = items[i]; + if (item.name.toLowerCase().includes(searchLower)) { + filteredModel.append(item); + } + } + } + } + + onSearchTextChanged: { + filterModel(); + listView.currentIndex = 0; + } + + NLabel { + label: root.label + description: root.description + showIndicator: root.isValueChanged + indicatorTooltip: root.indicatorTooltip + } + + Item { + Layout.fillWidth: true + } + + ComboBox { + id: combo + + opacity: enabled ? 1.0 : 0.6 + Layout.margins: Style.borderS + Layout.minimumWidth: Math.round(root.minimumWidth * Style.uiScaleRatio) + Layout.preferredHeight: Math.round(root.preferredHeight * Style.uiScaleRatio) + implicitWidth: Layout.minimumWidth + model: root.activeModel + textRole: "name" + currentIndex: findIndexInActiveModel(currentKey) + onActivated: { + if (combo.currentIndex >= 0 && root.activeModel && combo.currentIndex < root.activeModel.count) { + root.selected(root.activeModel.get(combo.currentIndex).key); + } + } + + background: Rectangle { + implicitWidth: Math.round(Style.baseWidgetSize * 3.75 * Style.uiScaleRatio) + implicitHeight: Math.round(root.preferredHeight * Style.uiScaleRatio) + color: Color.mSurface + border.color: combo.activeFocus ? Color.mSecondary : Color.mOutline + border.width: Style.borderS + radius: Style.iRadiusM + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + contentItem: NText { + leftPadding: Style.marginL + rightPadding: combo.indicator.width + Style.marginL + pointSize: Style.fontSizeM + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + + // Look up current selection directly in source model by key + readonly property int sourceIndex: root.findIndexByKey(root.currentKey) + readonly property bool hasSelection: root.model && sourceIndex >= 0 && sourceIndex < root.model.count + + color: hasSelection ? Color.mOnSurface : Color.mOnSurfaceVariant + text: hasSelection ? root.model.get(sourceIndex).name : root.placeholder + } + + indicator: NIcon { + x: combo.width - width - Style.marginM + y: combo.topPadding + (combo.availableHeight - height) / 2 + icon: "caret-down" + pointSize: Style.fontSizeL + } + + popup: Popup { + y: combo.height + Style.marginS + width: combo.width + height: Math.round((root.popupHeight + 60) * Style.uiScaleRatio) + padding: Style.marginM + + contentItem: ColumnLayout { + spacing: Style.marginS + + // Search input + NTextInput { + id: searchInput + inputIconName: "search" + Layout.fillWidth: true + placeholderText: root.searchPlaceholder + text: root.searchText + onTextChanged: root.searchText = text + fontSize: Style.fontSizeS + + Keys.onPressed: event => { + if (Keybinds.checkKey(event, 'enter', Settings)) { + selectHighlighted(); + combo.popup.close(); + event.accepted = true; + return; + } + + if (Keybinds.checkKey(event, 'escape', Settings)) { + combo.popup.close(); + event.accepted = true; + return; + } + + if (Keybinds.checkKey(event, 'up', Settings)) { + if (listView.currentIndex > 0) { + listView.currentIndex--; + if (root.selectOnNavigation) + selectHighlighted(); + } + event.accepted = true; + return; + } + + if (Keybinds.checkKey(event, 'down', Settings)) { + if (listView.currentIndex < listView.count - 1) { + listView.currentIndex++; + if (root.selectOnNavigation) + selectHighlighted(); + } + event.accepted = true; + return; + } + } + + function selectHighlighted() { + if (listView.currentIndex >= 0 && listView.model && listView.currentIndex < listView.model.count) { + var selectedKey = listView.model.get(listView.currentIndex).key; + root.selected(selectedKey); + } + } + } + + NListView { + id: listView + Layout.fillWidth: true + Layout.fillHeight: true + // Use activeModel (source model when not filtering, filtered results when searching) + model: combo.popup.visible ? root.activeModel : null + horizontalPolicy: ScrollBar.AlwaysOff + verticalPolicy: ScrollBar.AsNeeded + + onCurrentIndexChanged: { + if (currentIndex >= 0) + positionViewAtIndex(currentIndex, ListView.Contain); + } + + delegate: root.delegate ? root.delegate : defaultDelegate + + Component { + id: defaultDelegate + ItemDelegate { + id: delegateRoot + width: listView.availableWidth + leftPadding: Style.marginM + rightPadding: Style.marginM + topPadding: Style.marginS + bottomPadding: Style.marginS + hoverEnabled: true + highlighted: ListView.view.currentIndex === index + + onHoveredChanged: { + if (hovered) { + ListView.view.currentIndex = index; + } + } + + onClicked: { + var selectedKey = listView.model.get(index).key; + root.selected(selectedKey); + combo.popup.close(); + } + + contentItem: RowLayout { + width: delegateRoot.width - delegateRoot.leftPadding - delegateRoot.rightPadding + spacing: Style.marginM + + NText { + text: name + pointSize: Style.fontSizeM + color: highlighted ? Color.mOnHover : Color.mOnSurface + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + Layout.fillWidth: true + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + RowLayout { + spacing: Style.marginXXS + Layout.alignment: Qt.AlignRight + + // Generic badge renderer + Repeater { + model: { + if (typeof badges === 'undefined' || badges === null) + return 0; + // Handle both arrays and ListModels + if (typeof badges.length !== 'undefined') + return badges.length; + if (typeof badges.count !== 'undefined') + return badges.count; + return 0; + } + + delegate: NIcon { + required property int index + readonly property var badgeData: { + if (typeof badges === 'undefined' || badges === null) + return null; + // Handle both arrays and ListModels + if (typeof badges.length !== 'undefined') + return badges[index]; + if (typeof badges.get !== 'undefined') + return badges.get(index); + return null; + } + + icon: badgeData && badgeData.icon ? badgeData.icon : "" + pointSize: { + if (!badgeData || !badgeData.size) + return Style.fontSizeXS; + if (badgeData.size === "xsmall") + return Style.fontSizeXXS; + else if (badgeData.size === "medium") + return Style.fontSizeM; + else + return Style.fontSizeXS; + } + color: highlighted ? Color.mOnHover : (badgeData && badgeData.color ? badgeData.color : Color.mOnSurface) + Layout.preferredWidth: Math.round(Style.baseWidgetSize * 0.6) + Layout.preferredHeight: Math.round(Style.baseWidgetSize * 0.6) + visible: badgeData && badgeData.icon !== undefined && badgeData.icon !== "" + } + } + } + } + background: Rectangle { + anchors.fill: parent + color: highlighted ? Color.mHover : "transparent" + radius: Style.iRadiusS + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + } + } + } + } + + background: Rectangle { + color: Color.mSurfaceVariant + border.color: Color.mOutline + border.width: Style.borderS + radius: Style.iRadiusM + } + } + + // Update the currentIndex if the currentKey is changed externally + Connections { + target: root + function onCurrentKeyChanged() { + combo.currentIndex = root.findIndexInActiveModel(root.currentKey); + } + } + + // Focus search input when popup opens and ensure model is filtered + Connections { + target: combo.popup + function onVisibleChanged() { + if (combo.popup.visible) { + // Ensure the model is filtered when popup opens + filterModel(); + listView.currentIndex = Math.max(0, root.findIndexInActiveModel(root.currentKey)); + // Small delay to ensure the popup is fully rendered + Qt.callLater(() => { + if (searchInput && searchInput.inputItem) { + searchInput.inputItem.forceActiveFocus(); + } + }); + } else { + root.searchText = ""; + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NSectionEditor.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NSectionEditor.qml new file mode 100644 index 0000000..f204546 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NSectionEditor.qml @@ -0,0 +1,957 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import qs.Commons +import qs.Services.Noctalia +import qs.Widgets + +NBox { + id: root + + property string sectionName: "" + property string sectionSubtitle: "" + property string sectionId: "" + property var widgetModel: [] + property var availableWidgets: [] + property var availableSections: ["left", "center", "right"] + property var sectionLabels: ({}) // Map of sectionId -> display label + property var sectionIcons: ({}) // Map of sectionId -> icon name + property bool barIsVertical: false // When true, map left/right to top/bottom in labels + property int maxWidgets: -1 // -1 means unlimited + property bool draggable: true // Enable/disable drag reordering + property bool crossSectionDraggable: false + property alias dropTargetArea: gridContainer + + // Get display label for a section + function getSectionLabel(sectionId) { + if (sectionLabels && sectionLabels[sectionId]) { + return sectionLabels[sectionId]; + } + return sectionId; // Fallback to section ID + } + + // Get icon for a section + function getSectionIcon(sectionId) { + if (sectionIcons && sectionIcons[sectionId]) { + return sectionIcons[sectionId]; + } + return "arrow-right"; // Default fallback icon + } + + property var pluginSettingsEntryPoints: ["settings"] + property var widgetRegistry: null + property string settingsDialogComponent: "invalid-settings-dialog" + property var screen: null // Screen reference for per-screen widget settings + property var _activeDialog: null + property bool crossDropHoverActive: false + + function clearCrossSectionHover() { + crossDropHoverActive = false; + + var parentItem = root.parent; + if (!parentItem || !parentItem.children) { + return; + } + + for (var i = 0; i < parentItem.children.length; i++) { + var candidate = parentItem.children[i]; + if (!candidate || candidate.crossDropHoverActive === undefined) + continue; + candidate.crossDropHoverActive = false; + } + } + + function updateCrossSectionHover(globalX, globalY) { + var parentItem = root.parent; + if (!parentItem || !parentItem.children) { + crossDropHoverActive = false; + return; + } + + crossDropHoverActive = false; + + for (var i = 0; i < parentItem.children.length; i++) { + var candidate = parentItem.children[i]; + if (!candidate || candidate.sectionId === undefined || candidate.crossDropHoverActive === undefined) { + continue; + } + + var shouldHighlight = false; + if (candidate !== root && candidate.visible && candidate.enabled) { + var localPoint = candidate.mapFromGlobal(globalX, globalY); + var area = candidate.dropTargetArea ? candidate.dropTargetArea : candidate; + shouldHighlight = localPoint.x >= area.x && localPoint.y >= area.y && localPoint.x <= area.x + area.width && localPoint.y <= area.y + area.height; + } + + candidate.crossDropHoverActive = shouldHighlight; + } + } + + function isPointInsideSelf(globalX, globalY) { + if (globalX < 0 || globalY < 0) + return false; + var localPoint = root.mapFromGlobal(globalX, globalY); + return localPoint.x >= 0 && localPoint.y >= 0 && localPoint.x <= root.width && localPoint.y <= root.height; + } + + readonly property bool showCrossSectionDropHint: crossDropHoverActive + + Component.onDestruction: { + if (_activeDialog && _activeDialog.close) { + var dialog = _activeDialog; + _activeDialog = null; + dialog.close(); + dialog.destroy(); + } + } + + readonly property int gridColumns: 3 + readonly property real miniButtonSize: Style.baseWidgetSize * 0.65 + readonly property bool isAtMaxCapacity: maxWidgets >= 0 && widgetModel.length >= maxWidgets + readonly property real widgetItemHeight: Style.baseWidgetSize * 1.3 * Style.uiScaleRatio + + signal addWidget(string widgetId, string section) + signal removeWidget(string section, int index) + signal reorderWidget(string section, int fromIndex, int toIndex) + signal updateWidgetSettings(string section, int index, var settings) + signal moveWidget(string fromSection, int index, string toSection) + signal dragPotentialStarted + signal dragPotentialEnded + signal openPluginSettingsRequested(var pluginManifest, string settingsEntryPoint) + + color: Color.mSurface + Layout.fillWidth: true + z: flowDragArea.dragStarted ? 5000 : 0 + + // Calculate width to fit gridColumns widgets with spacing + function calculateWidgetWidth(gridWidth) { + var columnSpacing = (root.gridColumns - 1) * Style.marginM; + var widgetWidth = (gridWidth - columnSpacing) / root.gridColumns; + return Math.floor(widgetWidth); + } + + function findSectionAtGlobalPosition(globalX, globalY) { + var parentItem = root.parent; + if (!parentItem || !parentItem.children) { + return ""; + } + + for (var i = 0; i < parentItem.children.length; i++) { + var candidate = parentItem.children[i]; + if (!candidate || candidate === root) { + continue; + } + + // Only consider sibling section editors + if (candidate.sectionId === undefined || !candidate.visible || !candidate.enabled) { + continue; + } + + var localPoint = candidate.mapFromGlobal(globalX, globalY); + var area = candidate.dropTargetArea ? candidate.dropTargetArea : candidate; + if (localPoint.x >= area.x && localPoint.y >= area.y && localPoint.x <= area.x + area.width && localPoint.y <= area.y + area.height) { + return candidate.sectionId; + } + } + + return ""; + } + + Layout.minimumHeight: { + // header + minimal content area + var absoluteMin = Style.margin2L + (Style.fontSizeL * 2) + Style.marginM + (65 * Style.uiScaleRatio); + + var widgetCount = widgetModel.length; + if (widgetCount === 0) { + return absoluteMin; + } + + // Calculate rows based on grid layout + // Use actual parent width if available, otherwise estimate + var availableWidth = (parent && parent.width > 0) ? (parent.width - Style.margin2L) : 400; + var rows = Math.ceil(widgetCount / root.gridColumns); + + // Calculate widget width for height calculation + var containerWidth = availableWidth; + var widgetWidth = calculateWidgetWidth(containerWidth); + + // Header height + spacing + (rows * widget height) + (spacing between rows) + margins + var headerHeight = Style.fontSizeL * 2; + // Account for grid margins + var gridTopMargin = Style.marginS; + var gridBottomMargin = Style.marginL; + var widgetAreaHeight = gridTopMargin + (rows * widgetItemHeight) + ((rows - 1) * Style.marginL) + gridBottomMargin; + + return Math.max(absoluteMin, Style.margin2L + headerHeight + Style.marginM + widgetAreaHeight); + } + + // Generate widget color from name checksum + function getWidgetColor(widget) { + if (widget.id.startsWith('plugin:')) { + return [Color.mSecondary, Color.mOnSecondary]; + } + return [Color.mPrimary, Color.mOnPrimary]; + } + + // Check if widget has settings (either core widget with metadata or plugin with settings entry point) + function widgetHasSettings(widgetId) { + // Check if it's a plugin with settings + if (root.widgetRegistry && root.widgetRegistry.isPluginWidget(widgetId)) { + var pluginId = widgetId.replace("plugin:", ""); + var manifest = PluginRegistry.getPluginManifest(pluginId); + if (!manifest?.entryPoints) + return false; + for (var i = 0; i < root.pluginSettingsEntryPoints.length; i++) { + if (manifest.entryPoints[root.pluginSettingsEntryPoints[i]] !== undefined) + return true; + } + return false; + } + + // Check if it's a core widget with user settings + if (root.widgetRegistry && root.widgetRegistry.widgetHasUserSettings(widgetId)) { + return true; + } + + return false; + } + + // Open settings for a widget + function openWidgetSettings(index, widgetData) { + // Check if this is a plugin widget with a generic "settings" entry point + var isPlugin = root.widgetRegistry && root.widgetRegistry.isPluginWidget(widgetData.id); + + if (isPlugin) { + var pluginId = widgetData.id.replace("plugin:", ""); + var manifest = PluginRegistry.getPluginManifest(pluginId); + + var settingsKey = null; + if (manifest?.entryPoints) { + for (var i = 0; i < root.pluginSettingsEntryPoints.length; i++) { + if (manifest.entryPoints[root.pluginSettingsEntryPoints[i]] !== undefined) { + settingsKey = root.pluginSettingsEntryPoints[i]; + break; + } + } + } + if (!manifest || !settingsKey) { + Logger.e("NSectionEditor", "Plugin settings not found for:", pluginId); + return; + } + + // "desktopWidgetSettings" is handled by the settingsDialogComponent + // (DesktopWidgetSettingsDialog) which passes widgetSettings/save properly. + // Only generic "settings" goes through the plugin settings popup. + if (settingsKey !== "desktopWidgetSettings") { + root.openPluginSettingsRequested(manifest, settingsKey); + return; + } + } + + // Handle core widgets and plugin desktop widget settings + { + var component = Qt.createComponent(Qt.resolvedUrl(root.settingsDialogComponent)); + + function instantiateAndOpen() { + if (root._activeDialog) { + try { + root._activeDialog.close(); + root._activeDialog.destroy(); + } catch (e) {} + root._activeDialog = null; + } + + var dialog = component.createObject(Overlay.overlay, { + "widgetIndex": index, + "widgetData": widgetData, + "widgetId": widgetData.id, + "sectionId": root.sectionId, + "screen": root.screen + }); + + if (dialog) { + root._activeDialog = dialog; + dialog.updateWidgetSettings.connect(root.updateWidgetSettings); + dialog.closed.connect(() => { + if (root._activeDialog === dialog) { + root._activeDialog = null; + dialog.destroy(); + } + }); + dialog.open(); + } else { + Logger.e("NSectionEditor", "Failed to create settings dialog instance"); + } + } + + if (component.status === Component.Ready) { + instantiateAndOpen(); + } else if (component.status === Component.Error) { + Logger.e("NSectionEditor", component.errorString()); + } else { + component.statusChanged.connect(function () { + if (component.status === Component.Ready) { + instantiateAndOpen(); + } else if (component.status === Component.Error) { + Logger.e("NSectionEditor", component.errorString()); + } + }); + } + } + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL + spacing: Style.marginM + + RowLayout { + Layout.fillWidth: true + Layout.rightMargin: Style.marginS + + ColumnLayout { + spacing: Style.marginXXS + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: false + Layout.leftMargin: Style.marginS + Layout.maximumWidth: { + // Reserve space for other elements: count indicator, combo box (~200), button (~50), and margins + // Use a reasonable maximum that leaves room for controls on the right + // On smaller screens, use a smaller percentage to ensure controls fit + var rowLayout = parent; + if (rowLayout && rowLayout.width > 0) { + // Use smaller percentage on smaller screens, but ensure minimum space for text + var minWidth = 150 * Style.uiScaleRatio; + var maxWidth = rowLayout.width < 600 ? rowLayout.width * 0.35 : rowLayout.width * 0.4; + return Math.max(minWidth, maxWidth); + } + return 250 * Style.uiScaleRatio; + } + + NText { + text: sectionName + pointSize: Style.fontSizeL + font.weight: Style.fontWeightBold + color: Color.mOnSurface + elide: Text.ElideRight + Layout.fillWidth: true + } + + NText { + visible: sectionSubtitle !== "" + text: sectionSubtitle + pointSize: Style.fontSizeS + color: Color.mOnSurfaceVariant + elide: Text.ElideRight + Layout.fillWidth: true + } + } + + // Widget count indicator (when max is set) + NText { + visible: root.maxWidgets >= 0 + text: root.maxWidgets === 0 ? "(LOCKED)" : "(" + widgetModel.length + "/" + root.maxWidgets + ")" + pointSize: Style.fontSizeS + color: root.isAtMaxCapacity ? Color.mError : Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: Style.marginXS + } + + Item { + Layout.fillWidth: true + } + + NSearchableComboBox { + id: comboBox + model: availableWidgets ?? null + label: "" + description: "" + placeholder: I18n.tr("bar.section-editor.placeholder") + searchPlaceholder: I18n.tr("bar.section-editor.search-placeholder") + onSelected: key => comboBox.currentKey = key + popupHeight: 300 * Style.uiScaleRatio + minimumWidth: 200 * Style.uiScaleRatio + enabled: !root.isAtMaxCapacity + + Layout.alignment: Qt.AlignVCenter + + // Re-filter when the model count changes (when widgets are loaded) + Connections { + target: availableWidgets ?? null + ignoreUnknownSignals: true + function onCountChanged() { + // Trigger a re-filter by clearing and re-setting the search text + var currentSearch = comboBox.searchText; + comboBox.searchText = ""; + comboBox.searchText = currentSearch; + } + } + } + + NIconButton { + icon: "add" + colorBg: Color.mPrimary + colorFg: Color.mOnPrimary + colorBgHover: Color.mSecondary + colorFgHover: Color.mOnSecondary + enabled: comboBox.currentKey !== "" && !root.isAtMaxCapacity + tooltipText: root.isAtMaxCapacity ? I18n.tr("tooltips.max-widgets-reached") : I18n.tr("tooltips.add-widget") + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: Style.marginS + onClicked: { + if (comboBox.currentKey !== "" && !root.isAtMaxCapacity) { + addWidget(comboBox.currentKey, sectionId); + comboBox.currentKey = ""; + } + } + } + } + + // Drag and Drop Widget Area + Item { + id: gridContainer + Layout.fillWidth: true + Layout.preferredHeight: { + if (widgetModel.length === 0) { + return 65 * Style.uiScaleRatio; + } + // Use actual width, fallback to a reasonable default if not yet available + var containerWidth = width > 0 ? width : (parent ? parent.width : 400); + var rows = Math.ceil(widgetModel.length / root.gridColumns); + // Calculate height: (rows * item height) + (row spacing between items) + grid margins + var gridTopMargin = Style.marginS; + var gridBottomMargin = Style.marginS; + var calculatedHeight = gridTopMargin + (rows * root.widgetItemHeight) + ((rows - 1) * Style.marginS) + gridBottomMargin; + return calculatedHeight; + } + Layout.minimumHeight: widgetModel.length === 0 ? (65 * Style.uiScaleRatio) : (Style.margin2S + root.widgetItemHeight) + clip: !flowDragArea.dragStarted + + Rectangle { + anchors.fill: parent + radius: Style.iRadiusL + color: Qt.alpha(Color.mSecondary, 0.12) + border.color: Color.mSecondary + border.width: Style.borderM + visible: root.showCrossSectionDropHint + z: 1500 + } + + Grid { + id: widgetGrid + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.leftMargin: Style.marginS + anchors.rightMargin: Style.marginS + anchors.topMargin: Style.marginS + anchors.bottomMargin: Style.marginS + columns: root.gridColumns + rowSpacing: Style.marginS + columnSpacing: Style.marginM + + Repeater { + id: widgetRepeater + model: widgetModel + + delegate: Rectangle { + id: widgetItem + required property int index + required property var modelData + + width: root.calculateWidgetWidth(parent.width) + height: root.widgetItemHeight + radius: Style.iRadiusL + color: root.getWidgetColor(modelData)[0] + border.color: Color.mOutline + border.width: Style.borderS + + // Store the widget index for drag operations + property int widgetIndex: index + readonly property int buttonsWidth: Math.round(20) + readonly property int buttonsCount: root.widgetHasSettings(modelData.id) ? 1 : 0 + + // Visual feedback during drag + opacity: flowDragArea.draggedIndex === index ? 0.5 : 1.0 + scale: flowDragArea.draggedIndex === index ? 0.95 : 1.0 + z: flowDragArea.draggedIndex === index ? 1000 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + Behavior on scale { + NumberAnimation { + duration: Style.animationFast + } + } + + // Context menu for moving widget to other sections + NContextMenu { + id: contextMenu + parent: Overlay.overlay + width: 240 * Style.uiScaleRatio + model: { + var items = []; + // Add move options for each available section (except current) + for (var i = 0; i < root.availableSections.length; i++) { + var section = root.availableSections[i]; + if (section !== root.sectionId) { + var label = root.getSectionLabel(section); + var displayLabel = ''; + // Map section IDs to correct position keys based on bar orientation + var positionKey = section; + if (root.barIsVertical) { + if (section === "left") + positionKey = "top"; + else if (section === "right") + positionKey = "bottom"; + } + if (I18n.hasTranslation("positions." + positionKey)) { + displayLabel = I18n.tr("positions." + positionKey); + } else { + displayLabel = label.charAt(0).toUpperCase() + label.slice(1); + } + + items.push({ + "label": I18n.tr("tooltips.move-to-section", { + "section": displayLabel + }), + "action": section, + "icon": root.getSectionIcon(section), + "visible": true + }); + } + } + // Add remove option + items.push({ + "label": I18n.tr("tooltips.remove-widget"), + "action": "remove", + "icon": "trash", + "visible": true + }); + return items; + } + + onTriggered: action => { + if (action === "remove") { + root.removeWidget(root.sectionId, widgetItem.index); + } else { + root.moveWidget(root.sectionId, widgetItem.index, action); + } + } + } + + // MouseArea for the context menu - only active when not dragging + MouseArea { + id: contextMouseArea + anchors.fill: parent + acceptedButtons: Qt.RightButton + cursorShape: Qt.PointingHandCursor + z: -1 // Below the buttons but above background + enabled: !flowDragArea.dragStarted && !flowDragArea.potentialDrag + + onPressed: mouse => { + mouse.accepted = true; + // Check if click is not on the settings button area (if visible) + const localX = mouse.x; + const buttonsStartX = parent.width - (parent.buttonsCount * parent.buttonsWidth); + if (localX < buttonsStartX || parent.buttonsCount === 0) { + contextMenu.openAtItem(widgetItem, mouse.x, mouse.y); + } + } + } + + RowLayout { + id: widgetContent + anchors.fill: parent + anchors.margins: Style.marginXS + anchors.rightMargin: Style.marginS + spacing: Style.marginXXS + + NText { + text: { + // For plugin widgets, get the actual plugin name from manifest + if (root.widgetRegistry && root.widgetRegistry.isPluginWidget(modelData.id)) { + const pluginId = modelData.id.replace("plugin:", ""); + const manifest = PluginRegistry.getPluginManifest(pluginId); + if (manifest && manifest.name) { + return manifest.name; + } + // Fallback: just strip the prefix + return pluginId; + } + return modelData.id; + } + pointSize: Style.fontSizeXS + color: root.getWidgetColor(modelData)[1] + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + leftPadding: Style.marginS + rightPadding: Style.marginS + Layout.fillWidth: true + Layout.fillHeight: true + } + + // Plugin indicator icon + NIcon { + visible: root.widgetRegistry && root.widgetRegistry.isPluginWidget(modelData.id) + icon: "plugin" + pointSize: Style.fontSizeXXS + color: root.getWidgetColor(modelData)[1] + Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.5 : 0 + Layout.preferredHeight: Style.baseWidgetSize * 0.5 + } + + // CPU intensive indicator icon + NIcon { + visible: root.widgetRegistry && root.widgetRegistry.isCpuIntensive(modelData.id) + icon: "cpu-intensive" + pointSize: Style.fontSizeXXS + color: root.getWidgetColor(modelData)[1] + Layout.preferredWidth: visible ? Style.baseWidgetSize * 0.5 : 0 + Layout.preferredHeight: Style.baseWidgetSize * 0.5 + } + + RowLayout { + spacing: 0 + Layout.preferredWidth: buttonsCount * buttonsWidth * Style.uiScaleRatio + Layout.preferredHeight: parent.height + + Loader { + active: root.widgetHasSettings(modelData.id) && root.enabled + sourceComponent: NIconButton { + icon: "settings" + tooltipText: I18n.tr("actions.widget-settings") + baseSize: miniButtonSize + colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight) + colorBg: Color.mOnSurface + colorFg: Color.mOnPrimary + colorBgHover: Qt.alpha(Color.mOnPrimary, Style.opacityLight) + colorFgHover: Color.mOnPrimary + onClicked: { + root.openWidgetSettings(index, modelData); + } + } + } + } + } + } + } + } + + // Ghost/Clone widget for dragging + Rectangle { + id: dragGhost + width: 0 + height: Style.baseWidgetSize * 1.15 + radius: Style.iRadiusL + color: "transparent" + border.color: Color.mOutline + border.width: Style.borderS + opacity: 0.7 + visible: flowDragArea.dragStarted + z: 2000 + clip: false // Ensure ghost isn't clipped + + NText { + id: ghostText + anchors.centerIn: parent + pointSize: Style.fontSizeS + color: Color.mOnPrimary + } + } + + // Drop indicator - visual feedback for where the widget will be inserted + Rectangle { + id: dropIndicator + width: 3 + height: Style.baseWidgetSize * 1.15 + radius: Style.iRadiusXXS + color: Color.mSecondary + opacity: 0 + visible: opacity > 0 + z: 1999 + + SequentialAnimation on opacity { + id: pulseAnimation + running: false + loops: Animation.Infinite + NumberAnimation { + to: 1 + duration: 400 + easing.type: Easing.InOutQuad + } + NumberAnimation { + to: 0.6 + duration: 400 + easing.type: Easing.InOutQuad + } + } + + Behavior on x { + NumberAnimation { + duration: 100 + easing.type: Easing.OutCubic + } + } + Behavior on y { + NumberAnimation { + duration: 100 + easing.type: Easing.OutCubic + } + } + } + + // MouseArea for drag and drop + MouseArea { + id: flowDragArea + anchors.fill: parent + z: 100 // Above widgets to ensure it captures events first + enabled: root.draggable || root.crossSectionDraggable + + acceptedButtons: Qt.LeftButton + preventStealing: true // Always prevent stealing to ensure we get all events + propagateComposedEvents: true // Allow events to propagate when not handled + hoverEnabled: potentialDrag || dragStarted // Only track hover during drag operations + cursorShape: dragStarted ? Qt.ClosedHandCursor : Qt.ArrowCursor + + property point startPos: Qt.point(0, 0) + property bool dragStarted: false + property bool potentialDrag: false // Track if we're in a potential drag interaction + property int draggedIndex: -1 + property real dragThreshold: 8 // Reduced threshold for more responsive drag + property Item draggedWidget: null + property int dropTargetIndex: -1 + property var draggedModelData: null + property bool isOverButtonArea: false + + // Drop position calculation + // Map widget coordinates from grid-local to gridContainer coordinates + function mapWidgetCoords(widget) { + return { + x: widget.x + widgetGrid.x, + y: widget.y + widgetGrid.y, + width: widget.width, + height: widget.height + }; + } + + function updateDropIndicator(mouseX, mouseY) { + if (!dragStarted || draggedIndex === -1) { + dropIndicator.opacity = 0; + pulseAnimation.running = false; + return; + } + + let bestIndex = -1; + let bestPosition = null; + let minDistance = Infinity; + + // Check position relative to each widget + for (var i = 0; i < widgetModel.length; i++) { + if (i === draggedIndex) + continue; + const widget = widgetRepeater.itemAt(i); + if (!widget || widget.widgetIndex === undefined) + continue; + + const mapped = mapWidgetCoords(widget); + + // Check distance to left edge (insert before) + const leftDist = Math.sqrt(Math.pow(mouseX - mapped.x, 2) + Math.pow(mouseY - (mapped.y + mapped.height / 2), 2)); + + // Check distance to right edge (insert after) + const rightDist = Math.sqrt(Math.pow(mouseX - (mapped.x + mapped.width), 2) + Math.pow(mouseY - (mapped.y + mapped.height / 2), 2)); + + if (leftDist < minDistance) { + minDistance = leftDist; + bestIndex = i; + bestPosition = Qt.point(mapped.x - dropIndicator.width / 2 - Style.marginXS, mapped.y); + } + + if (rightDist < minDistance) { + minDistance = rightDist; + bestIndex = i + 1; + bestPosition = Qt.point(mapped.x + mapped.width + Style.marginXS - dropIndicator.width / 2, mapped.y); + } + } + + // Check if we should insert at position 0 (very beginning) + if (widgetModel.length > 0 && draggedIndex !== 0) { + const firstWidget = widgetRepeater.itemAt(0); + if (firstWidget) { + const mapped = mapWidgetCoords(firstWidget); + const dist = Math.sqrt(Math.pow(mouseX - mapped.x, 2) + Math.pow(mouseY - mapped.y, 2)); + if (dist < minDistance && mouseX < mapped.x + mapped.width / 2) { + minDistance = dist; + bestIndex = 0; + // Position indicator to the left of the first widget + bestPosition = Qt.point(mapped.x - dropIndicator.width / 2 - Style.marginXS, mapped.y); + } + } + } + + // Only show indicator if we're close enough and it's a different position + if (minDistance < 80 && bestIndex !== -1) { + // Adjust index if we're moving forward + let adjustedIndex = bestIndex; + if (bestIndex > draggedIndex) { + adjustedIndex = bestIndex - 1; + } + + // Don't show if it's the same position + if (adjustedIndex === draggedIndex) { + dropIndicator.opacity = 0; + pulseAnimation.running = false; + dropTargetIndex = -1; + return; + } + + dropTargetIndex = adjustedIndex; + if (bestPosition) { + dropIndicator.x = bestPosition.x; + dropIndicator.y = bestPosition.y; + dropIndicator.opacity = 1; + if (!pulseAnimation.running) { + pulseAnimation.running = true; + } + } + } else { + dropIndicator.opacity = 0; + pulseAnimation.running = false; + dropTargetIndex = -1; + } + } + + function resetDragState() { + root.clearCrossSectionHover(); + dragStarted = false; + potentialDrag = false; + draggedIndex = -1; + draggedWidget = null; + dropTargetIndex = -1; + draggedModelData = null; + isOverButtonArea = false; + dropIndicator.opacity = 0; + pulseAnimation.running = false; + dragGhost.width = 0; + } + + onPressed: mouse => { + // Reset state + startPos = Qt.point(mouse.x, mouse.y); + dragStarted = false; + potentialDrag = false; + draggedIndex = -1; + draggedWidget = null; + dropTargetIndex = -1; + draggedModelData = null; + isOverButtonArea = false; + + // Find which widget was clicked + for (var i = 0; i < widgetModel.length; i++) { + const widget = widgetRepeater.itemAt(i); + if (widget && widget.widgetIndex !== undefined) { + if (mouse.x >= widget.x && mouse.x <= widget.x + widget.width && mouse.y >= widget.y && mouse.y <= widget.y + widget.height) { + const localX = mouse.x - widget.x; + const buttonsStartX = widget.width - (widget.buttonsCount * widget.buttonsWidth * Style.uiScaleRatio); + + if (localX >= buttonsStartX && widget.buttonsCount > 0) { + // Click is on button area - don't start drag, propagate event + isOverButtonArea = true; + mouse.accepted = false; + return; + } else { + // This is a draggable area + draggedIndex = widget.widgetIndex; + draggedWidget = widget; + draggedModelData = widget.modelData; + potentialDrag = true; + mouse.accepted = true; + + // Signal that interaction started (prevents panel close) + root.dragPotentialStarted(); + return; + } + } + } + } + + // Click was not on any widget + mouse.accepted = false; + } + + onPositionChanged: mouse => { + if (potentialDrag && draggedIndex !== -1) { + const deltaX = mouse.x - startPos.x; + const deltaY = mouse.y - startPos.y; + const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + + // Start drag if threshold exceeded + if (!dragStarted && distance > dragThreshold) { + dragStarted = true; + + // Setup ghost widget + if (draggedWidget) { + dragGhost.width = draggedWidget.width; + dragGhost.color = root.getWidgetColor(draggedModelData)[0]; + ghostText.text = draggedModelData.id; + } + + var startGlobal = flowDragArea.mapToGlobal(mouse.x, mouse.y); + root.updateCrossSectionHover(startGlobal.x, startGlobal.y); + } + + if (dragStarted) { + // Move ghost widget + dragGhost.x = mouse.x - dragGhost.width / 2; + dragGhost.y = mouse.y - dragGhost.height / 2; + + var moveGlobal = flowDragArea.mapToGlobal(mouse.x, mouse.y); + root.updateCrossSectionHover(moveGlobal.x, moveGlobal.y); + + // Update drop indicator + updateDropIndicator(mouse.x, mouse.y); + } + } + } + + onReleased: mouse => { + if (root.draggable && dragStarted && dropTargetIndex !== -1 && dropTargetIndex !== draggedIndex) { + // Perform the reorder + reorderWidget(sectionId, draggedIndex, dropTargetIndex); + } else if (dragStarted && draggedIndex !== -1) { + var globalPos = flowDragArea.mapToGlobal(mouse.x, mouse.y); + var targetSectionId = root.findSectionAtGlobalPosition(globalPos.x, globalPos.y); + if (targetSectionId !== "" && targetSectionId !== root.sectionId) { + root.moveWidget(root.sectionId, draggedIndex, targetSectionId); + } + } + + // Always signal end of interaction if we started one + if (potentialDrag) { + root.dragPotentialEnded(); + } + + // Reset everything + resetDragState(); + mouse.accepted = true; + } + + onCanceled: { + // Handle cancel (e.g., ESC key pressed during drag) + if (potentialDrag) { + root.dragPotentialEnded(); + } + + resetDragState(); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NSettingsIndicator.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NSettingsIndicator.qml new file mode 100644 index 0000000..b1aeb80 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NSettingsIndicator.qml @@ -0,0 +1,47 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Services.UI + +Rectangle { + id: root + + property bool show: false + property var tooltipText + + implicitWidth: root.show ? 6 * Style.uiScaleRatio : 0 + implicitHeight: root.show ? 6 * Style.uiScaleRatio : 0 + width: root.show ? 6 * Style.uiScaleRatio : 0 + height: root.show ? 6 * Style.uiScaleRatio : 0 + radius: width / 2 + color: Color.mOnSurfaceVariant + opacity: 0.6 + + visible: root.show + + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + + MouseArea { + enabled: root.show && root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0) + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + cursorShape: Qt.PointingHandCursor + + onEntered: { + if (root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + TooltipService.show(root, root.tooltipText); + } + } + + onExited: { + if (root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + TooltipService.hide(); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NSlideSwapView.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NSlideSwapView.qml new file mode 100644 index 0000000..f37f14a --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NSlideSwapView.qml @@ -0,0 +1,135 @@ +import QtQuick +import qs.Commons + +Item { + id: root + + property Component sourceComponent + property bool animationsEnabled: true + property int duration: Style.animationNormal + property real transitionGap: Style.marginXL + property real incomingStartOpacity: 0.0 + property real outgoingTargetOpacity: 0.25 + + readonly property var item: contentLoader.item + readonly property bool running: _running + + property bool _running: false + property var _pendingApplyChange: null + property real _contentOffset: 0 + property real _contentOpacity: 1 + property real _snapshotOffset: 0 + property real _snapshotOpacity: 0 + property real _snapshotTargetOffset: 0 + + clip: true + + function resetVisuals() { + _running = false; + _pendingApplyChange = null; + _contentOffset = 0; + _contentOpacity = 1; + _snapshotOffset = 0; + _snapshotOpacity = 0; + snapshot.visible = false; + transition.stop(); + } + + function swap(direction, applyChange) { + if (!animationsEnabled || width <= 0 || height <= 0 || direction === 0) { + if (applyChange) + applyChange(); + return; + } + + if (_running) + resetVisuals(); + + const slideDistance = Math.max(1, width + transitionGap); + const movingForward = direction > 0; + + snapshot.visible = true; + _snapshotOffset = 0; + _snapshotOpacity = 1; + _snapshotTargetOffset = movingForward ? -slideDistance : slideDistance; + + _contentOffset = movingForward ? slideDistance : -slideDistance; + _contentOpacity = incomingStartOpacity; + + _pendingApplyChange = applyChange || null; + _running = true; + snapshot.scheduleUpdate(); + + Qt.callLater(() => { + if (!_running) { + return; + } + + const applyFn = _pendingApplyChange; + _pendingApplyChange = null; + const shouldAnimate = applyFn ? applyFn() !== false : true; + if (!shouldAnimate) { + resetVisuals(); + return; + } + transition.restart(); + }); + } + + ShaderEffectSource { + id: snapshot + visible: false + width: parent.width + height: parent.height + y: 0 + sourceItem: contentLoader + hideSource: false + live: false + smooth: true + z: 2 + x: root._snapshotOffset + opacity: root._snapshotOpacity + } + + Loader { + id: contentLoader + width: parent.width + height: parent.height + x: root._contentOffset + opacity: root._contentOpacity + sourceComponent: root.sourceComponent + } + + ParallelAnimation { + id: transition + NumberAnimation { + target: root + property: "_contentOffset" + to: 0 + duration: root.duration + easing.type: Easing.OutCubic + } + NumberAnimation { + target: root + property: "_contentOpacity" + to: 1 + duration: root.duration + easing.type: Easing.OutCubic + } + NumberAnimation { + target: root + property: "_snapshotOffset" + to: root._snapshotTargetOffset + duration: root.duration + easing.type: Easing.OutCubic + } + NumberAnimation { + target: root + property: "_snapshotOpacity" + to: root.outgoingTargetOpacity + duration: root.duration + easing.type: Easing.OutCubic + } + onFinished: root.resetVisuals() + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NSlider.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NSlider.qml new file mode 100644 index 0000000..c30d6b6 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NSlider.qml @@ -0,0 +1,253 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Shapes +import qs.Commons +import qs.Services.UI + +Slider { + id: root + + readonly property bool sliderActive: activeFocus || pressed + property color fillColor: Color.mPrimary + property var cutoutColor: Color.mSurface + property bool snapAlways: true + property real heightRatio: 0.7 + property var tooltipText + property string tooltipDirection: "auto" + property bool hovering: false + + readonly property color effectiveFillColor: enabled ? fillColor : Color.mOutline + + readonly property real knobDiameter: Math.round((Style.baseWidgetSize * heightRatio * Style.uiScaleRatio) / 2) * 2 + readonly property real trackHeight: Math.round((knobDiameter * 0.4 * Style.uiScaleRatio) / 2) * 2 + readonly property real trackRadius: Math.min(Style.iRadiusL, trackHeight / 2) + readonly property real cutoutExtra: Math.round((Style.baseWidgetSize * 0.1 * Style.uiScaleRatio) / 2) * 2 + + padding: cutoutExtra / 2 + + snapMode: snapAlways ? Slider.SnapAlways : Slider.SnapOnRelease + implicitHeight: Math.max(trackHeight, knobDiameter) + + background: Item { + id: bgContainer + x: root.leftPadding + y: root.topPadding + Style.pixelAlignCenter(root.availableHeight, root.trackHeight) + implicitWidth: Style.sliderWidth + implicitHeight: root.trackHeight + width: root.availableWidth + height: root.trackHeight + + readonly property real fillWidth: root.visualPosition * width + + // Background track + Shape { + anchors.fill: parent + visible: bgContainer.width > 0 && bgContainer.height > 0 + preferredRendererType: Shape.CurveRenderer + asynchronous: true + + ShapePath { + id: bgPath + strokeColor: Qt.alpha(Color.mOutline, 0.5) + strokeWidth: Style.borderS + fillColor: Qt.alpha(Color.mSurface, 0.5) + + readonly property real w: bgContainer.width + readonly property real h: bgContainer.height + readonly property real r: root.trackRadius + + startX: r + startY: 0 + + PathLine { + x: bgPath.w - bgPath.r + y: 0 + } + PathArc { + x: bgPath.w + y: bgPath.r + radiusX: bgPath.r + radiusY: bgPath.r + } + PathLine { + x: bgPath.w + y: bgPath.h - bgPath.r + } + PathArc { + x: bgPath.w - bgPath.r + y: bgPath.h + radiusX: bgPath.r + radiusY: bgPath.r + } + PathLine { + x: bgPath.r + y: bgPath.h + } + PathArc { + x: 0 + y: bgPath.h - bgPath.r + radiusX: bgPath.r + radiusY: bgPath.r + } + PathLine { + x: 0 + y: bgPath.r + } + PathArc { + x: bgPath.r + y: 0 + radiusX: bgPath.r + radiusY: bgPath.r + } + } + } + + LinearGradient { + id: fillGradient + x1: 0 + y1: 0 + x2: root.availableWidth + y2: 0 + GradientStop { + position: 0.0 + color: Qt.darker(effectiveFillColor, 1.2) + } + GradientStop { + position: 1.0 + color: effectiveFillColor + } + } + + // Active/filled track + Shape { + width: bgContainer.fillWidth + height: bgContainer.height + visible: bgContainer.fillWidth > 0 && bgContainer.height > 0 + preferredRendererType: Shape.CurveRenderer + asynchronous: true + clip: true + + ShapePath { + id: fillPath + strokeColor: "transparent" + fillGradient: fillGradient + + readonly property real fullWidth: root.availableWidth + readonly property real h: root.trackHeight + readonly property real r: root.trackRadius + + startX: r + startY: 0 + + PathLine { + x: fillPath.fullWidth - fillPath.r + y: 0 + } + PathArc { + x: fillPath.fullWidth + y: fillPath.r + radiusX: fillPath.r + radiusY: fillPath.r + } + PathLine { + x: fillPath.fullWidth + y: fillPath.h - fillPath.r + } + PathArc { + x: fillPath.fullWidth - fillPath.r + y: fillPath.h + radiusX: fillPath.r + radiusY: fillPath.r + } + PathLine { + x: fillPath.r + y: fillPath.h + } + PathArc { + x: 0 + y: fillPath.h - fillPath.r + radiusX: fillPath.r + radiusY: fillPath.r + } + PathLine { + x: 0 + y: fillPath.r + } + PathArc { + x: fillPath.r + y: 0 + radiusX: fillPath.r + radiusY: fillPath.r + } + } + } + + // Circular cutout + Rectangle { + id: knobCutout + implicitWidth: root.knobDiameter + root.cutoutExtra + implicitHeight: root.knobDiameter + root.cutoutExtra + radius: Math.min(Style.iRadiusL, width / 2) + color: root.cutoutColor !== undefined ? root.cutoutColor : Color.mSurface + x: root.visualPosition * (root.availableWidth - root.knobDiameter) - root.cutoutExtra / 2 + anchors.verticalCenter: parent.verticalCenter + } + } + + handle: Item { + implicitWidth: knobDiameter + implicitHeight: knobDiameter + x: root.leftPadding + root.visualPosition * (root.availableWidth - width) + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + id: knob + implicitWidth: knobDiameter + implicitHeight: knobDiameter + radius: Math.min(Style.iRadiusL, width / 2) + color: root.pressed ? Color.mHover : Color.mSurface + border.color: effectiveFillColor + border.width: Style.borderL + anchors.centerIn: parent + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + MouseArea { + enabled: true + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + acceptedButtons: Qt.NoButton // Don't accept any mouse buttons - only hover + propagateComposedEvents: true + + onEntered: { + root.hovering = true; + if (root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + TooltipService.show(knob, root.tooltipText, root.tooltipDirection); + } + } + + onExited: { + root.hovering = false; + if (root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + TooltipService.hide(); + } + } + } + + // Hide tooltip when slider is pressed (anywhere on the slider) + Connections { + target: root + function onPressedChanged() { + if (root.pressed && root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + TooltipService.hide(); + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NSpinBox.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NSpinBox.qml new file mode 100644 index 0000000..15c3e16 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NSpinBox.qml @@ -0,0 +1,314 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +RowLayout { + id: root + + // Public properties + property int value: 0 + property int from: 0 + property int to: 100 + property int stepSize: 1 + property string suffix: "" + property string prefix: "" + property string label: "" + property string description: "" + property bool hovering: false + property int baseSize: Style.baseWidgetSize + property var defaultValue: undefined + property string settingsPath: "" + + // Convenience properties for common naming + property alias minimum: root.from + property alias maximum: root.to + + // Properties for repeating + property int initialRepeatDelay: 400 // The "pause" after the first click (in ms) + property int repeatInterval: 80 // How often to step up after fist pause (ms) + property int rampFactor: 4 // How many ticks to wait before increasing the step multiplier + property int maxStepMultiplier: 10 // The max step (e.g., 10 * stepSize) + property int _holdTicks: 0 // Internal counter for hold duration + property int _repeatDirection: 0 // -1 for decrease, 1 for increase + + signal entered + signal exited + + Layout.fillWidth: true + + readonly property bool isValueChanged: (defaultValue !== undefined) && (value !== defaultValue) + readonly property string indicatorTooltip: defaultValue !== undefined ? I18n.tr("panels.indicator.default-value", { + "value": String(defaultValue) + }) : "" + + Timer { + id: repeatTimer + repeat: true + interval: root.initialRepeatDelay + + onTriggered: { + if (repeatTimer.interval === root.initialRepeatDelay) { + repeatTimer.interval = root.repeatInterval; + root._holdTicks = 0; + } + root._holdTicks++; + var stepMultiplier = Math.min(root.maxStepMultiplier, 1 + Math.floor(root._holdTicks / root.rampFactor)); + changeValue(root._repeatDirection, root.stepSize * stepMultiplier); + } + } + + function changeValue(direction, step) { + var currentStep = step || root.stepSize; + + if (direction === 1 && root.value < root.to) { + root.value = Math.min(root.to, root.value + currentStep); + } else if (direction === -1 && root.value > root.from) { + root.value = Math.max(root.from, root.value - currentStep); + } else { + return; + } + + if (root.value === root.to || root.value === root.from) { + stopRepeat(); + } + } + + function stopRepeat() { + root._repeatDirection = 0; + repeatTimer.stop(); + repeatTimer.interval = root.initialRepeatDelay; + } + + NLabel { + label: root.label + description: root.description + showIndicator: root.isValueChanged + indicatorTooltip: root.indicatorTooltip + } + + // Main spinbox container + Rectangle { + id: spinBoxContainer + Layout.margins: Style.borderS + implicitWidth: 120 + implicitHeight: Math.round((root.baseSize - 4) / 2) * 2 + radius: Style.iRadiusS + color: Color.mSurfaceVariant + border.color: (root.hovering || decreaseArea.containsMouse || increaseArea.containsMouse) ? Color.mHover : Color.mOutline + border.width: Style.borderS + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + // Mouse area for hover and scroll + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + hoverEnabled: true + onEntered: { + if (!root.enabled) + return; + root.hovering = true; + root.entered(); + } + onExited: { + root.hovering = false; + root.exited(); + } + onWheel: wheel => { + if (wheel.angleDelta.y > 0 && root.value < root.to) { + let newValue = Math.min(root.to, root.value + root.stepSize); + root.value = newValue; + } else if (wheel.angleDelta.y < 0 && root.value > root.from) { + let newValue = Math.max(root.from, root.value - root.stepSize); + root.value = newValue; + } + } + } + + // Decrease button (left) + Item { + id: decreaseButton + height: parent.height + width: height + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + opacity: (root.enabled && root.value > root.from) || decreaseArea.containsMouse ? 1.0 : 0.3 + + Rectangle { + anchors.centerIn: parent + width: parent.height + height: width + radius: spinBoxContainer.radius + color: Color.mHover + opacity: decreaseArea.containsMouse ? 1.0 : 0.0 + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + + NIcon { + anchors.centerIn: parent + icon: "chevron-left" + pointSize: Style.fontSizeS + color: decreaseArea.containsMouse ? Color.mOnHover : Color.mPrimary + } + + MouseArea { + id: decreaseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: root.enabled && root.value > root.from + onPressed: { + root._repeatDirection = -1; + changeValue(root._repeatDirection, root.stepSize); + repeatTimer.start(); + } + onReleased: stopRepeat() + onExited: stopRepeat() + } + } + + // Increase button (right) + Item { + id: increaseButton + height: parent.height + width: height + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + opacity: (root.enabled && root.value < root.to) || increaseArea.containsMouse ? 1.0 : 0.3 + + Rectangle { + anchors.centerIn: parent + width: parent.height + height: width + radius: spinBoxContainer.radius + color: Color.mHover + opacity: increaseArea.containsMouse ? 1.0 : 0.0 + Behavior on opacity { + NumberAnimation { + duration: Style.animationFast + } + } + } + + NIcon { + anchors.centerIn: parent + icon: "chevron-right" + pointSize: Style.fontSizeS + color: increaseArea.containsMouse ? Color.mOnHover : Color.mPrimary + } + + MouseArea { + id: increaseArea + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + enabled: root.enabled && root.value < root.to + onPressed: { + root._repeatDirection = 1; + changeValue(root._repeatDirection, root.stepSize); + repeatTimer.start(); + } + onReleased: stopRepeat() + onExited: stopRepeat() + } + } + + // Center value display with separate prefix, value, and suffix + Item { + id: valueContainer + anchors.left: decreaseButton.right + anchors.right: increaseButton.left + anchors.verticalCenter: parent.verticalCenter + anchors.margins: 4 + height: parent.height + + RowLayout { + anchors.centerIn: parent + spacing: 0 + + // Prefix text (non-editable) + NText { + text: root.prefix + family: Settings.data.ui.fontFixed + pointSize: Style.fontSizeM + font.weight: Style.fontWeightMedium + color: Qt.alpha(Color.mOnSurface, root.enabled ? 1.0 : 0.6) + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignVCenter + visible: root.prefix !== "" + } + + // Editable number input + TextInput { + id: valueInput + text: valueInput.focus ? valueInput.text : root.value.toString() + font.family: Settings.data.ui.fontFixed + font.pointSize: Style.fontSizeM + font.weight: Style.fontWeightMedium + color: Qt.alpha(Color.mOnSurface, root.enabled ? 1.0 : 0.6) + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignVCenter + selectByMouse: true + enabled: root.enabled + + // Only allow numeric input within range + validator: IntValidator { + bottom: root.from + top: root.to + } + + onAccepted: { + applyValue(); + focus = false; + } + + Keys.onEscapePressed: { + text = root.value.toString(); + focus = false; + } + + onFocusChanged: { + if (focus) { + selectAll(); + } else { + applyValue(); + } + } + + function applyValue() { + let newValue = parseInt(text); + if (!isNaN(newValue)) { + // Don't manually set text here - let the binding handle it + newValue = Math.max(root.from, Math.min(root.to, newValue)); + root.value = newValue; + } + } + } + + // Suffix text (non-editable) + NText { + text: root.suffix + family: Settings.data.ui.fontFixed + pointSize: Style.fontSizeM + font.weight: Style.fontWeightMedium + color: Qt.alpha(Color.mOnSurface, root.enabled ? 1.0 : 0.6) + verticalAlignment: Text.AlignVCenter + Layout.alignment: Qt.AlignVCenter + visible: root.suffix !== "" + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NTabBar.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NTabBar.qml new file mode 100644 index 0000000..c382ce1 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NTabBar.qml @@ -0,0 +1,90 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +Rectangle { + id: root + objectName: "NTabBar" + + // Public properties + property int currentIndex: 0 + property real spacing: Style.marginXS + property real margins: 0 + property real tabHeight: Style.baseWidgetSize + property bool distributeEvenly: false + default property alias content: tabRow.children + + onDistributeEvenlyChanged: _applyDistribution() + Component.onCompleted: _applyDistribution() + + function _updateFirstLast() { + // Defensive check for QML initialization timing + if (!tabRow || !tabRow.children) { + return; + } + var kids = tabRow.children; + var len = kids.length; + var firstVisible = -1; + var lastVisible = -1; + for (var i = 0; i < len; i++) { + var child = kids[i]; + // Only consider items that have isFirst/isLast (actual tab buttons, not Repeaters) + if (child.visible && "isFirst" in child) { + if (firstVisible === -1) + firstVisible = i; + lastVisible = i; + } + } + for (var i = 0; i < len; i++) { + var child = kids[i]; + if ("isFirst" in child) + child.isFirst = (i === firstVisible); + if ("isLast" in child) + child.isLast = (i === lastVisible); + } + } + + function _applyDistribution() { + if (!tabRow || !tabRow.children) { + return; + } + if (!distributeEvenly) { + for (var i = 0; i < tabRow.children.length; i++) { + var child = tabRow.children[i]; + child.Layout.fillWidth = true; + } + return; + } + + for (var i = 0; i < tabRow.children.length; i++) { + var child = tabRow.children[i]; + child.Layout.fillWidth = true; + child.Layout.preferredWidth = 1; + } + } + + // Styling + Layout.margins: Style.borderS + implicitWidth: tabRow.implicitWidth + (margins * 2) + implicitHeight: tabHeight + (margins * 2) + color: Color.smartAlpha(Color.mSurfaceVariant) + radius: Style.iRadiusM + + RowLayout { + id: tabRow + anchors.fill: parent + anchors.margins: margins + spacing: root.spacing + + onChildrenChanged: { + for (var i = 0; i < children.length; i++) { + var child = children[i]; + child.visibleChanged.connect(root._updateFirstLast); + } + root._updateFirstLast(); + root._applyDistribution(); + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NTabButton.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NTabButton.qml new file mode 100644 index 0000000..ea132a6 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NTabButton.qml @@ -0,0 +1,127 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Services.UI +import qs.Widgets + +Rectangle { + id: root + + // Public properties + property string text: "" + property string icon: "" + property var tooltipText + property bool checked: false + property int tabIndex: 0 + property real pointSize: Style.fontSizeM + property bool isFirst: false + property bool isLast: false + + // Internal state + property bool isHovered: false + + signal clicked + + // Sizing + Layout.fillHeight: true + implicitWidth: contentLayout.implicitWidth + Style.margin2M + + topLeftRadius: isFirst ? Style.iRadiusM : Style.iRadiusXXXS + bottomLeftRadius: isFirst ? Style.iRadiusM : Style.iRadiusXXXS + topRightRadius: isLast ? Style.iRadiusM : Style.iRadiusXXXS + bottomRightRadius: isLast ? Style.iRadiusM : Style.iRadiusXXXS + + color: root.isHovered ? Color.mHover : (root.checked ? Color.mPrimary : Color.smartAlpha(Color.mSurface)) + border.color: root.checked ? Color.mPrimary : Color.mOutline + border.width: Style.borderS + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + + // Content + RowLayout { + id: contentLayout + anchors.centerIn: parent + width: Math.min(implicitWidth, parent.width - Style.margin2S) + spacing: (root.icon !== "" && root.text !== "") ? Style.marginXS : 0 + + NIcon { + visible: root.icon !== "" + Layout.alignment: Qt.AlignVCenter + icon: root.icon + pointSize: root.pointSize * 1.2 + color: root.isHovered ? Color.mOnHover : (root.checked ? Color.mOnPrimary : Color.mOnSurface) + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + NText { + id: tabText + visible: root.text !== "" + Layout.alignment: Qt.AlignVCenter + text: root.text + pointSize: root.pointSize + font.weight: Style.fontWeightSemiBold + color: root.isHovered ? Color.mOnHover : (root.checked ? Color.mOnPrimary : Color.mOnSurface) + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + Behavior on color { + enabled: !Color.isTransitioning + ColorAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + } + + // Tooltip + Timer { + id: tooltipTimer + interval: 500 + onTriggered: { + if (root.isHovered && root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + TooltipService.show(root, root.tooltipText); + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onEntered: { + root.isHovered = true; + if (root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + tooltipTimer.start(); + } + } + onExited: { + root.isHovered = false; + tooltipTimer.stop(); + if (root.tooltipText && (!Array.isArray(root.tooltipText) || root.tooltipText.length > 0)) { + TooltipService.hide(); + } + } + onClicked: { + root.clicked(); + // Update parent NTabBar's currentIndex + if (root.parent && root.parent.parent && root.parent.parent.currentIndex !== undefined) { + root.parent.parent.currentIndex = root.tabIndex; + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NTabView.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NTabView.qml new file mode 100644 index 0000000..309dadc --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NTabView.qml @@ -0,0 +1,181 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons + +Item { + id: root + objectName: "NTabView" + + property int currentIndex: 0 + + // Private + property int previousIndex: 0 + property bool initialized: false + property bool animating: false + property real animatingHeight: 0 + property real transitionGap: Style.marginXL + property real transitionTime: Style.animationNormal + property list contentItems: [] + + default property alias content: container.data + + clip: true + Layout.fillWidth: true + + // During animation, use max height to prevent clipping. Otherwise use current item height. + implicitHeight: animating ? animatingHeight : (contentItems[currentIndex] ? contentItems[currentIndex].implicitHeight : 0) + + Item { + id: container + anchors.fill: parent + } + + // Set the visible tab to idx without triggering a slide animation. + // Call this BEFORE the bound currentIndex changes so that + // onCurrentIndexChanged sees previousIndex === currentIndex and skips. + function setIndexWithoutAnimation(idx) { + fromXAnim.stop(); + fromOpacityAnim.stop(); + toXAnim.stop(); + toOpacityAnim.stop(); + animating = false; + previousIndex = idx; + for (let i = 0; i < contentItems.length; i++) { + if (i === idx) { + contentItems[i].x = 0; + contentItems[i].visible = true; + contentItems[i].opacity = 1.0; + } else { + contentItems[i].x = root.width; + contentItems[i].visible = false; + contentItems[i].opacity = 1.0; + } + } + } + + Component.onCompleted: { + _initializeItems(); + } + + function _initializeItems() { + contentItems = []; + for (let i = 0; i < container.children.length; i++) { + const child = container.children[i]; + contentItems.push(child); + child.width = Qt.binding(() => root.width); + + if (i === currentIndex) { + child.x = 0; + child.visible = true; + } else { + child.x = root.width; + child.visible = false; + } + } + initialized = true; + } + + onCurrentIndexChanged: { + if (!initialized || contentItems.length === 0) + return; + if (previousIndex === currentIndex) + return; + + _animateTransition(previousIndex, currentIndex); + previousIndex = currentIndex; + } + + function _animateTransition(fromIdx, toIdx) { + // Stop any running animations + fromXAnim.stop(); + fromOpacityAnim.stop(); + toXAnim.stop(); + toOpacityAnim.stop(); + + // Reset all items to clean state (except target) + for (let i = 0; i < contentItems.length; i++) { + if (i !== toIdx) { + contentItems[i].visible = false; + contentItems[i].opacity = 1.0; + } + } + + const fromItem = contentItems[fromIdx]; + const toItem = contentItems[toIdx]; + const slideLeft = toIdx > fromIdx; + + // Set height to max of both items during animation + const fromHeight = fromItem ? fromItem.implicitHeight : 0; + const toHeight = toItem ? toItem.implicitHeight : 0; + animatingHeight = Math.max(fromHeight, toHeight); + animating = true; + + // Position outgoing item and make visible for animation + if (fromItem) { + fromItem.visible = true; + fromItem.x = 0; + fromItem.opacity = 1.0; + } + + // Position incoming item off-screen (with gap) and set initial opacity + if (toItem) { + toItem.visible = true; + toItem.x = slideLeft ? root.width + transitionGap : -root.width - transitionGap; + toItem.opacity = 0.0; + } + + // Animate both items together (with gap) + if (fromItem) { + fromXAnim.target = fromItem; + fromXAnim.to = slideLeft ? -root.width - transitionGap : root.width + transitionGap; + fromOpacityAnim.target = fromItem; + fromXAnim.start(); + fromOpacityAnim.start(); + } + + if (toItem) { + toXAnim.target = toItem; + toOpacityAnim.target = toItem; + toXAnim.start(); + toOpacityAnim.start(); + } + } + + NumberAnimation { + id: fromXAnim + property: "x" + duration: root.transitionTime + easing.type: Easing.OutCubic + onFinished: { + if (target && target !== contentItems[currentIndex]) { + target.visible = false; + target.opacity = 1.0; + } + animating = false; + } + } + + NumberAnimation { + id: fromOpacityAnim + property: "opacity" + to: 0.25 + duration: root.transitionTime + easing.type: Easing.OutCubic + } + + NumberAnimation { + id: toXAnim + property: "x" + to: 0 + duration: root.transitionTime + easing.type: Easing.OutCubic + } + + NumberAnimation { + id: toOpacityAnim + property: "opacity" + to: 1.0 + duration: root.transitionTime + easing.type: Easing.OutCubic + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NTagFilter.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NTagFilter.qml new file mode 100644 index 0000000..d2515ac --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NTagFilter.qml @@ -0,0 +1,48 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +NCollapsible { + id: root + + // Public API + property var tags: [] // Array of tag strings + property string selectedTag: "" + property alias label: root.label + property alias description: root.description + property alias expanded: root.expanded + + // Formatting function for tag display (optional override) + property var formatTag: function (tag) { + if (tag === "") + return I18n.tr("launcher.categories.all"); + // Default: capitalize first letter + return tag.charAt(0).toUpperCase() + tag.slice(1); + } + + Layout.fillWidth: true + contentSpacing: Style.marginXS + + Flow { + Layout.fillWidth: true + spacing: Style.marginXS + flow: Flow.LeftToRight + + Repeater { + model: [""].concat(root.tags) + + delegate: NButton { + text: root.formatTag(modelData) + backgroundColor: root.selectedTag === modelData ? Color.mPrimary : Color.mSurfaceVariant + textColor: root.selectedTag === modelData ? Color.mOnPrimary : Color.mOnSurfaceVariant + onClicked: root.selectedTag = modelData + fontSize: Style.fontSizeS + iconSize: Style.fontSizeS + fontWeight: Style.fontWeightSemiBold + buttonRadius: Style.iRadiusM + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NText.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NText.qml new file mode 100644 index 0000000..e7e2835 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NText.qml @@ -0,0 +1,41 @@ +import QtQuick +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +Text { + id: root + + property bool richTextEnabled: false + property bool markdownTextEnabled: false + property string family: Settings.data.ui.fontDefault + property real pointSize: Style.fontSizeM + property bool applyUiScale: true + property real fontScale: { + const fontScale = (root.family === Settings.data.ui.fontDefault ? Settings.data.ui.fontDefaultScale : Settings.data.ui.fontFixedScale); + if (applyUiScale) { + return fontScale * Style.uiScaleRatio; + } + return fontScale; + } + property var features: ({}) + + opacity: enabled ? 1.0 : 0.6 + font.family: root.family + font.weight: Style.fontWeightMedium + font.pointSize: Math.max(1, root.pointSize * fontScale) + font.features: root.features + color: Color.mOnSurface + elide: Text.ElideRight + wrapMode: Text.NoWrap + verticalAlignment: Text.AlignVCenter + + textFormat: { + if (root.richTextEnabled) { + return Text.RichText; + } else if (root.markdownTextEnabled) { + return Text.MarkdownText; + } + return Text.PlainText; + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NTextInput.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NTextInput.qml new file mode 100644 index 0000000..66fef69 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NTextInput.qml @@ -0,0 +1,238 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + + property string label: "" + property string description: "" + property string inputIconName: "" + property bool readOnly: false + property color labelColor: Color.mOnSurface + property color descriptionColor: Color.mOnSurfaceVariant + property string fontFamily: Settings.data.ui.fontDefault + property real fontSize: Style.fontSizeS + property int fontWeight: Style.fontWeightRegular + property var defaultValue: undefined + property string settingsPath: "" + property real radius: Style.iRadiusM + property real minimumInputWidth: 80 * Style.uiScaleRatio + property bool showClearButton: true + + property alias text: input.text + property alias placeholderText: input.placeholderText + property alias inputMethodHints: input.inputMethodHints + property alias horizontalAlignment: input.horizontalAlignment + property alias inputItem: input + + signal editingFinished + signal accepted + + opacity: enabled ? 1.0 : 0.3 + spacing: Style.marginS + + readonly property bool isValueChanged: (defaultValue !== undefined) && (text !== defaultValue) + readonly property string indicatorTooltip: defaultValue !== undefined ? I18n.tr("panels.indicator.default-value", { + "value": defaultValue === "" ? "(empty)" : String(defaultValue) + }) : "" + + NLabel { + label: root.label + description: root.description + labelColor: root.labelColor + descriptionColor: root.descriptionColor + visible: root.label !== "" || root.description !== "" + Layout.fillWidth: true + showIndicator: root.isValueChanged + indicatorTooltip: root.indicatorTooltip + } + + // An active control that blocks input, to avoid events leakage and dragging stuff in the background. + Control { + id: frameControl + + Layout.fillWidth: true + Layout.minimumWidth: root.minimumInputWidth + Layout.margins: Style.borderS + implicitHeight: Style.baseWidgetSize * 1.1 * Style.uiScaleRatio + + // This is important - makes the control accept focus + focusPolicy: Qt.StrongFocus + hoverEnabled: true + + background: Rectangle { + id: frame + + radius: root.radius + color: Color.mSurface + border.color: input.activeFocus ? Color.mSecondary : Color.mOutline + border.width: Style.borderS + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + } + + contentItem: Item { + // Invisible background that captures ALL mouse events + MouseArea { + id: backgroundCapture + anchors.fill: parent + z: 0 + acceptedButtons: Qt.AllButtons + hoverEnabled: true + preventStealing: true + propagateComposedEvents: false + + onPressed: mouse => { + mouse.accepted = true; + // Focus the input and position cursor + input.forceActiveFocus(); + var inputPos = mapToItem(inputContainer, mouse.x, mouse.y); + if (inputPos.x >= 0 && inputPos.x <= inputContainer.width) { + var textPos = inputPos.x - Style.marginM; + if (textPos >= 0 && textPos <= input.width) { + input.cursorPosition = input.positionAt(textPos, input.height / 2); + } + } + } + + onReleased: mouse => { + mouse.accepted = true; + } + onDoubleClicked: mouse => { + mouse.accepted = true; + input.selectAll(); + } + onPositionChanged: mouse => { + mouse.accepted = true; + } + onWheel: wheel => { + wheel.accepted = false; + } + } + + // Container for the actual text field + Item { + id: inputContainer + anchors.fill: parent + anchors.leftMargin: Style.marginM + anchors.rightMargin: 0 + clip: true + z: 1 + + RowLayout { + anchors.fill: parent + spacing: 0 + + NIcon { + id: inputIcon + icon: root.inputIconName + + visible: root.inputIconName !== "" + enabled: false + + Layout.alignment: Qt.AlignVCenter + Layout.rightMargin: visible ? Style.marginS : 0 + } + + TextField { + id: input + + Layout.fillWidth: true + Layout.fillHeight: true + + verticalAlignment: TextInput.AlignVCenter + + echoMode: TextInput.Normal + readOnly: root.readOnly + placeholderTextColor: Qt.alpha(Color.mOnSurfaceVariant, 0.6) + color: enabled ? Color.mOnSurface : Qt.alpha(Color.mOnSurface, 0.4) + + selectByMouse: true + + topPadding: 0 + bottomPadding: 0 + leftPadding: 0 + rightPadding: 0 + + background: null + + font.family: root.fontFamily + font.pointSize: root.fontSize * Style.uiScaleRatio + font.weight: root.fontWeight + + onEditingFinished: root.editingFinished() + onAccepted: root.accepted() + + // Override mouse handling to prevent propagation + MouseArea { + id: textFieldMouse + anchors.fill: parent + acceptedButtons: Qt.AllButtons + preventStealing: true + propagateComposedEvents: false + cursorShape: Qt.IBeamCursor + + property int selectionStart: 0 + + onPressed: mouse => { + mouse.accepted = true; + input.forceActiveFocus(); + var pos = input.positionAt(mouse.x, mouse.y); + input.cursorPosition = pos; + selectionStart = pos; + } + + onPositionChanged: mouse => { + if (mouse.buttons & Qt.LeftButton) { + mouse.accepted = true; + var pos = input.positionAt(mouse.x, mouse.y); + input.select(selectionStart, pos); + } + } + + onDoubleClicked: mouse => { + mouse.accepted = true; + input.selectAll(); + } + + onReleased: mouse => { + mouse.accepted = true; + } + onWheel: wheel => { + wheel.accepted = false; + } + } + } + NIconButton { + id: clearButton + icon: "x" + tooltipText: (input.text.length > 0 && !root.readOnly && root.enabled) ? I18n.tr("common.clear") : "" + + Layout.alignment: Qt.AlignVCenter + border.width: 0 + + colorBg: "transparent" + colorBgHover: "transparent" + colorFg: Color.mOnSurface + colorFgHover: Color.mError + + visible: root.showClearButton && input.text.length > 0 && !root.readOnly + enabled: input.text.length > 0 && !root.readOnly && root.enabled + + onClicked: { + input.clear(); + input.forceActiveFocus(); + } + } + } + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NTextInputButton.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NTextInputButton.qml new file mode 100644 index 0000000..8971110 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NTextInputButton.qml @@ -0,0 +1,57 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +ColumnLayout { + id: root + Layout.fillWidth: true + + property alias text: input.text + property alias placeholderText: input.placeholderText + property string label: "" + property string description: "" + property string inputIconName: "" + property alias buttonIcon: button.icon + property alias buttonTooltip: button.tooltipText + property alias buttonEnabled: button.enabled + property real maximumWidth: 0 + + signal buttonClicked + signal inputTextChanged(string text) + signal inputEditingFinished + + spacing: Style.marginS + + // Label and description + NLabel { + label: root.label + description: root.description + visible: root.label !== "" || root.description !== "" + Layout.fillWidth: true + } + + // Input field with button + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM + + NTextInput { + id: input + inputIconName: root.inputIconName + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + enabled: root.enabled + onTextChanged: root.inputTextChanged(text) + onEditingFinished: root.inputEditingFinished() + } + + // Button + NIconButton { + id: button + baseSize: Style.baseWidgetSize + onClicked: root.buttonClicked() + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NToggle.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NToggle.qml new file mode 100644 index 0000000..6c5158b --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NToggle.qml @@ -0,0 +1,110 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +RowLayout { + id: root + + property string label: "" + property string description: "" + property string icon: "" + property bool checked: false + property bool hovering: false + property int baseSize: Math.round(Style.baseWidgetSize * 0.8 * Style.uiScaleRatio) + property var defaultValue: undefined + property string settingsPath: "" + + signal toggled(bool checked) + signal entered + signal exited + + Layout.fillWidth: true + spacing: Style.marginM + + readonly property bool isValueChanged: (defaultValue !== undefined) && (checked !== defaultValue) + readonly property string indicatorTooltip: defaultValue !== undefined ? I18n.tr("panels.indicator.default-value", { + "value": typeof defaultValue === "boolean" ? (defaultValue ? "true" : "false") : String(defaultValue) + }) : "" + + NLabel { + Layout.fillWidth: true + label: root.label + description: root.description + icon: root.icon + iconColor: root.checked ? Color.mPrimary : Color.mOnSurface + visible: root.label !== "" || root.description !== "" + showIndicator: root.isValueChanged + indicatorTooltip: root.indicatorTooltip + } + + Rectangle { + id: switcher + + opacity: enabled ? 1.0 : 0.6 + Layout.alignment: Qt.AlignVCenter + Layout.margins: Style.borderS + implicitWidth: Math.round(root.baseSize * .85) * 2 + implicitHeight: Math.round(root.baseSize * .5) * 2 + radius: Math.min(Style.iRadiusL, height / 2) + color: root.checked ? Color.mPrimary : Color.mSurface + border.color: Color.mOutline + border.width: Style.borderS + + Behavior on color { + ColorAnimation { + duration: Style.animationFast + } + } + + Behavior on border.color { + ColorAnimation { + duration: Style.animationFast + } + } + + Rectangle { + implicitWidth: Math.round(root.baseSize * 0.4) * 2 + implicitHeight: Math.round(root.baseSize * 0.4) * 2 + radius: Math.min(Style.iRadiusL, height / 2) + color: root.checked ? Color.mOnPrimary : Color.mPrimary + border.color: root.checked ? Color.mSurface : Color.mSurface + border.width: Style.borderM + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 0 + x: root.checked ? switcher.width - width - 3 : 3 + + Behavior on x { + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + } + + MouseArea { + enabled: root.enabled + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onEntered: { + if (!enabled) + return; + hovering = true; + root.entered(); + } + onExited: { + if (!enabled) + return; + hovering = false; + root.exited(); + } + onClicked: { + if (!enabled) + return; + root.toggled(!root.checked); + } + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/Widgets/NValueSlider.qml b/fedora/.config/quickshell/noctalia-shell/Widgets/NValueSlider.qml new file mode 100644 index 0000000..08fc8ee --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/Widgets/NValueSlider.qml @@ -0,0 +1,116 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.Commons +import qs.Widgets + +RowLayout { + id: root + + property real from: 0 + property real to: 1 + property real value: 0 + property real stepSize: 0.01 + property var cutoutColor: Color.mSurface + property bool snapAlways: true + property real heightRatio: 0.7 + property string text: "" + property real textSize: Style.fontSizeM + property real customHeight: -1 + property real customHeightRatio: -1 + property string label: "" + property string description: "" + property var defaultValue: undefined + property bool showReset: false + + spacing: Style.marginL + Layout.fillWidth: true + + // Signals + signal moved(real value) + signal pressedChanged(bool pressed, real value) + + readonly property bool sliderActive: slider.activeFocus || slider.pressed + readonly property bool isValueChanged: defaultValue !== undefined && (value !== defaultValue) + readonly property string indicatorTooltip: { + if (defaultValue === undefined) + return ""; + var defaultVal = defaultValue; + if (typeof defaultVal === "number") { + // If it's a decimal between 0 and 1, format as percentage + if (defaultVal > 0 && defaultVal <= 1 && from >= 0 && from < 1) { + return I18n.tr("panels.indicator.default-value", { + "value": Math.floor(defaultVal * 100) + "%" + }); + } + return I18n.tr("panels.indicator.default-value", { + "value": String(defaultVal) + }); + } + return I18n.tr("panels.indicator.default-value", { + "value": String(defaultVal) + }); + } + + ColumnLayout { + spacing: Style.marginS + Layout.fillWidth: true + + NLabel { + label: root.label + description: root.description + visible: root.label !== "" || root.description !== "" + showIndicator: root.isValueChanged + indicatorTooltip: root.indicatorTooltip + opacity: root.enabled ? 1.0 : 0.6 + Layout.fillWidth: true + } + + RowLayout { + spacing: Style.marginL + Layout.fillWidth: true + + NSlider { + id: slider + Layout.fillWidth: true + from: root.from + to: root.to + value: root.value + stepSize: root.stepSize + cutoutColor: root.cutoutColor + snapAlways: root.snapAlways + heightRatio: root.customHeightRatio > 0 ? root.customHeightRatio : root.heightRatio + onMoved: root.moved(value) + onPressedChanged: root.pressedChanged(pressed, value) + } + + NText { + visible: root.text !== "" + text: root.text + pointSize: root.textSize + family: Settings.data.ui.fontFixed + opacity: root.enabled ? 1.0 : 0.6 + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: 45 * Style.uiScaleRatio + horizontalAlignment: Text.AlignRight + } + } + } + + Item { + id: buttonItem + visible: root.showReset && root.defaultValue !== undefined + Layout.preferredWidth: 30 * Style.uiScaleRatio + Layout.preferredHeight: 30 * Style.uiScaleRatio + + NIconButton { + icon: "restore" + enabled: root.enabled + baseSize: Style.baseWidgetSize * 0.8 + tooltipText: I18n.tr("common.reset") + onClicked: root.moved(root.defaultValue) + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + } + } +} diff --git a/fedora/.config/quickshell/noctalia-shell/lefthook.yml b/fedora/.config/quickshell/noctalia-shell/lefthook.yml new file mode 100644 index 0000000..a6b3856 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/lefthook.yml @@ -0,0 +1,6 @@ +pre-commit: + jobs: + - name: format qml + run: cd "$(git rev-parse --show-toplevel)" && ./Scripts/dev/qmlfmt.sh && git update-index --again + - name: build settings search index + run: cd "$(git rev-parse --show-toplevel)" && python3 Scripts/dev/build-settings-search-index.py && git add Assets/settings-search-index.json diff --git a/fedora/.config/quickshell/noctalia-shell/shell.nix b/fedora/.config/quickshell/noctalia-shell/shell.nix new file mode 100644 index 0000000..7700415 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/shell.nix @@ -0,0 +1 @@ +(import { }).callPackage ./nix/shell.nix { } diff --git a/fedora/.config/quickshell/noctalia-shell/shell.qml b/fedora/.config/quickshell/noctalia-shell/shell.qml new file mode 100644 index 0000000..9a9b886 --- /dev/null +++ b/fedora/.config/quickshell/noctalia-shell/shell.qml @@ -0,0 +1,259 @@ +/* +* Noctalia – made by https://github.com/noctalia-dev +* Licensed under the MIT License. +* Forks and modifications are allowed under the MIT License, +* but proper credit must be given to the original author. +*/ + +//@ pragma Env QT_FFMPEG_DECODING_HW_DEVICE_TYPES=vaapi,vdpau +//@ pragma Env QT_FFMPEG_ENCODING_HW_DEVICE_TYPES=vaapi,vdpau + +// Qt & Quickshell Core +import QtQuick +import Quickshell + +// Commons & Services +import qs.Commons + +// Modules +import qs.Modules.Background +import qs.Modules.Bar +import qs.Modules.DesktopWidgets +import qs.Modules.Dock +import qs.Modules.LockScreen +import qs.Modules.MainScreen +import qs.Modules.Notification +import qs.Modules.OSD + +import qs.Modules.Panels.Launcher +import qs.Modules.Panels.Settings +import qs.Modules.Toast +import qs.Services.Control +import qs.Services.Hardware +import qs.Services.Keyboard +import qs.Services.Location +import qs.Services.Networking +import qs.Services.Noctalia +import qs.Services.Power +import qs.Services.System +import qs.Services.Theming +import qs.Services.UI + +ShellRoot { + id: shellRoot + + property bool i18nLoaded: false + property bool settingsLoaded: false + property bool shellStateLoaded: false + + Component.onCompleted: { + Logger.i("Shell", "---------------------------"); + Logger.i("Shell", "Noctalia Hello!"); + + // Initialize plugin system early so Settings can validate plugin widgets + PluginRegistry.init(); + } + + Connections { + target: Quickshell + function onReloadCompleted() { + Quickshell.inhibitReloadPopup(); + } + function onReloadFailed() { + if (!Settings?.isDebug) { + Quickshell.inhibitReloadPopup(); + } + } + } + + Connections { + target: I18n ? I18n : null + function onTranslationsLoaded() { + i18nLoaded = true; + } + } + + Connections { + target: Settings ? Settings : null + function onSettingsLoaded() { + settingsLoaded = true; + } + } + + Connections { + target: ShellState ? ShellState : null + function onIsLoadedChanged() { + if (ShellState.isLoaded) { + shellStateLoaded = true; + } + } + } + + Loader { + active: i18nLoaded && settingsLoaded && shellStateLoaded + + sourceComponent: Item { + Component.onCompleted: { + Logger.i("Shell", "---------------------------"); + + // Critical services needed for initial UI rendering + WallpaperService.init(); + ImageCacheService.init(); + AppThemeService.init(); + ColorSchemeService.init(); + DarkModeService.init(); + + // Defer non-critical services to unblock first frame + Qt.callLater(function () { + LocationService.init(); + NightLightService.apply(); + IdleInhibitorService.init(); + IdleService.init(); + PowerProfileService.init(); + HostService.init(); + NotificationRulesService.init(); + GitHubService.init(); + SupporterService.init(); + CustomButtonIPCService.init(); + IPCService.init(screenDetector); + + // Force ClipboardService initialization so clipboard watchers + // start immediately instead of waiting for first launcher open + if (Settings.data.appLauncher.enableClipboardHistory) { + ClipboardService.checkCliphistAvailability(); + } + }); + + delayedInitTimer.running = true; + } + + Overview {} + Background {} + DesktopWidgets {} + AllScreens {} + Dock {} + Notification {} + ToastOverlay {} + OSD {} + + // Launcher overlay window (for overlay layer mode) + Loader { + active: Settings.data.appLauncher.overviewLayer + sourceComponent: Component { + LauncherOverlayWindow {} + } + } + + LockScreen {} + FadeOverlay {} + + // Settings window mode (single window across all monitors) + SettingsPanelWindow {} + + // Shared screen detector for IPC and plugins + CurrentScreenDetector { + id: screenDetector + } + + // IPCService is a singleton, initialized via init() in deferred services block + + // Container for plugins Main.qml instances (must be in graphics scene) + Item { + id: pluginContainer + visible: false + + Component.onCompleted: { + PluginService.pluginContainer = pluginContainer; + PluginService.screenDetector = screenDetector; + } + } + } + } + + // Delayed initialization and wizard/changelog + Timer { + id: delayedInitTimer + running: false + interval: 1500 + onTriggered: { + HooksService.init(); + FontService.init(); + UpdateService.init(); + showWizardOrChangelog(); + } + } + + // Retry timer for when panel isn't ready yet + Timer { + id: wizardRetryTimer + running: false + interval: 500 + property string pendingWizardType: "" // "setup", "telemetry", or "" + onTriggered: showWizardOrChangelog() + } + + // Connect to telemetry wizard signal from UpdateService (for async state loading) + Connections { + target: UpdateService + function onTelemetryWizardNeeded() { + wizardRetryTimer.pendingWizardType = "telemetry"; + showWizardOrChangelog(); + } + } + + property var telemetryWizardConnection: null + + function showWizardOrChangelog() { + // Determine what to show: setup wizard > telemetry wizard > changelog + var wizardType = wizardRetryTimer.pendingWizardType; + + if (wizardType === "") { + // First call - determine wizard type + if (Settings.shouldOpenSetupWizard) { + wizardType = "setup"; + } else if (UpdateService.shouldShowTelemetryWizard()) { + wizardType = "telemetry"; + } else { + // No wizard needed - init telemetry and show changelog + TelemetryService.init(); + UpdateService.checkTelemetryWizardOrChangelog(); + return; + } + } + + var targetScreen = PanelService.findScreenForPanels(); + if (!targetScreen) { + Logger.w("Shell", "No screen available to show wizard"); + wizardRetryTimer.pendingWizardType = ""; + return; + } + + var setupPanel = PanelService.getPanel("setupWizardPanel", targetScreen); + if (!setupPanel) { + // Panel not ready, retry + wizardRetryTimer.pendingWizardType = wizardType; + wizardRetryTimer.restart(); + return; + } + + // Panel is ready, show it + wizardRetryTimer.pendingWizardType = ""; + + if (wizardType === "telemetry") { + setupPanel.telemetryOnlyMode = true; + + // Connect to completion signal to show changelog afterward + if (telemetryWizardConnection) { + setupPanel.telemetryWizardCompleted.disconnect(telemetryWizardConnection); + } + telemetryWizardConnection = function () { + UpdateService.showLatestChangelog(); + }; + setupPanel.telemetryWizardCompleted.connect(telemetryWizardConnection); + } else { + setupPanel.telemetryOnlyMode = false; + } + + setupPanel.open(); + } +}